1
0
mirror of https://github.com/LaCasemate/fab-manager.git synced 2025-01-18 07:52:23 +01:00

(feat) customize VAT name

This commit is contained in:
Sylvain 2022-12-23 15:52:10 +01:00
parent 9a3b559c0f
commit f8904dfb9c
37 changed files with 626 additions and 355 deletions

View File

@ -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

View File

@ -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

View File

@ -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<Setting> {
const res: AxiosResponse<{setting: Setting}> = await apiClient.get(`/api/settings/${name}`);
static async get (name: SettingName, options?: SettingGetOptions): Promise<Setting> {
const res: AxiosResponse<{setting: Setting}> = await apiClient.get(`/api/settings/${name}${ApiLib.filtersToQuery(options)}`);
return res?.data?.setting;
}

View File

@ -12,6 +12,7 @@ interface FormInputProps<TFieldValues, TInputType> extends FormComponent<TFieldV
addOn?: ReactNode,
addOnAction?: (event: React.MouseEvent<HTMLButtonElement>) => 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<TFieldValues, TInputType> extends FormComponent<TFieldV
/**
* This component is a template for an input component to use within React Hook Form
*/
export const FormInput = <TFieldValues extends FieldValues, TInputType>({ 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<TFieldValues, TInputType>) => {
export const FormInput = <TFieldValues extends FieldValues, TInputType>({ 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<TFieldValues, TInputType>) => {
/**
* Debounced (ie. temporised) version of the 'on change' callback.
*/
@ -71,7 +72,8 @@ export const FormInput = <TFieldValues extends FieldValues, TInputType>({ id, re
placeholder={placeholder}
accept={accept} />
{(type === 'file' && placeholder) && <span className='fab-button is-black file-placeholder'>{placeholder}</span>}
{addOn && <span onClick={addOnAction} className={`addon ${addOnClassName || ''} ${addOnAction ? 'is-btn' : ''}`}>{addOn}</span>}
{addOn && addOnAction && <button aria-label={addOnAriaLabel} type="button" onClick={addOnAction} className={`addon ${addOnClassName || ''} is-btn`}>{addOn}</button>}
{addOn && !addOnAction && <span aria-label={addOnAriaLabel} className={`addon ${addOnClassName || ''}`}>{addOn}</span>}
</AbstractFormItem>
);
};

View File

@ -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;

View File

@ -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<VatSettingsModalProps> = ({ isOpen, toggleModal, onError, onSuccess }) => {
const { t } = useTranslation('admin');
const { handleSubmit, reset, control, register } = useForm<Record<SettingName, SettingValue>>();
const isActive = useWatch({ control, name: 'invoice_VAT-active' });
const generalRate = useWatch({ control, name: 'invoice_VAT-rate' });
const [modalWidth, setModalWidth] = useState<ModalSize>(ModalSize.small);
const [advancedLabel, setAdvancedLabel] = useState<string>(t('app.admin.vat_settings_modal.advanced'));
const [histories, setHistories] = useImmer<Map<SettingName, boolean>>(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<Record<SettingName, SettingValue>> = (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 (
<FabModal isOpen={isOpen}
toggleModal={toggleModal}
className="vat-settings-modal"
width={modalWidth}
title={t('app.admin.vat_settings_modal.title')}
closeButton>
<form onSubmit={handleSubmit(onSubmit)}>
<div className={`panes ${modalWidth === ModalSize.large ? 'panes-both' : 'panes-one'}`}>
<div className="pane">
<FormSwitch control={control}
id="invoice_VAT-active"
label={t('app.admin.vat_settings_modal.enable_VAT')} />
{isActive && <>
<FormInput register={register}
id="invoice_VAT-name"
rules={{ required: true }}
tooltip={t('app.admin.vat_settings_modal.VAT_name_help')}
label={t('app.admin.vat_settings_modal.VAT_name')} />
<FormInput register={register}
id="invoice_VAT-rate"
rules={{ required: true }}
tooltip={t('app.admin.vat_settings_modal.VAT_rate_help')}
type='number'
label={t('app.admin.vat_settings_modal.VAT_rate')}
addOn={<ClockCounterClockwise size={24}/>}
addOnAriaLabel={t('app.admin.vat_settings_modal.show_history')}
addOnAction={toggleHistoryModal('invoice_VAT-rate')} />
<SettingHistoryModal isOpen={histories.get('invoice_VAT-rate')}
toggleModal={toggleHistoryModal('invoice_VAT-rate')}
settings={['invoice_VAT-rate' as SettingName, 'invoice_VAT-active' as SettingName]}
onError={onError} />
</>}
{modalWidth === ModalSize.large && <FabAlert level="warning">
<HtmlTranslate trKey="app.admin.vat_settings_modal.multi_VAT_notice" options={{ RATE: String(generalRate) }} />
</FabAlert>}
</div>
{modalWidth === ModalSize.large && <div className="pane">
<FormInput register={register}
id="invoice_VAT-rate_Product"
type='number'
label={t('app.admin.vat_settings_modal.VAT_rate_product')}
addOn={<ClockCounterClockwise size={24}/>}
addOnAriaLabel={t('app.admin.vat_settings_modal.show_history')}
addOnAction={toggleHistoryModal('invoice_VAT-rate_Product')} />
<SettingHistoryModal isOpen={histories.get('invoice_VAT-rate_Product')}
toggleModal={toggleHistoryModal('invoice_VAT-rate_Product')}
setting={'invoice_VAT-rate_Product'}
onError={onError} />
<FormInput register={register}
id="invoice_VAT-rate_Event"
type='number'
label={t('app.admin.vat_settings_modal.VAT_rate_event')}
addOn={<ClockCounterClockwise size={24}/>}
addOnAriaLabel={t('app.admin.vat_settings_modal.show_history')}
addOnAction={toggleHistoryModal('invoice_VAT-rate_Event')} />
<SettingHistoryModal isOpen={histories.get('invoice_VAT-rate_Event')}
toggleModal={toggleHistoryModal('invoice_VAT-rate_Event')}
setting={'invoice_VAT-rate_Event'}
onError={onError} />
<FormInput register={register}
id="invoice_VAT-rate_Machine"
type='number'
label={t('app.admin.vat_settings_modal.VAT_rate_machine')}
addOn={<ClockCounterClockwise size={24}/>}
addOnAriaLabel={t('app.admin.vat_settings_modal.show_history')}
addOnAction={toggleHistoryModal('invoice_VAT-rate_Machine')} />
<SettingHistoryModal isOpen={histories.get('invoice_VAT-rate_Machine')}
toggleModal={toggleHistoryModal('invoice_VAT-rate_Machine')}
setting={'invoice_VAT-rate_Machine'}
onError={onError} />
<FormInput register={register}
id="invoice_VAT-rate_Subscription"
type='number'
label={t('app.admin.vat_settings_modal.VAT_rate_subscription')}
addOn={<ClockCounterClockwise size={24}/>}
addOnAriaLabel={t('app.admin.vat_settings_modal.show_history')}
addOnAction={toggleHistoryModal('invoice_VAT-rate_Subscription')} />
<SettingHistoryModal isOpen={histories.get('invoice_VAT-rate_Subscription')}
toggleModal={toggleHistoryModal('invoice_VAT-rate_Subscription')}
setting={'invoice_VAT-rate_Subscription'}
onError={onError} />
<FormInput register={register}
id="invoice_VAT-rate_Space"
type='number'
label={t('app.admin.vat_settings_modal.VAT_rate_space')}
addOn={<ClockCounterClockwise size={24}/>}
addOnAriaLabel={t('app.admin.vat_settings_modal.show_history')}
addOnAction={toggleHistoryModal('invoice_VAT-rate_Space')} />
<SettingHistoryModal isOpen={histories.get('invoice_VAT-rate_Space')}
toggleModal={toggleHistoryModal('invoice_VAT-rate_Space')}
setting={'invoice_VAT-rate_Space'}
onError={onError} />
<FormInput register={register}
id="invoice_VAT-rate_Training"
type='number'
label={t('app.admin.vat_settings_modal.VAT_rate_training')}
addOn={<ClockCounterClockwise size={24}/>}
addOnAriaLabel={t('app.admin.vat_settings_modal.show_history')}
addOnAction={toggleHistoryModal('invoice_VAT-rate_Training')} />
<SettingHistoryModal isOpen={histories.get('invoice_VAT-rate_Training')}
toggleModal={toggleHistoryModal('invoice_VAT-rate_Training')}
setting={'invoice_VAT-rate_Training'}
onError={onError} />
</div>}
</div>
<div className="actions">
{isActive && <FabButton type="button" onClick={toggleAdvancedRates}>{advancedLabel}</FabButton>}
<FabButton type="submit" className='save-btn'>{t('app.admin.vat_settings_modal.save')}</FabButton>
</div>
</form>
</FabModal>
);
};
const VatSettingsModalWrapper: React.FC<VatSettingsModalProps> = (props) => {
return (
<Loader>
<VatSettingsModal {...props} />
</Loader>
);
};
Application.Components.component('vatSettingsModal', react2angular(VatSettingsModalWrapper, ['isOpen', 'toggleModal', 'onError', 'onSuccess']));

View File

@ -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<SettingName> }
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<SettingHistoryModalProps> = ({ isOpen, toggleModal, setting, settings, onError }) => {
const { t } = useTranslation('admin');
const [settingData, setSettingData] = useImmer<Map<SettingName, Setting>>(new Map());
const [history, setHistory] = useState<Array<HistoryValue & { setting: SettingName }>>([]);
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 (
<FabModal isOpen={isOpen}
className="setting-history-modal"
toggleModal={toggleModal}
width={ModalSize.large}
title={t('app.admin.setting_history_modal.title')}
closeButton>
{history.length === 0 && <div>
{t('app.admin.setting_history_modal.no_history')}
</div>}
{history.length > 0 && <table role="table">
<thead>
<tr>
<th>{t('app.admin.setting_history_modal.setting')}</th>
<th>{t('app.admin.setting_history_modal.value')}</th>
<th>{t('app.admin.setting_history_modal.date')}</th>
<th>{t('app.admin.setting_history_modal.operator')}</th>
</tr>
</thead>
<tbody>
{history.map(hv => <tr key={hv.id}>
<td>{settingData.get(hv.setting).localized}</td>
<td>{hv.value}</td>
<td>{FormatLib.date(hv.created_at)}</td>
<td>{hv.user.name}</td>
</tr>)}
</tbody>
</table>}
</FabModal>
);
};

View File

@ -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');

View File

@ -5,10 +5,12 @@ export default class SettingLib {
/**
* Convert the provided data to a map, as expected by BulkUpdate
*/
static objectToBulkMap = (data: Record<SettingName, SettingValue>): Map<SettingName, string> => {
static objectToBulkMap = (data: Record<SettingName, SettingValue>, options?: { stripNaN: boolean }): Map<SettingName, string> => {
const res = new Map<SettingName, string>();
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;
};

View File

@ -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 }>;

View File

@ -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; }],

View File

@ -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";

View File

@ -71,6 +71,10 @@
}
}
button.addon {
border: 0;
}
& > input {
grid-area: field;
border: none;

View File

@ -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);
}
}
}
}

