/* eslint-disable handle-callback-err, no-return-assign, no-undef, no-useless-escape, */ // TODO: This file was created by bulk-decaffeinate. // Fix any style issues and re-enable lint. /* * decaffeinate suggestions: * DS101: Remove unnecessary use of Array.from * DS102: Remove unnecessary code created because of implicit returns * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md */ 'use strict'; /** * Controller used in the admin invoices listing page */ Application.Controllers.controller('InvoicesController', ['$scope', '$state', 'Invoice', 'AccountingPeriod', 'AuthService', 'invoices', 'closedPeriods', '$uibModal', 'growl', '$filter', 'Setting', 'settings', 'stripeSecretKey', '_t', 'Member', 'uiTourService', 'Payment', 'onlinePaymentStatus', function ($scope, $state, Invoice, AccountingPeriod, AuthService, invoices, closedPeriods, $uibModal, growl, $filter, Setting, settings, stripeSecretKey, _t, Member, uiTourService, Payment, onlinePaymentStatus) { /* PRIVATE STATIC CONSTANTS */ // number of invoices loaded each time we click on 'load more...' const INVOICES_PER_PAGE = 20; // fake stripe secret key const STRIPE_SK_HIDDEN = 'sk_test_hidden-hidden-hidden-hid'; /* PUBLIC SCOPE */ // default active tab $scope.tabs = { active: settings.invoicing_module === 'true' ? 0 : 1 }; // List of all invoices $scope.invoices = invoices; // Invoices filters $scope.searchInvoice = { date: null, name: '', reference: '' }; // currently displayed page of invoices (search results) $scope.page = 1; // true when all invoices are loaded $scope.noMoreResults = false; // Default invoices ordering/sorting $scope.orderInvoice = '-reference'; // Invoice PDF filename settings (and example) $scope.file = { prefix: settings.invoice_prefix, nextId: 40, date: moment().format('DDMMYYYY'), templateUrl: 'editPrefix.html' } // Invoices parameters $scope.invoice = { logo: null, reference: { model: '', help: null, templateUrl: 'editReference.html' }, code: { model: '', active: true, templateUrl: 'editCode.html' }, number: { model: '', help: null, templateUrl: 'editNumber.html' }, VAT: { rate: 19.6, active: false, templateUrl: 'editVAT.html' }, text: { content: '' }, legals: { content: '' } }; // Accounting codes $scope.settings = { journalCode: { name: 'accounting_journal_code', value: settings['accounting_journal_code'] }, cardClientCode: { name: 'accounting_card_client_code', value: settings['accounting_card_client_code'] }, cardClientLabel: { name: 'accounting_card_client_label', value: settings['accounting_card_client_label'] }, walletClientCode: { name: 'accounting_wallet_client_code', value: settings['accounting_wallet_client_code'] }, walletClientLabel: { name: 'accounting_wallet_client_label', value: settings['accounting_wallet_client_label'] }, otherClientCode: { name: 'accounting_other_client_code', value: settings['accounting_other_client_code'] }, otherClientLabel: { name: 'accounting_other_client_label', value: settings['accounting_other_client_label'] }, walletCode: { name: 'accounting_wallet_code', value: settings['accounting_wallet_code'] }, walletLabel: { name: 'accounting_wallet_label', value: settings['accounting_wallet_label'] }, vatCode: { name: 'accounting_VAT_code', value: settings['accounting_VAT_code'] }, vatLabel: { name: 'accounting_VAT_label', value: settings['accounting_VAT_label'] }, subscriptionCode: { name: 'accounting_subscription_code', value: settings['accounting_subscription_code'] }, subscriptionLabel: { name: 'accounting_subscription_label', value: settings['accounting_subscription_label'] }, machineCode: { name: 'accounting_Machine_code', value: settings['accounting_Machine_code'] }, machineLabel: { name: 'accounting_Machine_label', value: settings['accounting_Machine_label'] }, trainingCode: { name: 'accounting_Training_code', value: settings['accounting_Training_code'] }, trainingLabel: { name: 'accounting_Training_label', value: settings['accounting_Training_label'] }, eventCode: { name: 'accounting_Event_code', value: settings['accounting_Event_code'] }, eventLabel: { name: 'accounting_Event_label', value: settings['accounting_Event_label'] }, spaceCode: { name: 'accounting_Space_code', value: settings['accounting_Space_code'] }, spaceLabel: { name: 'accounting_Space_label', value: settings['accounting_Space_label'] } }; // all settings $scope.allSettings = settings; // is the stripe private set? $scope.stripeSecretKey = (stripeSecretKey.isPresent ? STRIPE_SK_HIDDEN : ''); // has any online payment been already made? $scope.onlinePaymentStatus = onlinePaymentStatus.status; // Placeholding date for the invoice creation $scope.today = moment(); // Placeholding date for the reservation begin $scope.inOneWeek = moment().add(1, 'week').startOf('hour'); // Placeholding date for the reservation end $scope.inOneWeekAndOneHour = moment().add(1, 'week').add(1, 'hour').startOf('hour'); /** * Change the invoices ordering criterion to the one provided * @param orderBy {string} ordering criterion */ $scope.setOrderInvoice = function (orderBy) { if ($scope.orderInvoice === orderBy) { $scope.orderInvoice = `-${orderBy}`; } else { $scope.orderInvoice = orderBy; } resetSearchInvoice(); return invoiceSearch(); }; /** * Open a modal window asking the admin the details to refund the user about the provided invoice * @param invoice {Object} invoice inherited from angular's $resource */ $scope.generateAvoirForInvoice = function (invoice) { // open modal const modalInstance = $uibModal.open({ templateUrl: '<%= asset_path "admin/invoices/avoirModal.html" %>', controller: 'AvoirModalController', resolve: { invoice () { return invoice; }, closedPeriods() { return AccountingPeriod.query().$promise; }, lastClosingEnd() { return AccountingPeriod.lastClosingEnd().$promise; } } }); // once done, update the invoice model and inform the admin return modalInstance.result.then(function (res) { $scope.invoices.unshift(res.avoir); return Invoice.get({ id: invoice.id }, function (data) { invoice.has_avoir = data.has_avoir; return growl.success(_t('app.admin.invoices.refund_invoice_successfully_created')); }); }); }; /** * Generate an invoice reference sample from the parametrized model * @returns {string} invoice reference sample */ $scope.mkReference = function () { let sample = $scope.invoice.reference.model; if (sample) { // invoice number per day (dd..dd) sample = sample.replace(/d+(?![^\[]*])/g, function (match, offset, string) { return padWithZeros(2, match.length); }); // invoice number per month (mm..mm) sample = sample.replace(/m+(?![^\[]*])/g, function (match, offset, string) { return padWithZeros(12, match.length); }); // invoice number per year (yy..yy) sample = sample.replace(/y+(?![^\[]*])/g, function (match, offset, string) { return padWithZeros(8, match.length); }); // date information sample = sample.replace(/[YMD]+(?![^\[]*])/g, function (match, offset, string) { return $scope.today.format(match); }); // information about online selling (X[text]) sample = sample.replace(/X\[([^\]]+)\]/g, function (match, p1, offset, string) { return p1; }); // information about wallet (W[text]) - does not apply here sample = sample.replace(/W\[([^\]]+)\]/g, ''); // information about refunds (R[text]) - does not apply here sample = sample.replace(/R\[([^\]]+)\]/g, ''); } return sample; }; /** * Generate an order number sample from the parametrized model * @returns {string} invoice reference sample */ $scope.mkNumber = function () { let sample = $scope.invoice.number.model; if (sample) { // global order number (nn..nn) sample = sample.replace(/n+(?![^\[]*])/g, function (match, offset, string) { return padWithZeros(327, match.length); }); // order number per year (yy..yy) sample = sample.replace(/y+(?![^\[]*])/g, function (match, offset, string) { return padWithZeros(8, match.length); }); // order number per month (mm..mm) sample = sample.replace(/m+(?![^\[]*])/g, function (match, offset, string) { return padWithZeros(12, match.length); }); // order number per day (dd..dd) sample = sample.replace(/d+(?![^\[]*])/g, function (match, offset, string) { return padWithZeros(2, match.length); }); // date information sample = sample.replace(/[YMD]+(?![^\[]*])/g, function (match, offset, string) { return $scope.today.format(match); }); } return sample; }; /** * Open a modal dialog allowing the user to edit the invoice reference generation template */ $scope.openEditReference = function () { const modalInstance = $uibModal.open({ animation: true, templateUrl: $scope.invoice.reference.templateUrl, size: 'lg', resolve: { model () { return $scope.invoice.reference.model; } }, controller: ['$scope', '$uibModalInstance', 'model', function ($scope, $uibModalInstance, model) { $scope.model = model; $scope.ok = function () { $uibModalInstance.close($scope.model); }; $scope.cancel = function () { $uibModalInstance.dismiss('cancel'); }; }] }); modalInstance.result.then(function (model) { Setting.update({ name: 'invoice_reference' }, { value: model }, function (data) { $scope.invoice.reference.model = model; growl.success(_t('app.admin.invoices.invoice_reference_successfully_saved')); } , function (error) { if (error.status === 304) return; growl.error(_t('app.admin.invoices.an_error_occurred_while_saving_invoice_reference')); console.error(error); }); }); }; /** * Open a modal dialog allowing the user to edit the invoice code */ $scope.openEditCode = function () { const modalInstance = $uibModal.open({ animation: true, templateUrl: $scope.invoice.code.templateUrl, size: 'lg', resolve: { model () { return $scope.invoice.code.model; }, active () { return $scope.invoice.code.active; } }, controller: ['$scope', '$uibModalInstance', 'model', 'active', function ($scope, $uibModalInstance, model, active) { $scope.codeModel = model; $scope.isSelected = active; $scope.ok = function () { $uibModalInstance.close({ model: $scope.codeModel, active: $scope.isSelected }); }; $scope.cancel = function () { $uibModalInstance.dismiss('cancel'); }; }] }); return modalInstance.result.then(function (result) { Setting.update({ name: 'invoice_code-value' }, { value: result.model }, function (data) { $scope.invoice.code.model = result.model; if (result.active) { return growl.success(_t('app.admin.invoices.invoicing_code_succesfully_saved')); } } , function (error) { if (error.status === 304) return; growl.error(_t('app.admin.invoices.an_error_occurred_while_saving_the_invoicing_code')); console.error(error); }); return Setting.update({ name: 'invoice_code-active' }, { value: result.active ? 'true' : 'false' }, function (data) { $scope.invoice.code.active = result.active; if (result.active) { return growl.success(_t('app.admin.invoices.code_successfully_activated')); } else { return growl.success(_t('app.admin.invoices.code_successfully_disabled')); } } , function (error) { if (error.status === 304) return; growl.error(_t('app.admin.invoices.an_error_occurred_while_activating_the_invoicing_code')); console.error(error); }); }); }; /** * Open a modal dialog allowing the user to edit the invoice number */ $scope.openEditInvoiceNb = function () { const modalInstance = $uibModal.open({ animation: true, templateUrl: $scope.invoice.number.templateUrl, size: 'lg', resolve: { model () { return $scope.invoice.number.model; } }, controller: ['$scope', '$uibModalInstance', 'model', function ($scope, $uibModalInstance, model) { $scope.model = model; $scope.ok = function () { $uibModalInstance.close($scope.model); }; $scope.cancel = function () { $uibModalInstance.dismiss('cancel'); }; }] }); return modalInstance.result.then(function (model) { Setting.update({ name: 'invoice_order-nb' }, { value: model }, function (data) { $scope.invoice.number.model = model; return growl.success(_t('app.admin.invoices.order_number_successfully_saved')); } , function (error) { if (error.status === 304) return; growl.error(_t('app.admin.invoices.an_error_occurred_while_saving_the_order_number')); console.error(error); }); }); }; /** * Open a modal dialog allowing the user to edit the VAT parameters for the invoices * The VAT can be disabled and its rate can be configured */ $scope.openEditVAT = function () { const modalInstance = $uibModal.open({ animation: true, templateUrl: $scope.invoice.VAT.templateUrl, size: 'lg', resolve: { rate () { return $scope.invoice.VAT.rate; }, active () { return $scope.invoice.VAT.active; }, 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', function ($scope, $uibModalInstance, rate, active, rateHistory, activeHistory) { $scope.rate = rate; $scope.isSelected = active; $scope.history = []; $scope.ok = function () { $uibModalInstance.close({ rate: $scope.rate, active: $scope.isSelected }); }; $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 }) }); 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); }); }); }; /** * Open a modal dialog allowing the user to edit the prefix of the invoice file name */ $scope.openEditPrefix = function () { const modalInstance = $uibModal.open({ animation: true, templateUrl: $scope.file.templateUrl, size: 'lg', resolve: { model () { return $scope.file.prefix;} }, controller: ['$scope', '$uibModalInstance', 'model', function ($scope, $uibModalInstance, model) { $scope.model = model; $scope.ok = function () { $uibModalInstance.close($scope.model); }; $scope.cancel = function () { $uibModalInstance.dismiss('cancel'); }; }] }); return modalInstance.result.then(function (model) { Setting.update({ name: 'invoice_prefix' }, { value: model }, function (data) { $scope.file.prefix = model; return growl.success(_t('app.admin.invoices.prefix_successfully_saved')); } , function (error) { if (error.status === 304) return; growl.error(_t('app.admin.invoices.an_error_occurred_while_saving_the_prefix')); console.error(error); }); }); }; /** * Callback to save the value of the text zone when editing is done */ $scope.textEditEnd = function (event) { const parsed = parseHtml($scope.invoice.text.content); return Setting.update({ name: 'invoice_text' }, { value: parsed }, function (data) { $scope.invoice.text.content = parsed; return growl.success(_t('app.admin.invoices.text_successfully_saved')); } , function (error) { if (error.status === 304) return; growl.error(_t('app.admin.invoices.an_error_occurred_while_saving_the_text')); console.error(error); }); }; /** * Callback to save the value of the legal information zone when editing is done */ $scope.legalsEditEnd = function (event) { const parsed = parseHtml($scope.invoice.legals.content); return Setting.update({ name: 'invoice_legals' }, { value: parsed }, function (data) { $scope.invoice.legals.content = parsed; return growl.success(_t('app.admin.invoices.address_and_legal_information_successfully_saved')); } , function (error) { if (error.status === 304) return; growl.error(_t('app.admin.invoices.an_error_occurred_while_saving_the_address_and_the_legal_information')); console.error(error); }); }; /** * Callback when any of the filters changes. * Full reload the results list */ $scope.handleFilterChange = function () { if (searchTimeout) clearTimeout(searchTimeout); searchTimeout = setTimeout(function() { resetSearchInvoice(); invoiceSearch(); }, 300); }; /** * Callback for the 'load more' button. * Will load the next results of the current search, if any */ $scope.showNextInvoices = function () { $scope.page += 1; invoiceSearch(true); }; /** * Open a modal allowing the user to close an accounting period and to * view all periods already closed. */ $scope.closeAnAccountingPeriod = function() { // open modal $uibModal.open({ templateUrl: '<%= asset_path "admin/invoices/closePeriodModal.html" %>', controller: 'ClosePeriodModalController', backdrop: 'static', keyboard: false, size: 'lg', resolve: { periods() { return AccountingPeriod.query().$promise; }, lastClosingEnd() { return AccountingPeriod.lastClosingEnd().$promise; } } }); } $scope.toggleExportModal = function() { $uibModal.open({ templateUrl: '<%= asset_path "admin/invoices/accountingExportModal.html" %>', controller: 'AccountingExportModalController', size: 'xl' }); } /** * Test if the given date is within a closed accounting period * @param date {Date} date to test * @returns {boolean} true if closed, false otherwise */ $scope.isDateClosed = function(date) { for (const period of closedPeriods) { if (moment(date).isBetween(moment.utc(period.start_at).startOf('day'), moment.utc(period.end_at).endOf('day'), null, '[]')) { return true; } } return false; } /** * Callback to bulk save all settings in the page to the database with their values */ $scope.save = function() { Setting.bulkUpdate( { settings: Object.values($scope.settings) }, function () { growl.success(_t('app.admin.invoices.codes_customization_success')); }, function (error) { growl.error('app.admin.invoices.unexpected_error_occurred'); console.error(error); } ); } /** * Return the name of the operator that creates the invoice */ $scope.operatorName = function(invoice) { if (!invoice.operator) return ''; return `${invoice.operator.first_name} ${invoice.operator.last_name}`; } /** * Open a modal dialog which ask for the stripe keys * @param onlinePaymentModule {{name: String, value: String}} setting that defines the next status of the online payment module * @return {boolean} false if the keys were not provided */ $scope.requireStripeKeys = function(onlinePaymentModule) { // if the online payment is about to be disabled, accept the change without any further question if (onlinePaymentModule.value === false) return true; // otherwise, open a modal to ask for the stripe keys const modalInstance = $uibModal.open({ templateUrl: 'stripeKeys.html', controller: 'StripeKeysModalController', resolve: { stripeKeys: ['Setting', function (Setting) { return Setting.query({ names: "['stripe_public_key', 'stripe_secret_key']" }).$promise; }] } }); modalInstance.result.then(function (success) { if (success) { Setting.get({ name: 'stripe_public_key' }, function (res) { $scope.allSettings.stripe_public_key = res.setting.value; }) Setting.isPresent({ name: 'stripe_secret_key' }, function (res) { $scope.stripeSecretKey = (res.isPresent ? STRIPE_SK_HIDDEN : ''); }) Payment.onlinePaymentStatus(function (res) { $scope.onlinePaymentStatus = res.status; }); } }) // return the promise return modalInstance.result; } /** * Setup the feature-tour for the admin/invoices page. * This is intended as a contextual help (when pressing F1) */ $scope.setupInvoicesTour = function () { // get the tour defined by the ui-tour directive const uitour = uiTourService.getTourByName('invoices'); if (AuthService.isAuthorized('admin')) { uitour.createStep({ selector: 'body', stepId: 'welcome', order: 0, title: _t('app.admin.tour.invoices.welcome.title'), content: _t('app.admin.tour.invoices.welcome.content'), placement: 'bottom', orphan: true }); } else { uitour.createStep({ selector: 'body', stepId: 'welcome_manager', order: 0, title: _t('app.admin.tour.invoices.welcome_manager.title'), content: _t('app.admin.tour.invoices.welcome_manager.content'), placement: 'bottom', orphan: true }); } if (settings.invoicing_module === 'true' && $scope.invoices.length > 0) { uitour.createStep({ selector: '.invoices-management .invoices-list', stepId: 'list', order: 1, title: _t('app.admin.tour.invoices.list.title'), content: _t('app.admin.tour.invoices.list.content'), placement: 'top' }); uitour.createStep({ selector: '.invoices-management .invoices-list .chained-indicator', stepId: 'chained', order: 2, title: _t('app.admin.tour.invoices.chained.title'), content: _t('app.admin.tour.invoices.chained.content'), placement: 'right' }); uitour.createStep({ selector: '.invoices-management .invoices-list .download-button', stepId: 'download', order: 3, title: _t('app.admin.tour.invoices.download.title'), content: _t('app.admin.tour.invoices.download.content'), placement: 'left' }); uitour.createStep({ selector: '.invoices-management .invoices-list .refund-button', stepId: 'refund', order: 4, title: _t('app.admin.tour.invoices.refund.title'), content: _t('app.admin.tour.invoices.refund.content'), placement: 'left' }); } if (AuthService.isAuthorized('admin')) { uitour.createStep({ selector: '.invoices-management .invoices-settings', stepId: 'settings', order: 5, title: _t('app.admin.tour.invoices.settings.title'), content: _t('app.admin.tour.invoices.settings.content'), placement: 'bottom' }); uitour.createStep({ selector: '.invoices-management .accounting-codes-tab', stepId: 'codes', order: 6, title: _t('app.admin.tour.invoices.codes.title'), content: _t('app.admin.tour.invoices.codes.content'), placement: 'bottom' }); uitour.createStep({ selector: '.heading .export-accounting-button', stepId: 'export', order: 7, title: _t('app.admin.tour.invoices.export.title'), content: _t('app.admin.tour.invoices.export.content'), placement: 'bottom' }); uitour.createStep({ selector: '.invoices-management .payment-settings', stepId: 'payment', order: 8, title: _t('app.admin.tour.invoices.payment.title'), content: _t('app.admin.tour.invoices.payment.content'), placement: 'bottom', popupClass: 'shift-left-50' }); uitour.createStep({ selector: '.heading .close-accounting-periods-button', stepId: 'periods', order: 9, title: _t('app.admin.tour.invoices.periods.title'), content: _t('app.admin.tour.invoices.periods.content'), placement: 'bottom', popupClass: 'shift-left-50' }); } uitour.createStep({ selector: 'body', stepId: 'conclusion', order: 10, title: _t('app.admin.tour.conclusion.title'), content: _t('app.admin.tour.conclusion.content'), placement: 'bottom', orphan: true }); // on step change, change the active tab if needed uitour.on('stepChanged', function (nextStep) { if (nextStep.stepId === 'list' || nextStep.stepId === 'settings') { $scope.tabs.active = 0; } if (nextStep.stepId === 'settings') { $scope.tabs.active = 1; } if (nextStep.stepId === 'codes' || nextStep.stepId === 'export') { $scope.tabs.active = 2; } if (nextStep.stepId === 'payment') { $scope.tabs.active = 3; } }); // on tour end, save the status in database uitour.on('ended', function () { if (uitour.getStatus() === uitour.Status.ON && $scope.currentUser.profile.tours.indexOf('invoices') < 0) { Member.completeTour({ id: $scope.currentUser.id }, { tour: 'invoices' }, function (res) { $scope.currentUser.profile.tours = res.tours; }); } }); // if the user has never seen the tour, show him now if (settings.feature_tour_display !== 'manual' && $scope.currentUser.profile.tours.indexOf('invoices') < 0) { uitour.start(); } } /* PRIVATE SCOPE */ /** * Kind of constructor: these actions will be realized first when the controller is loaded */ const initialize = function () { if (!invoices[0] || (invoices[0].maxInvoices <= $scope.invoices.length)) { $scope.noMoreResults = true; } // retrieve settings from the DB through the API $scope.invoice.legals.content = settings['invoice_legals']; $scope.invoice.text.content = settings['invoice_text']; $scope.invoice.VAT.rate = parseFloat(settings['invoice_VAT-rate']); $scope.invoice.VAT.active = (settings['invoice_VAT-active'] === 'true'); $scope.invoice.number.model = settings['invoice_order-nb']; $scope.invoice.code.model = settings['invoice_code-value']; $scope.invoice.code.active = (settings['invoice_code-active'] === 'true'); $scope.invoice.reference.model = settings['invoice_reference']; $scope.invoice.logo = { filetype: 'image/png', filename: 'logo.png', base64: settings['invoice_logo'] }; // Watch the logo, when a change occurs, save it $scope.$watch('invoice.logo', function () { if ($scope.invoice.logo && $scope.invoice.logo.filesize) { return Setting.update( { name: 'invoice_logo' }, { value: $scope.invoice.logo.base64 }, function (data) { growl.success(_t('app.admin.invoices.logo_successfully_saved')); }, function (error) { if (error.status === 304) return; growl.error(_t('app.admin.invoices.an_error_occurred_while_saving_the_logo')); console.error(error); } ); } }); }; /** * Will temporize the search query to prevent overloading the API */ var searchTimeout = null; /** * Output the given integer with leading zeros. If the given value is longer than the given * length, it will be truncated. * @param value {number} the integer to pad * @param length {number} the length of the resulting string. */ var padWithZeros = function (value, length) { return (1e15 + value + '').slice(-length); }; /** * Remove every unsupported html tag from the given html text (like
, , ...).
* The supported tags are , , and
.
* @param html {string} single line html text
* @return {string} multi line simplified html text
*/
var parseHtml = function (html) {
return html.replace(/<\/?(\w+)((\s+\w+(\s*=\s*(?:".*?"|'.*?'|[^'">\s]+))?)+\s*|\s*)\/?>/g, function (match, p1, offset, string) {
if (['b', 'u', 'i', 'br'].includes(p1)) {
return match;
} else {
return '';
}
});
};
/**
* Reinitialize the context of invoices' search to display new results set
*/
var resetSearchInvoice = function () {
$scope.page = 1;
return $scope.noMoreResults = false;
};
/**
* Run a search query with the current parameters set concerning invoices, then affect or concat the results
* to $scope.invoices
* @param [concat] {boolean} if true, the result will be append to $scope.invoices instead of being affected
*/
var invoiceSearch = function (concat) {
Invoice.list({
query: {
number: $scope.searchInvoice.reference,
customer: $scope.searchInvoice.name,
date: $scope.searchInvoice.date,
order_by: $scope.orderInvoice,
page: $scope.page,
size: INVOICES_PER_PAGE
}
}, function (invoices) {
if (concat) {
$scope.invoices = $scope.invoices.concat(invoices);
} else {
$scope.invoices = invoices;
}
if (!invoices[0] || (invoices[0].maxInvoices <= $scope.invoices.length)) {
return $scope.noMoreResults = true;
}
});
};
// !!! MUST BE CALLED AT THE END of the controller
return initialize();
}
]);
/**
* Controller used in the invoice refunding modal window
*/
Application.Controllers.controller('AvoirModalController', ['$scope', '$uibModalInstance', 'invoice', 'closedPeriods', 'lastClosingEnd', 'Invoice', 'growl', '_t',
function ($scope, $uibModalInstance, invoice, closedPeriods, lastClosingEnd, Invoice, growl, _t) {
/* PUBLIC SCOPE */
// invoice linked to the current refund
$scope.invoice = invoice;
// Associative array containing invoice_item ids associated with boolean values
$scope.partial = {};
// Default refund parameters
$scope.avoir = {
invoice_id: invoice.id,
subscription_to_expire: false,
invoice_items_ids: []
};
// End date of last closed accounting period or date of first invoice
$scope.lastClosingEnd = moment.utc(lastClosingEnd.last_end_date).toDate();
// Possible refunding methods
$scope.avoirModes = [
{ name: _t('app.admin.invoices.none'), value: 'none' },
{ name: _t('app.admin.invoices.by_cash'), value: 'cash' },
{ name: _t('app.admin.invoices.by_cheque'), value: 'cheque' },
{ name: _t('app.admin.invoices.by_transfer'), value: 'transfer' },
{ name: _t('app.admin.invoices.by_wallet'), value: 'wallet' }
];
// If a subscription was took with the current invoice, should it be canceled or not
$scope.subscriptionExpireOptions = {};
$scope.subscriptionExpireOptions[_t('app.shared.buttons.yes')] = true;
$scope.subscriptionExpireOptions[_t('app.shared.buttons.no')] = false;
// AngularUI-Bootstrap datepicker parameters to define when to refund
$scope.datePicker = {
format: Fablab.uibDateFormat,
opened: false, // default: datePicker is not shown
options: {
startingDay: Fablab.weekStartingDay
}
};
/**
* Callback to open the datepicker
*/
$scope.openDatePicker = function ($event) {
$event.preventDefault();
$event.stopPropagation();
$scope.datePicker.opened = true;
};
/**
* Validate the refunding and generate a refund invoice
*/
$scope.ok = function () {
// check that at least 1 element of the invoice is refunded
$scope.avoir.invoice_items_ids = [];
for (let itemId in $scope.partial) {
if (Object.prototype.hasOwnProperty.call($scope.partial, itemId)) {
const refundItem = $scope.partial[itemId];
if (refundItem) {
$scope.avoir.invoice_items_ids.push(parseInt(itemId));
}
}
}
if ($scope.avoir.invoice_items_ids.length === 0) {
return growl.error(_t('app.admin.invoices.you_must_select_at_least_one_element_to_create_a_refund'));
} else {
return Invoice.save(
{ avoir: $scope.avoir },
function (avoir) { // success
$uibModalInstance.close({ avoir, invoice: $scope.invoice });
},
function (err) { // failed
growl.error(_t('app.admin.invoices.unable_to_create_the_refund'));
}
);
}
};
/**
* Cancel the refund, dismiss the modal window
*/
$scope.cancel = function () { $uibModalInstance.dismiss('cancel'); };
/**
* Test if the given date is within a closed accounting period
* @param date {Date} date to test
* @returns {boolean} true if closed, false otherwise
*/
$scope.isDateClosed = function(date) {
for (const period of closedPeriods) {
if (moment(date).isBetween(moment.utc(period.start_at).startOf('day'), moment.utc(period.end_at).endOf('day'), null, '[]')) {
return true;
}
}
return false;
}
/* PRIVATE SCOPE */
/**
* Kind of constructor: these actions will be realized first when the controller is loaded
*/
const initialize = function () {
// if the invoice was payed with stripe, allow to refund through stripe
Invoice.get({ id: invoice.id }, function (data) {
$scope.invoice = data;
// default : all elements of the invoice are refund
return Array.from(data.items).map(function (item) {
return ($scope.partial[item.id] = (typeof item.avoir_item_id !== 'number'));
});
});
if (invoice.stripe) {
return $scope.avoirModes.push({ name: _t('app.admin.invoices.online_payment'), value: 'stripe' });
}
};
// !!! MUST BE CALLED AT THE END of the controller
return initialize();
}
]);
/**
* Controller used in the modal window allowing an admin to close an accounting period
*/
Application.Controllers.controller('ClosePeriodModalController', ['$scope', '$uibModalInstance', '$window', '$sce', 'Invoice', 'AccountingPeriod', 'periods', 'lastClosingEnd','dialogs', 'growl', '_t',
function ($scope, $uibModalInstance, $window, $sce, Invoice, AccountingPeriod, periods, lastClosingEnd, dialogs, growl, _t) {
const YESTERDAY = moment.utc({ h: 0, m: 0, s: 0, ms: 0 }).subtract(1, 'day').toDate();
const LAST_CLOSING = moment.utc(lastClosingEnd.last_end_date).toDate();
const MAX_END = moment.utc(lastClosingEnd.last_end_date).add(1, 'year').subtract(1, 'day').toDate();
/* PUBLIC SCOPE */
// date pickers values are bound to these variables
$scope.period = {
start_at: LAST_CLOSING,
end_at: moment(YESTERDAY).isBefore(MAX_END) ? YESTERDAY : MAX_END
};
// any form errors will come here
$scope.errors = {};
// will match any error about invoices
$scope.invoiceErrorRE = /^invoice_(.+)$/;
// existing closed periods, provided by the API
$scope.accountingPeriods = periods;
// closing a period may take a long time so we need to prevent the user from double-clicking the close button while processing
$scope.pendingCreation = false;
// AngularUI-Bootstrap datepickers parameters to define the period to close
$scope.datePicker = {
format: Fablab.uibDateFormat,
// default: datePicker are not shown
startOpened: false,
endOpened: false,
minDate: LAST_CLOSING,
maxDate: moment(YESTERDAY).isBefore(MAX_END) ? YESTERDAY : MAX_END,
options: {
startingDay: Fablab.weekStartingDay
}
};
/**
* Callback to open the datepicker
*/
$scope.toggleDatePicker = function ($event) {
$event.preventDefault();
$event.stopPropagation();
$scope.datePicker.endOpened = !$scope.datePicker.endOpened;
};
/**
* Validate the close period creation
*/
$scope.ok = function () {
dialogs.confirm(
{
resolve: {
object () {
return {
title: _t('app.admin.invoices.confirmation_required'),
msg: $sce.trustAsHtml(
_t(
'app.admin.invoices.confirm_close_START_END',
{ START: moment.utc($scope.period.start_at).format('LL'), END: moment.utc($scope.period.end_at).format('LL') }
)
+ '
'
+ _t('app.admin.invoices.period_must_match_fiscal_year')
+ '
'
+ _t('app.admin.invoices.this_may_take_a_while')
)
};
}
}
},
function () { // creation confirmed
$scope.pendingCreation = true;
AccountingPeriod.save(
{
accounting_period: {
start_at: moment.utc($scope.period.start_at).toDate(),
end_at: moment.utc($scope.period.end_at).endOf('day').toDate()
}
},
function (resp) {
$scope.pendingCreation = false;
growl.success(_t(
'app.admin.invoices.period_START_END_closed_success',
{ START: moment.utc(resp.start_at).format('LL'), END: moment.utc(resp.end_at).format('LL') }
));
$uibModalInstance.close(resp);
},
function(error) {
$scope.pendingCreation = false;
growl.error(_t('app.admin.invoices.failed_to_close_period'));
$scope.errors = error.data;
}
);
}
);
};
/**
* Just dismiss the modal window
*/
$scope.cancel = function () { $uibModalInstance.dismiss('cancel'); };
/**
* Trigger the API call to download the JSON archive of the closed accounting period
*/
$scope.downloadArchive = function(period) {
$window.location.href = `/api/accounting_periods/${period.id}/archive`;
}
}
]);
Application.Controllers.controller('AccountingExportModalController', ['$scope', '$uibModalInstance', 'Invoice', 'Export', 'CSRF', 'growl', '_t',
function ($scope, $uibModalInstance, Invoice, Export, CSRF, growl, _t) {
// Retrieve Anti-CSRF tokens from cookies
CSRF.setMetaTags();
const SETTINGS = {
acd: {
format: 'csv',
encoding: 'ISO-8859-1',
separator: ';',
dateFormat: '%d/%m/%Y',
labelMaxLength: 50,
decimalSeparator: ',',
exportInvoicesAtZero: false,
columns: ['journal_code', 'date', 'account_code', 'account_label', 'piece', 'line_label', 'debit_origin', 'credit_origin', 'debit_euro', 'credit_euro', 'lettering']
}
};
/* PUBLIC SCOPE */
// API URL where the form will be posted
$scope.actionUrl = '/api/accounting/export';
// Form action on the above URL
$scope.method = 'post';
// Anti-CSRF token to inject into the download form
$scope.csrfToken = angular.element('meta[name="csrf-token"]')[0].content;
// API request body to generate the export
$scope.query = null;
// binding to radio button "export to"
$scope.exportTarget = {
software: null,
startDate: null,
endDate: null,
settings: null
};
// AngularUI-Bootstrap datepicker parameters to define export dates range
$scope.datePicker = {
format: Fablab.uibDateFormat,
opened: { // default: datePickers are not shown
start: false,
end: false
},
options: {
startingDay: Fablab.weekStartingDay
}
};
// Date of the first invoice
$scope.firstInvoice = null;
/**
* Validate the export
*/
$scope.ok = function () {
const statusQry = mkQuery();
$scope.query = statusQry;
Export.status(statusQry).then(function (res) {
if (!res.data.exists) {
growl.success(_t('app.admin.invoices.export_is_running'));
}
$uibModalInstance.close(res);
});
};
/**
* Callback to open/close one of the datepickers
* @param event {Object} see https://docs.angularjs.org/guide/expression#-event-
* @param picker {string} start | end
*/
$scope.toggleDatePicker = function(event, picker) {
event.preventDefault();
$scope.datePicker.opened[picker] = !$scope.datePicker.opened[picker];
};
/**
* Will fill the export settings, according to the selected software
* @param software {String} must be one of SETTINGS.*
*/
$scope.fillSettings = function(software) {
$scope.exportTarget.settings = SETTINGS[software];
};
/**
* Just dismiss the modal window
*/
$scope.cancel = function () { $uibModalInstance.dismiss('cancel'); };
/* PRIVATE SCOPE */
/**
* Kind of constructor: these actions will be realized first when the controller is loaded
*/
const initialize = function () {
// if the invoice was payed with stripe, allow to refund through stripe
Invoice.first(function (data) {
$scope.firstInvoice = data.date;
$scope.exportTarget.startDate = data.date;
$scope.exportTarget.endDate = moment().toISOString();
});
};
/**
* Prepare the query for the export API
* @returns {{extension: *, query: *, category: string, type: *, key: *}}
*/
const mkQuery = function() {
return {
category: 'accounting',
type: $scope.exportTarget.software,
extension: $scope.exportTarget.settings.format,
key: $scope.exportTarget.settings.separator,
query: JSON.stringify({
columns: $scope.exportTarget.settings.columns,
encoding: $scope.exportTarget.settings.encoding,
date_format: $scope.exportTarget.settings.dateFormat,
start_date: $scope.exportTarget.startDate,
end_date: $scope.exportTarget.endDate,
label_max_length: $scope.exportTarget.settings.labelMaxLength,
decimal_separator: $scope.exportTarget.settings.decimalSeparator,
export_invoices_at_zero: $scope.exportTarget.settings.exportInvoicesAtZero
})
};
}
// !!! MUST BE CALLED AT THE END of the controller
return initialize();
}]);
/**
* Controller used in the modal window allowing an admin to close an accounting period
*/
Application.Controllers.controller('StripeKeysModalController', ['$scope', '$uibModalInstance', '$http', '$httpParamSerializerJQLike', 'stripeKeys', 'Setting', 'growl', '_t',
function ($scope, $uibModalInstance, $http, $httpParamSerializerJQLike, stripeKeys, Setting, growl, _t) {
/* PUBLIC SCOPE */
// public key
$scope.publicKey = stripeKeys.stripe_public_key;
// test status of the public key
$scope.publicKeyStatus = undefined;
// secret key
$scope.secretKey = stripeKeys.stripe_secret_key;
// test status of the secret key
$scope.secretKeyStatus = undefined;
/**
* Trigger the test of the secret key and set the result in $scope.secretKeyStatus
*/
$scope.testSecretKey = function () {
if (!$scope.secretKey.match(/^sk_/)) {
$scope.secretKeyStatus = false;
return;
}
$http({
method: 'GET',
url: 'https://api.stripe.com/v1/charges',
headers: {
Authorization: `Bearer ${$scope.secretKey}`,
}
}).then(function () {
$scope.secretKeyStatus = true;
}, function (err) {
if (err.status === 401) $scope.secretKeyStatus = false;
});
}
/**
* Trigger the test of the secret key and set the result in $scope.secretKeyStatus
*/
$scope.testPublicKey = function () {
if (!$scope.publicKey.match(/^pk_/)) {
$scope.publicKeyStatus = false;
return;
}
const today = new Date();
$http({
method: 'POST',
url: 'https://api.stripe.com/v1/tokens',
headers: {
Authorization: `Bearer ${$scope.publicKey}`,
'Content-Type': 'application/x-www-form-urlencoded'
},
data: $httpParamSerializerJQLike({
'card[number]': 4242424242424242,
'card[cvc]': 123,
'card[exp_month]': 12,
'card[exp_year]': today.getFullYear().toString().substring(2),
})
}).then(function () {
$scope.publicKeyStatus = true;
}, function (err) {
if (err.status === 401) $scope.publicKeyStatus = false;
});
}
/**
* Validate the keys
*/
$scope.ok = function () {
if ($scope.secretKeyStatus && $scope.publicKeyStatus) {
Setting.bulkUpdate(
{ settings: [
{
name: 'stripe_public_key',
value: $scope.publicKey
},
{
name: 'stripe_secret_key',
value: $scope.secretKey
}
] },
function () {
growl.success(_t('app.admin.invoices.payment.stripe_keys_saved'));
$uibModalInstance.close(true);
},
function (error) {
growl.error('app.admin.invoices.payment.error_saving_stripe_keys');
console.error(error);
}
);
} else {
growl.error(_t('app.admin.invoices.payment.error_check_keys'))
}
};
/**
* Just dismiss the modal window
*/
$scope.cancel = function () {
$uibModalInstance.dismiss('cancel');
};
/* PRIVATE SCOPE */
/**
* Kind of constructor: these actions will be realized first when the controller is loaded
*/
const initialize = function () {
if (stripeKeys.stripe_public_key) {
$scope.testPublicKey();
}
if (stripeKeys.stripe_secret_key) {
$scope.testSecretKey();
}
};
// !!! MUST BE CALLED AT THE END of the controller!
return initialize();
}
]);