mirror of
https://github.com/LaCasemate/fab-manager.git
synced 2025-01-30 19:52:20 +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
|
- Ability to disable generation of invoices at zero
|
||||||
- Accounting data is now built each night and saved in database
|
- Accounting data is now built each night and saved in database
|
||||||
- Ability to define multiple accounting journal codes
|
- Ability to define multiple accounting journal codes
|
||||||
|
- Ability to change the name of the VAT
|
||||||
- OpenAPI endpoint to fetch accounting data
|
- OpenAPI endpoint to fetch accounting data
|
||||||
- Add reservation deadline parameter (#414)
|
- Add reservation deadline parameter (#414)
|
||||||
- Verify current password at server side when changing password
|
- Verify current password at server side when changing password
|
||||||
|
@ -34,7 +34,9 @@ class API::SettingsController < API::ApiController
|
|||||||
if !SettingService.update_allowed?(db_setting)
|
if !SettingService.update_allowed?(db_setting)
|
||||||
db_setting.errors.add(:-, "#{I18n.t("settings.#{setting[:name]}")}: #{I18n.t('settings.locked_setting')}")
|
db_setting.errors.add(:-, "#{I18n.t("settings.#{setting[:name]}")}: #{I18n.t('settings.locked_setting')}")
|
||||||
elsif db_setting.save
|
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
|
end
|
||||||
|
|
||||||
@settings.push db_setting
|
@settings.push db_setting
|
||||||
|
@ -4,14 +4,15 @@ import {
|
|||||||
Setting,
|
Setting,
|
||||||
SettingBulkArray,
|
SettingBulkArray,
|
||||||
SettingBulkResult,
|
SettingBulkResult,
|
||||||
SettingError,
|
SettingError, SettingGetOptions,
|
||||||
SettingName,
|
SettingName,
|
||||||
SettingValue
|
SettingValue
|
||||||
} from '../models/setting';
|
} from '../models/setting';
|
||||||
|
import ApiLib from '../lib/api';
|
||||||
|
|
||||||
export default class SettingAPI {
|
export default class SettingAPI {
|
||||||
static async get (name: SettingName): Promise<Setting> {
|
static async get (name: SettingName, options?: SettingGetOptions): Promise<Setting> {
|
||||||
const res: AxiosResponse<{setting: Setting}> = await apiClient.get(`/api/settings/${name}`);
|
const res: AxiosResponse<{setting: Setting}> = await apiClient.get(`/api/settings/${name}${ApiLib.filtersToQuery(options)}`);
|
||||||
return res?.data?.setting;
|
return res?.data?.setting;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -12,6 +12,7 @@ interface FormInputProps<TFieldValues, TInputType> extends FormComponent<TFieldV
|
|||||||
addOn?: ReactNode,
|
addOn?: ReactNode,
|
||||||
addOnAction?: (event: React.MouseEvent<HTMLButtonElement>) => void,
|
addOnAction?: (event: React.MouseEvent<HTMLButtonElement>) => void,
|
||||||
addOnClassName?: string,
|
addOnClassName?: string,
|
||||||
|
addOnAriaLabel?: string,
|
||||||
debounce?: number,
|
debounce?: number,
|
||||||
type?: 'text' | 'date' | 'password' | 'url' | 'time' | 'tel' | 'search' | 'number' | 'month' | 'email' | 'datetime-local' | 'week' | 'hidden' | 'file',
|
type?: 'text' | 'date' | 'password' | 'url' | 'time' | 'tel' | 'search' | 'number' | 'month' | 'email' | 'datetime-local' | 'week' | 'hidden' | 'file',
|
||||||
accept?: string,
|
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
|
* 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.
|
* Debounced (ie. temporised) version of the 'on change' callback.
|
||||||
*/
|
*/
|
||||||
@ -71,7 +72,8 @@ export const FormInput = <TFieldValues extends FieldValues, TInputType>({ id, re
|
|||||||
placeholder={placeholder}
|
placeholder={placeholder}
|
||||||
accept={accept} />
|
accept={accept} />
|
||||||
{(type === 'file' && placeholder) && <span className='fab-button is-black file-placeholder'>{placeholder}</span>}
|
{(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>
|
</AbstractFormItem>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -15,7 +15,7 @@ import { react2angular } from 'react2angular';
|
|||||||
import FormatLib from '../../lib/format';
|
import FormatLib from '../../lib/format';
|
||||||
import { FormInput } from '../form/form-input';
|
import { FormInput } from '../form/form-input';
|
||||||
import { UnsavedFormAlert } from '../form/unsaved-form-alert';
|
import { UnsavedFormAlert } from '../form/unsaved-form-alert';
|
||||||
import { UIRouter } from '@uirouter/angularjs';
|
import type { UIRouter } from '@uirouter/angularjs';
|
||||||
|
|
||||||
declare const Application: IApplication;
|
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
|
// Default invoices ordering/sorting
|
||||||
$scope.orderInvoice = '-date';
|
$scope.orderInvoice = '-date';
|
||||||
|
|
||||||
|
$scope.isOpenVatModal = false;
|
||||||
|
|
||||||
// Invoices parameters
|
// Invoices parameters
|
||||||
$scope.invoice = {
|
$scope.invoice = {
|
||||||
logo: null,
|
logo: null,
|
||||||
@ -73,18 +75,9 @@ Application.Controllers.controller('InvoicesController', ['$scope', '$state', 'I
|
|||||||
},
|
},
|
||||||
VAT: {
|
VAT: {
|
||||||
rate: 19.6,
|
rate: 19.6,
|
||||||
active: false,
|
name: 'VAT',
|
||||||
templateUrl: '/admin/invoices/settings/editVAT.html'
|
|
||||||
},
|
|
||||||
multiVAT: {
|
|
||||||
rateMachine: '',
|
rateMachine: '',
|
||||||
rateSpace: '',
|
active: false
|
||||||
rateTraining: '',
|
|
||||||
rateEvent: '',
|
|
||||||
rateSubscription: '',
|
|
||||||
rateProduct: '',
|
|
||||||
editTemplateUrl: '/admin/invoices/settings/editMultiVAT.html',
|
|
||||||
historyTemplateUrl: '/admin/invoices/settings/multiVATHistory.html'
|
|
||||||
},
|
},
|
||||||
text: {
|
text: {
|
||||||
content: ''
|
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
|
// the following item is used by the UnsavedFormAlert component to detect a page change
|
||||||
$scope.uiRouter = $uiRouter;
|
$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 the VAT rate applicable to the machine reservations
|
||||||
* @return {number}
|
* @return {number}
|
||||||
*/
|
*/
|
||||||
$scope.getMachineExampleRate = function () {
|
$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
|
* The VAT can be disabled and its rate can be configured
|
||||||
*/
|
*/
|
||||||
$scope.openEditVAT = function () {
|
$scope.openEditVAT = function () {
|
||||||
const modalInstance = $uibModal.open({
|
$scope.toggleVatModal();
|
||||||
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);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -855,12 +726,8 @@ Application.Controllers.controller('InvoicesController', ['$scope', '$state', 'I
|
|||||||
$scope.invoice.text.content = settings.invoice_text;
|
$scope.invoice.text.content = settings.invoice_text;
|
||||||
$scope.invoice.VAT.rate = parseFloat(settings['invoice_VAT-rate']);
|
$scope.invoice.VAT.rate = parseFloat(settings['invoice_VAT-rate']);
|
||||||
$scope.invoice.VAT.active = (settings['invoice_VAT-active'] === 'true');
|
$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.VAT.name = settings['invoice_VAT-name'];
|
||||||
$scope.invoice.multiVAT.rateSpace = settings['invoice_VAT-rate_Space'] ? parseFloat(settings['invoice_VAT-rate_Space']) : '';
|
$scope.invoice.VAT.rateMachine = settings['invoice_VAT-rate_Machine'] ? parseFloat(settings['invoice_VAT-rate_Machine']) : '';
|
||||||
$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.number.model = settings['invoice_order-nb'];
|
$scope.invoice.number.model = settings['invoice_order-nb'];
|
||||||
$scope.invoice.code.model = settings['invoice_code-value'];
|
$scope.invoice.code.model = settings['invoice_code-value'];
|
||||||
$scope.invoice.code.active = (settings['invoice_code-active'] === 'true');
|
$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
|
* 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>();
|
const res = new Map<SettingName, string>();
|
||||||
for (const key in data) {
|
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;
|
return res;
|
||||||
};
|
};
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import { HistoryValue } from './history-value';
|
import { HistoryValue } from './history-value';
|
||||||
import { TDateISO } from '../typings/date-iso';
|
import { TDateISO } from '../typings/date-iso';
|
||||||
|
import { ApiFilter } from './api';
|
||||||
|
|
||||||
export const homePageSettings = [
|
export const homePageSettings = [
|
||||||
'twitter_name',
|
'twitter_name',
|
||||||
@ -65,7 +66,8 @@ export const invoicesSettings = [
|
|||||||
'invoice_legals',
|
'invoice_legals',
|
||||||
'invoice_prefix',
|
'invoice_prefix',
|
||||||
'payment_schedule_prefix',
|
'payment_schedule_prefix',
|
||||||
'prevent_invoices_zero'
|
'prevent_invoices_zero',
|
||||||
|
'invoice_VAT-name'
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
export const bookingSettings = [
|
export const bookingSettings = [
|
||||||
@ -284,4 +286,8 @@ export interface SettingBulkResult {
|
|||||||
localized?: string,
|
localized?: string,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface SettingGetOptions extends ApiFilter {
|
||||||
|
history?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
export type SettingBulkArray = Array<{ name: SettingName, value: SettingValue }>;
|
export type SettingBulkArray = Array<{ name: SettingName, value: SettingValue }>;
|
||||||
|
@ -950,10 +950,10 @@ angular.module('application.router', ['ui.router'])
|
|||||||
resolve: {
|
resolve: {
|
||||||
settings: ['Setting', function (Setting) {
|
settings: ['Setting', function (Setting) {
|
||||||
return Setting.query({
|
return Setting.query({
|
||||||
names: "['invoice_legals', 'invoice_text', 'invoice_VAT-rate', 'invoice_VAT-rate_Machine', 'invoice_VAT-rate_Training', 'invoice_VAT-rate_Space', " +
|
names: "['invoice_legals', 'invoice_text', 'invoice_VAT-rate', 'invoice_VAT-rate_Machine', " +
|
||||||
"'invoice_VAT-rate_Event', 'invoice_VAT-rate_Subscription', 'invoice_VAT-rate_Product', 'invoice_VAT-active', 'invoice_order-nb', 'invoice_code-value', " +
|
"'invoice_VAT-active', 'invoice_order-nb', 'invoice_code-value', " +
|
||||||
"'invoice_code-active', 'invoice_reference', 'invoice_logo', 'payment_gateway', 'payment_schedule_prefix', 'invoicing_module', " +
|
"'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;
|
}).$promise;
|
||||||
}],
|
}],
|
||||||
stripeSecretKey: ['Setting', function (Setting) { return Setting.isPresent({ name: 'stripe_secret_key' }).$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/colors";
|
||||||
@import "variables/typography";
|
@import "variables/typography";
|
||||||
@import "variables/decoration";
|
@import "variables/decoration";
|
||||||
@ -55,6 +56,7 @@
|
|||||||
@import "modules/form/form-image-upload";
|
@import "modules/form/form-image-upload";
|
||||||
@import "modules/group/change-group";
|
@import "modules/group/change-group";
|
||||||
@import "modules/invoices/invoices-settings-panel";
|
@import "modules/invoices/invoices-settings-panel";
|
||||||
|
@import "modules/invoices/vat-settings-modal";
|
||||||
@import "modules/layout/header-page";
|
@import "modules/layout/header-page";
|
||||||
@import "modules/machines/machine-card";
|
@import "modules/machines/machine-card";
|
||||||
@import "modules/machines/machine-form";
|
@import "modules/machines/machine-form";
|
||||||
@ -102,6 +104,7 @@
|
|||||||
@import "modules/select-gateway-modal";
|
@import "modules/select-gateway-modal";
|
||||||
@import "modules/settings/boolean-setting";
|
@import "modules/settings/boolean-setting";
|
||||||
@import "modules/settings/check-list-setting";
|
@import "modules/settings/check-list-setting";
|
||||||
|
@import "modules/settings/setting-history-modal";
|
||||||
@import "modules/settings/user-validation-setting";
|
@import "modules/settings/user-validation-setting";
|
||||||
@import "modules/socials/fab-socials";
|
@import "modules/socials/fab-socials";
|
||||||
@import "modules/spaces/space-form";
|
@import "modules/spaces/space-form";
|
||||||
|
@ -71,6 +71,10 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
button.addon {
|
||||||
|
border: 0;
|
||||||
|
}
|
||||||
|
|
||||||
& > input {
|
& > input {
|
||||||
grid-area: field;
|
grid-area: field;
|
||||||
border: none;
|
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;
|
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>
|
||||||
|
|
||||||
<tr class="invoice-vat invoice-editable vat-line italic" ng-click="openEditVAT()" ng-show="invoice.VAT.active">
|
<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>
|
<td>{{30-(30/(getMachineExampleRate()/100+1)) | currency}}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr class="invoice-ht vat-line italic" ng-show="invoice.VAT.active">
|
<tr class="invoice-ht vat-line italic" ng-show="invoice.VAT.active">
|
||||||
@ -93,6 +93,7 @@
|
|||||||
ng-blur="legalsEditEnd($event)">
|
ng-blur="legalsEditEnd($event)">
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
<vat-settings-modal is-open="isOpenVatModal" toggle-modal="toggleVatModal" on-error="onError" on-success="onSuccess"></vat-settings-modal>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</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
|
store_hidden
|
||||||
advanced_accounting
|
advanced_accounting
|
||||||
external_id
|
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:
|
# WARNING: when adding a new key, you may also want to add it in:
|
||||||
# - config/locales/en.yml#settings
|
# - config/locales/en.yml#settings
|
||||||
# - app/frontend/src/javascript/models/setting.ts#SettingName
|
# - app/frontend/src/javascript/models/setting.ts#SettingName
|
||||||
|
@ -171,7 +171,10 @@ class PDF::Invoice < Prawn::Document
|
|||||||
else
|
else
|
||||||
data += [[I18n.t('invoices.total_including_all_taxes'), number_to_currency(total)]]
|
data += [[I18n.t('invoices.total_including_all_taxes'), number_to_currency(total)]]
|
||||||
vat_rate_group.each do |_type, rate|
|
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)]]
|
number_to_currency(rate[:total_vat] / 100.00)]]
|
||||||
end
|
end
|
||||||
data += [[I18n.t('invoices.including_total_excluding_taxes'), number_to_currency(total_ht / 100.00)]]
|
data += [[I18n.t('invoices.including_total_excluding_taxes'), number_to_currency(total_ht / 100.00)]]
|
||||||
|
@ -16,6 +16,7 @@ class Accounting::VatExportService
|
|||||||
@decimal_separator = '.'
|
@decimal_separator = '.'
|
||||||
@date_format = '%Y-%m-%d'
|
@date_format = '%Y-%m-%d'
|
||||||
@columns = columns
|
@columns = columns
|
||||||
|
@vat_name = Setting.get('invoice_VAT-name')
|
||||||
end
|
end
|
||||||
|
|
||||||
def set_options(decimal_separator: ',', date_format: '%d/%m/%Y', label_max_length: nil, export_zeros: nil)
|
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
|
def header_row
|
||||||
row = ''
|
row = ''
|
||||||
columns.each do |column|
|
columns.each do |column|
|
||||||
row << I18n.t("vat_export.#{column}") << separator
|
row << I18n.t("vat_export.#{column}", NAME: @vat_name) << separator
|
||||||
end
|
end
|
||||||
"#{row}\n"
|
"#{row}\n"
|
||||||
end
|
end
|
||||||
|
@ -15,7 +15,7 @@ class AccountingWorker
|
|||||||
|
|
||||||
def invoices(invoices_ids)
|
def invoices(invoices_ids)
|
||||||
# clean
|
# clean
|
||||||
AccountingLine.where(invoice_id: ids).delete_all
|
AccountingLine.where(invoice_id: invoices_ids).delete_all
|
||||||
# build
|
# build
|
||||||
service = Accounting::AccountingService.new
|
service = Accounting::AccountingService.new
|
||||||
invoices = Invoice.where(id: invoices_ids)
|
invoices = Invoice.where(id: invoices_ids)
|
||||||
|
@ -692,7 +692,7 @@ en:
|
|||||||
total_including_all_taxes: "Total incl. all taxes"
|
total_including_all_taxes: "Total incl. all taxes"
|
||||||
VAT_disabled: "VAT disabled"
|
VAT_disabled: "VAT disabled"
|
||||||
VAT_enabled: "VAT enabled"
|
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_total_excluding_taxes: "Including Total excl. taxes"
|
||||||
including_amount_payed_on_ordering: "Including amount payed on ordering"
|
including_amount_payed_on_ordering: "Including amount payed on ordering"
|
||||||
settlement_by_debit_card_on_DATE_at_TIME_for_an_amount_of_AMOUNT: "Settlement by debit card on {DATE} at {TIME}, for an amount of {AMOUNT}"
|
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."
|
default_value_is_24_hours: "If the field is leaved empty: 24 hours."
|
||||||
visibility_yearly: "maximum visibility for annual subscribers"
|
visibility_yearly: "maximum visibility for annual subscribers"
|
||||||
visibility_others: "maximum visibility for other members"
|
visibility_others: "maximum visibility for other members"
|
||||||
reservation_deadline: "reservation deadline"
|
|
||||||
display: "Display"
|
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_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"
|
display_reservation_user_name: "Display the full name of the user(s) who booked a slots"
|
||||||
@ -2241,3 +2240,29 @@ en:
|
|||||||
example: "Example"
|
example: "Example"
|
||||||
save: "Save"
|
save: "Save"
|
||||||
update_success: "The settings were successfully updated"
|
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"
|
other: "%{count} %{NAME} tickets"
|
||||||
coupon_CODE_discount_of_DISCOUNT: "Coupon {CODE}: discount of {DISCOUNT}{TYPE, select, percent_off{%} other{}}" #messageFormat interpolation
|
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"
|
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_total_excluding_taxes: "Including Total excl. taxes"
|
||||||
including_amount_payed_on_ordering: "Including amount payed on ordering"
|
including_amount_payed_on_ordering: "Including amount payed on ordering"
|
||||||
total_amount: "Total amount"
|
total_amount: "Total amount"
|
||||||
@ -169,7 +170,7 @@ en:
|
|||||||
vat_export:
|
vat_export:
|
||||||
start_date: "Start date"
|
start_date: "Start date"
|
||||||
end_date: "End date"
|
end_date: "End date"
|
||||||
vat_rate: "VAT rate"
|
vat_rate: "%{NAME} rate"
|
||||||
amount: "Total amount"
|
amount: "Total amount"
|
||||||
#training availabilities
|
#training availabilities
|
||||||
trainings:
|
trainings:
|
||||||
@ -507,6 +508,12 @@ en:
|
|||||||
invoice_order-nb: "Invoice's order number"
|
invoice_order-nb: "Invoice's order number"
|
||||||
invoice_VAT-active: "Activation of the VAT"
|
invoice_VAT-active: "Activation of the VAT"
|
||||||
invoice_VAT-rate: "VAT rate"
|
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_text: "Invoices' text"
|
||||||
invoice_legals: "Invoices' legal information"
|
invoice_legals: "Invoices' legal information"
|
||||||
booking_window_start: "Opening time"
|
booking_window_start: "Opening time"
|
||||||
@ -635,3 +642,4 @@ en:
|
|||||||
advanced_accounting: "Advanced accounting"
|
advanced_accounting: "Advanced accounting"
|
||||||
external_id: "external identifier"
|
external_id: "external identifier"
|
||||||
prevent_invoices_zero: "prevent building invoices at 0"
|
prevent_invoices_zero: "prevent building invoices at 0"
|
||||||
|
invoice_VAT-name: "VAT name"
|
||||||
|
@ -106,6 +106,7 @@ fr:
|
|||||||
other: "%{count} places %{NAME}"
|
other: "%{count} places %{NAME}"
|
||||||
coupon_CODE_discount_of_DISCOUNT: "Code {CODE} : remise de {DISCOUNT} {TYPE, select, percent_off{%} other{}}" #messageFormat interpolation
|
coupon_CODE_discount_of_DISCOUNT: "Code {CODE} : remise de {DISCOUNT} {TYPE, select, percent_off{%} other{}}" #messageFormat interpolation
|
||||||
total_including_all_taxes: "Total TTC"
|
total_including_all_taxes: "Total TTC"
|
||||||
|
VAT: "TVA"
|
||||||
including_VAT_RATE: "Dont TVA %{RATE} % de %{AMOUNT}"
|
including_VAT_RATE: "Dont TVA %{RATE} % de %{AMOUNT}"
|
||||||
including_total_excluding_taxes: "Dont total HT"
|
including_total_excluding_taxes: "Dont total HT"
|
||||||
including_amount_payed_on_ordering: "Dont montant payé à la commande"
|
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('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)
|
unless Setting.find_by(name: 'overlapping_categories').try(:value)
|
||||||
Setting.set('overlapping_categories', 'training_reservations,machine_reservations,space_reservations,events_reservations')
|
Setting.set('overlapping_categories', 'training_reservations,machine_reservations,space_reservations,events_reservations')
|
||||||
end
|
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
|
updated_at: 2022-11-29 21:02:47.354751000 Z
|
||||||
footprint:
|
footprint:
|
||||||
invoicing_profile_id: 1
|
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
|
name: reservation_deadline
|
||||||
created_at: 2022-11-29 21:02:47.354751000 Z
|
created_at: 2022-11-29 21:02:47.354751000 Z
|
||||||
updated_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> = [
|
export const settings: Array<Setting> = [
|
||||||
{
|
{
|
||||||
@ -738,5 +741,24 @@ export const settings: Array<Setting> = [
|
|||||||
value: '0',
|
value: '0',
|
||||||
last_update: '2022-11-29T21:02:47-0300',
|
last_update: '2022-11-29T21:02:47-0300',
|
||||||
localized: "Empêcher la réservation avant qu'elle ne commence"
|
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 plans from '../__fixtures__/plans';
|
||||||
import planCategories from '../__fixtures__/plan_categories';
|
import planCategories from '../__fixtures__/plan_categories';
|
||||||
import { partners, managers, users } from '../__fixtures__/users';
|
import { partners, managers, users } from '../__fixtures__/users';
|
||||||
import { settings } from '../__fixtures__/settings';
|
import { buildHistoryItem, settings } from '../__fixtures__/settings';
|
||||||
import products from '../__fixtures__/products';
|
import products from '../__fixtures__/products';
|
||||||
import productCategories from '../__fixtures__/product_categories';
|
import productCategories from '../__fixtures__/product_categories';
|
||||||
import productStockMovements from '../__fixtures__/product_stock_movements';
|
import productStockMovements from '../__fixtures__/product_stock_movements';
|
||||||
@ -48,7 +48,12 @@ export const server = setupServer(
|
|||||||
}),
|
}),
|
||||||
rest.get('/api/settings/:name', (req, res, ctx) => {
|
rest.get('/api/settings/:name', (req, res, ctx) => {
|
||||||
const setting = settings.find(s => s.name === req.params.name);
|
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) => {
|
rest.get('/api/settings', (req, res, ctx) => {
|
||||||
const names = new URLSearchParams(req.url.search).get('names');
|
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 { InvoicesSettingsPanel } from '../../../../app/frontend/src/javascript/components/invoices/invoices-settings-panel';
|
||||||
import { render, fireEvent, waitFor, screen } from '@testing-library/react';
|
import { render, fireEvent, waitFor, screen } from '@testing-library/react';
|
||||||
import '@testing-library/jest-dom';
|
import '@testing-library/jest-dom';
|
||||||
|
import { uiRouter } from '../../__lib__/ui-router';
|
||||||
|
|
||||||
describe('InvoicesSettingsPanel', () => {
|
describe('InvoicesSettingsPanel', () => {
|
||||||
const onError = jest.fn();
|
const onError = jest.fn();
|
||||||
const onSuccess = jest.fn();
|
const onSuccess = jest.fn();
|
||||||
|
|
||||||
test('render InvoicesSettingsPanel', async () => {
|
test('render InvoicesSettingsPanel', async () => {
|
||||||
render(<InvoicesSettingsPanel onError={onError} onSuccess={onSuccess} />);
|
render(<InvoicesSettingsPanel onError={onError} onSuccess={onSuccess} uiRouter={uiRouter} />);
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(screen.getByLabelText(/app.admin.invoices_settings_panel.disable_invoices_zero_label/)).toBeInTheDocument();
|
expect(screen.getByLabelText(/app.admin.invoices_settings_panel.disable_invoices_zero_label/)).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
@ -18,7 +19,7 @@ describe('InvoicesSettingsPanel', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('update filename example', async () => {
|
test('update filename example', async () => {
|
||||||
render(<InvoicesSettingsPanel onError={onError} onSuccess={onSuccess} />);
|
render(<InvoicesSettingsPanel onError={onError} onSuccess={onSuccess} uiRouter={uiRouter} />);
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(screen.getAllByLabelText(/app.admin.invoices_settings_panel.prefix/)).toHaveLength(2);
|
expect(screen.getAllByLabelText(/app.admin.invoices_settings_panel.prefix/)).toHaveLength(2);
|
||||||
});
|
});
|
||||||
@ -28,7 +29,7 @@ describe('InvoicesSettingsPanel', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('update schedule filename example', async () => {
|
test('update schedule filename example', async () => {
|
||||||
render(<InvoicesSettingsPanel onError={onError} onSuccess={onSuccess} />);
|
render(<InvoicesSettingsPanel onError={onError} onSuccess={onSuccess} uiRouter={uiRouter} />);
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(screen.getAllByLabelText(/app.admin.invoices_settings_panel.prefix/)).toHaveLength(2);
|
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.
|
# Force the invoice generation worker to run NOW and check the resulting file generated.
|
||||||
# Delete the file afterwards.
|
# Delete the file afterwards.
|
||||||
# @param invoice {Invoice}
|
# @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)
|
def assert_invoice_pdf(invoice)
|
||||||
assert_not_nil invoice, 'Invoice was not created'
|
assert_not_nil invoice, 'Invoice was not created'
|
||||||
|
|
||||||
@ -21,6 +22,8 @@ module InvoiceHelper
|
|||||||
check_amounts(invoice, lines)
|
check_amounts(invoice, lines)
|
||||||
check_user(invoice, lines)
|
check_user(invoice, lines)
|
||||||
|
|
||||||
|
yield lines if block_given?
|
||||||
|
|
||||||
File.delete(invoice.file)
|
File.delete(invoice.file)
|
||||||
end
|
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