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:
parent
9a3b559c0f
commit
f8904dfb9c
@ -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
|
||||
|
@ -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
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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']));
|
@ -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>
|
||||
);
|
||||
};
|
@ -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');
|
||||
|
@ -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;
|
||||
};
|
||||
|
@ -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 }>;
|
||||
|
@ -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; }],
|
||||
|
@ -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";
|
||||
|
@ -71,6 +71,10 @@
|
||||
}
|
||||
}
|
||||
|
||||
button.addon {
|
||||
border: 0;
|
||||
}
|
||||
|
||||
& > input {
|
||||
grid-area: field;
|
||||
border: none;
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,5 @@
|
||||
.setting-history-modal {
|
||||
table {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
@ -6,10 +6,3 @@
|
||||
transform-origin: top center;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes show {
|
||||
100% {
|
||||
opacity: 1;
|
||||
transform: none;
|
||||
}
|
||||
}
|
||||
|
6
app/frontend/src/stylesheets/variables/animations.scss
Normal file
6
app/frontend/src/stylesheets/variables/animations.scss
Normal file
@ -0,0 +1,6 @@
|
||||
@keyframes show {
|
||||
100% {
|
||||
opacity: 1;
|
||||
transform: none;
|
||||
}
|
||||
}
|
@ -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>
|
||||
|
||||
|
@ -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>
|
@ -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>
|
@ -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>
|
@ -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
|
||||
|
@ -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)]]
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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"
|
||||
|
@ -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"
|
||||
|
@ -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"
|
||||
|
@ -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
|
||||
|
9
test/fixtures/history_values.yml
vendored
9
test/fixtures/history_values.yml
vendored
@ -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
|
||||
|
6
test/fixtures/settings.yml
vendored
6
test/fixtures/settings.yml
vendored
@ -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
|
||||
|
@ -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
|
||||
};
|
||||
};
|
||||
|
@ -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');
|
||||
|
@ -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);
|
||||
});
|
||||
|
@ -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();
|
||||
});
|
||||
});
|
||||
});
|
@ -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
|
||||
|
||||
|
46
test/integration/invoices/vat_test.rb
Normal file
46
test/integration/invoices/vat_test.rb
Normal 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
|
Loading…
x
Reference in New Issue
Block a user