/* 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', 'invoices', 'closedPeriods', '$uibModal', 'growl', '$filter', 'Setting', 'settings', '_t', function ($scope, $state, Invoice, AccountingPeriod, invoices, closedPeriods, $uibModal, growl, $filter, Setting, settings, _t) { /* PRIVATE STATIC CONSTANTS */ // number of invoices loaded each time we click on 'load more...' const INVOICES_PER_PAGE = 20; /* PUBLIC SCOPE */ // List of all users 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'; // 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: '' } }; // 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('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 nmuber 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('invoices.invoice_reference_successfully_saved')); } , function (error) { growl.error(_t('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('invoices.invoicing_code_succesfully_saved')); } } , function (error) { growl.error(_t('invoices.an_error_occurred_while_saving_the_invoicing_code')); return 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('invoices.code_successfully_activated')); } else { return growl.success(_t('invoices.code_successfully_disabled')); } } , function (error) { growl.error(_t('invoices.an_error_occurred_while_activating_the_invoicing_code')); return 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('invoices.order_number_successfully_saved')); } , function (error) { growl.error(_t('invoices.an_error_occurred_while_saving_the_order_number')); return 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) { if (v.value === 'false') { $scope.history.push({ date: v.created_at, rate: 0, 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('invoices.VAT_rate_successfully_saved')); } } , function (error) { growl.error(_t('invoices.an_error_occurred_while_saving_the_VAT_rate')); return 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('invoices.VAT_successfully_activated')); } else { return growl.success(_t('invoices.VAT_successfully_disabled')); } } , function (error) { growl.error(_t('invoices.an_error_occurred_while_activating_the_VAT')); return 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('invoices.text_successfully_saved')); } , function (error) { growl.error(_t('invoices.an_error_occurred_while_saving_the_text')); return 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('invoices.address_and_legal_information_successfully_saved')); } , function (error) { growl.error(_t('invoices.an_error_occurred_while_saving_the_address_and_the_legal_information')); return 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; } } }); } /** * 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 (!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 return $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('invoices.logo_successfully_saved')); }, function (error) { growl.error(_t('invoices.an_error_occurred_while_saving_the_logo')); return 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('invoices.none'), value: 'none' }, { name: _t('invoices.by_cash'), value: 'cash' }, { name: _t('invoices.by_cheque'), value: 'cheque' }, { name: _t('invoices.by_transfer'), value: 'transfer' }, { name: _t('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('yes')] = true; $scope.subscriptionExpireOptions[_t('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('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('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('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('invoices.confirmation_required'), msg: $sce.trustAsHtml( _t( 'invoices.confirm_close_START_END', { START: moment.utc($scope.period.start_at).format('LL'), END: moment.utc($scope.period.end_at).format('LL') } ) + '

' + _t('invoices.period_must_match_fiscal_year') + '

' + _t('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( '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('invoices.failed_to_close_period')); $scope.errors = error.data; } ); } ); }; /** * Cancel the refund, 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`; } } ]);