From f8904dfb9c651b8a079b9ae80d8cd2a5d8c50a58 Mon Sep 17 00:00:00 2001 From: Sylvain Date: Fri, 23 Dec 2022 15:52:10 +0100 Subject: [PATCH] (feat) customize VAT name --- CHANGELOG.md | 1 + app/controllers/api/settings_controller.rb | 4 +- app/frontend/src/javascript/api/setting.ts | 7 +- .../javascript/components/form/form-input.tsx | 6 +- .../invoices/invoices-settings-panel.tsx | 2 +- .../invoices/vat-settings-modal.tsx | 217 ++++++++++++++++++ .../settings/setting-history-modal.tsx | 102 ++++++++ .../javascript/controllers/admin/invoices.js | 197 +++------------- app/frontend/src/javascript/lib/setting.ts | 6 +- app/frontend/src/javascript/models/setting.ts | 8 +- app/frontend/src/javascript/router.js | 6 +- app/frontend/src/stylesheets/application.scss | 3 + .../modules/form/abstract-form-item.scss | 4 + .../modules/invoices/vat-settings-modal.scss | 34 +++ .../settings/setting-history-modal.scss | 5 + .../modules/user/change-password.scss | 7 - .../src/stylesheets/variables/animations.scss | 6 + .../templates/admin/invoices/settings.html | 3 +- .../admin/invoices/settings/editMultiVAT.html | 65 ------ .../admin/invoices/settings/editVAT.html | 58 ----- .../invoices/settings/multiVATHistory.html | 32 --- app/models/setting.rb | 3 +- app/pdfs/pdf/invoice.rb | 5 +- app/services/accounting/vat_export_service.rb | 3 +- app/workers/accounting_worker.rb | 2 +- config/locales/app.admin.en.yml | 29 ++- config/locales/en.yml | 12 +- config/locales/fr.yml | 1 + db/seeds.rb | 2 + test/fixtures/history_values.yml | 9 + test/fixtures/settings.yml | 6 + test/frontend/__fixtures__/settings.ts | 24 +- test/frontend/__setup__/server.js | 9 +- .../invoices/invoices-settings-panel.test.tsx | 7 +- .../invoices/vat-settings-modal.test.tsx | 47 ++++ test/helpers/invoice_helper.rb | 3 + test/integration/invoices/vat_test.rb | 46 ++++ 37 files changed, 626 insertions(+), 355 deletions(-) create mode 100644 app/frontend/src/javascript/components/invoices/vat-settings-modal.tsx create mode 100644 app/frontend/src/javascript/components/settings/setting-history-modal.tsx create mode 100644 app/frontend/src/stylesheets/modules/invoices/vat-settings-modal.scss create mode 100644 app/frontend/src/stylesheets/modules/settings/setting-history-modal.scss create mode 100644 app/frontend/src/stylesheets/variables/animations.scss delete mode 100644 app/frontend/templates/admin/invoices/settings/editMultiVAT.html delete mode 100644 app/frontend/templates/admin/invoices/settings/editVAT.html delete mode 100644 app/frontend/templates/admin/invoices/settings/multiVATHistory.html create mode 100644 test/frontend/components/invoices/vat-settings-modal.test.tsx create mode 100644 test/integration/invoices/vat_test.rb diff --git a/CHANGELOG.md b/CHANGELOG.md index e6e02e84a..7fbeb7e85 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ - Ability to disable generation of invoices at zero - Accounting data is now built each night and saved in database - Ability to define multiple accounting journal codes +- Ability to change the name of the VAT - OpenAPI endpoint to fetch accounting data - Add reservation deadline parameter (#414) - Verify current password at server side when changing password diff --git a/app/controllers/api/settings_controller.rb b/app/controllers/api/settings_controller.rb index 34579c5df..e65d07861 100644 --- a/app/controllers/api/settings_controller.rb +++ b/app/controllers/api/settings_controller.rb @@ -34,7 +34,9 @@ class API::SettingsController < API::ApiController if !SettingService.update_allowed?(db_setting) db_setting.errors.add(:-, "#{I18n.t("settings.#{setting[:name]}")}: #{I18n.t('settings.locked_setting')}") elsif db_setting.save - db_setting.history_values.create(value: setting[:value], invoicing_profile: current_user.invoicing_profile) + unless db_setting.value == setting[:value] + db_setting.history_values.create(value: setting[:value], invoicing_profile: current_user.invoicing_profile) + end end @settings.push db_setting diff --git a/app/frontend/src/javascript/api/setting.ts b/app/frontend/src/javascript/api/setting.ts index c0f8832e7..9ff6cbc33 100644 --- a/app/frontend/src/javascript/api/setting.ts +++ b/app/frontend/src/javascript/api/setting.ts @@ -4,14 +4,15 @@ import { Setting, SettingBulkArray, SettingBulkResult, - SettingError, + SettingError, SettingGetOptions, SettingName, SettingValue } from '../models/setting'; +import ApiLib from '../lib/api'; export default class SettingAPI { - static async get (name: SettingName): Promise { - const res: AxiosResponse<{setting: Setting}> = await apiClient.get(`/api/settings/${name}`); + static async get (name: SettingName, options?: SettingGetOptions): Promise { + const res: AxiosResponse<{setting: Setting}> = await apiClient.get(`/api/settings/${name}${ApiLib.filtersToQuery(options)}`); return res?.data?.setting; } diff --git a/app/frontend/src/javascript/components/form/form-input.tsx b/app/frontend/src/javascript/components/form/form-input.tsx index 6d9ce4f25..9507349bb 100644 --- a/app/frontend/src/javascript/components/form/form-input.tsx +++ b/app/frontend/src/javascript/components/form/form-input.tsx @@ -12,6 +12,7 @@ interface FormInputProps extends FormComponent) => void, addOnClassName?: string, + addOnAriaLabel?: string, debounce?: number, type?: 'text' | 'date' | 'password' | 'url' | 'time' | 'tel' | 'search' | 'number' | 'month' | 'email' | 'datetime-local' | 'week' | 'hidden' | 'file', accept?: string, @@ -26,7 +27,7 @@ interface FormInputProps extends FormComponent({ id, register, label, tooltip, defaultValue, icon, className, rules, disabled, type, addOn, addOnAction, addOnClassName, placeholder, error, warning, formState, step, onChange, debounce, accept, nullable = false, ariaLabel }: FormInputProps) => { +export const FormInput = ({ id, register, label, tooltip, defaultValue, icon, className, rules, disabled, type, addOn, addOnAction, addOnClassName, addOnAriaLabel, placeholder, error, warning, formState, step, onChange, debounce, accept, nullable = false, ariaLabel }: FormInputProps) => { /** * Debounced (ie. temporised) version of the 'on change' callback. */ @@ -71,7 +72,8 @@ export const FormInput = ({ id, re placeholder={placeholder} accept={accept} /> {(type === 'file' && placeholder) && {placeholder}} - {addOn && {addOn}} + {addOn && addOnAction && } + {addOn && !addOnAction && {addOn}} ); }; diff --git a/app/frontend/src/javascript/components/invoices/invoices-settings-panel.tsx b/app/frontend/src/javascript/components/invoices/invoices-settings-panel.tsx index cdc057daf..553583b39 100644 --- a/app/frontend/src/javascript/components/invoices/invoices-settings-panel.tsx +++ b/app/frontend/src/javascript/components/invoices/invoices-settings-panel.tsx @@ -15,7 +15,7 @@ import { react2angular } from 'react2angular'; import FormatLib from '../../lib/format'; import { FormInput } from '../form/form-input'; import { UnsavedFormAlert } from '../form/unsaved-form-alert'; -import { UIRouter } from '@uirouter/angularjs'; +import type { UIRouter } from '@uirouter/angularjs'; declare const Application: IApplication; diff --git a/app/frontend/src/javascript/components/invoices/vat-settings-modal.tsx b/app/frontend/src/javascript/components/invoices/vat-settings-modal.tsx new file mode 100644 index 000000000..8e32908f4 --- /dev/null +++ b/app/frontend/src/javascript/components/invoices/vat-settings-modal.tsx @@ -0,0 +1,217 @@ +import React, { useEffect, useState } from 'react'; +import { FabModal, ModalSize } from '../base/fab-modal'; +import { IApplication } from '../../models/application'; +import { Loader } from '../base/loader'; +import { react2angular } from 'react2angular'; +import SettingAPI from '../../api/setting'; +import { SubmitHandler, useForm, useWatch } from 'react-hook-form'; +import { SettingName, SettingValue } from '../../models/setting'; +import { useTranslation } from 'react-i18next'; +import SettingLib from '../../lib/setting'; +import { FormSwitch } from '../form/form-switch'; +import { FormInput } from '../form/form-input'; +import { FabButton } from '../base/fab-button'; +import { FabAlert } from '../base/fab-alert'; +import { HtmlTranslate } from '../base/html-translate'; +import { SettingHistoryModal } from '../settings/setting-history-modal'; +import { useImmer } from 'use-immer'; +import { enableMapSet } from 'immer'; +import { ClockCounterClockwise } from 'phosphor-react'; + +declare const Application: IApplication; + +const vatSettings: SettingName[] = ['invoice_VAT-rate', 'invoice_VAT-active', 'invoice_VAT-name', 'invoice_VAT-rate_Product', 'invoice_VAT-rate_Event', + 'invoice_VAT-rate_Machine', 'invoice_VAT-rate_Subscription', 'invoice_VAT-rate_Space', 'invoice_VAT-rate_Training']; + +interface VatSettingsModalProps { + isOpen: boolean, + toggleModal: () => void, + onError: (message: string) => void, + onSuccess: (message: string) => void, +} + +enableMapSet(); + +/** + * Modal dialog to configure VAT settings + */ +export const VatSettingsModal: React.FC = ({ isOpen, toggleModal, onError, onSuccess }) => { + const { t } = useTranslation('admin'); + + const { handleSubmit, reset, control, register } = useForm>(); + const isActive = useWatch({ control, name: 'invoice_VAT-active' }); + const generalRate = useWatch({ control, name: 'invoice_VAT-rate' }); + + const [modalWidth, setModalWidth] = useState(ModalSize.small); + const [advancedLabel, setAdvancedLabel] = useState(t('app.admin.vat_settings_modal.advanced')); + const [histories, setHistories] = useImmer>(new Map()); + + useEffect(() => { + SettingAPI.query(vatSettings) + .then(settings => { + const data = SettingLib.bulkMapToObject(settings); + reset(data); + }) + .catch(onError); + }, [isOpen]); + + /** + * Callback triggered when the form is submitted: save the settings + */ + const onSubmit: SubmitHandler> = (data) => { + SettingAPI.bulkUpdate(SettingLib.objectToBulkMap(data, { stripNaN: true })).then(() => { + onSuccess(t('app.admin.vat_settings_modal.update_success')); + toggleModal(); + }, reason => { + onError(reason); + }); + }; + + /** + * Show the panel allowing to configure a rate per resource type + */ + const toggleAdvancedRates = () => { + if (modalWidth === ModalSize.small) { + setModalWidth(ModalSize.large); + setAdvancedLabel(t('app.admin.vat_settings_modal.hide_advanced')); + } else { + setModalWidth(ModalSize.small); + setAdvancedLabel(t('app.admin.vat_settings_modal.advanced')); + } + }; + + /** + * Open/closes the modal dialog showing the changes history for the given paramater name + */ + const toggleHistoryModal = (name: SettingName) => { + return () => { + setHistories(draft => { + draft.set(name, !draft.get(name)); + }); + }; + }; + + return ( + +
+
+
+ + {isActive && <> + + } + addOnAriaLabel={t('app.admin.vat_settings_modal.show_history')} + addOnAction={toggleHistoryModal('invoice_VAT-rate')} /> + + } + {modalWidth === ModalSize.large && + + } +
+ {modalWidth === ModalSize.large &&
+ } + addOnAriaLabel={t('app.admin.vat_settings_modal.show_history')} + addOnAction={toggleHistoryModal('invoice_VAT-rate_Product')} /> + + } + addOnAriaLabel={t('app.admin.vat_settings_modal.show_history')} + addOnAction={toggleHistoryModal('invoice_VAT-rate_Event')} /> + + } + addOnAriaLabel={t('app.admin.vat_settings_modal.show_history')} + addOnAction={toggleHistoryModal('invoice_VAT-rate_Machine')} /> + + } + addOnAriaLabel={t('app.admin.vat_settings_modal.show_history')} + addOnAction={toggleHistoryModal('invoice_VAT-rate_Subscription')} /> + + } + addOnAriaLabel={t('app.admin.vat_settings_modal.show_history')} + addOnAction={toggleHistoryModal('invoice_VAT-rate_Space')} /> + + } + addOnAriaLabel={t('app.admin.vat_settings_modal.show_history')} + addOnAction={toggleHistoryModal('invoice_VAT-rate_Training')} /> + +
} +
+
+ {isActive && {advancedLabel}} + {t('app.admin.vat_settings_modal.save')} +
+
+
+ ); +}; + +const VatSettingsModalWrapper: React.FC = (props) => { + return ( + + + + ); +}; + +Application.Components.component('vatSettingsModal', react2angular(VatSettingsModalWrapper, ['isOpen', 'toggleModal', 'onError', 'onSuccess'])); diff --git a/app/frontend/src/javascript/components/settings/setting-history-modal.tsx b/app/frontend/src/javascript/components/settings/setting-history-modal.tsx new file mode 100644 index 000000000..51b200f6d --- /dev/null +++ b/app/frontend/src/javascript/components/settings/setting-history-modal.tsx @@ -0,0 +1,102 @@ +import type { Setting, SettingName } from '../../models/setting'; +import React, { useEffect, useState } from 'react'; +import { sortBy as _sortBy } from 'lodash'; +import SettingAPI from '../../api/setting'; +import { FabModal, ModalSize } from '../base/fab-modal'; +import FormatLib from '../../lib/format'; +import { useTranslation } from 'react-i18next'; +import { useImmer } from 'use-immer'; +import { HistoryValue } from '../../models/history-value'; + +interface CommonProps { + isOpen: boolean, + toggleModal: () => void, + onError: (error: string) => void, +} + +type SettingsProps = + { setting: SettingName, settings?: never } | + { setting?: never, settings: Array } + +type SettingHistoryModalProps = CommonProps & SettingsProps; + +/** + * Shows the history of the changes for the provided setting. + * Support for a cross history of several settings. + */ +export const SettingHistoryModal: React.FC = ({ isOpen, toggleModal, setting, settings, onError }) => { + const { t } = useTranslation('admin'); + + const [settingData, setSettingData] = useImmer>(new Map()); + const [history, setHistory] = useState>([]); + + useEffect(() => { + if (isOpen) { + settings?.forEach((setting) => { + SettingAPI.get(setting, { history: true }).then(res => { + setSettingData(draft => { + draft.set(setting, res); + }); + }).catch(onError); + }); + if (setting) { + SettingAPI.get(setting, { history: true }).then(res => { + setSettingData(draft => { + draft.set(setting, res); + }); + }).catch(onError); + } + } + }, [isOpen]); + + useEffect(() => { + setHistory(buildHistory()); + }, [settingData]); + + /** + * Build the cross history for all the given settings + */ + const buildHistory = () => { + let history = []; + for (const stng of settingData.keys()) { + history = _sortBy(history.concat(settingData.get(stng as SettingName)?.history?.map(hv => { + return { + ...hv, + setting: stng + }; + })), 'created_at'); + } + return history; + }; + + return ( + + {history.length === 0 &&
+ {t('app.admin.setting_history_modal.no_history')} +
} + {history.length > 0 && + + + + + + + + + + {history.map(hv => + + + + + )} + +
{t('app.admin.setting_history_modal.setting')}{t('app.admin.setting_history_modal.value')}{t('app.admin.setting_history_modal.date')}{t('app.admin.setting_history_modal.operator')}
{settingData.get(hv.setting).localized}{hv.value}{FormatLib.date(hv.created_at)}{hv.user.name}
} +
+ ); +}; diff --git a/app/frontend/src/javascript/controllers/admin/invoices.js b/app/frontend/src/javascript/controllers/admin/invoices.js index 20e8942e0..db87c99fc 100644 --- a/app/frontend/src/javascript/controllers/admin/invoices.js +++ b/app/frontend/src/javascript/controllers/admin/invoices.js @@ -53,6 +53,8 @@ Application.Controllers.controller('InvoicesController', ['$scope', '$state', 'I // Default invoices ordering/sorting $scope.orderInvoice = '-date'; + $scope.isOpenVatModal = false; + // Invoices parameters $scope.invoice = { logo: null, @@ -73,18 +75,9 @@ Application.Controllers.controller('InvoicesController', ['$scope', '$state', 'I }, VAT: { rate: 19.6, - active: false, - templateUrl: '/admin/invoices/settings/editVAT.html' - }, - multiVAT: { + name: 'VAT', rateMachine: '', - rateSpace: '', - rateTraining: '', - rateEvent: '', - rateSubscription: '', - rateProduct: '', - editTemplateUrl: '/admin/invoices/settings/editMultiVAT.html', - historyTemplateUrl: '/admin/invoices/settings/multiVATHistory.html' + active: false }, text: { content: '' @@ -118,12 +111,36 @@ Application.Controllers.controller('InvoicesController', ['$scope', '$state', 'I // the following item is used by the UnsavedFormAlert component to detect a page change $scope.uiRouter = $uiRouter; + /** + * This callback triggers the opening/closing of the VAT configuration modal + */ + $scope.toggleVatModal = function () { + setTimeout(() => { + $scope.isOpenVatModal = !$scope.isOpenVatModal; + $scope.$apply(); + }, 50); + }; + + /** + * Callback triggered in case of error + */ + $scope.onError = (message) => { + growl.error(message); + }; + + /** + * Callback triggered in case of success + */ + $scope.onSuccess = (message) => { + growl.success(message); + }; + /** * Return the VAT rate applicable to the machine reservations * @return {number} */ $scope.getMachineExampleRate = function () { - return $scope.invoice.multiVAT.rateMachine || $scope.invoice.VAT.rate; + return $scope.invoice.VAT.rateMachine || $scope.invoice.VAT.rate; }; /** @@ -344,153 +361,7 @@ Application.Controllers.controller('InvoicesController', ['$scope', '$state', 'I * The VAT can be disabled and its rate can be configured */ $scope.openEditVAT = function () { - const modalInstance = $uibModal.open({ - animation: true, - templateUrl: $scope.invoice.VAT.templateUrl, - size: 'lg', - resolve: { - rate () { - return $scope.invoice.VAT.rate; - }, - active () { - return $scope.invoice.VAT.active; - }, - multiVAT () { - return $scope.invoice.multiVAT; - }, - rateHistory () { - return Setting.get({ name: 'invoice_VAT-rate', history: true }).$promise; - }, - activeHistory () { - return Setting.get({ name: 'invoice_VAT-active', history: true }).$promise; - } - }, - controller: ['$scope', '$uibModalInstance', 'rate', 'active', 'rateHistory', 'activeHistory', 'multiVAT', function ($scope, $uibModalInstance, rate, active, rateHistory, activeHistory, multiVAT) { - $scope.rate = rate; - $scope.isSelected = active; - $scope.history = []; - - // callback on "enable VAT" switch toggle - $scope.enableVATChanged = function (checked) { - setTimeout(() => { - $scope.isSelected = checked; - $scope.$apply(); - }, 1); - }; - $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'); }; - - $scope.rateValue = function (value) { - if (value.rate === 'null' || value.value === 'undefined' || value.rate === 'NaN') { - return ''; - } - return value.rate; - }; - - 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', 'Product'].forEach(rateType => { - const value = _.isFinite(result.multiVAT[`rate${rateType}`]) ? result.multiVAT[`rate${rateType}`] + '' : ''; - Setting.update({ name: `invoice_VAT-rate_${rateType}` }, { value }, 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) { - $scope.history.push({ date: rate.created_at, rate: rate.value, user: rate.user }); - }); - activeHistory.setting.history.forEach(function (v) { - $scope.history.push({ date: v.created_at, enabled: v.value === 'true', user: v.user }); - }); - }; - - initialize(); - }] - }); - - return modalInstance.result.then(function (result) { - Setting.update({ name: 'invoice_VAT-rate' }, { value: result.rate + '' }, function (data) { - $scope.invoice.VAT.rate = result.rate; - if (result.active) { - 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); - }); - - return Setting.update({ name: 'invoice_VAT-active' }, { value: result.active ? 'true' : 'false' }, function (data) { - $scope.invoice.VAT.active = result.active; - if (result.active) { - return growl.success(_t('app.admin.invoices.VAT_successfully_activated')); - } else { - return growl.success(_t('app.admin.invoices.VAT_successfully_disabled')); - } - } - , function (error) { - if (error.status === 304) return; - - growl.error(_t('app.admin.invoices.an_error_occurred_while_activating_the_VAT')); - console.error(error); - }); - }); + $scope.toggleVatModal(); }; /** @@ -855,12 +726,8 @@ 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.multiVAT.rateProduct = settings['invoice_VAT-rate_Product'] ? parseFloat(settings['invoice_VAT-rate_Product']) : ''; + $scope.invoice.VAT.name = settings['invoice_VAT-name']; + $scope.invoice.VAT.rateMachine = settings['invoice_VAT-rate_Machine'] ? parseFloat(settings['invoice_VAT-rate_Machine']) : ''; $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'); diff --git a/app/frontend/src/javascript/lib/setting.ts b/app/frontend/src/javascript/lib/setting.ts index 327112c9a..dc066db22 100644 --- a/app/frontend/src/javascript/lib/setting.ts +++ b/app/frontend/src/javascript/lib/setting.ts @@ -5,10 +5,12 @@ export default class SettingLib { /** * Convert the provided data to a map, as expected by BulkUpdate */ - static objectToBulkMap = (data: Record): Map => { + static objectToBulkMap = (data: Record, options?: { stripNaN: boolean }): Map => { const res = new Map(); for (const key in data) { - res.set(key as SettingName, `${data[key]}`); + if (!options?.stripNaN || !Number.isNaN(data[key])) { + res.set(key as SettingName, `${data[key]}`); + } } return res; }; diff --git a/app/frontend/src/javascript/models/setting.ts b/app/frontend/src/javascript/models/setting.ts index 438c74550..af89e0aca 100644 --- a/app/frontend/src/javascript/models/setting.ts +++ b/app/frontend/src/javascript/models/setting.ts @@ -1,5 +1,6 @@ import { HistoryValue } from './history-value'; import { TDateISO } from '../typings/date-iso'; +import { ApiFilter } from './api'; export const homePageSettings = [ 'twitter_name', @@ -65,7 +66,8 @@ export const invoicesSettings = [ 'invoice_legals', 'invoice_prefix', 'payment_schedule_prefix', - 'prevent_invoices_zero' + 'prevent_invoices_zero', + 'invoice_VAT-name' ] as const; export const bookingSettings = [ @@ -284,4 +286,8 @@ export interface SettingBulkResult { localized?: string, } +export interface SettingGetOptions extends ApiFilter { + history?: boolean +} + export type SettingBulkArray = Array<{ name: SettingName, value: SettingValue }>; diff --git a/app/frontend/src/javascript/router.js b/app/frontend/src/javascript/router.js index 57efd6b65..5315827d1 100644 --- a/app/frontend/src/javascript/router.js +++ b/app/frontend/src/javascript/router.js @@ -950,10 +950,10 @@ angular.module('application.router', ['ui.router']) resolve: { settings: ['Setting', function (Setting) { return Setting.query({ - 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-rate_Product', 'invoice_VAT-active', 'invoice_order-nb', 'invoice_code-value', " + + names: "['invoice_legals', 'invoice_text', 'invoice_VAT-rate', 'invoice_VAT-rate_Machine', " + + "'invoice_VAT-active', 'invoice_order-nb', 'invoice_code-value', " + "'invoice_code-active', 'invoice_reference', 'invoice_logo', 'payment_gateway', 'payment_schedule_prefix', 'invoicing_module', " + - "'feature_tour_display', 'online_payment_module', 'stripe_public_key', 'stripe_currency', 'invoice_prefix']" + "'feature_tour_display', 'online_payment_module', 'stripe_public_key', 'stripe_currency', 'invoice_prefix', 'invoice_VAT-name']" }).$promise; }], stripeSecretKey: ['Setting', function (Setting) { return Setting.isPresent({ name: 'stripe_secret_key' }).$promise; }], diff --git a/app/frontend/src/stylesheets/application.scss b/app/frontend/src/stylesheets/application.scss index ec9adbfc2..f36b5d9d9 100644 --- a/app/frontend/src/stylesheets/application.scss +++ b/app/frontend/src/stylesheets/application.scss @@ -1,3 +1,4 @@ +@import "variables/animations"; @import "variables/colors"; @import "variables/typography"; @import "variables/decoration"; @@ -55,6 +56,7 @@ @import "modules/form/form-image-upload"; @import "modules/group/change-group"; @import "modules/invoices/invoices-settings-panel"; +@import "modules/invoices/vat-settings-modal"; @import "modules/layout/header-page"; @import "modules/machines/machine-card"; @import "modules/machines/machine-form"; @@ -102,6 +104,7 @@ @import "modules/select-gateway-modal"; @import "modules/settings/boolean-setting"; @import "modules/settings/check-list-setting"; +@import "modules/settings/setting-history-modal"; @import "modules/settings/user-validation-setting"; @import "modules/socials/fab-socials"; @import "modules/spaces/space-form"; diff --git a/app/frontend/src/stylesheets/modules/form/abstract-form-item.scss b/app/frontend/src/stylesheets/modules/form/abstract-form-item.scss index 775fb4a2f..7f7d33d89 100644 --- a/app/frontend/src/stylesheets/modules/form/abstract-form-item.scss +++ b/app/frontend/src/stylesheets/modules/form/abstract-form-item.scss @@ -71,6 +71,10 @@ } } + button.addon { + border: 0; + } + & > input { grid-area: field; border: none; diff --git a/app/frontend/src/stylesheets/modules/invoices/vat-settings-modal.scss b/app/frontend/src/stylesheets/modules/invoices/vat-settings-modal.scss new file mode 100644 index 000000000..d39e3503a --- /dev/null +++ b/app/frontend/src/stylesheets/modules/invoices/vat-settings-modal.scss @@ -0,0 +1,34 @@ +.vat-settings-modal { + .panes { + display: flex; + flex-direction: row; + justify-content: flex-start; + + &-one > .pane { + width: 100%; + } + &-both { + & > .pane { width: 50%; } + & > .pane:first-child { margin-right: 0.5em; } + & > .pane:last-child { + margin-left: 0.5em; + animation: show 200ms linear forwards; + opacity: 0; + transform: translateX(-20%); + transform-origin: left; + } + } + } + .actions { + display: flex; + justify-content: space-between; + .save-btn { + background-color: var(--main); + color: var(--main-text-color); + + &:hover { + background-color: var(--main-light); + } + } + } +} diff --git a/app/frontend/src/stylesheets/modules/settings/setting-history-modal.scss b/app/frontend/src/stylesheets/modules/settings/setting-history-modal.scss new file mode 100644 index 000000000..03b780828 --- /dev/null +++ b/app/frontend/src/stylesheets/modules/settings/setting-history-modal.scss @@ -0,0 +1,5 @@ +.setting-history-modal { + table { + width: 100%; + } +} diff --git a/app/frontend/src/stylesheets/modules/user/change-password.scss b/app/frontend/src/stylesheets/modules/user/change-password.scss index a17e83f56..7c106d197 100644 --- a/app/frontend/src/stylesheets/modules/user/change-password.scss +++ b/app/frontend/src/stylesheets/modules/user/change-password.scss @@ -6,10 +6,3 @@ transform-origin: top center; } } - -@keyframes show { - 100% { - opacity: 1; - transform: none; - } -} diff --git a/app/frontend/src/stylesheets/variables/animations.scss b/app/frontend/src/stylesheets/variables/animations.scss new file mode 100644 index 000000000..ab0d52478 --- /dev/null +++ b/app/frontend/src/stylesheets/variables/animations.scss @@ -0,0 +1,6 @@ +@keyframes show { + 100% { + opacity: 1; + transform: none; + } +} diff --git a/app/frontend/templates/admin/invoices/settings.html b/app/frontend/templates/admin/invoices/settings.html index 37d1a4718..c28d8c68c 100644 --- a/app/frontend/templates/admin/invoices/settings.html +++ b/app/frontend/templates/admin/invoices/settings.html @@ -60,7 +60,7 @@ - {{ 'app.admin.invoices.including_VAT' }} + {{ 'app.admin.invoices.including_VAT' }} {{30-(30/(getMachineExampleRate()/100+1)) | currency}} @@ -93,6 +93,7 @@ ng-blur="legalsEditEnd($event)"> + diff --git a/app/frontend/templates/admin/invoices/settings/editMultiVAT.html b/app/frontend/templates/admin/invoices/settings/editMultiVAT.html deleted file mode 100644 index a22690209..000000000 --- a/app/frontend/templates/admin/invoices/settings/editMultiVAT.html +++ /dev/null @@ -1,65 +0,0 @@ -
- - - -
diff --git a/app/frontend/templates/admin/invoices/settings/editVAT.html b/app/frontend/templates/admin/invoices/settings/editVAT.html deleted file mode 100644 index e6f253268..000000000 --- a/app/frontend/templates/admin/invoices/settings/editVAT.html +++ /dev/null @@ -1,58 +0,0 @@ -
- - - -
diff --git a/app/frontend/templates/admin/invoices/settings/multiVATHistory.html b/app/frontend/templates/admin/invoices/settings/multiVATHistory.html deleted file mode 100644 index c1b669438..000000000 --- a/app/frontend/templates/admin/invoices/settings/multiVATHistory.html +++ /dev/null @@ -1,32 +0,0 @@ -
- - - -
diff --git a/app/models/setting.rb b/app/models/setting.rb index d3825195e..a2e0f651d 100644 --- a/app/models/setting.rb +++ b/app/models/setting.rb @@ -164,7 +164,8 @@ class Setting < ApplicationRecord store_hidden advanced_accounting external_id - prevent_invoices_zero] } + prevent_invoices_zero + invoice_VAT-name] } # WARNING: when adding a new key, you may also want to add it in: # - config/locales/en.yml#settings # - app/frontend/src/javascript/models/setting.ts#SettingName diff --git a/app/pdfs/pdf/invoice.rb b/app/pdfs/pdf/invoice.rb index d89508368..603ccc022 100644 --- a/app/pdfs/pdf/invoice.rb +++ b/app/pdfs/pdf/invoice.rb @@ -171,7 +171,10 @@ class PDF::Invoice < Prawn::Document else data += [[I18n.t('invoices.total_including_all_taxes'), number_to_currency(total)]] 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)), + data += [[I18n.t('invoices.including_VAT_RATE', + RATE: rate[:vat_rate], + AMOUNT: number_to_currency(rate[:amount] / 100.00), + NAME: Setting.get('invoice_VAT-name')), number_to_currency(rate[:total_vat] / 100.00)]] end data += [[I18n.t('invoices.including_total_excluding_taxes'), number_to_currency(total_ht / 100.00)]] diff --git a/app/services/accounting/vat_export_service.rb b/app/services/accounting/vat_export_service.rb index 6fd05df70..ee8054d36 100644 --- a/app/services/accounting/vat_export_service.rb +++ b/app/services/accounting/vat_export_service.rb @@ -16,6 +16,7 @@ class Accounting::VatExportService @decimal_separator = '.' @date_format = '%Y-%m-%d' @columns = columns + @vat_name = Setting.get('invoice_VAT-name') end def set_options(decimal_separator: ',', date_format: '%d/%m/%Y', label_max_length: nil, export_zeros: nil) @@ -42,7 +43,7 @@ class Accounting::VatExportService def header_row row = '' columns.each do |column| - row << I18n.t("vat_export.#{column}") << separator + row << I18n.t("vat_export.#{column}", NAME: @vat_name) << separator end "#{row}\n" end diff --git a/app/workers/accounting_worker.rb b/app/workers/accounting_worker.rb index 3632acca4..9c7983cec 100644 --- a/app/workers/accounting_worker.rb +++ b/app/workers/accounting_worker.rb @@ -15,7 +15,7 @@ class AccountingWorker def invoices(invoices_ids) # clean - AccountingLine.where(invoice_id: ids).delete_all + AccountingLine.where(invoice_id: invoices_ids).delete_all # build service = Accounting::AccountingService.new invoices = Invoice.where(id: invoices_ids) diff --git a/config/locales/app.admin.en.yml b/config/locales/app.admin.en.yml index 104cc5821..c01960007 100644 --- a/config/locales/app.admin.en.yml +++ b/config/locales/app.admin.en.yml @@ -692,7 +692,7 @@ en: total_including_all_taxes: "Total incl. all taxes" VAT_disabled: "VAT disabled" VAT_enabled: "VAT enabled" - including_VAT: "Including VAT {RATE}% of {AMOUNT}" + including_VAT: "Including {NAME} {RATE}% of {AMOUNT}" including_total_excluding_taxes: "Including Total excl. taxes" 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}" @@ -1494,7 +1494,6 @@ en: default_value_is_24_hours: "If the field is leaved empty: 24 hours." visibility_yearly: "maximum visibility for annual subscribers" visibility_others: "maximum visibility for other members" - reservation_deadline: "reservation deadline" display: "Display" display_name_info_html: "When enabled, connected members browsing the calendar or booking a resource will see the name of the members who has already booked some slots. When disabled, only administrators and managers will view the names.
Warning: if you enable this feature, please write it down in your privacy policy." display_reservation_user_name: "Display the full name of the user(s) who booked a slots" @@ -2241,3 +2240,29 @@ en: example: "Example" save: "Save" update_success: "The settings were successfully updated" + vat_settings_modal: + title: "VAT settings" + update_success: "The VAT settings were successfully updated" + enable_VAT: "Enable VAT" + VAT_name: "VAT name" + VAT_name_help: "Some countries or regions may require that the VAT is named according to their specific local regulation" + VAT_rate: "VAT rate" + VAT_rate_help: "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." + advanced: "More rates" + hide_advanced: "Less rates" + show_history: "Show the changes history" + VAT_rate_machine: "Machine reservation" + VAT_rate_space: "Space reservation" + VAT_rate_training: "Training reservation" + VAT_rate_event: "Event reservation" + VAT_rate_subscription: "Subscription" + VAT_rate_product: "Products (store)" + multi_VAT_notice: "Please note: The current general rate is {RATE}%. 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 beside. If you don't fill any value, the general rate will apply." + save: "Save" + setting_history_modal: + title: "Changes history" + no_history: "No changes for now." + setting: "Setting" + value: "Value" + date: "Changed at" + operator: "By" diff --git a/config/locales/en.yml b/config/locales/en.yml index c65b6393b..08624202b 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -106,7 +106,8 @@ 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}% of %{AMOUNT}" + VAT: "VAT" + including_VAT_RATE: "Including %{NAME} %{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" @@ -169,7 +170,7 @@ en: vat_export: start_date: "Start date" end_date: "End date" - vat_rate: "VAT rate" + vat_rate: "%{NAME} rate" amount: "Total amount" #training availabilities trainings: @@ -507,6 +508,12 @@ en: invoice_order-nb: "Invoice's order number" invoice_VAT-active: "Activation of the VAT" invoice_VAT-rate: "VAT rate" + invoice_VAT-rate_Product: "VAT rate for shop's product sales" + invoice_VAT-rate_Event: "VAT rate for event reservations" + invoice_VAT-rate_Machine: "VAT rate for machine reservations" + invoice_VAT-rate_Subscription: "VAT rate for subscriptions" + invoice_VAT-rate_Space: "VAT rate for space reservations" + invoice_VAT-rate_Training: "VAT rate for traning reservations" invoice_text: "Invoices' text" invoice_legals: "Invoices' legal information" booking_window_start: "Opening time" @@ -635,3 +642,4 @@ en: advanced_accounting: "Advanced accounting" external_id: "external identifier" prevent_invoices_zero: "prevent building invoices at 0" + invoice_VAT-name: "VAT name" diff --git a/config/locales/fr.yml b/config/locales/fr.yml index 7f91cdfc0..c3847ac27 100644 --- a/config/locales/fr.yml +++ b/config/locales/fr.yml @@ -106,6 +106,7 @@ fr: other: "%{count} places %{NAME}" coupon_CODE_discount_of_DISCOUNT: "Code {CODE} : remise de {DISCOUNT} {TYPE, select, percent_off{%} other{}}" #messageFormat interpolation total_including_all_taxes: "Total TTC" + VAT: "TVA" including_VAT_RATE: "Dont TVA %{RATE} % de %{AMOUNT}" including_total_excluding_taxes: "Dont total HT" including_amount_payed_on_ordering: "Dont montant payé à la commande" diff --git a/db/seeds.rb b/db/seeds.rb index f30432d3b..68fd6b45c 100644 --- a/db/seeds.rb +++ b/db/seeds.rb @@ -981,6 +981,8 @@ Setting.set('public_registrations', true) unless Setting.find_by(name: 'public_r Setting.set('user_change_group', true) unless Setting.find_by(name: 'user_change_group').try(:value) +Setting.set('invoice_VAT-name', I18n.t('invoices.VAT')) unless Setting.find_by(name: 'invoice_VAT-name').try(:value) + unless Setting.find_by(name: 'overlapping_categories').try(:value) Setting.set('overlapping_categories', 'training_reservations,machine_reservations,space_reservations,events_reservations') end diff --git a/test/fixtures/history_values.yml b/test/fixtures/history_values.yml index 7e53319f3..5fcfbed02 100644 --- a/test/fixtures/history_values.yml +++ b/test/fixtures/history_values.yml @@ -923,3 +923,12 @@ history_value_97: updated_at: 2022-11-29 21:02:47.354751000 Z footprint: invoicing_profile_id: 1 + +history_value_98: + id: 98 + setting_id: 97 + value: 'TVA' + created_at: 2022-12-23 14:39:12.214510000 Z + updated_at: 2022-12-23 14:39:12.214510000 Z + footprint: + invoicing_profile_id: 1 diff --git a/test/fixtures/settings.yml b/test/fixtures/settings.yml index f4545c9e2..4e157a3b0 100644 --- a/test/fixtures/settings.yml +++ b/test/fixtures/settings.yml @@ -568,3 +568,9 @@ setting_96: name: reservation_deadline created_at: 2022-11-29 21:02:47.354751000 Z updated_at: 2022-11-29 21:02:47.354751000 Z + +setting_97: + id: 97 + name: invoice_VAT-name + created_at: 2022-12-23 14:39:12.214510000 Z + updated_at: 2022-12-23 14:39:12.214510000 Z diff --git a/test/frontend/__fixtures__/settings.ts b/test/frontend/__fixtures__/settings.ts index 819d39168..bfe6a1e64 100644 --- a/test/frontend/__fixtures__/settings.ts +++ b/test/frontend/__fixtures__/settings.ts @@ -1,4 +1,7 @@ -import { Setting } from '../../../app/frontend/src/javascript/models/setting'; +import type { Setting } from '../../../app/frontend/src/javascript/models/setting'; +import { admins } from './users'; +import { sample as _sample } from 'lodash'; +import type { HistoryValue } from '../../../app/frontend/src/javascript/models/history-value'; export const settings: Array = [ { @@ -738,5 +741,24 @@ export const settings: Array = [ value: '0', last_update: '2022-11-29T21:02:47-0300', localized: "Empêcher la réservation avant qu'elle ne commence" + }, + { + name: 'invoice_VAT-name', + value: 'TVA', + last_update: '2022-12-23T14:39:12+0100', + localized: 'Nom de la TVA' } ]; + +export const buildHistoryItem = (setting: Setting): HistoryValue => { + const user = _sample(admins); + return { + id: Math.ceil(Math.random() * 1000), + value: setting.value, + user: { + id: user.id, + name: user.name + }, + created_at: setting.last_update + }; +}; diff --git a/test/frontend/__setup__/server.js b/test/frontend/__setup__/server.js index af6cfbc7b..68fde56b7 100644 --- a/test/frontend/__setup__/server.js +++ b/test/frontend/__setup__/server.js @@ -4,7 +4,7 @@ import groups from '../__fixtures__/groups'; import plans from '../__fixtures__/plans'; import planCategories from '../__fixtures__/plan_categories'; import { partners, managers, users } from '../__fixtures__/users'; -import { settings } from '../__fixtures__/settings'; +import { buildHistoryItem, settings } from '../__fixtures__/settings'; import products from '../__fixtures__/products'; import productCategories from '../__fixtures__/product_categories'; import productStockMovements from '../__fixtures__/product_stock_movements'; @@ -48,7 +48,12 @@ export const server = setupServer( }), rest.get('/api/settings/:name', (req, res, ctx) => { const setting = settings.find(s => s.name === req.params.name); - return res(ctx.json({ setting })); + const history = new URLSearchParams(req.url.search).get('history'); + const result = { setting }; + if (history) { + result.setting.history = [buildHistoryItem(setting)]; + } + return res(ctx.json(result)); }), rest.get('/api/settings', (req, res, ctx) => { const names = new URLSearchParams(req.url.search).get('names'); diff --git a/test/frontend/components/invoices/invoices-settings-panel.test.tsx b/test/frontend/components/invoices/invoices-settings-panel.test.tsx index 4e5b57564..4959dce0e 100644 --- a/test/frontend/components/invoices/invoices-settings-panel.test.tsx +++ b/test/frontend/components/invoices/invoices-settings-panel.test.tsx @@ -1,13 +1,14 @@ import { InvoicesSettingsPanel } from '../../../../app/frontend/src/javascript/components/invoices/invoices-settings-panel'; import { render, fireEvent, waitFor, screen } from '@testing-library/react'; import '@testing-library/jest-dom'; +import { uiRouter } from '../../__lib__/ui-router'; describe('InvoicesSettingsPanel', () => { const onError = jest.fn(); const onSuccess = jest.fn(); test('render InvoicesSettingsPanel', async () => { - render(); + render(); await waitFor(() => { expect(screen.getByLabelText(/app.admin.invoices_settings_panel.disable_invoices_zero_label/)).toBeInTheDocument(); }); @@ -18,7 +19,7 @@ describe('InvoicesSettingsPanel', () => { }); test('update filename example', async () => { - render(); + render(); await waitFor(() => { expect(screen.getAllByLabelText(/app.admin.invoices_settings_panel.prefix/)).toHaveLength(2); }); @@ -28,7 +29,7 @@ describe('InvoicesSettingsPanel', () => { }); test('update schedule filename example', async () => { - render(); + render(); await waitFor(() => { expect(screen.getAllByLabelText(/app.admin.invoices_settings_panel.prefix/)).toHaveLength(2); }); diff --git a/test/frontend/components/invoices/vat-settings-modal.test.tsx b/test/frontend/components/invoices/vat-settings-modal.test.tsx new file mode 100644 index 000000000..b9d7e869b --- /dev/null +++ b/test/frontend/components/invoices/vat-settings-modal.test.tsx @@ -0,0 +1,47 @@ +import { fireEvent, render, screen, waitFor } from '@testing-library/react'; +import '@testing-library/jest-dom'; +import { VatSettingsModal } from '../../../../app/frontend/src/javascript/components/invoices/vat-settings-modal'; + +describe('VatSettingsModal', () => { + const onError = jest.fn(); + const onSuccess = jest.fn(); + const toggleModal = jest.fn(); + + test('render VatSettingsModal', async () => { + render(); + await waitFor(() => { + expect(screen.getByLabelText(/app.admin.vat_settings_modal.enable_VAT/)).toBeChecked(); + expect(screen.getByLabelText(/app.admin.vat_settings_modal.VAT_name/)).toHaveValue('TVA'); + expect(screen.getByLabelText(/app.admin.vat_settings_modal.VAT_rate/)).toHaveValue(20); + }); + // the following buttons must be selected with hidden:true because of an issue in react-modal in conjunction with react2angular; + // this will be fixed when the full migration to react is over. + expect(screen.getByRole('button', { name: /app.admin.vat_settings_modal.advanced/, hidden: true })).toBeInTheDocument(); + expect(screen.getByRole('button', { name: /app.admin.vat_settings_modal.save/, hidden: true })).toBeInTheDocument(); + }); + + test('show advanced rates', async () => { + render(); + await waitFor(() => { + expect(screen.getByLabelText(/app.admin.vat_settings_modal.enable_VAT/)).toBeChecked(); + }); + fireEvent.click(screen.getByRole('button', { name: /app.admin.vat_settings_modal.advanced/, hidden: true })); + expect(screen.getByLabelText(/app.admin.vat_settings_modal.VAT_rate_product/)).toBeInTheDocument(); + expect(screen.getByLabelText(/app.admin.vat_settings_modal.VAT_rate_event/)).toBeInTheDocument(); + expect(screen.getByLabelText(/app.admin.vat_settings_modal.VAT_rate_machine/)).toBeInTheDocument(); + expect(screen.getByLabelText(/app.admin.vat_settings_modal.VAT_rate_subscription/)).toBeInTheDocument(); + expect(screen.getByLabelText(/app.admin.vat_settings_modal.VAT_rate_space/)).toBeInTheDocument(); + expect(screen.getByLabelText(/app.admin.vat_settings_modal.VAT_rate_training/)).toBeInTheDocument(); + }); + + test('show history', async () => { + render(); + await waitFor(() => { + expect(screen.getByLabelText(/app.admin.vat_settings_modal.enable_VAT/)).toBeChecked(); + }); + fireEvent.click(screen.getByRole('button', { name: /app.admin.vat_settings_modal.show_history/, hidden: true })); + await waitFor(() => { + expect(screen.getByRole('heading', { name: /app.admin.setting_history_modal.title/, hidden: true })).toBeInTheDocument(); + }); + }); +}); diff --git a/test/helpers/invoice_helper.rb b/test/helpers/invoice_helper.rb index 829f52c31..ce7198d6c 100644 --- a/test/helpers/invoice_helper.rb +++ b/test/helpers/invoice_helper.rb @@ -5,6 +5,7 @@ module InvoiceHelper # Force the invoice generation worker to run NOW and check the resulting file generated. # Delete the file afterwards. # @param invoice {Invoice} + # @param &block an optional block may be provided for additional specific assertions on the invoices PDF lines def assert_invoice_pdf(invoice) assert_not_nil invoice, 'Invoice was not created' @@ -21,6 +22,8 @@ module InvoiceHelper check_amounts(invoice, lines) check_user(invoice, lines) + yield lines if block_given? + File.delete(invoice.file) end diff --git a/test/integration/invoices/vat_test.rb b/test/integration/invoices/vat_test.rb new file mode 100644 index 000000000..a9d8a64fc --- /dev/null +++ b/test/integration/invoices/vat_test.rb @@ -0,0 +1,46 @@ +# frozen_string_literal: true + +require 'test_helper' + +module Invoices; end + +class Invoices::VATTest < ActionDispatch::IntegrationTest + include ActionView::Helpers::NumberHelper + + def setup + @admin = User.find_by(username: 'admin') + login_as(@admin, scope: :user) + end + + test 'renamed VAT' do + user = User.find_by(username: 'vlonchamp') + plan = Plan.find(5) + + Setting.set('invoice_VAT-active', true) + Setting.set('invoice_VAT-name', 'TVQ+TPS') + + post '/api/local_payment/confirm_payment', params: { + customer_id: user.id, + items: [ + { + subscription: { + plan_id: plan.id + } + } + ] + }.to_json, headers: default_headers + + # Check response format & status + assert_equal 201, response.status, response.body + assert_equal Mime[:json], response.content_type + + invoice = Invoice.last + assert_invoice_pdf invoice do |lines| + vat_line = I18n.t('invoices.including_VAT_RATE', + RATE: Setting.get('invoice_VAT-rate'), + AMOUNT: number_to_currency(invoice.total / 100.00), + NAME: 'TVQ+TPS') + assert(lines.any? { |l| /#{Regexp.escape(vat_line)}/.match(l) }) + end + end +end