diff --git a/CHANGELOG.md b/CHANGELOG.md index 3813d3b4d..20b3f8a00 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog Fab-manager +- Ability to configure multiple VAT rates, per kind of invoiced item +- Refactored the extended prices frontend code to allow future customization +- Fix a bug: the amount label in not correctly shown in the extended prices modal + ## v5.2.0 2021 December 23 - Ability to configure prices for spaces by time slots different than the default hourly rate diff --git a/app/controllers/api/exports_controller.rb b/app/controllers/api/exports_controller.rb index 66dc270c3..2b8ab1980 100644 --- a/app/controllers/api/exports_controller.rb +++ b/app/controllers/api/exports_controller.rb @@ -70,6 +70,8 @@ class API::ExportsController < API::ApiController case type when 'acd' export = export.where('created_at > ?', Invoice.maximum('updated_at')) + when 'vat' + export = export.where('created_at > ?', Invoice.maximum('updated_at')) else raise ArgumentError, "Unknown type accounting/#{type}" end diff --git a/app/controllers/api/prices_controller.rb b/app/controllers/api/prices_controller.rb index de84774a2..fdcf6de1b 100644 --- a/app/controllers/api/prices_controller.rb +++ b/app/controllers/api/prices_controller.rb @@ -36,7 +36,7 @@ class API::PricesController < API::ApiController def destroy authorize @price - @price.destroy + @price.safe_destroy head :no_content end diff --git a/app/frontend/src/javascript/api/space.ts b/app/frontend/src/javascript/api/space.ts index 5633bd658..6f1d9c9b9 100644 --- a/app/frontend/src/javascript/api/space.ts +++ b/app/frontend/src/javascript/api/space.ts @@ -12,4 +12,5 @@ export default class SpaceAPI { const res: AxiosResponse = await apiClient.get(`/api/spaces/${id}`); return res?.data; } + } diff --git a/app/frontend/src/javascript/components/pricing/machines/configure-packs-button.tsx b/app/frontend/src/javascript/components/pricing/machines/configure-packs-button.tsx index c5b1ff43c..ec3cbd2be 100644 --- a/app/frontend/src/javascript/components/pricing/machines/configure-packs-button.tsx +++ b/app/frontend/src/javascript/components/pricing/machines/configure-packs-button.tsx @@ -64,8 +64,8 @@ export const ConfigurePacksButton: React.FC = ({ pack }; return ( -
- {showList && @@ -73,7 +73,7 @@ export const ConfigurePacksButton: React.FC = ({ pack {packs?.map(p =>
  • {formatDuration(p.minutes)} - {FormatLib.price(p.amount)} - + diff --git a/app/frontend/src/javascript/components/pricing/machines/delete-pack.tsx b/app/frontend/src/javascript/components/pricing/machines/delete-pack.tsx index dc1d01089..bfd8a40a0 100644 --- a/app/frontend/src/javascript/components/pricing/machines/delete-pack.tsx +++ b/app/frontend/src/javascript/components/pricing/machines/delete-pack.tsx @@ -42,8 +42,8 @@ const DeletePackComponent: React.FC = ({ onSuccess, onError, pa }; return ( -
    - } onClick={toggleDeletionModal} /> +
    + } onClick={toggleDeletionModal} /> = ({ pack, onSuccess, onError }) }; return ( -
    - } onClick={handleRequestEdit} /> +
    + } onClick={handleRequestEdit} /> - {packData && } + onConfirmSendFormId="edit-pack"> + {packData && }
    ); diff --git a/app/frontend/src/javascript/components/pricing/machines/machines-pricing.tsx b/app/frontend/src/javascript/components/pricing/machines/machines-pricing.tsx index 5b3618b7d..80334aab0 100644 --- a/app/frontend/src/javascript/components/pricing/machines/machines-pricing.tsx +++ b/app/frontend/src/javascript/components/pricing/machines/machines-pricing.tsx @@ -107,7 +107,7 @@ const MachinesPricing: React.FC = ({ onError, onSuccess }) }; return ( -
    +

    diff --git a/app/frontend/src/javascript/components/pricing/machines/pack-form.tsx b/app/frontend/src/javascript/components/pricing/machines/pack-form.tsx index 831e0d899..ba4381c46 100644 --- a/app/frontend/src/javascript/components/pricing/machines/pack-form.tsx +++ b/app/frontend/src/javascript/components/pricing/machines/pack-form.tsx @@ -103,7 +103,7 @@ export const PackForm: React.FC = ({ formId, onSubmit, pack }) => }; return ( -
    + (false); /** - * Open/closes the popover listing the existing extended prices + * Return the number of minutes, user-friendly formatted + */ + const formatDuration = (minutes: number): string => { + return t('app.admin.configure_extended_prices_button.extended_price_DURATION', { DURATION: minutes }); + }; + + /** + * Open/closes the popover listing the existing packs */ const toggleShowList = (): void => { setShowList(!showList); @@ -57,22 +64,22 @@ export const ConfigureExtendedPriceButton: React.FC - - {showList && + {showList &&
      {extendedPrices?.map(extendedPrice =>
    • - {extendedPrice.duration} {t('app.admin.calendar.minutes')} - {FormatLib.price(extendedPrice.amount)} - + {formatDuration(extendedPrice.duration)} - {FormatLib.price(extendedPrice.amount)} +
    • )}
    - {extendedPrices?.length === 0 && {t('app.admin.configure_extendedPrices_button.no_extendedPrices')}} + {extendedPrices?.length === 0 && {t('app.admin.configure_extended_prices_button.no_extended_prices')}}
    }
    ); diff --git a/app/frontend/src/javascript/components/pricing/spaces/create-extended-price.tsx b/app/frontend/src/javascript/components/pricing/spaces/create-extended-price.tsx index c01be253a..082cab8ad 100644 --- a/app/frontend/src/javascript/components/pricing/spaces/create-extended-price.tsx +++ b/app/frontend/src/javascript/components/pricing/spaces/create-extended-price.tsx @@ -43,24 +43,24 @@ export const CreateExtendedPrice: React.FC = ({ onSucc // create it on the API PriceAPI.create(newExtendedPrice) .then(() => { - onSuccess(t('app.admin.create_extendedPrice.extendedPrice_successfully_created')); + onSuccess(t('app.admin.create_extended_price.extended_price_successfully_created')); toggleModal(); }) .catch(error => onError(error)); }; return ( -
    - +
    + - {t('app.admin.create_extendedPrice.new_extendedPrice_info', { TYPE: priceableType })} + {t('app.admin.create_extended_price.new_extended_price_info', { TYPE: priceableType })} diff --git a/app/frontend/src/javascript/components/pricing/spaces/delete-extended-price.tsx b/app/frontend/src/javascript/components/pricing/spaces/delete-extended-price.tsx index 56af8784e..75931d2b5 100644 --- a/app/frontend/src/javascript/components/pricing/spaces/delete-extended-price.tsx +++ b/app/frontend/src/javascript/components/pricing/spaces/delete-extended-price.tsx @@ -33,23 +33,23 @@ export const DeleteExtendedPrice: React.FC = ({ onSucc */ const onDeleteConfirmed = (): void => { PriceAPI.destroy(price.id).then(() => { - onSuccess(t('app.admin.delete_extendedPrice.extendedPrice_deleted')); + onSuccess(t('app.admin.delete_extended_price.extended_price_deleted')); }).catch((error) => { - onError(t('app.admin.delete_extendedPrice.unable_to_delete') + error); + onError(t('app.admin.delete_extended_price.unable_to_delete') + error); }); toggleDeletionModal(); }; return ( -
    - } onClick={toggleDeletionModal} /> - + } onClick={toggleDeletionModal} /> + - {t('app.admin.delete_extendedPrice.delete_confirmation')} + {t('app.admin.delete_extended_price.delete_confirmation')}
    ); diff --git a/app/frontend/src/javascript/components/pricing/spaces/edit-extended-price.tsx b/app/frontend/src/javascript/components/pricing/spaces/edit-extended-price.tsx index 994432850..3b1608a29 100644 --- a/app/frontend/src/javascript/components/pricing/spaces/edit-extended-price.tsx +++ b/app/frontend/src/javascript/components/pricing/spaces/edit-extended-price.tsx @@ -42,7 +42,7 @@ export const EditExtendedPrice: React.FC = ({ price, onS const handleUpdate = (price: Price): void => { PriceAPI.update(price) .then(() => { - onSuccess(t('app.admin.edit_extendedPrice.extendedPrice_successfully_updated')); + onSuccess(t('app.admin.edit_extended_price.extended_price_successfully_updated')); setExtendedPriceData(price); toggleModal(); }) @@ -50,15 +50,16 @@ export const EditExtendedPrice: React.FC = ({ price, onS }; return ( -
    - } onClick={handleRequestEdit} /> +
    + } onClick={handleRequestEdit} /> - {extendedPriceData && } + confirmButton={t('app.admin.edit_extended_price.confirm_changes')} + onConfirmSendFormId="edit-extended-price"> + {extendedPriceData && }
    ); diff --git a/app/frontend/src/javascript/components/pricing/spaces/extended-price-form.tsx b/app/frontend/src/javascript/components/pricing/spaces/extended-price-form.tsx index 31445ee81..eff0bcc45 100644 --- a/app/frontend/src/javascript/components/pricing/spaces/extended-price-form.tsx +++ b/app/frontend/src/javascript/components/pricing/spaces/extended-price-form.tsx @@ -9,7 +9,7 @@ declare let Fablab: IFablab; interface ExtendedPriceFormProps { formId: string, - onSubmit: (pack: Price) => void, + onSubmit: (price: Price) => void, price?: Price, } @@ -49,8 +49,8 @@ export const ExtendedPriceForm: React.FC = ({ formId, on }; return ( - - + + = ({ onError, onSuccess }) => }; return ( -
    +
    -

    -

    -

    {t('app.admin.pricing.you_can_override')}

    +

    +

    +

    {t('app.admin.spaces_pricing.you_can_override')}

    +

    {t('app.admin.spaces_pricing.extended_prices')}

    - + {groups?.map(group => )} diff --git a/app/frontend/src/javascript/controllers/admin/invoices.js b/app/frontend/src/javascript/controllers/admin/invoices.js index 4cff1dc62..960025762 100644 --- a/app/frontend/src/javascript/controllers/admin/invoices.js +++ b/app/frontend/src/javascript/controllers/admin/invoices.js @@ -92,6 +92,15 @@ Application.Controllers.controller('InvoicesController', ['$scope', '$state', 'I active: false, templateUrl: '/admin/invoices/settings/editVAT.html' }, + multiVAT: { + rateMachine: '', + rateSpace: '', + rateTraining: '', + rateEvent: '', + rateSubscription: '', + editTemplateUrl: '/admin/invoices/settings/editMultiVAT.html', + historyTemplateUrl: '/admin/invoices/settings/multiVATHistory.html' + }, text: { content: '' }, @@ -217,6 +226,14 @@ Application.Controllers.controller('InvoicesController', ['$scope', '$state', 'I // Is shown the modal dialog to select a payment gateway $scope.openSelectGatewayModal = false; + /** + * Return the VAT rate applicable to the machine reservations + * @return {number} + */ + $scope.getMachineExampleRate = function () { + return $scope.invoice.multiVAT.rateMachine || $scope.invoice.VAT.rate; + }; + /** * Change the invoices ordering criterion to the one provided * @param orderBy {string} ordering criterion @@ -446,6 +463,9 @@ Application.Controllers.controller('InvoicesController', ['$scope', '$state', 'I active () { return $scope.invoice.VAT.active; }, + multiVAT () { + return $scope.invoice.multiVAT; + }, rateHistory () { return Setting.get({ name: 'invoice_VAT-rate', history: true }).$promise; }, @@ -453,13 +473,74 @@ Application.Controllers.controller('InvoicesController', ['$scope', '$state', 'I return Setting.get({ name: 'invoice_VAT-active', history: true }).$promise; } }, - controller: ['$scope', '$uibModalInstance', 'rate', 'active', 'rateHistory', 'activeHistory', function ($scope, $uibModalInstance, rate, active, rateHistory, activeHistory) { + controller: ['$scope', '$uibModalInstance', 'rate', 'active', 'rateHistory', 'activeHistory', 'multiVAT', function ($scope, $uibModalInstance, rate, active, rateHistory, activeHistory, multiVAT) { $scope.rate = rate; $scope.isSelected = active; $scope.history = []; $scope.ok = function () { $uibModalInstance.close({ rate: $scope.rate, active: $scope.isSelected }); }; $scope.cancel = function () { $uibModalInstance.dismiss('cancel'); }; + $scope.editMultiVAT = function () { + const editMultiVATModalInstance = $uibModal.open({ + animation: true, + templateUrl: multiVAT.editTemplateUrl, + size: 'lg', + resolve: { + rate () { + return $scope.rate; + }, + multiVAT () { + return multiVAT; + } + }, + controller: ['$scope', '$uibModalInstance', 'rate', 'multiVAT', function ($scope, $uibModalInstance, rate, multiVAT) { + $scope.rate = rate; + $scope.multiVAT = multiVAT; + + $scope.ok = function () { $uibModalInstance.close({ multiVAT: $scope.multiVAT }); }; + $scope.cancel = function () { $uibModalInstance.dismiss('cancel'); }; + + $scope.showMultiRateHistory = function (rateType) { + $uibModal.open({ + animation: true, + templateUrl: multiVAT.historyTemplateUrl, + size: 'lg', + resolve: { + rateHistory () { + return Setting.get({ name: `invoice_VAT-rate_${rateType}`, history: true }).$promise; + } + }, + controller: ['$scope', '$uibModalInstance', 'rateHistory', function ($scope, $uibModalInstance, rateHistory) { + $scope.history = []; + + $scope.cancel = function () { $uibModalInstance.dismiss('cancel'); }; + + const initialize = function () { + rateHistory.setting.history.forEach(function (rate) { + $scope.history.push({ date: rate.created_at, rate: rate.value, user: rate.user }); + }); + }; + + initialize(); + }] + }); + }; + }] + }); + return editMultiVATModalInstance.result.then(function (result) { + ['Machine', 'Space', 'Training', 'Event', 'Subscription'].forEach(rateType => { + Setting.update({ name: `invoice_VAT-rate_${rateType}` }, { value: result.multiVAT[`rate${rateType}`] + '' }, function (data) { + return growl.success(_t('app.admin.invoices.VAT_rate_successfully_saved')); + } + , function (error) { + if (error.status === 304) return; + + growl.error(_t('app.admin.invoices.an_error_occurred_while_saving_the_VAT_rate')); + console.error(error); + }); + }); + }); + }; const initialize = function () { rateHistory.setting.history.forEach(function (rate) { @@ -943,6 +1024,11 @@ Application.Controllers.controller('InvoicesController', ['$scope', '$state', 'I $scope.invoice.text.content = settings.invoice_text; $scope.invoice.VAT.rate = parseFloat(settings['invoice_VAT-rate']); $scope.invoice.VAT.active = (settings['invoice_VAT-active'] === 'true'); + $scope.invoice.multiVAT.rateMachine = settings['invoice_VAT-rate_Machine'] ? parseFloat(settings['invoice_VAT-rate_Machine']) : ''; + $scope.invoice.multiVAT.rateSpace = settings['invoice_VAT-rate_Space'] ? parseFloat(settings['invoice_VAT-rate_Space']) : ''; + $scope.invoice.multiVAT.rateTraining = settings['invoice_VAT-rate_Training'] ? parseFloat(settings['invoice_VAT-rate_Training']) : ''; + $scope.invoice.multiVAT.rateEvent = settings['invoice_VAT-rate_Event'] ? parseFloat(settings['invoice_VAT-rate_Event']) : ''; + $scope.invoice.multiVAT.rateSubscription = settings['invoice_VAT-rate_Subscription'] ? parseFloat(settings['invoice_VAT-rate_Subscription']) : ''; $scope.invoice.number.model = settings['invoice_order-nb']; $scope.invoice.code.model = settings['invoice_code-value']; $scope.invoice.code.active = (settings['invoice_code-active'] === 'true'); @@ -1328,6 +1414,16 @@ Application.Controllers.controller('AccountingExportModalController', ['$scope', decimalSeparator: ',', exportInvoicesAtZero: false, columns: ['journal_code', 'date', 'account_code', 'account_label', 'piece', 'line_label', 'debit_origin', 'credit_origin', 'debit_euro', 'credit_euro', 'lettering'] + }, + vat: { + format: 'csv', + encoding: 'UTF-8', + separator: ';', + dateFormat: '%Y-%m-%d', + labelMaxLength: 'N/A', + decimalSeparator: '.', + exportInvoicesAtZero: false, + columns: ['start_date', 'end_date', 'vat_rate', 'amount'] } }; @@ -1347,6 +1443,7 @@ Application.Controllers.controller('AccountingExportModalController', ['$scope', // binding to radio button "export to" $scope.exportTarget = { + type: null, software: null, startDate: null, endDate: null, diff --git a/app/frontend/src/javascript/models/setting.ts b/app/frontend/src/javascript/models/setting.ts index 08c01cd01..420d1f374 100644 --- a/app/frontend/src/javascript/models/setting.ts +++ b/app/frontend/src/javascript/models/setting.ts @@ -20,6 +20,11 @@ export enum SettingName { InvoiceOrderNb = 'invoice_order-nb', InvoiceVATActive = 'invoice_VAT-active', InvoiceVATRate = 'invoice_VAT-rate', + InvoiceVATRateMachine = 'invoice_VAT-rate_Machine', + InvoiceVATRateTraining = 'invoice_VAT-rate_Training', + InvoiceVATRateSpace = 'invoice_VAT-rate_Space', + InvoiceVATRateEvent = 'invoice_VAT-rate_Event', + InvoiceVATRateSubscription = 'invoice_VAT-rate_Subscription', InvoiceText = 'invoice_text', InvoiceLegals = 'invoice_legals', BookingWindowStart = 'booking_window_start', diff --git a/app/frontend/src/javascript/router.js b/app/frontend/src/javascript/router.js index 0f24a91b5..bb5a7d6c6 100644 --- a/app/frontend/src/javascript/router.js +++ b/app/frontend/src/javascript/router.js @@ -869,7 +869,8 @@ angular.module('application.router', ['ui.router']) resolve: { settings: ['Setting', function (Setting) { return Setting.query({ - names: "['invoice_legals', 'invoice_text', 'invoice_VAT-rate', 'invoice_VAT-active', 'invoice_order-nb', 'invoice_code-value', " + + names: "['invoice_legals', 'invoice_text', 'invoice_VAT-rate', 'invoice_VAT-rate_Machine', 'invoice_VAT-rate_Training', 'invoice_VAT-rate_Space', " + + "'invoice_VAT-rate_Event', 'invoice_VAT-rate_Subscription', 'invoice_VAT-active', 'invoice_order-nb', 'invoice_code-value', " + "'invoice_code-active', 'invoice_reference', 'invoice_logo', 'accounting_journal_code', 'accounting_card_client_code', " + "'accounting_card_client_label', 'accounting_wallet_client_code', 'accounting_wallet_client_label', 'invoicing_module', " + "'accounting_other_client_code', 'accounting_other_client_label', 'accounting_wallet_code', 'accounting_wallet_label', " + diff --git a/app/frontend/src/stylesheets/application.scss b/app/frontend/src/stylesheets/application.scss index 32d9cbad9..fb262274b 100644 --- a/app/frontend/src/stylesheets/application.scss +++ b/app/frontend/src/stylesheets/application.scss @@ -57,12 +57,18 @@ @import "modules/machines/machines-filters"; @import "modules/machines/required-training-modal"; @import "modules/user/avatar"; -@import "modules/pricing/pricing-list"; @import "modules/pricing/editable-price"; -@import "modules/pricing/configure-group-button"; -@import "modules/pricing/group-form"; -@import "modules/pricing/delete-group"; -@import "modules/pricing/edit-group"; +@import "modules/pricing/machines/machines-pricing"; +@import "modules/pricing/machines/configure-packs-button"; +@import "modules/pricing/machines/pack-form"; +@import "modules/pricing/machines/delete-pack"; +@import "modules/pricing/machines/edit-pack"; +@import "modules/pricing/machines/create-pack"; +@import "modules/pricing/spaces/configure-extended-prices-button"; +@import "modules/pricing/spaces/create-extended-price"; +@import "modules/pricing/spaces/delete-extended-price"; +@import "modules/pricing/spaces/edit-extended-price"; +@import "modules/pricing/spaces/spaces-pricing"; @import "modules/settings/check-list-setting"; @import "modules/prepaid-packs/propose-packs-modal"; @import "modules/prepaid-packs/packs-summary"; diff --git a/app/frontend/src/stylesheets/modules/invoice.scss b/app/frontend/src/stylesheets/modules/invoice.scss index dafdb538c..d81b08307 100644 --- a/app/frontend/src/stylesheets/modules/invoice.scss +++ b/app/frontend/src/stylesheets/modules/invoice.scss @@ -367,3 +367,7 @@ table.export-table-template { height: 30px; } } + +.multi-vat-rate-input { + width: 90% !important; +} diff --git a/app/frontend/src/stylesheets/modules/pricing/configure-group-button.scss b/app/frontend/src/stylesheets/modules/pricing/machines/configure-packs-button.scss similarity index 84% rename from app/frontend/src/stylesheets/modules/pricing/configure-group-button.scss rename to app/frontend/src/stylesheets/modules/pricing/machines/configure-packs-button.scss index dec3bf005..7af32419c 100644 --- a/app/frontend/src/stylesheets/modules/pricing/configure-group-button.scss +++ b/app/frontend/src/stylesheets/modules/pricing/machines/configure-packs-button.scss @@ -1,9 +1,9 @@ -.configure-group { +.configure-packs-button { display: inline-block; margin-left: 6px; position: relative; - &-button { + .packs-button { border: 1px solid #d0cccc; border-radius: 50%; cursor: pointer; @@ -18,13 +18,6 @@ color: white; } } - .popover-title { - .add-pack-button { - position: absolute; - right: 5px; - top: 10px; - } - } .popover-content { ul { @@ -44,7 +37,7 @@ line-height: 24px; } - .group-actions button { + .pack-actions button { font-size: 10px; vertical-align: middle; line-height: 10px; diff --git a/app/frontend/src/stylesheets/modules/pricing/machines/create-pack.scss b/app/frontend/src/stylesheets/modules/pricing/machines/create-pack.scss new file mode 100644 index 000000000..72856dca5 --- /dev/null +++ b/app/frontend/src/stylesheets/modules/pricing/machines/create-pack.scss @@ -0,0 +1,7 @@ +.create-pack { + .add-pack-button { + position: absolute; + right: 5px; + top: 10px; + } +} diff --git a/app/frontend/src/stylesheets/modules/pricing/delete-group.scss b/app/frontend/src/stylesheets/modules/pricing/machines/delete-pack.scss similarity index 65% rename from app/frontend/src/stylesheets/modules/pricing/delete-group.scss rename to app/frontend/src/stylesheets/modules/pricing/machines/delete-pack.scss index dd1f2d259..37d87274c 100644 --- a/app/frontend/src/stylesheets/modules/pricing/delete-group.scss +++ b/app/frontend/src/stylesheets/modules/pricing/machines/delete-pack.scss @@ -1,7 +1,7 @@ -.delete-group { +.delete-pack { display: inline; - &-button { + .remove-pack-button { background-color: #cb1117; color: white; } diff --git a/app/frontend/src/stylesheets/modules/pricing/edit-group.scss b/app/frontend/src/stylesheets/modules/pricing/machines/edit-pack.scss similarity index 65% rename from app/frontend/src/stylesheets/modules/pricing/edit-group.scss rename to app/frontend/src/stylesheets/modules/pricing/machines/edit-pack.scss index f8c48ff92..1b87b732c 100644 --- a/app/frontend/src/stylesheets/modules/pricing/edit-group.scss +++ b/app/frontend/src/stylesheets/modules/pricing/machines/edit-pack.scss @@ -1,3 +1,3 @@ -.edit-group { +.edit-pack { display: inline-block; } diff --git a/app/frontend/src/stylesheets/modules/pricing/machines/machines-pricing.scss b/app/frontend/src/stylesheets/modules/pricing/machines/machines-pricing.scss new file mode 100644 index 000000000..99c04a00b --- /dev/null +++ b/app/frontend/src/stylesheets/modules/pricing/machines/machines-pricing.scss @@ -0,0 +1,31 @@ +.machines-pricing { + .fab-alert { + margin: 15px 0; + } + table { + overflow-y: scroll; + thead > tr > th:first-child { + width: 20%; + } + + thead > tr > th.group-name { + width: 20%; + text-transform: uppercase; + font-size: 1.4rem; + } + + thead > tr > th { + vertical-align: bottom; + border-bottom: 2px solid #ddd; + padding: 8px; + line-height: 1.5; + } + + tbody > tr > td { + padding: 8px; + line-height: 1.5; + vertical-align: top; + border-top: 1px solid #ddd; + } + } +} diff --git a/app/frontend/src/stylesheets/modules/pricing/group-form.scss b/app/frontend/src/stylesheets/modules/pricing/machines/pack-form.scss similarity index 89% rename from app/frontend/src/stylesheets/modules/pricing/group-form.scss rename to app/frontend/src/stylesheets/modules/pricing/machines/pack-form.scss index 3da69b3e4..1d10d59aa 100644 --- a/app/frontend/src/stylesheets/modules/pricing/group-form.scss +++ b/app/frontend/src/stylesheets/modules/pricing/machines/pack-form.scss @@ -1,4 +1,4 @@ -.group-form { +.pack-form { .interval-inputs { display: flex; diff --git a/app/frontend/src/stylesheets/modules/pricing/spaces/configure-extended-prices-button.scss b/app/frontend/src/stylesheets/modules/pricing/spaces/configure-extended-prices-button.scss new file mode 100644 index 000000000..3931c3a79 --- /dev/null +++ b/app/frontend/src/stylesheets/modules/pricing/spaces/configure-extended-prices-button.scss @@ -0,0 +1,49 @@ +.configure-extended-prices-button { + display: inline-block; + margin-left: 6px; + position: relative; + + .extended-prices-button { + border: 1px solid #d0cccc; + border-radius: 50%; + cursor: pointer; + width: 30px; + height: 30px; + display: inline-block; + padding: 2px 6px; + box-shadow: 0 1px 1px 0 #abaaaa; + + &:hover { + background-color: #b9b9b9; + color: white; + } + } + + .popover-content { + ul { + padding-left: 19px; + + li { + display: flex; + justify-content: space-between; + &::before { + content: '\f466'; + font-family: 'Font Awesome 5 Free'; + position: absolute; + left: 11px; + font-weight: 800; + font-size: 12px; + vertical-align: middle; + line-height: 24px; + } + + .extended-prices-actions button { + font-size: 10px; + vertical-align: middle; + line-height: 10px; + height: auto; + } + } + } + } +} diff --git a/app/frontend/src/stylesheets/modules/pricing/spaces/create-extended-price.scss b/app/frontend/src/stylesheets/modules/pricing/spaces/create-extended-price.scss new file mode 100644 index 000000000..6f87fab8b --- /dev/null +++ b/app/frontend/src/stylesheets/modules/pricing/spaces/create-extended-price.scss @@ -0,0 +1,7 @@ +.create-extended-price { + .add-price-button { + position: absolute; + right: 5px; + top: 10px; + } +} diff --git a/app/frontend/src/stylesheets/modules/pricing/spaces/delete-extended-price.scss b/app/frontend/src/stylesheets/modules/pricing/spaces/delete-extended-price.scss new file mode 100644 index 000000000..a19f09c09 --- /dev/null +++ b/app/frontend/src/stylesheets/modules/pricing/spaces/delete-extended-price.scss @@ -0,0 +1,8 @@ +.delete-extended-price { + display: inline; + + .remove-price-button { + background-color: #cb1117; + color: white; + } +} diff --git a/app/frontend/src/stylesheets/modules/pricing/spaces/edit-extended-price.scss b/app/frontend/src/stylesheets/modules/pricing/spaces/edit-extended-price.scss new file mode 100644 index 000000000..98ce6bfde --- /dev/null +++ b/app/frontend/src/stylesheets/modules/pricing/spaces/edit-extended-price.scss @@ -0,0 +1,3 @@ +.edit-extended-price { + display: inline-block; +} diff --git a/app/frontend/src/stylesheets/modules/pricing/spaces/extended-price-form.scss b/app/frontend/src/stylesheets/modules/pricing/spaces/extended-price-form.scss new file mode 100644 index 000000000..312ea1f0b --- /dev/null +++ b/app/frontend/src/stylesheets/modules/pricing/spaces/extended-price-form.scss @@ -0,0 +1,10 @@ +.extended-price-form { + .interval-inputs { + display: flex; + + .select-interval { + min-width: 49%; + margin-left: 4px; + } + } +} diff --git a/app/frontend/src/stylesheets/modules/pricing/pricing-list.scss b/app/frontend/src/stylesheets/modules/pricing/spaces/spaces-pricing.scss similarity index 96% rename from app/frontend/src/stylesheets/modules/pricing/pricing-list.scss rename to app/frontend/src/stylesheets/modules/pricing/spaces/spaces-pricing.scss index 5169794f4..b449492de 100644 --- a/app/frontend/src/stylesheets/modules/pricing/pricing-list.scss +++ b/app/frontend/src/stylesheets/modules/pricing/spaces/spaces-pricing.scss @@ -1,4 +1,4 @@ -.pricing-list { +.spaces-pricing { .fab-alert { margin: 15px 0; } @@ -28,4 +28,4 @@ border-top: 1px solid #ddd; } } -} \ No newline at end of file +} diff --git a/app/frontend/templates/admin/invoices/accountingExportModal.html b/app/frontend/templates/admin/invoices/accountingExportModal.html index 4c952b4c9..4d6f97523 100644 --- a/app/frontend/templates/admin/invoices/accountingExportModal.html +++ b/app/frontend/templates/admin/invoices/accountingExportModal.html @@ -42,11 +42,15 @@
    -

    {{ 'app.admin.invoices.export_to' }}

    +

    {{ 'app.admin.invoices.export_what' }}

    -
    diff --git a/app/frontend/templates/admin/invoices/settings.html b/app/frontend/templates/admin/invoices/settings.html index 83260986a..63554df1e 100644 --- a/app/frontend/templates/admin/invoices/settings.html +++ b/app/frontend/templates/admin/invoices/settings.html @@ -54,12 +54,12 @@ - - + + - + diff --git a/app/frontend/templates/admin/invoices/settings/editMultiVAT.html b/app/frontend/templates/admin/invoices/settings/editMultiVAT.html new file mode 100644 index 000000000..d14f26bd8 --- /dev/null +++ b/app/frontend/templates/admin/invoices/settings/editMultiVAT.html @@ -0,0 +1,57 @@ +
    + + + +
    diff --git a/app/frontend/templates/admin/invoices/settings/editVAT.html b/app/frontend/templates/admin/invoices/settings/editVAT.html index 8ffd8ce9a..5423aa74c 100644 --- a/app/frontend/templates/admin/invoices/settings/editVAT.html +++ b/app/frontend/templates/admin/invoices/settings/editVAT.html @@ -22,6 +22,12 @@ + +

    + + {{ 'app.admin.invoices.VAT_notice' | translate }} +

    +

    {{ 'app.admin.invoices.VAT_history' }}

    @@ -48,6 +54,7 @@
    diff --git a/app/frontend/templates/admin/invoices/settings/multiVATHistory.html b/app/frontend/templates/admin/invoices/settings/multiVATHistory.html new file mode 100644 index 000000000..77f7e2e1a --- /dev/null +++ b/app/frontend/templates/admin/invoices/settings/multiVATHistory.html @@ -0,0 +1,32 @@ +
    + +
    {t('app.admin.pricing.spaces')}{t('app.admin.spaces_pricing.spaces')}{group.name}
    {{ 'app.admin.invoices.including_VAT' | translate }} {{invoice.VAT.rate}} %{{30-(30/(invoice.VAT.rate/100+1)) | currency}}{{ 'app.admin.invoices.including_VAT' }}{{30-(30/(getMachineExampleRate()/100+1)) | currency}}
    {{ 'app.admin.invoices.including_total_excluding_taxes' }}{{30/(invoice.VAT.rate/100+1) | currency}}{{30/(getMachineExampleRate()/100+1) | currency}}
    {{ 'app.admin.invoices.including_amount_payed_on_ordering' }}
    + + + + + + + + + + + + + + +
    {{ 'app.admin.invoices.VAT_rate' }}{{ 'app.admin.invoices.changed_at' }}{{ 'app.admin.invoices.changed_by' }}
    + {{'app.admin.invoices.VAT_disabled'}} + {{'app.admin.invoices.VAT_enabled'}} + {{value.rate}} + {{value.date | amDateFormat:'L LT'}}{{value.user.name}}{{ 'app.admin.invoices.deleted_user' }}
    +
    +
    + +
    diff --git a/app/models/accounting_period.rb b/app/models/accounting_period.rb index d068d5624..a0377c1d7 100644 --- a/app/models/accounting_period.rb +++ b/app/models/accounting_period.rb @@ -34,7 +34,12 @@ class AccountingPeriod < ApplicationRecord def invoices_with_vat(invoices) vat_service = VatHistoryService.new invoices.map do |i| - { invoice: i, vat_rate: vat_service.invoice_vat(i) / 100.0 } + vat_rate_group = {} + i.invoice_items.each do |item| + vat_type = item.invoice_item_type + vat_rate_group[vat_type] = vat_service.invoice_item_vat(item) / 100.0 unless vat_rate_group[vat_type] + end + { invoice: i, vat_rate: vat_rate_group } end end @@ -70,7 +75,7 @@ class AccountingPeriod < ApplicationRecord end def price_without_taxe(invoice) - invoice[:invoice].total - (invoice[:invoice].total * invoice[:vat_rate]) + invoice[:invoice].invoice_items.map(&:net_amount).sum end def compute_totals diff --git a/app/models/invoice_item.rb b/app/models/invoice_item.rb index 3b9865100..9d8cfe867 100644 --- a/app/models/invoice_item.rb +++ b/app/models/invoice_item.rb @@ -27,7 +27,7 @@ class InvoiceItem < Footprintable def net_amount # deduct VAT vat_service = VatHistoryService.new - vat_rate = vat_service.invoice_vat(invoice) + vat_rate = vat_service.invoice_item_vat(self) Rational(amount_after_coupon / (vat_rate / 100.00 + 1)).round.to_f end @@ -36,6 +36,19 @@ class InvoiceItem < Footprintable amount_after_coupon - net_amount end + # return invoice item type (Machine/Training/Space/Event/Subscription) used to determine the VAT rate + def invoice_item_type + if object_type == Reservation.name + object.try(:reservable_type) || '' + elsif [Subscription.name, OfferDay.name].include? object_type + Subscription.name + elsif object_type == StatisticProfilePrepaidPack.name + object.prepaid_pack.priceable_type + else + '' + end + end + private def log_changes diff --git a/app/models/price.rb b/app/models/price.rb index 94e2dc5e9..b96d10406 100644 --- a/app/models/price.rb +++ b/app/models/price.rb @@ -8,4 +8,8 @@ class Price < ApplicationRecord validates :priceable, :group_id, :amount, presence: true validates :priceable_id, uniqueness: { scope: %i[priceable_type plan_id group_id duration] } + + def safe_destroy + destroy unless duration == 60 + end end diff --git a/app/models/setting.rb b/app/models/setting.rb index e6ed14298..0f664503c 100644 --- a/app/models/setting.rb +++ b/app/models/setting.rb @@ -28,6 +28,11 @@ class Setting < ApplicationRecord invoice_order-nb invoice_VAT-active invoice_VAT-rate + invoice_VAT-rate_Machine + invoice_VAT-rate_Training + invoice_VAT-rate_Space + invoice_VAT-rate_Event + invoice_VAT-rate_Subscription invoice_text invoice_legals booking_window_start diff --git a/app/pdfs/pdf/invoice.rb b/app/pdfs/pdf/invoice.rb index 4065fe6b1..8036951b8 100644 --- a/app/pdfs/pdf/invoice.rb +++ b/app/pdfs/pdf/invoice.rb @@ -230,10 +230,12 @@ class PDF::Invoice < Prawn::Document # TVA vat_service = VatHistoryService.new - vat_rate = vat_service.invoice_vat(invoice) - if vat_rate != 0 + vat_rate_group = vat_service.invoice_vat(invoice) + if total_vat != 0 data += [[I18n.t('invoices.total_including_all_taxes'), number_to_currency(total)]] - data += [[I18n.t('invoices.including_VAT_RATE', RATE: vat_rate), number_to_currency(total_vat / 100.00)]] + vat_rate_group.each do |_type, rate| + data += [[I18n.t('invoices.including_VAT_RATE', RATE: rate[:vat_rate], AMOUNT: number_to_currency(rate[:amount] / 100.00)), number_to_currency(rate[:total_vat] / 100.00)]] + end data += [[I18n.t('invoices.including_total_excluding_taxes'), number_to_currency(total_ht / 100.00)]] data += [[I18n.t('invoices.including_amount_payed_on_ordering'), number_to_currency(total)]] @@ -252,23 +254,25 @@ class PDF::Invoice < Prawn::Document row(0).font_style = :bold column(1).style align: :right - if Setting.get('invoice_VAT-active') + if total_vat != 0 # Total incl. taxes row(-1).style align: :right row(-1).background_color = 'E4E4E4' row(-1).font_style = :bold - # including VAT xx% - row(-2).style align: :right - row(-2).background_color = 'E4E4E4' - row(-2).font_style = :italic + vat_rate_group.size.times do |i| + # including VAT xx% + row(-2 - i).style align: :right + row(-2 - i).background_color = 'E4E4E4' + row(-2 - i).font_style = :italic + end # including total excl. taxes - row(-3).style align: :right - row(-3).background_color = 'E4E4E4' - row(-3).font_style = :italic + row(-3 - vat_rate_group.size + 1).style align: :right + row(-3 - vat_rate_group.size + 1).background_color = 'E4E4E4' + row(-3 - vat_rate_group.size + 1).font_style = :italic # including amount payed on ordering - row(-4).style align: :right - row(-4).background_color = 'E4E4E4' - row(-4).font_style = :bold + row(-4 - vat_rate_group.size + 1).style align: :right + row(-4 - vat_rate_group.size + 1).background_color = 'E4E4E4' + row(-4 - vat_rate_group.size + 1).font_style = :bold end end diff --git a/app/services/accounting_export_service.rb b/app/services/accounting_export_service.rb index b3c03feb6..e9f15f302 100644 --- a/app/services/accounting_export_service.rb +++ b/app/services/accounting_export_service.rb @@ -16,7 +16,6 @@ class AccountingExportService @label_max_length = 50 @export_zeros = false @journal_code = Setting.get('accounting_journal_code') || '' - @date_format = date_format @columns = columns end @@ -134,9 +133,9 @@ class AccountingExportService # Generate the "VAT" row, which contains the credit to the VAT account, with VAT amount only def vat_row(invoice) - rate = VatHistoryService.new.invoice_vat(invoice) + total = invoice.invoice_items.map(&:net_amount).sum # we do not render the VAT row if it was disabled for this invoice - return nil if rate.zero? + return nil if total == invoice.total row( invoice, diff --git a/app/services/vat_export_service.rb b/app/services/vat_export_service.rb new file mode 100644 index 000000000..a5a9ebcbf --- /dev/null +++ b/app/services/vat_export_service.rb @@ -0,0 +1,97 @@ +# frozen_string_literal: false + +# Provides the routine to export the collected VAT data to a CSV file. +class VatExportService + include ActionView::Helpers::NumberHelper + + attr_reader :encoding, :format, :separator, :date_format, :columns, :decimal_separator + + def initialize(columns, encoding: 'UTF-8', format: 'CSV', separator: ';') + @encoding = encoding + @format = format + @separator = separator + @decimal_separator = '.' + @date_format = '%Y-%m-%d' + @columns = columns + end + + def set_options(decimal_separator: ',', date_format: '%d/%m/%Y', label_max_length: nil, export_zeros: nil) + @decimal_separator = decimal_separator + @date_format = date_format + end + + def export(start_date, end_date, file) + # build CSV content + content = header_row + invoices = Invoice.where('created_at >= ? AND created_at <= ?', start_date, end_date).order('created_at ASC') + vat_totals = compute_vat_totals(invoices) + content << generate_rows(vat_totals, start_date, end_date) + + # write content to file + File.open(file, "w:#{encoding}") { |f| f.puts content.encode(encoding, invalid: :replace, undef: :replace) } + end + + private + + def header_row + row = '' + columns.each do |column| + row << I18n.t("vat_export.#{column}") << separator + end + "#{row}\n" + end + + def generate_rows(vat_totals, start_date, end_date) + rows = '' + + vat_totals.each do |rate, total| + next if rate.zero? + + rows += "#{row( + start_date, + end_date, + rate, + total + )}\n" + end + + rows + end + + def compute_vat_totals(invoices) + vat_total = [] + service = VatHistoryService.new + invoices.each do |i| + puts "processing invoice #{i.id}..." unless Rails.env.test? + vat_total.push service.invoice_vat(i) + end + + vat_total.map(&:values).flatten.group_by { |tot| tot[:vat_rate] }.map { |k, v| [k, v.map { |t| t[:total_vat] }.reduce(:+)] }.to_h + end + + # Generate a row of the export, filling the configured columns with the provided values + def row(start_date, end_date, vat_rate, amount) + row = '' + columns.each do |column| + case column + when 'start_date' + row << DateTime.parse(start_date).strftime(date_format) + when 'end_date' + row << DateTime.parse(end_date).strftime(date_format) + when 'vat_rate' + row << vat_rate.to_s + when 'amount' + row << format_number(amount / 100.0) + else + puts "Unsupported column: #{column}" + end + row << separator + end + row + end + + # Format the given number as a string, using the configured separator + def format_number(num) + number_to_currency(num, unit: '', separator: decimal_separator, delimiter: '', precision: 2) + end +end diff --git a/app/services/vat_history_service.rb b/app/services/vat_history_service.rb index 4c03cfe71..705aad368 100644 --- a/app/services/vat_history_service.rb +++ b/app/services/vat_history_service.rb @@ -2,30 +2,42 @@ # Provides the VAT rate in use at the given date class VatHistoryService - # return the VAT rate for the given Invoice/Avoir + # @return the VAT rate for the given Invoice def invoice_vat(invoice) - if invoice.is_a?(Avoir) - vat_rate(invoice.avoir_date) + vat_rate_group = {} + invoice.invoice_items.each do |item| + vat_type = item.invoice_item_type + vat_rate_group[vat_type] = { vat_rate: invoice_item_vat(item), total_vat: 0, amount: 0 } unless vat_rate_group[vat_type] + vat_rate_group[vat_type][:total_vat] += item.vat + vat_rate_group[vat_type][:amount] += item.amount.to_i + end + vat_rate_group + end + + # return the VAT rate for the given InvoiceItem + def invoice_item_vat(invoice_item) + if invoice_item.invoice.is_a?(Avoir) + vat_rate(invoice_item.invoice.avoir_date, invoice_item.invoice_item_type) else - vat_rate(invoice.created_at) + vat_rate(invoice_item.invoice.created_at, invoice_item.invoice_item_type) end end - # return the VAT rate for the given date - def vat_rate(date) - @vat_rates = vat_history if @vat_rates.nil? + # return the VAT rate for the given date and vat type + def vat_rate(date, vat_rate_type) + vat_rates = vat_history(vat_rate_type) - first_rate = @vat_rates.first + first_rate = vat_rates.first return first_rate[:rate] if date < first_rate[:date] - @vat_rates.each_index do |i| - return @vat_rates[i][:rate] if date >= @vat_rates[i][:date] && (@vat_rates[i + 1].nil? || date < @vat_rates[i + 1][:date]) + vat_rates.each_index do |i| + return vat_rates[i][:rate] if date >= vat_rates[i][:date] && (vat_rates[i + 1].nil? || date < vat_rates[i + 1][:date]) end end private - def vat_history + def vat_history(vat_rate_type) chronology = [] end_date = DateTime.current Setting.find_by(name: 'invoice_VAT-active').history_values.order(created_at: 'DESC').each do |v| @@ -33,15 +45,65 @@ class VatHistoryService end_date = v.created_at end chronology.push(start: DateTime.new(0), end: end_date, enabled: false) + # now chronology contains something like one of the following: + # - [{start: 0000-01-01, end: now, enabled: false}] => VAT was never enabled + # - [ + # {start: fab-manager initial setup date, end: now, enabled: true}, + # {start: 0000-01-01, end: fab-manager initial setup date, enabled: false} + # ] => VAT was enabled from the beginning + # - [ + # {start: [date disabled], end: now, enabled: false}, + # {start: [date enable], end: [date disabled], enabled: true}, + # {start: fab-manager initial setup date, end: [date enabled], enabled: false}, + # {start: 0000-01-01, end: fab-manager initial setup date, enabled: false} + # ] => VAT was enabled at some point, and disabled at some other point later + date_rates = [] - Setting.find_by(name: 'invoice_VAT-rate').history_values.order(created_at: 'ASC').each do |rate| - range = chronology.select { |p| rate.created_at.to_i.between?(p[:start].to_i, p[:end].to_i) }.first - date = range[:enabled] ? rate.created_at : range[:end] - date_rates.push(date: date, rate: rate.value.to_i) - end - chronology.reverse_each do |period| - date_rates.push(date: period[:start], rate: 0) unless period[:enabled] + if vat_rate_type.present? + vat_rate_by_type = Setting.find_by(name: "invoice_VAT-rate_#{vat_rate_type}")&.history_values&.order(created_at: 'ASC') + first_vat_rate_by_type = vat_rate_by_type&.select { |v| v.value.present? }&.first + if first_vat_rate_by_type + # before the first VAT rate was defined for the given type, the general VAT rate is used + vat_rate_history_values = Setting.find_by(name: 'invoice_VAT-rate') + .history_values.where('created_at < ?', first_vat_rate_by_type.created_at) + .order(created_at: 'ASC').to_a + # after that, the VAT rate for the given type is used + vat_rate_by_type = Setting.find_by(name: "invoice_VAT-rate_#{vat_rate_type}") + .history_values.where('created_at >= ?', first_vat_rate_by_type.created_at) + .order(created_at: 'ASC') + vat_rate_by_type.each do |rate| + if rate.value.blank? + # if, at some point in the history, a blank rate was set, the general VAT rate is used instead + vat_rate = Setting.find_by(name: 'invoice_VAT-rate') + .history_values.where('created_at < ?', rate.created_at) + .order(created_at: 'DESC') + .first + rate.value = vat_rate.value + end + vat_rate_history_values.push(rate) + end + else + # if no VAT rate is defined for the given type, the general VAT rate is always used + vat_rate_history_values = Setting.find_by(name: 'invoice_VAT-rate').history_values.order(created_at: 'ASC').to_a + end + + # Now we have all the rates history, we can build the final chronology, depending on whether VAT was enabled or not + vat_rate_history_values.each do |rate| + # when the VAT rate was enabled, set the date it was enabled and the rate + range = chronology.select { |p| rate.created_at.to_i.between?(p[:start].to_i, p[:end].to_i) }.first + date = range[:enabled] ? rate.created_at : range[:end] + date_rates.push(date: date, rate: rate.value.to_i) + end + chronology.reverse_each do |period| + # when the VAT rate was disabled, set the date it was disabled and rate=0 + date_rates.push(date: period[:start], rate: 0) unless period[:enabled] + end + else + # if no VAT rate type is given, we return rate=0 from 0000-01-01 + date_rates.push(date: chronology[-1][:start], rate: 0) end + + # finally, we return the chronology, sorted by dates (ascending) date_rates.sort_by { |k| k[:date] } end end diff --git a/app/views/archive/_accounting.json.jbuilder b/app/views/archive/_accounting.json.jbuilder index 48b024c44..124e1ff75 100644 --- a/app/views/archive/_accounting.json.jbuilder +++ b/app/views/archive/_accounting.json.jbuilder @@ -34,7 +34,7 @@ json.invoices do json.id item.object_id json.main item.main end - json.partial! 'archive/vat', price: item.amount, vat_rate: invoice[:vat_rate] + json.partial! 'archive/vat', price: item.amount, vat_rate: invoice[:vat_rate][item.invoice_item_type] end end end diff --git a/app/workers/accounting_export_worker.rb b/app/workers/accounting_export_worker.rb index a0bd28c22..2fe5b217a 100644 --- a/app/workers/accounting_export_worker.rb +++ b/app/workers/accounting_export_worker.rb @@ -10,7 +10,8 @@ class AccountingExportWorker raise SecurityError, 'Not allowed to export' unless export.user.admin? data = JSON.parse(export.query) - service = AccountingExportService.new( + service = export.export_type == 'vat' ? VatExportService : AccountingExportService + service = service.new( data['columns'], encoding: data['encoding'], format: export.extension, separator: export.key ) diff --git a/config/locales/app.admin.en.yml b/config/locales/app.admin.en.yml index a2cac5534..5d1f85fe5 100644 --- a/config/locales/app.admin.en.yml +++ b/config/locales/app.admin.en.yml @@ -369,6 +369,11 @@ en: status_disabled: "Disabled" status_all: "All" spaces_pricing: + prices_match_space_hours_rates_html: "The prices below match one hour of space reservation, without subscription." + prices_calculated_on_hourly_rate_html: "All the prices will be automatically calculated based on the hourly rate defined here.
    For example, if you define an hourly rate at {RATE}: a slot of {DURATION} minutes, will be charged {PRICE}." + you_can_override: "You can override this duration for each availability you create in the agenda. The price will then be adjusted accordingly." + extended_prices: "Moreover, you can define extended prices which will apply in priority over the hourly rate below. Extended prices allow you, for example, to set a favorable price for a booking of several hours." + spaces: "Spaces" price_updated: "Price successfully updated" machines_pricing: prices_match_machine_hours_rates_html: "The prices below match one hour of machine usage, without subscription." @@ -380,10 +385,12 @@ en: packs: "Prepaid packs" no_packs: "No packs for now" pack_DURATION: "{DURATION} hours" - configure_extendedPrices_button: - extendedPrices: "Extended prices" - no_extendedPrices: "No extended price for now" - extended_prices_form: + configure_extended_prices_button: + extended_prices: "Extended prices" + no_extended_prices: "No extended price for now" + extended_price_DURATION: "{DURATION} minutes" + extended_price_form: + duration: "Duration (minutes)" amount: "Price" pack_form: hours: "Hours" @@ -411,21 +418,21 @@ en: edit_pack: "Edit the pack" confirm_changes: "Confirm changes" pack_successfully_updated: "The prepaid pack was successfully updated." - create_extendedPrice: - new_extendedPrice: "New extended price" - new_extendedPrice_info: "Extended prices allows you to define prices based on custom durations, intead on the default hourly rates." - create_extendedPrice: "Create extended price" - extendedPrice_successfully_created: "The new extended price was successfully created." - delete_extendedPrice: - extendedPrice_deleted: "The extended price was successfully deleted." + create_extended_price: + new_extended_price: "New extended price" + new_extended_price_info: "Extended prices allows you to define prices based on custom durations, instead of the default hourly rates." + create_extended_price: "Create extended price" + extended_price_successfully_created: "The new extended price was successfully created." + delete_extended_price: + extended_price_deleted: "The extended price was successfully deleted." unable_to_delete: "Unable to delete the extended price: " - delete_extendedPrice: "Delete the extended price" + delete_extended_price: "Delete the extended price" confirm_delete: "Delete" - delete_confirmation: "Are you sure you want to delete this extended price? This won't be possible if it was already bought by users." - edit_extendedPrice: - edit_extendedPrice: "Edit the extended price" + delete_confirmation: "Are you sure you want to delete this extended price?" + edit_extended_price: + edit_extended_price: "Edit the extended price" confirm_changes: "Confirm changes" - extendedPrice_successfully_updated: "The extended price was successfully updated." + extended_price_successfully_updated: "The extended price was successfully updated." #ajouter un code promotionnel coupons_new: add_a_coupon: "Add a coupon" @@ -488,13 +495,14 @@ en: details: "Details" amount: "Amount" machine_booking-3D_printer: "Machine booking - 3D printer" + training_booking-3D_print: "Training booking - initiation to 3d printing" total_amount: "Total amount" total_including_all_taxes: "Total incl. all taxes" VAT_disabled: "VAT disabled" VAT_enabled: "VAT enabled" - including_VAT: "Including VAT" + including_VAT: "Including VAT {RATE}% of {AMOUNT}" including_total_excluding_taxes: "Including Total excl. taxes" - including_amount_payed_on_ordering: "Including Amount payed on ordering" + including_amount_payed_on_ordering: "Including amount payed on ordering" settlement_by_debit_card_on_DATE_at_TIME_for_an_amount_of_AMOUNT: "Settlement by debit card on {DATE} at {TIME}, for an amount of {AMOUNT}" important_notes: "Important notes" address_and_legal_information: "Address and legal information" @@ -544,6 +552,15 @@ en: enable_VAT: "Enable VAT" VAT_rate: "VAT rate" VAT_history: "VAT rates history" + VAT_notice: "This parameter configures the general case of the VAT rate and applies to everything sold by the Fablab. It is possible to override this parameter by setting a specific VAT rate for each object." + edit_multi_VAT_button: "More options" + multiVAT: "Advanced VAT" + multi_VAT_notice: "Please note: The current general rate is {RATE}%. Here you can define different VAT rates for each category.

    For example, you can override this value, only for machine reservations, by filling in the corresponding field below. If no value is filled in, the general rate will apply." + VAT_rate_machine: "Machine reservation" + VAT_rate_space: "Space reservation" + VAT_rate_training: "Training reservation" + VAT_rate_event: "Event reservation" + VAT_rate_subscription: "Subscription" changed_at: "Changed at" changed_by: "By" deleted_user: "Deleted user" @@ -662,9 +679,10 @@ en: codes_customization_success: "Customization of the accounting codes successfully saved." unexpected_error_occurred: "An unexpected error occurred while saving the codes. Please try again later." export_accounting_data: "Export accounting data" - export_to: "Export to the accounting software" + export_what: "What do you want to export?" + export_VAT: "Export the collected VAT" + export_to_ACD: "Export all data to the accounting software ACD" export_is_running: "Export is running. You'll be notified when it's ready." - acd: "ACD" export_form_date: "Export from" export_to_date: "Export until" format: "File format" @@ -687,6 +705,10 @@ en: debit_euro: "Euro debit" credit_euro: "Euro credit" lettering: "Lettering" + start_date: "Start date" + end_date: "End date" + vat_rate: "VAT rate" + amount: "Total amount" payment: payment_settings: "Payment settings" online_payment: "Online payment" diff --git a/config/locales/en.yml b/config/locales/en.yml index a6e36f3f4..84e6745e2 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -90,7 +90,7 @@ en: other: "%{count} %{NAME} tickets" coupon_CODE_discount_of_DISCOUNT: "Coupon {CODE}: discount of {DISCOUNT}{TYPE, select, percent_off{%} other{}}" #messageFormat interpolation total_including_all_taxes: "Total incl. all taxes" - including_VAT_RATE: "Including VAT %{RATE}%" + including_VAT_RATE: "Including VAT %{RATE}% of %{AMOUNT}" including_total_excluding_taxes: "Including Total excl. taxes" including_amount_payed_on_ordering: "Including amount payed on ordering" total_amount: "Total amount" @@ -147,6 +147,11 @@ en: Event_reservation: "event reserv." Space_reservation: "space reserv." wallet: "wallet" + vat_export: + start_date: "Start date" + end_date: "End date" + vat_rate: "VAT rate" + amount: "Total amount" #training availabilities trainings: i_ve_reserved: "I've reserved" @@ -331,6 +336,7 @@ en: users_reservations: "of the reservations' list" availabilities_index: "of the reservations availabilities" accounting_acd: "of the accounting data to ACD" + accounting_vat: "of the collected VAT" is_over: "is over." download_here: "Download here" notify_admin_import_complete: diff --git a/config/locales/mails.en.yml b/config/locales/mails.en.yml index 2063f8c60..f4d6d3f54 100644 --- a/config/locales/mails.en.yml +++ b/config/locales/mails.en.yml @@ -231,7 +231,8 @@ en: users_subscriptions: "of the subscriptions' list" users_reservations: "of the reservations' list" availabilities_index: "of the reservations availabilities" - accounting_accounting-software: "of the accounting data" + accounting_acd: "of the accounting data to ACD" + accounting_vat: "of the collected VAT data" click_to_download: "Excel file generated successfully. To download it, click" here: "here" file_type: diff --git a/test/test_helper.rb b/test/test_helper.rb index 2f550685b..c6930d66d 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -104,13 +104,15 @@ class ActiveSupport::TestCase end vat_service = VatHistoryService.new - vat_rate = vat_service.invoice_vat(invoice) - if vat_rate.positive? - computed_ht = sprintf('%.2f', (invoice.total / (vat_rate / 100.00 + 1)) / 100.00).to_f + invoice.invoice_items.each do |item| + vat_rate = vat_service.invoice_item_vat(item) + if vat_rate.positive? + computed_ht = sprintf('%.2f', (item.amount_after_coupon / (vat_rate / 100.00 + 1)) / 100.00).to_f - assert_equal computed_ht, ht_amount, 'Total excluding taxes rendered in the PDF file is not computed correctly' - else - assert_equal invoice.total, ht_amount, 'VAT information was rendered in the PDF file despite that VAT was disabled' + assert_equal computed_ht, item.net_amount / 100.00, 'Total excluding taxes rendered in the PDF file is not computed correctly' + else + assert_equal item.amount_after_coupon, item.net_amount, 'VAT information was rendered in the PDF file despite that VAT was disabled' + end end # check the recipient & the address