mirror of
https://github.com/LaCasemate/fab-manager.git
synced 2024-12-01 12:24:28 +01:00
Merge branch 'dev' into l10n_dev
This commit is contained in:
commit
a0d5a688d2
@ -1,5 +1,9 @@
|
|||||||
# Changelog Fab-manager
|
# Changelog Fab-manager
|
||||||
|
|
||||||
|
- Ability to configure multiple VAT rates, per kind of invoiced item
|
||||||
|
- Refactored the extended prices frontend code to allow future customization
|
||||||
|
- Fix a bug: the amount label in not correctly shown in the extended prices modal
|
||||||
|
|
||||||
## v5.2.0 2021 December 23
|
## v5.2.0 2021 December 23
|
||||||
|
|
||||||
- Ability to configure prices for spaces by time slots different than the default hourly rate
|
- Ability to configure prices for spaces by time slots different than the default hourly rate
|
||||||
|
@ -70,6 +70,8 @@ class API::ExportsController < API::ApiController
|
|||||||
case type
|
case type
|
||||||
when 'acd'
|
when 'acd'
|
||||||
export = export.where('created_at > ?', Invoice.maximum('updated_at'))
|
export = export.where('created_at > ?', Invoice.maximum('updated_at'))
|
||||||
|
when 'vat'
|
||||||
|
export = export.where('created_at > ?', Invoice.maximum('updated_at'))
|
||||||
else
|
else
|
||||||
raise ArgumentError, "Unknown type accounting/#{type}"
|
raise ArgumentError, "Unknown type accounting/#{type}"
|
||||||
end
|
end
|
||||||
|
@ -36,7 +36,7 @@ class API::PricesController < API::ApiController
|
|||||||
|
|
||||||
def destroy
|
def destroy
|
||||||
authorize @price
|
authorize @price
|
||||||
@price.destroy
|
@price.safe_destroy
|
||||||
head :no_content
|
head :no_content
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -12,4 +12,5 @@ export default class SpaceAPI {
|
|||||||
const res: AxiosResponse<Space> = await apiClient.get(`/api/spaces/${id}`);
|
const res: AxiosResponse<Space> = await apiClient.get(`/api/spaces/${id}`);
|
||||||
return res?.data;
|
return res?.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -64,8 +64,8 @@ export const ConfigurePacksButton: React.FC<ConfigurePacksButtonProps> = ({ pack
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="configure-group">
|
<div className="configure-packs-button">
|
||||||
<button className="configure-group-button" onClick={toggleShowList}>
|
<button className="packs-button" onClick={toggleShowList}>
|
||||||
<i className="fas fa-box" />
|
<i className="fas fa-box" />
|
||||||
</button>
|
</button>
|
||||||
{showList && <FabPopover title={t('app.admin.configure_packs_button.packs')} headerButton={renderAddButton()} className="fab-popover__right">
|
{showList && <FabPopover title={t('app.admin.configure_packs_button.packs')} headerButton={renderAddButton()} className="fab-popover__right">
|
||||||
@ -73,7 +73,7 @@ export const ConfigurePacksButton: React.FC<ConfigurePacksButtonProps> = ({ pack
|
|||||||
{packs?.map(p =>
|
{packs?.map(p =>
|
||||||
<li key={p.id} className={p.disabled ? 'disabled' : ''}>
|
<li key={p.id} className={p.disabled ? 'disabled' : ''}>
|
||||||
{formatDuration(p.minutes)} - {FormatLib.price(p.amount)}
|
{formatDuration(p.minutes)} - {FormatLib.price(p.amount)}
|
||||||
<span className="group-actions">
|
<span className="pack-actions">
|
||||||
<EditPack onSuccess={handleSuccess} onError={onError} pack={p} />
|
<EditPack onSuccess={handleSuccess} onError={onError} pack={p} />
|
||||||
<DeletePack onSuccess={handleSuccess} onError={onError} pack={p} />
|
<DeletePack onSuccess={handleSuccess} onError={onError} pack={p} />
|
||||||
</span>
|
</span>
|
||||||
|
@ -42,8 +42,8 @@ const DeletePackComponent: React.FC<DeletePackProps> = ({ onSuccess, onError, pa
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="delete-group">
|
<div className="delete-pack">
|
||||||
<FabButton type='button' className="delete-group-button" icon={<i className="fa fa-trash" />} onClick={toggleDeletionModal} />
|
<FabButton type='button' className="remove-pack-button" icon={<i className="fa fa-trash" />} onClick={toggleDeletionModal} />
|
||||||
<FabModal title={t('app.admin.delete_pack.delete_pack')}
|
<FabModal title={t('app.admin.delete_pack.delete_pack')}
|
||||||
isOpen={deletionModal}
|
isOpen={deletionModal}
|
||||||
toggleModal={toggleDeletionModal}
|
toggleModal={toggleDeletionModal}
|
||||||
|
@ -54,15 +54,16 @@ export const EditPack: React.FC<EditPackProps> = ({ pack, onSuccess, onError })
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="edit-group">
|
<div className="edit-pack">
|
||||||
<FabButton type='button' icon={<i className="fas fa-edit" />} onClick={handleRequestEdit} />
|
<FabButton type='button' className="edit-pack-button" icon={<i className="fas fa-edit" />} onClick={handleRequestEdit} />
|
||||||
<FabModal isOpen={isOpen}
|
<FabModal isOpen={isOpen}
|
||||||
toggleModal={toggleModal}
|
toggleModal={toggleModal}
|
||||||
title={t('app.admin.edit_pack.edit_pack')}
|
title={t('app.admin.edit_pack.edit_pack')}
|
||||||
|
className="edit-pack-modal"
|
||||||
closeButton
|
closeButton
|
||||||
confirmButton={t('app.admin.edit_pack.confirm_changes')}
|
confirmButton={t('app.admin.edit_pack.confirm_changes')}
|
||||||
onConfirmSendFormId="edit-group">
|
onConfirmSendFormId="edit-pack">
|
||||||
{packData && <PackForm formId="edit-group" onSubmit={handleUpdate} pack={packData} />}
|
{packData && <PackForm formId="edit-pack" onSubmit={handleUpdate} pack={packData} />}
|
||||||
</FabModal>
|
</FabModal>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -107,7 +107,7 @@ const MachinesPricing: React.FC<MachinesPricingProps> = ({ onError, onSuccess })
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="pricing-list">
|
<div className="machines-pricing">
|
||||||
<FabAlert level="warning">
|
<FabAlert level="warning">
|
||||||
<p><HtmlTranslate trKey="app.admin.machines_pricing.prices_match_machine_hours_rates_html"/></p>
|
<p><HtmlTranslate trKey="app.admin.machines_pricing.prices_match_machine_hours_rates_html"/></p>
|
||||||
<p><HtmlTranslate trKey="app.admin.machines_pricing.prices_calculated_on_hourly_rate_html" options={{ DURATION: `${EXEMPLE_DURATION}`, RATE: examplePrice('hourly_rate'), PRICE: examplePrice('final_price') }} /></p>
|
<p><HtmlTranslate trKey="app.admin.machines_pricing.prices_calculated_on_hourly_rate_html" options={{ DURATION: `${EXEMPLE_DURATION}`, RATE: examplePrice('hourly_rate'), PRICE: examplePrice('final_price') }} /></p>
|
||||||
|
@ -103,7 +103,7 @@ export const PackForm: React.FC<PackFormProps> = ({ formId, onSubmit, pack }) =>
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form id={formId} onSubmit={handleSubmit} className="group-form">
|
<form id={formId} onSubmit={handleSubmit} className="pack-form">
|
||||||
<label htmlFor="hours">{t('app.admin.pack_form.hours')} *</label>
|
<label htmlFor="hours">{t('app.admin.pack_form.hours')} *</label>
|
||||||
<FabInput id="hours"
|
<FabInput id="hours"
|
||||||
type="number"
|
type="number"
|
||||||
|
@ -28,7 +28,14 @@ export const ConfigureExtendedPriceButton: React.FC<ConfigureExtendedPriceButton
|
|||||||
const [showList, setShowList] = useState<boolean>(false);
|
const [showList, setShowList] = useState<boolean>(false);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Open/closes the popover listing the existing extended prices
|
* Return the number of minutes, user-friendly formatted
|
||||||
|
*/
|
||||||
|
const formatDuration = (minutes: number): string => {
|
||||||
|
return t('app.admin.configure_extended_prices_button.extended_price_DURATION', { DURATION: minutes });
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Open/closes the popover listing the existing packs
|
||||||
*/
|
*/
|
||||||
const toggleShowList = (): void => {
|
const toggleShowList = (): void => {
|
||||||
setShowList(!showList);
|
setShowList(!showList);
|
||||||
@ -57,22 +64,22 @@ export const ConfigureExtendedPriceButton: React.FC<ConfigureExtendedPriceButton
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="configure-group">
|
<div className="configure-extended-prices-button">
|
||||||
<button className="configure-group-button" onClick={toggleShowList}>
|
<button className="extended-prices-button" onClick={toggleShowList}>
|
||||||
<i className="fas fa-stopwatch" />
|
<i className="fas fa-stopwatch" />
|
||||||
</button>
|
</button>
|
||||||
{showList && <FabPopover title={t('app.admin.configure_extendedPrices_button.extendedPrices')} headerButton={renderAddButton()} className="fab-popover__right">
|
{showList && <FabPopover title={t('app.admin.configure_extended_prices_button.extended_prices')} headerButton={renderAddButton()} className="fab-popover__right">
|
||||||
<ul>
|
<ul>
|
||||||
{extendedPrices?.map(extendedPrice =>
|
{extendedPrices?.map(extendedPrice =>
|
||||||
<li key={extendedPrice.id}>
|
<li key={extendedPrice.id}>
|
||||||
{extendedPrice.duration} {t('app.admin.calendar.minutes')} - {FormatLib.price(extendedPrice.amount)}
|
{formatDuration(extendedPrice.duration)} - {FormatLib.price(extendedPrice.amount)}
|
||||||
<span className="group-actions">
|
<span className="extended-prices-actions">
|
||||||
<EditExtendedPrice onSuccess={handleSuccess} onError={onError} price={extendedPrice} />
|
<EditExtendedPrice onSuccess={handleSuccess} onError={onError} price={extendedPrice} />
|
||||||
<DeleteExtendedPrice onSuccess={handleSuccess} onError={onError} price={extendedPrice} />
|
<DeleteExtendedPrice onSuccess={handleSuccess} onError={onError} price={extendedPrice} />
|
||||||
</span>
|
</span>
|
||||||
</li>)}
|
</li>)}
|
||||||
</ul>
|
</ul>
|
||||||
{extendedPrices?.length === 0 && <span>{t('app.admin.configure_extendedPrices_button.no_extendedPrices')}</span>}
|
{extendedPrices?.length === 0 && <span>{t('app.admin.configure_extended_prices_button.no_extended_prices')}</span>}
|
||||||
</FabPopover>}
|
</FabPopover>}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -43,24 +43,24 @@ export const CreateExtendedPrice: React.FC<CreateExtendedPriceProps> = ({ onSucc
|
|||||||
// create it on the API
|
// create it on the API
|
||||||
PriceAPI.create(newExtendedPrice)
|
PriceAPI.create(newExtendedPrice)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
onSuccess(t('app.admin.create_extendedPrice.extendedPrice_successfully_created'));
|
onSuccess(t('app.admin.create_extended_price.extended_price_successfully_created'));
|
||||||
toggleModal();
|
toggleModal();
|
||||||
})
|
})
|
||||||
.catch(error => onError(error));
|
.catch(error => onError(error));
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="create-pack">
|
<div className="create-extended-price">
|
||||||
<button className="add-pack-button" onClick={toggleModal}><i className="fas fa-plus"/></button>
|
<button className="add-price-button" onClick={toggleModal}><i className="fas fa-plus"/></button>
|
||||||
<FabModal isOpen={isOpen}
|
<FabModal isOpen={isOpen}
|
||||||
toggleModal={toggleModal}
|
toggleModal={toggleModal}
|
||||||
title={t('app.admin.create_extendedPrice.new_extendedPrice')}
|
title={t('app.admin.create_extended_price.new_extended_price')}
|
||||||
className="new-pack-modal"
|
className="new-extended-price-modal"
|
||||||
closeButton
|
closeButton
|
||||||
confirmButton={t('app.admin.create_extendedPrice.create_extendedPrice')}
|
confirmButton={t('app.admin.create_extended_price.create_extended_price')}
|
||||||
onConfirmSendFormId="new-extended-price">
|
onConfirmSendFormId="new-extended-price">
|
||||||
<FabAlert level="info">
|
<FabAlert level="info">
|
||||||
{t('app.admin.create_extendedPrice.new_extendedPrice_info', { TYPE: priceableType })}
|
{t('app.admin.create_extended_price.new_extended_price_info', { TYPE: priceableType })}
|
||||||
</FabAlert>
|
</FabAlert>
|
||||||
<ExtendedPriceForm formId="new-extended-price" onSubmit={handleSubmit} />
|
<ExtendedPriceForm formId="new-extended-price" onSubmit={handleSubmit} />
|
||||||
</FabModal>
|
</FabModal>
|
||||||
|
@ -33,23 +33,23 @@ export const DeleteExtendedPrice: React.FC<DeleteExtendedPriceProps> = ({ onSucc
|
|||||||
*/
|
*/
|
||||||
const onDeleteConfirmed = (): void => {
|
const onDeleteConfirmed = (): void => {
|
||||||
PriceAPI.destroy(price.id).then(() => {
|
PriceAPI.destroy(price.id).then(() => {
|
||||||
onSuccess(t('app.admin.delete_extendedPrice.extendedPrice_deleted'));
|
onSuccess(t('app.admin.delete_extended_price.extended_price_deleted'));
|
||||||
}).catch((error) => {
|
}).catch((error) => {
|
||||||
onError(t('app.admin.delete_extendedPrice.unable_to_delete') + error);
|
onError(t('app.admin.delete_extended_price.unable_to_delete') + error);
|
||||||
});
|
});
|
||||||
toggleDeletionModal();
|
toggleDeletionModal();
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="delete-group">
|
<div className="delete-extended-price">
|
||||||
<FabButton type='button' className="delete-group-button" icon={<i className="fa fa-trash" />} onClick={toggleDeletionModal} />
|
<FabButton type='button' className="remove-price-button" icon={<i className="fa fa-trash" />} onClick={toggleDeletionModal} />
|
||||||
<FabModal title={t('app.admin.delete_extendedPrice.delete_extendedPrice')}
|
<FabModal title={t('app.admin.delete_extended_price.delete_extended_price')}
|
||||||
isOpen={deletionModal}
|
isOpen={deletionModal}
|
||||||
toggleModal={toggleDeletionModal}
|
toggleModal={toggleDeletionModal}
|
||||||
closeButton={true}
|
closeButton={true}
|
||||||
confirmButton={t('app.admin.delete_extendedPrice.confirm_delete')}
|
confirmButton={t('app.admin.delete_extended_price.confirm_delete')}
|
||||||
onConfirm={onDeleteConfirmed}>
|
onConfirm={onDeleteConfirmed}>
|
||||||
<span>{t('app.admin.delete_extendedPrice.delete_confirmation')}</span>
|
<span>{t('app.admin.delete_extended_price.delete_confirmation')}</span>
|
||||||
</FabModal>
|
</FabModal>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -42,7 +42,7 @@ export const EditExtendedPrice: React.FC<EditExtendedPriceProps> = ({ price, onS
|
|||||||
const handleUpdate = (price: Price): void => {
|
const handleUpdate = (price: Price): void => {
|
||||||
PriceAPI.update(price)
|
PriceAPI.update(price)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
onSuccess(t('app.admin.edit_extendedPrice.extendedPrice_successfully_updated'));
|
onSuccess(t('app.admin.edit_extended_price.extended_price_successfully_updated'));
|
||||||
setExtendedPriceData(price);
|
setExtendedPriceData(price);
|
||||||
toggleModal();
|
toggleModal();
|
||||||
})
|
})
|
||||||
@ -50,15 +50,16 @@ export const EditExtendedPrice: React.FC<EditExtendedPriceProps> = ({ price, onS
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="edit-group">
|
<div className="edit-extended-price">
|
||||||
<FabButton type='button' icon={<i className="fas fa-edit" />} onClick={handleRequestEdit} />
|
<FabButton type='button' className="edit-price-button" icon={<i className="fas fa-edit" />} onClick={handleRequestEdit} />
|
||||||
<FabModal isOpen={isOpen}
|
<FabModal isOpen={isOpen}
|
||||||
toggleModal={toggleModal}
|
toggleModal={toggleModal}
|
||||||
title={t('app.admin.edit_extendedPrice.edit_extendedPrice')}
|
title={t('app.admin.edit_extended_price.edit_extended_price')}
|
||||||
|
className="edit-pack-modal"
|
||||||
closeButton
|
closeButton
|
||||||
confirmButton={t('app.admin.edit_extendedPrice.confirm_changes')}
|
confirmButton={t('app.admin.edit_extended_price.confirm_changes')}
|
||||||
onConfirmSendFormId="edit-group">
|
onConfirmSendFormId="edit-extended-price">
|
||||||
{extendedPriceData && <ExtendedPriceForm formId="edit-group" onSubmit={handleUpdate} price={extendedPriceData} />}
|
{extendedPriceData && <ExtendedPriceForm formId="edit-extended-price" onSubmit={handleUpdate} price={extendedPriceData} />}
|
||||||
</FabModal>
|
</FabModal>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -9,7 +9,7 @@ declare let Fablab: IFablab;
|
|||||||
|
|
||||||
interface ExtendedPriceFormProps {
|
interface ExtendedPriceFormProps {
|
||||||
formId: string,
|
formId: string,
|
||||||
onSubmit: (pack: Price) => void,
|
onSubmit: (price: Price) => void,
|
||||||
price?: Price,
|
price?: Price,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -49,8 +49,8 @@ export const ExtendedPriceForm: React.FC<ExtendedPriceFormProps> = ({ formId, on
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form id={formId} onSubmit={handleSubmit} className="group-form">
|
<form id={formId} onSubmit={handleSubmit} className="extended-price-form">
|
||||||
<label htmlFor="duration">{t('app.admin.calendar.minutes')} *</label>
|
<label htmlFor="duration">{t('app.admin.extended_price_form.duration')} *</label>
|
||||||
<FabInput id="duration"
|
<FabInput id="duration"
|
||||||
type="number"
|
type="number"
|
||||||
defaultValue={extendedPriceData?.duration || ''}
|
defaultValue={extendedPriceData?.duration || ''}
|
||||||
|
@ -101,16 +101,17 @@ const SpacesPricing: React.FC<SpacesPricingProps> = ({ onError, onSuccess }) =>
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="pricing-list">
|
<div className="spaces-pricing">
|
||||||
<FabAlert level="warning">
|
<FabAlert level="warning">
|
||||||
<p><HtmlTranslate trKey="app.admin.pricing.these_prices_match_space_hours_rates_html"/></p>
|
<p><HtmlTranslate trKey="app.admin.spaces_pricing.prices_match_space_hours_rates_html"/></p>
|
||||||
<p><HtmlTranslate trKey="app.admin.pricing.prices_calculated_on_hourly_rate_html" options={{ DURATION: `${EXEMPLE_DURATION}`, RATE: examplePrice('hourly_rate'), PRICE: examplePrice('final_price') }} /></p>
|
<p><HtmlTranslate trKey="app.admin.spaces_pricing.prices_calculated_on_hourly_rate_html" options={{ DURATION: `${EXEMPLE_DURATION}`, RATE: examplePrice('hourly_rate'), PRICE: examplePrice('final_price') }} /></p>
|
||||||
<p>{t('app.admin.pricing.you_can_override')}</p>
|
<p>{t('app.admin.spaces_pricing.you_can_override')}</p>
|
||||||
|
<p>{t('app.admin.spaces_pricing.extended_prices')}</p>
|
||||||
</FabAlert>
|
</FabAlert>
|
||||||
<table>
|
<table>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>{t('app.admin.pricing.spaces')}</th>
|
<th>{t('app.admin.spaces_pricing.spaces')}</th>
|
||||||
{groups?.map(group => <th key={group.id} className="group-name">{group.name}</th>)}
|
{groups?.map(group => <th key={group.id} className="group-name">{group.name}</th>)}
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
|
@ -92,6 +92,15 @@ Application.Controllers.controller('InvoicesController', ['$scope', '$state', 'I
|
|||||||
active: false,
|
active: false,
|
||||||
templateUrl: '/admin/invoices/settings/editVAT.html'
|
templateUrl: '/admin/invoices/settings/editVAT.html'
|
||||||
},
|
},
|
||||||
|
multiVAT: {
|
||||||
|
rateMachine: '',
|
||||||
|
rateSpace: '',
|
||||||
|
rateTraining: '',
|
||||||
|
rateEvent: '',
|
||||||
|
rateSubscription: '',
|
||||||
|
editTemplateUrl: '/admin/invoices/settings/editMultiVAT.html',
|
||||||
|
historyTemplateUrl: '/admin/invoices/settings/multiVATHistory.html'
|
||||||
|
},
|
||||||
text: {
|
text: {
|
||||||
content: ''
|
content: ''
|
||||||
},
|
},
|
||||||
@ -217,6 +226,14 @@ Application.Controllers.controller('InvoicesController', ['$scope', '$state', 'I
|
|||||||
// Is shown the modal dialog to select a payment gateway
|
// Is shown the modal dialog to select a payment gateway
|
||||||
$scope.openSelectGatewayModal = false;
|
$scope.openSelectGatewayModal = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the VAT rate applicable to the machine reservations
|
||||||
|
* @return {number}
|
||||||
|
*/
|
||||||
|
$scope.getMachineExampleRate = function () {
|
||||||
|
return $scope.invoice.multiVAT.rateMachine || $scope.invoice.VAT.rate;
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Change the invoices ordering criterion to the one provided
|
* Change the invoices ordering criterion to the one provided
|
||||||
* @param orderBy {string} ordering criterion
|
* @param orderBy {string} ordering criterion
|
||||||
@ -446,6 +463,9 @@ Application.Controllers.controller('InvoicesController', ['$scope', '$state', 'I
|
|||||||
active () {
|
active () {
|
||||||
return $scope.invoice.VAT.active;
|
return $scope.invoice.VAT.active;
|
||||||
},
|
},
|
||||||
|
multiVAT () {
|
||||||
|
return $scope.invoice.multiVAT;
|
||||||
|
},
|
||||||
rateHistory () {
|
rateHistory () {
|
||||||
return Setting.get({ name: 'invoice_VAT-rate', history: true }).$promise;
|
return Setting.get({ name: 'invoice_VAT-rate', history: true }).$promise;
|
||||||
},
|
},
|
||||||
@ -453,13 +473,74 @@ Application.Controllers.controller('InvoicesController', ['$scope', '$state', 'I
|
|||||||
return Setting.get({ name: 'invoice_VAT-active', history: true }).$promise;
|
return Setting.get({ name: 'invoice_VAT-active', history: true }).$promise;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
controller: ['$scope', '$uibModalInstance', 'rate', 'active', 'rateHistory', 'activeHistory', function ($scope, $uibModalInstance, rate, active, rateHistory, activeHistory) {
|
controller: ['$scope', '$uibModalInstance', 'rate', 'active', 'rateHistory', 'activeHistory', 'multiVAT', function ($scope, $uibModalInstance, rate, active, rateHistory, activeHistory, multiVAT) {
|
||||||
$scope.rate = rate;
|
$scope.rate = rate;
|
||||||
$scope.isSelected = active;
|
$scope.isSelected = active;
|
||||||
$scope.history = [];
|
$scope.history = [];
|
||||||
|
|
||||||
$scope.ok = function () { $uibModalInstance.close({ rate: $scope.rate, active: $scope.isSelected }); };
|
$scope.ok = function () { $uibModalInstance.close({ rate: $scope.rate, active: $scope.isSelected }); };
|
||||||
$scope.cancel = function () { $uibModalInstance.dismiss('cancel'); };
|
$scope.cancel = function () { $uibModalInstance.dismiss('cancel'); };
|
||||||
|
$scope.editMultiVAT = function () {
|
||||||
|
const editMultiVATModalInstance = $uibModal.open({
|
||||||
|
animation: true,
|
||||||
|
templateUrl: multiVAT.editTemplateUrl,
|
||||||
|
size: 'lg',
|
||||||
|
resolve: {
|
||||||
|
rate () {
|
||||||
|
return $scope.rate;
|
||||||
|
},
|
||||||
|
multiVAT () {
|
||||||
|
return multiVAT;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
controller: ['$scope', '$uibModalInstance', 'rate', 'multiVAT', function ($scope, $uibModalInstance, rate, multiVAT) {
|
||||||
|
$scope.rate = rate;
|
||||||
|
$scope.multiVAT = multiVAT;
|
||||||
|
|
||||||
|
$scope.ok = function () { $uibModalInstance.close({ multiVAT: $scope.multiVAT }); };
|
||||||
|
$scope.cancel = function () { $uibModalInstance.dismiss('cancel'); };
|
||||||
|
|
||||||
|
$scope.showMultiRateHistory = function (rateType) {
|
||||||
|
$uibModal.open({
|
||||||
|
animation: true,
|
||||||
|
templateUrl: multiVAT.historyTemplateUrl,
|
||||||
|
size: 'lg',
|
||||||
|
resolve: {
|
||||||
|
rateHistory () {
|
||||||
|
return Setting.get({ name: `invoice_VAT-rate_${rateType}`, history: true }).$promise;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
controller: ['$scope', '$uibModalInstance', 'rateHistory', function ($scope, $uibModalInstance, rateHistory) {
|
||||||
|
$scope.history = [];
|
||||||
|
|
||||||
|
$scope.cancel = function () { $uibModalInstance.dismiss('cancel'); };
|
||||||
|
|
||||||
|
const initialize = function () {
|
||||||
|
rateHistory.setting.history.forEach(function (rate) {
|
||||||
|
$scope.history.push({ date: rate.created_at, rate: rate.value, user: rate.user });
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
initialize();
|
||||||
|
}]
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}]
|
||||||
|
});
|
||||||
|
return editMultiVATModalInstance.result.then(function (result) {
|
||||||
|
['Machine', 'Space', 'Training', 'Event', 'Subscription'].forEach(rateType => {
|
||||||
|
Setting.update({ name: `invoice_VAT-rate_${rateType}` }, { value: result.multiVAT[`rate${rateType}`] + '' }, function (data) {
|
||||||
|
return growl.success(_t('app.admin.invoices.VAT_rate_successfully_saved'));
|
||||||
|
}
|
||||||
|
, function (error) {
|
||||||
|
if (error.status === 304) return;
|
||||||
|
|
||||||
|
growl.error(_t('app.admin.invoices.an_error_occurred_while_saving_the_VAT_rate'));
|
||||||
|
console.error(error);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const initialize = function () {
|
const initialize = function () {
|
||||||
rateHistory.setting.history.forEach(function (rate) {
|
rateHistory.setting.history.forEach(function (rate) {
|
||||||
@ -943,6 +1024,11 @@ Application.Controllers.controller('InvoicesController', ['$scope', '$state', 'I
|
|||||||
$scope.invoice.text.content = settings.invoice_text;
|
$scope.invoice.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.multiVAT.rateSpace = settings['invoice_VAT-rate_Space'] ? parseFloat(settings['invoice_VAT-rate_Space']) : '';
|
||||||
|
$scope.invoice.multiVAT.rateTraining = settings['invoice_VAT-rate_Training'] ? parseFloat(settings['invoice_VAT-rate_Training']) : '';
|
||||||
|
$scope.invoice.multiVAT.rateEvent = settings['invoice_VAT-rate_Event'] ? parseFloat(settings['invoice_VAT-rate_Event']) : '';
|
||||||
|
$scope.invoice.multiVAT.rateSubscription = settings['invoice_VAT-rate_Subscription'] ? parseFloat(settings['invoice_VAT-rate_Subscription']) : '';
|
||||||
$scope.invoice.number.model = settings['invoice_order-nb'];
|
$scope.invoice.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');
|
||||||
@ -1328,6 +1414,16 @@ Application.Controllers.controller('AccountingExportModalController', ['$scope',
|
|||||||
decimalSeparator: ',',
|
decimalSeparator: ',',
|
||||||
exportInvoicesAtZero: false,
|
exportInvoicesAtZero: false,
|
||||||
columns: ['journal_code', 'date', 'account_code', 'account_label', 'piece', 'line_label', 'debit_origin', 'credit_origin', 'debit_euro', 'credit_euro', 'lettering']
|
columns: ['journal_code', 'date', 'account_code', 'account_label', 'piece', 'line_label', 'debit_origin', 'credit_origin', 'debit_euro', 'credit_euro', 'lettering']
|
||||||
|
},
|
||||||
|
vat: {
|
||||||
|
format: 'csv',
|
||||||
|
encoding: 'UTF-8',
|
||||||
|
separator: ';',
|
||||||
|
dateFormat: '%Y-%m-%d',
|
||||||
|
labelMaxLength: 'N/A',
|
||||||
|
decimalSeparator: '.',
|
||||||
|
exportInvoicesAtZero: false,
|
||||||
|
columns: ['start_date', 'end_date', 'vat_rate', 'amount']
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -1347,6 +1443,7 @@ Application.Controllers.controller('AccountingExportModalController', ['$scope',
|
|||||||
|
|
||||||
// binding to radio button "export to"
|
// binding to radio button "export to"
|
||||||
$scope.exportTarget = {
|
$scope.exportTarget = {
|
||||||
|
type: null,
|
||||||
software: null,
|
software: null,
|
||||||
startDate: null,
|
startDate: null,
|
||||||
endDate: null,
|
endDate: null,
|
||||||
|
@ -20,6 +20,11 @@ export enum SettingName {
|
|||||||
InvoiceOrderNb = 'invoice_order-nb',
|
InvoiceOrderNb = 'invoice_order-nb',
|
||||||
InvoiceVATActive = 'invoice_VAT-active',
|
InvoiceVATActive = 'invoice_VAT-active',
|
||||||
InvoiceVATRate = 'invoice_VAT-rate',
|
InvoiceVATRate = 'invoice_VAT-rate',
|
||||||
|
InvoiceVATRateMachine = 'invoice_VAT-rate_Machine',
|
||||||
|
InvoiceVATRateTraining = 'invoice_VAT-rate_Training',
|
||||||
|
InvoiceVATRateSpace = 'invoice_VAT-rate_Space',
|
||||||
|
InvoiceVATRateEvent = 'invoice_VAT-rate_Event',
|
||||||
|
InvoiceVATRateSubscription = 'invoice_VAT-rate_Subscription',
|
||||||
InvoiceText = 'invoice_text',
|
InvoiceText = 'invoice_text',
|
||||||
InvoiceLegals = 'invoice_legals',
|
InvoiceLegals = 'invoice_legals',
|
||||||
BookingWindowStart = 'booking_window_start',
|
BookingWindowStart = 'booking_window_start',
|
||||||
|
@ -869,7 +869,8 @@ 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-active', 'invoice_order-nb', 'invoice_code-value', " +
|
names: "['invoice_legals', 'invoice_text', 'invoice_VAT-rate', 'invoice_VAT-rate_Machine', 'invoice_VAT-rate_Training', 'invoice_VAT-rate_Space', " +
|
||||||
|
"'invoice_VAT-rate_Event', 'invoice_VAT-rate_Subscription', 'invoice_VAT-active', 'invoice_order-nb', 'invoice_code-value', " +
|
||||||
"'invoice_code-active', 'invoice_reference', 'invoice_logo', 'accounting_journal_code', 'accounting_card_client_code', " +
|
"'invoice_code-active', 'invoice_reference', 'invoice_logo', 'accounting_journal_code', 'accounting_card_client_code', " +
|
||||||
"'accounting_card_client_label', 'accounting_wallet_client_code', 'accounting_wallet_client_label', 'invoicing_module', " +
|
"'accounting_card_client_label', 'accounting_wallet_client_code', 'accounting_wallet_client_label', 'invoicing_module', " +
|
||||||
"'accounting_other_client_code', 'accounting_other_client_label', 'accounting_wallet_code', 'accounting_wallet_label', " +
|
"'accounting_other_client_code', 'accounting_other_client_label', 'accounting_wallet_code', 'accounting_wallet_label', " +
|
||||||
|
@ -57,12 +57,18 @@
|
|||||||
@import "modules/machines/machines-filters";
|
@import "modules/machines/machines-filters";
|
||||||
@import "modules/machines/required-training-modal";
|
@import "modules/machines/required-training-modal";
|
||||||
@import "modules/user/avatar";
|
@import "modules/user/avatar";
|
||||||
@import "modules/pricing/pricing-list";
|
|
||||||
@import "modules/pricing/editable-price";
|
@import "modules/pricing/editable-price";
|
||||||
@import "modules/pricing/configure-group-button";
|
@import "modules/pricing/machines/machines-pricing";
|
||||||
@import "modules/pricing/group-form";
|
@import "modules/pricing/machines/configure-packs-button";
|
||||||
@import "modules/pricing/delete-group";
|
@import "modules/pricing/machines/pack-form";
|
||||||
@import "modules/pricing/edit-group";
|
@import "modules/pricing/machines/delete-pack";
|
||||||
|
@import "modules/pricing/machines/edit-pack";
|
||||||
|
@import "modules/pricing/machines/create-pack";
|
||||||
|
@import "modules/pricing/spaces/configure-extended-prices-button";
|
||||||
|
@import "modules/pricing/spaces/create-extended-price";
|
||||||
|
@import "modules/pricing/spaces/delete-extended-price";
|
||||||
|
@import "modules/pricing/spaces/edit-extended-price";
|
||||||
|
@import "modules/pricing/spaces/spaces-pricing";
|
||||||
@import "modules/settings/check-list-setting";
|
@import "modules/settings/check-list-setting";
|
||||||
@import "modules/prepaid-packs/propose-packs-modal";
|
@import "modules/prepaid-packs/propose-packs-modal";
|
||||||
@import "modules/prepaid-packs/packs-summary";
|
@import "modules/prepaid-packs/packs-summary";
|
||||||
|
@ -367,3 +367,7 @@ table.export-table-template {
|
|||||||
height: 30px;
|
height: 30px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.multi-vat-rate-input {
|
||||||
|
width: 90% !important;
|
||||||
|
}
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
.configure-group {
|
.configure-packs-button {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
margin-left: 6px;
|
margin-left: 6px;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
||||||
&-button {
|
.packs-button {
|
||||||
border: 1px solid #d0cccc;
|
border: 1px solid #d0cccc;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
@ -18,13 +18,6 @@
|
|||||||
color: white;
|
color: white;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.popover-title {
|
|
||||||
.add-pack-button {
|
|
||||||
position: absolute;
|
|
||||||
right: 5px;
|
|
||||||
top: 10px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.popover-content {
|
.popover-content {
|
||||||
ul {
|
ul {
|
||||||
@ -44,7 +37,7 @@
|
|||||||
line-height: 24px;
|
line-height: 24px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.group-actions button {
|
.pack-actions button {
|
||||||
font-size: 10px;
|
font-size: 10px;
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
line-height: 10px;
|
line-height: 10px;
|
@ -0,0 +1,7 @@
|
|||||||
|
.create-pack {
|
||||||
|
.add-pack-button {
|
||||||
|
position: absolute;
|
||||||
|
right: 5px;
|
||||||
|
top: 10px;
|
||||||
|
}
|
||||||
|
}
|
@ -1,7 +1,7 @@
|
|||||||
.delete-group {
|
.delete-pack {
|
||||||
display: inline;
|
display: inline;
|
||||||
|
|
||||||
&-button {
|
.remove-pack-button {
|
||||||
background-color: #cb1117;
|
background-color: #cb1117;
|
||||||
color: white;
|
color: white;
|
||||||
}
|
}
|
@ -1,3 +1,3 @@
|
|||||||
.edit-group {
|
.edit-pack {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
}
|
}
|
@ -0,0 +1,31 @@
|
|||||||
|
.machines-pricing {
|
||||||
|
.fab-alert {
|
||||||
|
margin: 15px 0;
|
||||||
|
}
|
||||||
|
table {
|
||||||
|
overflow-y: scroll;
|
||||||
|
thead > tr > th:first-child {
|
||||||
|
width: 20%;
|
||||||
|
}
|
||||||
|
|
||||||
|
thead > tr > th.group-name {
|
||||||
|
width: 20%;
|
||||||
|
text-transform: uppercase;
|
||||||
|
font-size: 1.4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
thead > tr > th {
|
||||||
|
vertical-align: bottom;
|
||||||
|
border-bottom: 2px solid #ddd;
|
||||||
|
padding: 8px;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
tbody > tr > td {
|
||||||
|
padding: 8px;
|
||||||
|
line-height: 1.5;
|
||||||
|
vertical-align: top;
|
||||||
|
border-top: 1px solid #ddd;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,4 +1,4 @@
|
|||||||
.group-form {
|
.pack-form {
|
||||||
.interval-inputs {
|
.interval-inputs {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
@ -0,0 +1,49 @@
|
|||||||
|
.configure-extended-prices-button {
|
||||||
|
display: inline-block;
|
||||||
|
margin-left: 6px;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
.extended-prices-button {
|
||||||
|
border: 1px solid #d0cccc;
|
||||||
|
border-radius: 50%;
|
||||||
|
cursor: pointer;
|
||||||
|
width: 30px;
|
||||||
|
height: 30px;
|
||||||
|
display: inline-block;
|
||||||
|
padding: 2px 6px;
|
||||||
|
box-shadow: 0 1px 1px 0 #abaaaa;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: #b9b9b9;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.popover-content {
|
||||||
|
ul {
|
||||||
|
padding-left: 19px;
|
||||||
|
|
||||||
|
li {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
&::before {
|
||||||
|
content: '\f466';
|
||||||
|
font-family: 'Font Awesome 5 Free';
|
||||||
|
position: absolute;
|
||||||
|
left: 11px;
|
||||||
|
font-weight: 800;
|
||||||
|
font-size: 12px;
|
||||||
|
vertical-align: middle;
|
||||||
|
line-height: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.extended-prices-actions button {
|
||||||
|
font-size: 10px;
|
||||||
|
vertical-align: middle;
|
||||||
|
line-height: 10px;
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,7 @@
|
|||||||
|
.create-extended-price {
|
||||||
|
.add-price-button {
|
||||||
|
position: absolute;
|
||||||
|
right: 5px;
|
||||||
|
top: 10px;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,8 @@
|
|||||||
|
.delete-extended-price {
|
||||||
|
display: inline;
|
||||||
|
|
||||||
|
.remove-price-button {
|
||||||
|
background-color: #cb1117;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,3 @@
|
|||||||
|
.edit-extended-price {
|
||||||
|
display: inline-block;
|
||||||
|
}
|
@ -0,0 +1,10 @@
|
|||||||
|
.extended-price-form {
|
||||||
|
.interval-inputs {
|
||||||
|
display: flex;
|
||||||
|
|
||||||
|
.select-interval {
|
||||||
|
min-width: 49%;
|
||||||
|
margin-left: 4px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,4 +1,4 @@
|
|||||||
.pricing-list {
|
.spaces-pricing {
|
||||||
.fab-alert {
|
.fab-alert {
|
||||||
margin: 15px 0;
|
margin: 15px 0;
|
||||||
}
|
}
|
@ -42,11 +42,15 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<h4 class="control-label m-l" translate>{{ 'app.admin.invoices.export_to' }}</h4>
|
<h4 class="control-label m-l" translate>{{ 'app.admin.invoices.export_what' }}</h4>
|
||||||
<div class="form-group m-l-lg">
|
<div class="form-group m-l-lg">
|
||||||
<label for="acd">
|
<label for="vat" class="block">
|
||||||
|
<input type="radio" name="vat" id="vat" ng-model="exportTarget.software" ng-value="'vat'" ng-click="fillSettings('vat')" required/>
|
||||||
|
{{ 'app.admin.invoices.export_VAT' | translate }}
|
||||||
|
</label>
|
||||||
|
<label for="acd" class="block">
|
||||||
<input type="radio" name="acd" id="acd" ng-model="exportTarget.software" ng-value="'acd'" ng-click="fillSettings('acd')" required/>
|
<input type="radio" name="acd" id="acd" ng-model="exportTarget.software" ng-value="'acd'" ng-click="fillSettings('acd')" required/>
|
||||||
{{ 'app.admin.invoices.acd' | translate }}
|
{{ 'app.admin.invoices.export_to_ACD' | translate }}
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -54,12 +54,12 @@
|
|||||||
</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>{{ 'app.admin.invoices.including_VAT' | translate }} {{invoice.VAT.rate}} %</td>
|
<td translate translate-values="{RATE:getMachineExampleRate(), AMOUNT:(30.0 | currency)}">{{ 'app.admin.invoices.including_VAT' }}</td>
|
||||||
<td>{{30-(30/(invoice.VAT.rate/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">
|
||||||
<td translate>{{ 'app.admin.invoices.including_total_excluding_taxes' }}</td>
|
<td translate>{{ 'app.admin.invoices.including_total_excluding_taxes' }}</td>
|
||||||
<td>{{30/(invoice.VAT.rate/100+1) | currency}}</td>
|
<td>{{30/(getMachineExampleRate()/100+1) | currency}}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr class="invoice-payed vat-line bold" ng-show="invoice.VAT.active">
|
<tr class="invoice-payed vat-line bold" ng-show="invoice.VAT.active">
|
||||||
<td translate>{{ 'app.admin.invoices.including_amount_payed_on_ordering' }}</td>
|
<td translate>{{ 'app.admin.invoices.including_amount_payed_on_ordering' }}</td>
|
||||||
|
@ -0,0 +1,57 @@
|
|||||||
|
<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>
|
||||||
|
<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>
|
@ -22,6 +22,12 @@
|
|||||||
<input id="vatRate" type="number" ng-model="rate" class="form-control" min="0" max="100"/>
|
<input id="vatRate" type="number" ng-model="rate" class="form-control" min="0" max="100"/>
|
||||||
</div>
|
</div>
|
||||||
</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">
|
<div class="m-t-lg">
|
||||||
<h4 translate>{{ 'app.admin.invoices.VAT_history' }}</h4>
|
<h4 translate>{{ 'app.admin.invoices.VAT_history' }}</h4>
|
||||||
@ -48,6 +54,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-footer">
|
<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-warning" ng-click="ok()" translate>{{ 'app.shared.buttons.confirm' }}</button>
|
||||||
<button class="btn btn-default" ng-click="cancel()" translate>{{ 'app.shared.buttons.cancel' }}</button>
|
<button class="btn btn-default" ng-click="cancel()" translate>{{ 'app.shared.buttons.cancel' }}</button>
|
||||||
</div>
|
</div>
|
||||||
|
@ -0,0 +1,32 @@
|
|||||||
|
<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">{{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-default" ng-click="cancel()" translate>{{ 'app.shared.buttons.cancel' }}</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
@ -34,7 +34,12 @@ class AccountingPeriod < ApplicationRecord
|
|||||||
def invoices_with_vat(invoices)
|
def invoices_with_vat(invoices)
|
||||||
vat_service = VatHistoryService.new
|
vat_service = VatHistoryService.new
|
||||||
invoices.map do |i|
|
invoices.map do |i|
|
||||||
{ invoice: i, vat_rate: vat_service.invoice_vat(i) / 100.0 }
|
vat_rate_group = {}
|
||||||
|
i.invoice_items.each do |item|
|
||||||
|
vat_type = item.invoice_item_type
|
||||||
|
vat_rate_group[vat_type] = vat_service.invoice_item_vat(item) / 100.0 unless vat_rate_group[vat_type]
|
||||||
|
end
|
||||||
|
{ invoice: i, vat_rate: vat_rate_group }
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -70,7 +75,7 @@ class AccountingPeriod < ApplicationRecord
|
|||||||
end
|
end
|
||||||
|
|
||||||
def price_without_taxe(invoice)
|
def price_without_taxe(invoice)
|
||||||
invoice[:invoice].total - (invoice[:invoice].total * invoice[:vat_rate])
|
invoice[:invoice].invoice_items.map(&:net_amount).sum
|
||||||
end
|
end
|
||||||
|
|
||||||
def compute_totals
|
def compute_totals
|
||||||
|
@ -27,7 +27,7 @@ class InvoiceItem < Footprintable
|
|||||||
def net_amount
|
def net_amount
|
||||||
# deduct VAT
|
# deduct VAT
|
||||||
vat_service = VatHistoryService.new
|
vat_service = VatHistoryService.new
|
||||||
vat_rate = vat_service.invoice_vat(invoice)
|
vat_rate = vat_service.invoice_item_vat(self)
|
||||||
Rational(amount_after_coupon / (vat_rate / 100.00 + 1)).round.to_f
|
Rational(amount_after_coupon / (vat_rate / 100.00 + 1)).round.to_f
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -36,6 +36,19 @@ class InvoiceItem < Footprintable
|
|||||||
amount_after_coupon - net_amount
|
amount_after_coupon - net_amount
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# return invoice item type (Machine/Training/Space/Event/Subscription) used to determine the VAT rate
|
||||||
|
def invoice_item_type
|
||||||
|
if object_type == Reservation.name
|
||||||
|
object.try(:reservable_type) || ''
|
||||||
|
elsif [Subscription.name, OfferDay.name].include? object_type
|
||||||
|
Subscription.name
|
||||||
|
elsif object_type == StatisticProfilePrepaidPack.name
|
||||||
|
object.prepaid_pack.priceable_type
|
||||||
|
else
|
||||||
|
''
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def log_changes
|
def log_changes
|
||||||
|
@ -8,4 +8,8 @@ class Price < ApplicationRecord
|
|||||||
|
|
||||||
validates :priceable, :group_id, :amount, presence: true
|
validates :priceable, :group_id, :amount, presence: true
|
||||||
validates :priceable_id, uniqueness: { scope: %i[priceable_type plan_id group_id duration] }
|
validates :priceable_id, uniqueness: { scope: %i[priceable_type plan_id group_id duration] }
|
||||||
|
|
||||||
|
def safe_destroy
|
||||||
|
destroy unless duration == 60
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
@ -28,6 +28,11 @@ class Setting < ApplicationRecord
|
|||||||
invoice_order-nb
|
invoice_order-nb
|
||||||
invoice_VAT-active
|
invoice_VAT-active
|
||||||
invoice_VAT-rate
|
invoice_VAT-rate
|
||||||
|
invoice_VAT-rate_Machine
|
||||||
|
invoice_VAT-rate_Training
|
||||||
|
invoice_VAT-rate_Space
|
||||||
|
invoice_VAT-rate_Event
|
||||||
|
invoice_VAT-rate_Subscription
|
||||||
invoice_text
|
invoice_text
|
||||||
invoice_legals
|
invoice_legals
|
||||||
booking_window_start
|
booking_window_start
|
||||||
|
@ -230,10 +230,12 @@ class PDF::Invoice < Prawn::Document
|
|||||||
|
|
||||||
# TVA
|
# TVA
|
||||||
vat_service = VatHistoryService.new
|
vat_service = VatHistoryService.new
|
||||||
vat_rate = vat_service.invoice_vat(invoice)
|
vat_rate_group = vat_service.invoice_vat(invoice)
|
||||||
if vat_rate != 0
|
if total_vat != 0
|
||||||
data += [[I18n.t('invoices.total_including_all_taxes'), number_to_currency(total)]]
|
data += [[I18n.t('invoices.total_including_all_taxes'), number_to_currency(total)]]
|
||||||
data += [[I18n.t('invoices.including_VAT_RATE', RATE: vat_rate), number_to_currency(total_vat / 100.00)]]
|
vat_rate_group.each do |_type, rate|
|
||||||
|
data += [[I18n.t('invoices.including_VAT_RATE', RATE: rate[:vat_rate], AMOUNT: number_to_currency(rate[:amount] / 100.00)), number_to_currency(rate[:total_vat] / 100.00)]]
|
||||||
|
end
|
||||||
data += [[I18n.t('invoices.including_total_excluding_taxes'), number_to_currency(total_ht / 100.00)]]
|
data += [[I18n.t('invoices.including_total_excluding_taxes'), number_to_currency(total_ht / 100.00)]]
|
||||||
data += [[I18n.t('invoices.including_amount_payed_on_ordering'), number_to_currency(total)]]
|
data += [[I18n.t('invoices.including_amount_payed_on_ordering'), number_to_currency(total)]]
|
||||||
|
|
||||||
@ -252,23 +254,25 @@ class PDF::Invoice < Prawn::Document
|
|||||||
row(0).font_style = :bold
|
row(0).font_style = :bold
|
||||||
column(1).style align: :right
|
column(1).style align: :right
|
||||||
|
|
||||||
if Setting.get('invoice_VAT-active')
|
if total_vat != 0
|
||||||
# Total incl. taxes
|
# Total incl. taxes
|
||||||
row(-1).style align: :right
|
row(-1).style align: :right
|
||||||
row(-1).background_color = 'E4E4E4'
|
row(-1).background_color = 'E4E4E4'
|
||||||
row(-1).font_style = :bold
|
row(-1).font_style = :bold
|
||||||
|
vat_rate_group.size.times do |i|
|
||||||
# including VAT xx%
|
# including VAT xx%
|
||||||
row(-2).style align: :right
|
row(-2 - i).style align: :right
|
||||||
row(-2).background_color = 'E4E4E4'
|
row(-2 - i).background_color = 'E4E4E4'
|
||||||
row(-2).font_style = :italic
|
row(-2 - i).font_style = :italic
|
||||||
|
end
|
||||||
# including total excl. taxes
|
# including total excl. taxes
|
||||||
row(-3).style align: :right
|
row(-3 - vat_rate_group.size + 1).style align: :right
|
||||||
row(-3).background_color = 'E4E4E4'
|
row(-3 - vat_rate_group.size + 1).background_color = 'E4E4E4'
|
||||||
row(-3).font_style = :italic
|
row(-3 - vat_rate_group.size + 1).font_style = :italic
|
||||||
# including amount payed on ordering
|
# including amount payed on ordering
|
||||||
row(-4).style align: :right
|
row(-4 - vat_rate_group.size + 1).style align: :right
|
||||||
row(-4).background_color = 'E4E4E4'
|
row(-4 - vat_rate_group.size + 1).background_color = 'E4E4E4'
|
||||||
row(-4).font_style = :bold
|
row(-4 - vat_rate_group.size + 1).font_style = :bold
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -16,7 +16,6 @@ class AccountingExportService
|
|||||||
@label_max_length = 50
|
@label_max_length = 50
|
||||||
@export_zeros = false
|
@export_zeros = false
|
||||||
@journal_code = Setting.get('accounting_journal_code') || ''
|
@journal_code = Setting.get('accounting_journal_code') || ''
|
||||||
@date_format = date_format
|
|
||||||
@columns = columns
|
@columns = columns
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -134,9 +133,9 @@ class AccountingExportService
|
|||||||
|
|
||||||
# Generate the "VAT" row, which contains the credit to the VAT account, with VAT amount only
|
# Generate the "VAT" row, which contains the credit to the VAT account, with VAT amount only
|
||||||
def vat_row(invoice)
|
def vat_row(invoice)
|
||||||
rate = VatHistoryService.new.invoice_vat(invoice)
|
total = invoice.invoice_items.map(&:net_amount).sum
|
||||||
# we do not render the VAT row if it was disabled for this invoice
|
# we do not render the VAT row if it was disabled for this invoice
|
||||||
return nil if rate.zero?
|
return nil if total == invoice.total
|
||||||
|
|
||||||
row(
|
row(
|
||||||
invoice,
|
invoice,
|
||||||
|
97
app/services/vat_export_service.rb
Normal file
97
app/services/vat_export_service.rb
Normal file
@ -0,0 +1,97 @@
|
|||||||
|
# frozen_string_literal: false
|
||||||
|
|
||||||
|
# Provides the routine to export the collected VAT data to a CSV file.
|
||||||
|
class VatExportService
|
||||||
|
include ActionView::Helpers::NumberHelper
|
||||||
|
|
||||||
|
attr_reader :encoding, :format, :separator, :date_format, :columns, :decimal_separator
|
||||||
|
|
||||||
|
def initialize(columns, encoding: 'UTF-8', format: 'CSV', separator: ';')
|
||||||
|
@encoding = encoding
|
||||||
|
@format = format
|
||||||
|
@separator = separator
|
||||||
|
@decimal_separator = '.'
|
||||||
|
@date_format = '%Y-%m-%d'
|
||||||
|
@columns = columns
|
||||||
|
end
|
||||||
|
|
||||||
|
def set_options(decimal_separator: ',', date_format: '%d/%m/%Y', label_max_length: nil, export_zeros: nil)
|
||||||
|
@decimal_separator = decimal_separator
|
||||||
|
@date_format = date_format
|
||||||
|
end
|
||||||
|
|
||||||
|
def export(start_date, end_date, file)
|
||||||
|
# build CSV content
|
||||||
|
content = header_row
|
||||||
|
invoices = Invoice.where('created_at >= ? AND created_at <= ?', start_date, end_date).order('created_at ASC')
|
||||||
|
vat_totals = compute_vat_totals(invoices)
|
||||||
|
content << generate_rows(vat_totals, start_date, end_date)
|
||||||
|
|
||||||
|
# write content to file
|
||||||
|
File.open(file, "w:#{encoding}") { |f| f.puts content.encode(encoding, invalid: :replace, undef: :replace) }
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def header_row
|
||||||
|
row = ''
|
||||||
|
columns.each do |column|
|
||||||
|
row << I18n.t("vat_export.#{column}") << separator
|
||||||
|
end
|
||||||
|
"#{row}\n"
|
||||||
|
end
|
||||||
|
|
||||||
|
def generate_rows(vat_totals, start_date, end_date)
|
||||||
|
rows = ''
|
||||||
|
|
||||||
|
vat_totals.each do |rate, total|
|
||||||
|
next if rate.zero?
|
||||||
|
|
||||||
|
rows += "#{row(
|
||||||
|
start_date,
|
||||||
|
end_date,
|
||||||
|
rate,
|
||||||
|
total
|
||||||
|
)}\n"
|
||||||
|
end
|
||||||
|
|
||||||
|
rows
|
||||||
|
end
|
||||||
|
|
||||||
|
def compute_vat_totals(invoices)
|
||||||
|
vat_total = []
|
||||||
|
service = VatHistoryService.new
|
||||||
|
invoices.each do |i|
|
||||||
|
puts "processing invoice #{i.id}..." unless Rails.env.test?
|
||||||
|
vat_total.push service.invoice_vat(i)
|
||||||
|
end
|
||||||
|
|
||||||
|
vat_total.map(&:values).flatten.group_by { |tot| tot[:vat_rate] }.map { |k, v| [k, v.map { |t| t[:total_vat] }.reduce(:+)] }.to_h
|
||||||
|
end
|
||||||
|
|
||||||
|
# Generate a row of the export, filling the configured columns with the provided values
|
||||||
|
def row(start_date, end_date, vat_rate, amount)
|
||||||
|
row = ''
|
||||||
|
columns.each do |column|
|
||||||
|
case column
|
||||||
|
when 'start_date'
|
||||||
|
row << DateTime.parse(start_date).strftime(date_format)
|
||||||
|
when 'end_date'
|
||||||
|
row << DateTime.parse(end_date).strftime(date_format)
|
||||||
|
when 'vat_rate'
|
||||||
|
row << vat_rate.to_s
|
||||||
|
when 'amount'
|
||||||
|
row << format_number(amount / 100.0)
|
||||||
|
else
|
||||||
|
puts "Unsupported column: #{column}"
|
||||||
|
end
|
||||||
|
row << separator
|
||||||
|
end
|
||||||
|
row
|
||||||
|
end
|
||||||
|
|
||||||
|
# Format the given number as a string, using the configured separator
|
||||||
|
def format_number(num)
|
||||||
|
number_to_currency(num, unit: '', separator: decimal_separator, delimiter: '', precision: 2)
|
||||||
|
end
|
||||||
|
end
|
@ -2,30 +2,42 @@
|
|||||||
|
|
||||||
# Provides the VAT rate in use at the given date
|
# Provides the VAT rate in use at the given date
|
||||||
class VatHistoryService
|
class VatHistoryService
|
||||||
# return the VAT rate for the given Invoice/Avoir
|
# @return the VAT rate for the given Invoice
|
||||||
def invoice_vat(invoice)
|
def invoice_vat(invoice)
|
||||||
if invoice.is_a?(Avoir)
|
vat_rate_group = {}
|
||||||
vat_rate(invoice.avoir_date)
|
invoice.invoice_items.each do |item|
|
||||||
|
vat_type = item.invoice_item_type
|
||||||
|
vat_rate_group[vat_type] = { vat_rate: invoice_item_vat(item), total_vat: 0, amount: 0 } unless vat_rate_group[vat_type]
|
||||||
|
vat_rate_group[vat_type][:total_vat] += item.vat
|
||||||
|
vat_rate_group[vat_type][:amount] += item.amount.to_i
|
||||||
|
end
|
||||||
|
vat_rate_group
|
||||||
|
end
|
||||||
|
|
||||||
|
# return the VAT rate for the given InvoiceItem
|
||||||
|
def invoice_item_vat(invoice_item)
|
||||||
|
if invoice_item.invoice.is_a?(Avoir)
|
||||||
|
vat_rate(invoice_item.invoice.avoir_date, invoice_item.invoice_item_type)
|
||||||
else
|
else
|
||||||
vat_rate(invoice.created_at)
|
vat_rate(invoice_item.invoice.created_at, invoice_item.invoice_item_type)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# return the VAT rate for the given date
|
# return the VAT rate for the given date and vat type
|
||||||
def vat_rate(date)
|
def vat_rate(date, vat_rate_type)
|
||||||
@vat_rates = vat_history if @vat_rates.nil?
|
vat_rates = vat_history(vat_rate_type)
|
||||||
|
|
||||||
first_rate = @vat_rates.first
|
first_rate = vat_rates.first
|
||||||
return first_rate[:rate] if date < first_rate[:date]
|
return first_rate[:rate] if date < first_rate[:date]
|
||||||
|
|
||||||
@vat_rates.each_index do |i|
|
vat_rates.each_index do |i|
|
||||||
return @vat_rates[i][:rate] if date >= @vat_rates[i][:date] && (@vat_rates[i + 1].nil? || date < @vat_rates[i + 1][:date])
|
return vat_rates[i][:rate] if date >= vat_rates[i][:date] && (vat_rates[i + 1].nil? || date < vat_rates[i + 1][:date])
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def vat_history
|
def vat_history(vat_rate_type)
|
||||||
chronology = []
|
chronology = []
|
||||||
end_date = DateTime.current
|
end_date = DateTime.current
|
||||||
Setting.find_by(name: 'invoice_VAT-active').history_values.order(created_at: 'DESC').each do |v|
|
Setting.find_by(name: 'invoice_VAT-active').history_values.order(created_at: 'DESC').each do |v|
|
||||||
@ -33,15 +45,65 @@ class VatHistoryService
|
|||||||
end_date = v.created_at
|
end_date = v.created_at
|
||||||
end
|
end
|
||||||
chronology.push(start: DateTime.new(0), end: end_date, enabled: false)
|
chronology.push(start: DateTime.new(0), end: end_date, enabled: false)
|
||||||
|
# now chronology contains something like one of the following:
|
||||||
|
# - [{start: 0000-01-01, end: now, enabled: false}] => VAT was never enabled
|
||||||
|
# - [
|
||||||
|
# {start: fab-manager initial setup date, end: now, enabled: true},
|
||||||
|
# {start: 0000-01-01, end: fab-manager initial setup date, enabled: false}
|
||||||
|
# ] => VAT was enabled from the beginning
|
||||||
|
# - [
|
||||||
|
# {start: [date disabled], end: now, enabled: false},
|
||||||
|
# {start: [date enable], end: [date disabled], enabled: true},
|
||||||
|
# {start: fab-manager initial setup date, end: [date enabled], enabled: false},
|
||||||
|
# {start: 0000-01-01, end: fab-manager initial setup date, enabled: false}
|
||||||
|
# ] => VAT was enabled at some point, and disabled at some other point later
|
||||||
|
|
||||||
date_rates = []
|
date_rates = []
|
||||||
Setting.find_by(name: 'invoice_VAT-rate').history_values.order(created_at: 'ASC').each do |rate|
|
if vat_rate_type.present?
|
||||||
|
vat_rate_by_type = Setting.find_by(name: "invoice_VAT-rate_#{vat_rate_type}")&.history_values&.order(created_at: 'ASC')
|
||||||
|
first_vat_rate_by_type = vat_rate_by_type&.select { |v| v.value.present? }&.first
|
||||||
|
if first_vat_rate_by_type
|
||||||
|
# before the first VAT rate was defined for the given type, the general VAT rate is used
|
||||||
|
vat_rate_history_values = Setting.find_by(name: 'invoice_VAT-rate')
|
||||||
|
.history_values.where('created_at < ?', first_vat_rate_by_type.created_at)
|
||||||
|
.order(created_at: 'ASC').to_a
|
||||||
|
# after that, the VAT rate for the given type is used
|
||||||
|
vat_rate_by_type = Setting.find_by(name: "invoice_VAT-rate_#{vat_rate_type}")
|
||||||
|
.history_values.where('created_at >= ?', first_vat_rate_by_type.created_at)
|
||||||
|
.order(created_at: 'ASC')
|
||||||
|
vat_rate_by_type.each do |rate|
|
||||||
|
if rate.value.blank?
|
||||||
|
# if, at some point in the history, a blank rate was set, the general VAT rate is used instead
|
||||||
|
vat_rate = Setting.find_by(name: 'invoice_VAT-rate')
|
||||||
|
.history_values.where('created_at < ?', rate.created_at)
|
||||||
|
.order(created_at: 'DESC')
|
||||||
|
.first
|
||||||
|
rate.value = vat_rate.value
|
||||||
|
end
|
||||||
|
vat_rate_history_values.push(rate)
|
||||||
|
end
|
||||||
|
else
|
||||||
|
# if no VAT rate is defined for the given type, the general VAT rate is always used
|
||||||
|
vat_rate_history_values = Setting.find_by(name: 'invoice_VAT-rate').history_values.order(created_at: 'ASC').to_a
|
||||||
|
end
|
||||||
|
|
||||||
|
# Now we have all the rates history, we can build the final chronology, depending on whether VAT was enabled or not
|
||||||
|
vat_rate_history_values.each do |rate|
|
||||||
|
# when the VAT rate was enabled, set the date it was enabled and the rate
|
||||||
range = chronology.select { |p| rate.created_at.to_i.between?(p[:start].to_i, p[:end].to_i) }.first
|
range = chronology.select { |p| rate.created_at.to_i.between?(p[:start].to_i, p[:end].to_i) }.first
|
||||||
date = range[:enabled] ? rate.created_at : range[:end]
|
date = range[:enabled] ? rate.created_at : range[:end]
|
||||||
date_rates.push(date: date, rate: rate.value.to_i)
|
date_rates.push(date: date, rate: rate.value.to_i)
|
||||||
end
|
end
|
||||||
chronology.reverse_each do |period|
|
chronology.reverse_each do |period|
|
||||||
|
# when the VAT rate was disabled, set the date it was disabled and rate=0
|
||||||
date_rates.push(date: period[:start], rate: 0) unless period[:enabled]
|
date_rates.push(date: period[:start], rate: 0) unless period[:enabled]
|
||||||
end
|
end
|
||||||
|
else
|
||||||
|
# if no VAT rate type is given, we return rate=0 from 0000-01-01
|
||||||
|
date_rates.push(date: chronology[-1][:start], rate: 0)
|
||||||
|
end
|
||||||
|
|
||||||
|
# finally, we return the chronology, sorted by dates (ascending)
|
||||||
date_rates.sort_by { |k| k[:date] }
|
date_rates.sort_by { |k| k[:date] }
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -34,7 +34,7 @@ json.invoices do
|
|||||||
json.id item.object_id
|
json.id item.object_id
|
||||||
json.main item.main
|
json.main item.main
|
||||||
end
|
end
|
||||||
json.partial! 'archive/vat', price: item.amount, vat_rate: invoice[:vat_rate]
|
json.partial! 'archive/vat', price: item.amount, vat_rate: invoice[:vat_rate][item.invoice_item_type]
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -10,7 +10,8 @@ class AccountingExportWorker
|
|||||||
raise SecurityError, 'Not allowed to export' unless export.user.admin?
|
raise SecurityError, 'Not allowed to export' unless export.user.admin?
|
||||||
|
|
||||||
data = JSON.parse(export.query)
|
data = JSON.parse(export.query)
|
||||||
service = AccountingExportService.new(
|
service = export.export_type == 'vat' ? VatExportService : AccountingExportService
|
||||||
|
service = service.new(
|
||||||
data['columns'],
|
data['columns'],
|
||||||
encoding: data['encoding'], format: export.extension, separator: export.key
|
encoding: data['encoding'], format: export.extension, separator: export.key
|
||||||
)
|
)
|
||||||
|
@ -369,6 +369,11 @@ en:
|
|||||||
status_disabled: "Disabled"
|
status_disabled: "Disabled"
|
||||||
status_all: "All"
|
status_all: "All"
|
||||||
spaces_pricing:
|
spaces_pricing:
|
||||||
|
prices_match_space_hours_rates_html: "The prices below match one hour of space reservation, <strong>without subscription</strong>."
|
||||||
|
prices_calculated_on_hourly_rate_html: "All the prices will be automatically calculated based on the hourly rate defined here.<br/><em>For example</em>, if you define an hourly rate at {RATE}: a slot of {DURATION} minutes, will be charged <strong>{PRICE}</strong>."
|
||||||
|
you_can_override: "You can override this duration for each availability you create in the agenda. The price will then be adjusted accordingly."
|
||||||
|
extended_prices: "Moreover, you can define extended prices which will apply in priority over the hourly rate below. Extended prices allow you, for example, to set a favorable price for a booking of several hours."
|
||||||
|
spaces: "Spaces"
|
||||||
price_updated: "Price successfully updated"
|
price_updated: "Price successfully updated"
|
||||||
machines_pricing:
|
machines_pricing:
|
||||||
prices_match_machine_hours_rates_html: "The prices below match one hour of machine usage, <strong>without subscription</strong>."
|
prices_match_machine_hours_rates_html: "The prices below match one hour of machine usage, <strong>without subscription</strong>."
|
||||||
@ -380,10 +385,12 @@ en:
|
|||||||
packs: "Prepaid packs"
|
packs: "Prepaid packs"
|
||||||
no_packs: "No packs for now"
|
no_packs: "No packs for now"
|
||||||
pack_DURATION: "{DURATION} hours"
|
pack_DURATION: "{DURATION} hours"
|
||||||
configure_extendedPrices_button:
|
configure_extended_prices_button:
|
||||||
extendedPrices: "Extended prices"
|
extended_prices: "Extended prices"
|
||||||
no_extendedPrices: "No extended price for now"
|
no_extended_prices: "No extended price for now"
|
||||||
extended_prices_form:
|
extended_price_DURATION: "{DURATION} minutes"
|
||||||
|
extended_price_form:
|
||||||
|
duration: "Duration (minutes)"
|
||||||
amount: "Price"
|
amount: "Price"
|
||||||
pack_form:
|
pack_form:
|
||||||
hours: "Hours"
|
hours: "Hours"
|
||||||
@ -411,21 +418,21 @@ en:
|
|||||||
edit_pack: "Edit the pack"
|
edit_pack: "Edit the pack"
|
||||||
confirm_changes: "Confirm changes"
|
confirm_changes: "Confirm changes"
|
||||||
pack_successfully_updated: "The prepaid pack was successfully updated."
|
pack_successfully_updated: "The prepaid pack was successfully updated."
|
||||||
create_extendedPrice:
|
create_extended_price:
|
||||||
new_extendedPrice: "New extended price"
|
new_extended_price: "New extended price"
|
||||||
new_extendedPrice_info: "Extended prices allows you to define prices based on custom durations, intead on the default hourly rates."
|
new_extended_price_info: "Extended prices allows you to define prices based on custom durations, instead of the default hourly rates."
|
||||||
create_extendedPrice: "Create extended price"
|
create_extended_price: "Create extended price"
|
||||||
extendedPrice_successfully_created: "The new extended price was successfully created."
|
extended_price_successfully_created: "The new extended price was successfully created."
|
||||||
delete_extendedPrice:
|
delete_extended_price:
|
||||||
extendedPrice_deleted: "The extended price was successfully deleted."
|
extended_price_deleted: "The extended price was successfully deleted."
|
||||||
unable_to_delete: "Unable to delete the extended price: "
|
unable_to_delete: "Unable to delete the extended price: "
|
||||||
delete_extendedPrice: "Delete the extended price"
|
delete_extended_price: "Delete the extended price"
|
||||||
confirm_delete: "Delete"
|
confirm_delete: "Delete"
|
||||||
delete_confirmation: "Are you sure you want to delete this extended price? This won't be possible if it was already bought by users."
|
delete_confirmation: "Are you sure you want to delete this extended price?"
|
||||||
edit_extendedPrice:
|
edit_extended_price:
|
||||||
edit_extendedPrice: "Edit the extended price"
|
edit_extended_price: "Edit the extended price"
|
||||||
confirm_changes: "Confirm changes"
|
confirm_changes: "Confirm changes"
|
||||||
extendedPrice_successfully_updated: "The extended price was successfully updated."
|
extended_price_successfully_updated: "The extended price was successfully updated."
|
||||||
#ajouter un code promotionnel
|
#ajouter un code promotionnel
|
||||||
coupons_new:
|
coupons_new:
|
||||||
add_a_coupon: "Add a coupon"
|
add_a_coupon: "Add a coupon"
|
||||||
@ -488,13 +495,14 @@ en:
|
|||||||
details: "Details"
|
details: "Details"
|
||||||
amount: "Amount"
|
amount: "Amount"
|
||||||
machine_booking-3D_printer: "Machine booking - 3D printer"
|
machine_booking-3D_printer: "Machine booking - 3D printer"
|
||||||
|
training_booking-3D_print: "Training booking - initiation to 3d printing"
|
||||||
total_amount: "Total amount"
|
total_amount: "Total amount"
|
||||||
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"
|
including_VAT: "Including VAT {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}"
|
||||||
important_notes: "Important notes"
|
important_notes: "Important notes"
|
||||||
address_and_legal_information: "Address and legal information"
|
address_and_legal_information: "Address and legal information"
|
||||||
@ -544,6 +552,15 @@ en:
|
|||||||
enable_VAT: "Enable VAT"
|
enable_VAT: "Enable VAT"
|
||||||
VAT_rate: "VAT rate"
|
VAT_rate: "VAT rate"
|
||||||
VAT_history: "VAT rates history"
|
VAT_history: "VAT rates history"
|
||||||
|
VAT_notice: "This parameter configures the general case of the VAT rate and applies to everything sold by the Fablab. It is possible to override this parameter by setting a specific VAT rate for each object."
|
||||||
|
edit_multi_VAT_button: "More options"
|
||||||
|
multiVAT: "Advanced VAT"
|
||||||
|
multi_VAT_notice: "<strong>Please note</strong>: The current general rate is {RATE}%. Here 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 below. If no value is filled in, the general rate will apply."
|
||||||
|
VAT_rate_machine: "Machine reservation"
|
||||||
|
VAT_rate_space: "Space reservation"
|
||||||
|
VAT_rate_training: "Training reservation"
|
||||||
|
VAT_rate_event: "Event reservation"
|
||||||
|
VAT_rate_subscription: "Subscription"
|
||||||
changed_at: "Changed at"
|
changed_at: "Changed at"
|
||||||
changed_by: "By"
|
changed_by: "By"
|
||||||
deleted_user: "Deleted user"
|
deleted_user: "Deleted user"
|
||||||
@ -662,9 +679,10 @@ en:
|
|||||||
codes_customization_success: "Customization of the accounting codes successfully saved."
|
codes_customization_success: "Customization of the accounting codes successfully saved."
|
||||||
unexpected_error_occurred: "An unexpected error occurred while saving the codes. Please try again later."
|
unexpected_error_occurred: "An unexpected error occurred while saving the codes. Please try again later."
|
||||||
export_accounting_data: "Export accounting data"
|
export_accounting_data: "Export accounting data"
|
||||||
export_to: "Export to the accounting software"
|
export_what: "What do you want to export?"
|
||||||
|
export_VAT: "Export the collected VAT"
|
||||||
|
export_to_ACD: "Export all data to the accounting software ACD"
|
||||||
export_is_running: "Export is running. You'll be notified when it's ready."
|
export_is_running: "Export is running. You'll be notified when it's ready."
|
||||||
acd: "ACD"
|
|
||||||
export_form_date: "Export from"
|
export_form_date: "Export from"
|
||||||
export_to_date: "Export until"
|
export_to_date: "Export until"
|
||||||
format: "File format"
|
format: "File format"
|
||||||
@ -687,6 +705,10 @@ en:
|
|||||||
debit_euro: "Euro debit"
|
debit_euro: "Euro debit"
|
||||||
credit_euro: "Euro credit"
|
credit_euro: "Euro credit"
|
||||||
lettering: "Lettering"
|
lettering: "Lettering"
|
||||||
|
start_date: "Start date"
|
||||||
|
end_date: "End date"
|
||||||
|
vat_rate: "VAT rate"
|
||||||
|
amount: "Total amount"
|
||||||
payment:
|
payment:
|
||||||
payment_settings: "Payment settings"
|
payment_settings: "Payment settings"
|
||||||
online_payment: "Online payment"
|
online_payment: "Online payment"
|
||||||
|
@ -90,7 +90,7 @@ 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}%"
|
including_VAT_RATE: "Including VAT %{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"
|
||||||
@ -147,6 +147,11 @@ en:
|
|||||||
Event_reservation: "event reserv."
|
Event_reservation: "event reserv."
|
||||||
Space_reservation: "space reserv."
|
Space_reservation: "space reserv."
|
||||||
wallet: "wallet"
|
wallet: "wallet"
|
||||||
|
vat_export:
|
||||||
|
start_date: "Start date"
|
||||||
|
end_date: "End date"
|
||||||
|
vat_rate: "VAT rate"
|
||||||
|
amount: "Total amount"
|
||||||
#training availabilities
|
#training availabilities
|
||||||
trainings:
|
trainings:
|
||||||
i_ve_reserved: "I've reserved"
|
i_ve_reserved: "I've reserved"
|
||||||
@ -331,6 +336,7 @@ en:
|
|||||||
users_reservations: "of the reservations' list"
|
users_reservations: "of the reservations' list"
|
||||||
availabilities_index: "of the reservations availabilities"
|
availabilities_index: "of the reservations availabilities"
|
||||||
accounting_acd: "of the accounting data to ACD"
|
accounting_acd: "of the accounting data to ACD"
|
||||||
|
accounting_vat: "of the collected VAT"
|
||||||
is_over: "is over."
|
is_over: "is over."
|
||||||
download_here: "Download here"
|
download_here: "Download here"
|
||||||
notify_admin_import_complete:
|
notify_admin_import_complete:
|
||||||
|
@ -231,7 +231,8 @@ en:
|
|||||||
users_subscriptions: "of the subscriptions' list"
|
users_subscriptions: "of the subscriptions' list"
|
||||||
users_reservations: "of the reservations' list"
|
users_reservations: "of the reservations' list"
|
||||||
availabilities_index: "of the reservations availabilities"
|
availabilities_index: "of the reservations availabilities"
|
||||||
accounting_accounting-software: "of the accounting data"
|
accounting_acd: "of the accounting data to ACD"
|
||||||
|
accounting_vat: "of the collected VAT data"
|
||||||
click_to_download: "Excel file generated successfully. To download it, click"
|
click_to_download: "Excel file generated successfully. To download it, click"
|
||||||
here: "here"
|
here: "here"
|
||||||
file_type:
|
file_type:
|
||||||
|
@ -104,13 +104,15 @@ class ActiveSupport::TestCase
|
|||||||
end
|
end
|
||||||
|
|
||||||
vat_service = VatHistoryService.new
|
vat_service = VatHistoryService.new
|
||||||
vat_rate = vat_service.invoice_vat(invoice)
|
invoice.invoice_items.each do |item|
|
||||||
|
vat_rate = vat_service.invoice_item_vat(item)
|
||||||
if vat_rate.positive?
|
if vat_rate.positive?
|
||||||
computed_ht = sprintf('%.2f', (invoice.total / (vat_rate / 100.00 + 1)) / 100.00).to_f
|
computed_ht = sprintf('%.2f', (item.amount_after_coupon / (vat_rate / 100.00 + 1)) / 100.00).to_f
|
||||||
|
|
||||||
assert_equal computed_ht, ht_amount, 'Total excluding taxes rendered in the PDF file is not computed correctly'
|
assert_equal computed_ht, item.net_amount / 100.00, 'Total excluding taxes rendered in the PDF file is not computed correctly'
|
||||||
else
|
else
|
||||||
assert_equal invoice.total, ht_amount, 'VAT information was rendered in the PDF file despite that VAT was disabled'
|
assert_equal item.amount_after_coupon, item.net_amount, 'VAT information was rendered in the PDF file despite that VAT was disabled'
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# check the recipient & the address
|
# check the recipient & the address
|
||||||
|
Loading…
Reference in New Issue
Block a user