View File

@ -0,0 +1,5 @@
.setting-history-modal {
table {
width: 100%;
}
}

View File

@ -6,10 +6,3 @@
transform-origin: top center;
}
}
@keyframes show {
100% {
opacity: 1;
transform: none;
}
}

View File

@ -0,0 +1,6 @@
@keyframes show {
100% {
opacity: 1;
transform: none;
}
}

View File

@ -60,7 +60,7 @@
</tr>
<tr class="invoice-vat invoice-editable vat-line italic" ng-click="openEditVAT()" ng-show="invoice.VAT.active">
<td translate translate-values="{RATE:getMachineExampleRate(), AMOUNT:(30.0 | currency)}">{{ 'app.admin.invoices.including_VAT' }}</td>
<td translate translate-values="{RATE:getMachineExampleRate(), AMOUNT:(30.0 | currency), NAME: invoice.VAT.name}">{{ 'app.admin.invoices.including_VAT' }}</td>
<td>{{30-(30/(getMachineExampleRate()/100+1)) | currency}}</td>
</tr>
<tr class="invoice-ht vat-line italic" ng-show="invoice.VAT.active">
@ -93,6 +93,7 @@
ng-blur="legalsEditEnd($event)">
</div>
</form>
<vat-settings-modal is-open="isOpenVatModal" toggle-modal="toggleVatModal" on-error="onError" on-success="onSuccess"></vat-settings-modal>
</section>
</div>

View File

@ -1,65 +0,0 @@
<div class="custom-invoice">
<div class="modal-header">
<h3 class="modal-title" translate>{{ 'app.admin.invoices.multiVAT' }}</h3>
</div>
<div class="modal-body">
<uib-alert type="warning">
<p class="text-sm">
<i class="fa fa-warning"></i>
<span ng-bind-html="'app.admin.invoices.multi_VAT_notice' | translate:{ RATE: rate }"></span>
</p>
</uib-alert>
<div class="form-group">
<label for="vatRateMachine" class="control-label" translate>{{ 'app.admin.invoices.VAT_rate_machine' }}</label>
<div class="input-group">
<span class="input-group-addon">% </span>
<input id="vatRateMachine" type="number" ng-model="multiVAT.rateMachine" class="form-control multi-vat-rate-input" min="0" max="100"/>
<button class="btn pull-right" ng-click="showMultiRateHistory('Machine')"><i class="fa fa-history"></i></button>
</div>
</div>
<div class="form-group">
<label for="vatRateSpace" class="control-label" translate>{{ 'app.admin.invoices.VAT_rate_space' }}</label>
<div class="input-group">
<span class="input-group-addon">% </span>
<input id="vatRateSpace" type="number" ng-model="multiVAT.rateSpace" class="form-control multi-vat-rate-input" min="0" max="100"/>
<button class="btn pull-right" ng-click="showMultiRateHistory('Space')"><i class="fa fa-history"></i></button>
</div>
</div>
<div class="form-group">
<label for="vatRateTraining" class="control-label" translate>{{ 'app.admin.invoices.VAT_rate_training' }}</label>
<div class="input-group">
<span class="input-group-addon">% </span>
<input id="vatRateTraining" type="number" ng-model="multiVAT.rateTraining" class="form-control multi-vat-rate-input" min="0" max="100"/>
<button class="btn pull-right" ng-click="showMultiRateHistory('Training')"><i class="fa fa-history"></i></button>
</div>
</div>
<div class="form-group">
<label for="vatRateEvent" class="control-label" translate>{{ 'app.admin.invoices.VAT_rate_event' }}</label>
<div class="input-group">
<span class="input-group-addon">% </span>
<input id="vatRateEvent" type="number" ng-model="multiVAT.rateEvent" class="form-control multi-vat-rate-input" min="0" max="100"/>
<button class="btn pull-right" ng-click="showMultiRateHistory('Event')"><i class="fa fa-history"></i></button>
</div>
</div>
<div class="form-group">
<label for="vatRateSubscription" class="control-label" translate>{{ 'app.admin.invoices.VAT_rate_subscription' }}</label>
<div class="input-group">
<span class="input-group-addon">% </span>
<input id="vatRateSubscription" type="number" ng-model="multiVAT.rateSubscription" class="form-control multi-vat-rate-input" min="0" max="100"/>
<button class="btn pull-right" ng-click="showMultiRateHistory('Subscription')"><i class="fa fa-history"></i></button>
</div>
</div>
<div class="form-group">
<label for="vatRateProduct" class="control-label" translate>{{ 'app.admin.invoices.VAT_rate_product' }}</label>
<div class="input-group">
<span class="input-group-addon">% </span>
<input id="vatRateSubscription" type="number" ng-model="multiVAT.rateProduct" class="form-control multi-vat-rate-input" min="0" max="100"/>
<button class="btn pull-right" ng-click="showMultiRateHistory('Product')"><i class="fa fa-history"></i></button>
</div>
</div>
</div>
<div class="modal-footer">
<button class="btn btn-warning" ng-click="ok()" translate>{{ 'app.shared.buttons.confirm' }}</button>
<button class="btn btn-default" ng-click="cancel()" translate>{{ 'app.shared.buttons.cancel' }}</button>
</div>
</div>

View File

@ -1,58 +0,0 @@
<div class="custom-invoice">
<div class="modal-header">
<h3 class="modal-title" translate>{{ 'app.admin.invoices.VAT' }}</h3>
</div>
<div class="modal-body">
<div class="form-group">
<label for="enableVAT" class="control-label" translate>{{ 'app.admin.invoices.enable_VAT' }}</label>
<switch id="enableVAT"
checked="isSelected"
on-change="enableVATChanged"
classname="form-control m-l-sm">
</switch>
</div>
<div class="form-group" ng-show="isSelected">
<label for="vatRate" class="control-label" translate>{{ 'app.admin.invoices.VAT_rate' }}</label>
<div class="input-group">
<span class="input-group-addon">% </span>
<input id="vatRate" type="number" ng-model="rate" class="form-control" min="0" max="100"/>
</div>
</div>
<uib-alert type="warning" ng-show="isSelected">
<p class="text-sm">
<i class="fa fa-warning"></i>
<span>{{ 'app.admin.invoices.VAT_notice' | translate }}</span>
</p>
</uib-alert>
<div class="m-t-lg">
<h4 translate>{{ 'app.admin.invoices.VAT_history' }}</h4>
<table class="table scrollable-3-cols">
<thead>
<tr>
<th translate>{{ 'app.admin.invoices.VAT_rate' }}</th>
<th translate>{{ 'app.admin.invoices.changed_at' }}</th>
<th translate>{{ 'app.admin.invoices.changed_by' }}</th>
</tr>
</thead>
<tbody>
<tr ng-repeat="value in history | orderBy:'-date'">
<td>
<span class="no-user-label" ng-show="value.enabled === false" translate>{{'app.admin.invoices.VAT_disabled'}}</span>
<span class="no-user-label" ng-show="value.enabled === true" translate>{{'app.admin.invoices.VAT_enabled'}}</span>
<span ng-show="value.rate">{{value.rate}}</span>
</td>
<td>{{value.date | amDateFormat:'L LT'}}</td>
<td>{{value.user.name}}<span class="no-user-label" ng-hide="value.user" translate>{{ 'app.admin.invoices.deleted_user' }}</span></td>
</tr>
</tbody>
</table>
</div>
</div>
<div class="modal-footer">
<button class="btn btn-warning pull-left" ng-click="editMultiVAT()" ng-show="isSelected" translate>{{ 'app.admin.invoices.edit_multi_VAT_button' }}</button>
<button class="btn btn-warning" ng-click="ok()" translate>{{ 'app.shared.buttons.confirm' }}</button>
<button class="btn btn-default" ng-click="cancel()" translate>{{ 'app.shared.buttons.cancel' }}</button>
</div>
</div>

View File

@ -1,32 +0,0 @@
<div class="custom-invoice">
<div class="modal-header">
<h3 class="modal-title" translate>{{ 'app.admin.invoices.VAT_history' }}</h3>
</div>
<div class="modal-body">
<div>
<table class="table scrollable-3-cols">
<thead>
<tr>
<th translate>{{ 'app.admin.invoices.VAT_rate' }}</th>
<th translate>{{ 'app.admin.invoices.changed_at' }}</th>
<th translate>{{ 'app.admin.invoices.changed_by' }}</th>
</tr>
</thead>
<tbody>
<tr ng-repeat="value in history | orderBy:'-date'">
<td>
<span class="no-user-label" ng-show="value.enabled === false" translate>{{'app.admin.invoices.VAT_disabled'}}</span>
<span class="no-user-label" ng-show="value.enabled === true" translate>{{'app.admin.invoices.VAT_enabled'}}</span>
<span ng-show="value.rate">{{rateValue(value)}}</span>
</td>
<td>{{value.date | amDateFormat:'L LT'}}</td>
<td>{{value.user.name}}<span class="no-user-label" ng-hide="value.user" translate>{{ 'app.admin.invoices.deleted_user' }}</span></td>
</tr>
</tbody>
</table>
</div>
</div>
<div class="modal-footer">
<button class="btn btn-default" ng-click="cancel()" translate>{{ 'app.shared.buttons.cancel' }}</button>
</div>
</div>

View File

@ -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

View File

@ -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)]]

View File

@ -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

View File

@ -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)

View File

@ -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.<br/><strong>Warning:</strong> 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: "<strong>Please note</strong>: The current general rate is {RATE}%. You can define different VAT rates for each category.<br><br>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"

View File

@ -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"

View File

@ -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"

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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<Setting> = [
{
@ -738,5 +741,24 @@ export const settings: Array<Setting> = [
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
};
};

View File

@ -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');

View File

@ -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(<InvoicesSettingsPanel onError={onError} onSuccess={onSuccess} />);
render(<InvoicesSettingsPanel onError={onError} onSuccess={onSuccess} uiRouter={uiRouter} />);
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(<InvoicesSettingsPanel onError={onError} onSuccess={onSuccess} />);
render(<InvoicesSettingsPanel onError={onError} onSuccess={onSuccess} uiRouter={uiRouter} />);
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(<InvoicesSettingsPanel onError={onError} onSuccess={onSuccess} />);
render(<InvoicesSettingsPanel onError={onError} onSuccess={onSuccess} uiRouter={uiRouter} />);
await waitFor(() => {
expect(screen.getAllByLabelText(/app.admin.invoices_settings_panel.prefix/)).toHaveLength(2);
});

View File

@ -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(<VatSettingsModal isOpen={true} toggleModal={toggleModal} onError={onError} onSuccess={onSuccess} />);
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(<VatSettingsModal isOpen={true} toggleModal={toggleModal} onError={onError} onSuccess={onSuccess} />);
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(<VatSettingsModal isOpen={true} toggleModal={toggleModal} onError={onError} onSuccess={onSuccess} />);
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();
});
});
});

View File

@ -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

View File

@ -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