diff --git a/.dockerignore b/.dockerignore index cc02d7439..4c1f0b007 100644 --- a/.dockerignore +++ b/.dockerignore @@ -12,6 +12,7 @@ postgresql elasticsearch redis + # Ignore public assets public/uploads public/assets diff --git a/.rubocop.yml b/.rubocop.yml index 9d0978cf1..364b0382f 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -1,9 +1,9 @@ Metrics/LineLength: Max: 140 Metrics/MethodLength: - Max: 30 + Max: 35 Metrics/CyclomaticComplexity: - Max: 9 + Max: 13 Metrics/PerceivedComplexity: Max: 9 Metrics/AbcSize: @@ -16,6 +16,8 @@ Metrics/BlockLength: - 'lib/tasks/**/*.rake' - 'config/routes.rb' - 'app/pdfs/pdf/*.rb' +Metrics/ParameterLists: + CountKeywordArgs: false Style/BracesAroundHashParameters: EnforcedStyle: context_dependent Style/RegexpLiteral: diff --git a/CHANGELOG.md b/CHANGELOG.md index 50b1613c6..f53655604 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,10 +1,18 @@ # Changelog Fab Manager +- Ability to configure and export the accounting data to the ACD accounting software +- Compute the VAT per item in each invoices, instead of globally +- Use Alpine Linux to build the Docker image (#147) +- Fix a bug: invoices with total = 0, are marked as paid on site even if paid by card +- Fix a bug: after disabling a group, its associated plans are hidden from the interface +- Fix a bug: in case of unexpected server error during stripe payment process, the confirm button is not unlocked +- [TODO DEPLOY] `rake db:migrate` + ## v4.1.1 2019 september 20 -- fix a bug: api/reservations#index was using user_id instead of statistic_profile_id -- fix a bug: event_service#date_range method, test on all_day was never truthy -- fix a bug: sidekiq 5 does not have delay_for method anymore, uses perform_in instead +- Fix a bug: api/reservations#index was using user_id instead of statistic_profile_id +- Fix a bug: event_service#date_range method, test on all_day was never truthy +- Fix a bug: sidekiq 5 does not have delay_for method anymore, uses perform_in instead ## v4.1.0 2019 September 12 diff --git a/Procfile b/Procfile index f8a9f7b3c..ffe497c23 100644 --- a/Procfile +++ b/Procfile @@ -1,3 +1,3 @@ web: bundle exec rails server puma -p $PORT -b0.0.0.0 worker: bundle exec sidekiq -C ./config/sidekiq.yml -mail: bundle exec mailcatcher --foreground +mail: bundle exec mailcatcher --foreground --http-ip=0.0.0.0 diff --git a/app/assets/javascripts/controllers/admin/invoices.js.erb b/app/assets/javascripts/controllers/admin/invoices.js.erb index 3115f50f2..8f62f7081 100644 --- a/app/assets/javascripts/controllers/admin/invoices.js.erb +++ b/app/assets/javascripts/controllers/admin/invoices.js.erb @@ -76,6 +76,94 @@ Application.Controllers.controller('InvoicesController', ['$scope', '$state', 'I } }; + // 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'] + } + }; + // Placeholding date for the invoice creation $scope.today = moment(); @@ -325,9 +413,7 @@ Application.Controllers.controller('InvoicesController', ['$scope', '$state', 'I $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 }) - } + $scope.history.push({ date: v.created_at, enabled: v.value === 'true', user: v.user }) }); } @@ -432,6 +518,14 @@ Application.Controllers.controller('InvoicesController', ['$scope', '$state', 'I }); } + $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 @@ -446,6 +540,20 @@ Application.Controllers.controller('InvoicesController', ['$scope', '$state', 'I 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('invoices.codes_customization_success')); }, + function (error) { + growl.error('unexpected_error_occurred'); + console.error(error); + } + ); + } + /* PRIVATE SCOPE */ /** @@ -791,7 +899,7 @@ Application.Controllers.controller('ClosePeriodModalController', ['$scope', '$ui }; /** - * Cancel the refund, dismiss the modal window + * Just dismiss the modal window */ $scope.cancel = function () { $uibModalInstance.dismiss('cancel'); }; @@ -803,3 +911,136 @@ Application.Controllers.controller('ClosePeriodModalController', ['$scope', '$ui } } ]); + +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('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 + */ + $scope.fillSettings = function() { + $scope.exportTarget.settings = SETTINGS[$scope.exportTarget.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(); +}]); diff --git a/app/assets/javascripts/directives/stripe-form.js.erb b/app/assets/javascripts/directives/stripe-form.js.erb index e61416f2b..15835c91c 100644 --- a/app/assets/javascripts/directives/stripe-form.js.erb +++ b/app/assets/javascripts/directives/stripe-form.js.erb @@ -63,7 +63,7 @@ Application.Directives.directive('stripeForm', ['Payment', 'growl', '_t', Payment.confirm({ payment_method_id: paymentMethod.id, cart_items: $scope.cartItems }, function (response) { // Handle server response (see Step 3) handleServerResponse(response, button); - }, function(error) { handleServerResponse({ error }) }); + }, function(error) { handleServerResponse({ error }, button) }); } }); }); @@ -89,7 +89,7 @@ Application.Directives.directive('stripeForm', ['Payment', 'growl', '_t', // The PaymentIntent can be confirmed again on the server Payment.confirm({ payment_intent_id: result.paymentIntent.id, cart_items: $scope.cartItems }, function(confirmResult) { handleServerResponse(confirmResult, confirmButton); - }, function(error) { handleServerResponse({ error }) }); + }, function(error) { handleServerResponse({ error }, confirmButton) }); } }); } else { diff --git a/app/assets/javascripts/router.js.erb b/app/assets/javascripts/router.js.erb index 3616fac07..0c40c4d7f 100644 --- a/app/assets/javascripts/router.js.erb +++ b/app/assets/javascripts/router.js.erb @@ -899,15 +899,13 @@ angular.module('application.router', ['ui.router']) resolve: { settings: ['Setting', function (Setting) { return Setting.query({ - names: `['invoice_legals', \ - 'invoice_text', \ - 'invoice_VAT-rate', \ - 'invoice_VAT-active', \ - 'invoice_order-nb', \ - 'invoice_code-value', \ - 'invoice_code-active', \ - 'invoice_reference', \ - 'invoice_logo']` }).$promise; + names: `['invoice_legals', 'invoice_text', 'invoice_VAT-rate', 'invoice_VAT-active', 'invoice_order-nb', 'invoice_code-value', \ + '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', \ + 'accounting_other_client_code', 'accounting_other_client_label', 'accounting_wallet_code', 'accounting_wallet_label', \ + 'accounting_VAT_code', 'accounting_VAT_label', 'accounting_subscription_code', 'accounting_subscription_label', \ + 'accounting_Machine_code', 'accounting_Machine_label', 'accounting_Training_code', 'accounting_Training_label', \ + 'accounting_Event_code', 'accounting_Event_label', 'accounting_Space_code', 'accounting_Space_label']` }).$promise; }], invoices: [ 'Invoice', function (Invoice) { return Invoice.list({ diff --git a/app/assets/javascripts/services/invoice.js b/app/assets/javascripts/services/invoice.js index d2cc57795..ab38a0e6a 100644 --- a/app/assets/javascripts/services/invoice.js +++ b/app/assets/javascripts/services/invoice.js @@ -10,6 +10,10 @@ Application.Services.factory('Invoice', ['$resource', function ($resource) { url: '/api/invoices/list', method: 'POST', isArray: true + }, + first: { + url: '/api/invoices/first', + method: 'GET' } } ); diff --git a/app/assets/javascripts/services/setting.js b/app/assets/javascripts/services/setting.js index 9306435d3..c0b43b645 100644 --- a/app/assets/javascripts/services/setting.js +++ b/app/assets/javascripts/services/setting.js @@ -9,6 +9,10 @@ Application.Services.factory('Setting', ['$resource', function ($resource) { return angular.toJson({ setting: data }); } }, + bulkUpdate: { + url: '/api/settings/bulk_update', + method: 'PATCH' + }, query: { isArray: false } diff --git a/app/assets/stylesheets/app.components.scss b/app/assets/stylesheets/app.components.scss index d58a57afb..f477fd3dc 100644 --- a/app/assets/stylesheets/app.components.scss +++ b/app/assets/stylesheets/app.components.scss @@ -65,6 +65,10 @@ height: 100%; } +.modal-xl { + width: 900px; +} + // component card .card { position: relative; diff --git a/app/assets/stylesheets/modules/invoice.scss b/app/assets/stylesheets/modules/invoice.scss index d4904f49c..fb524d20e 100644 --- a/app/assets/stylesheets/modules/invoice.scss +++ b/app/assets/stylesheets/modules/invoice.scss @@ -276,3 +276,31 @@ table.scrollable-3-cols { input.form-control.as-writable { background-color: white; } + +.accounting-codes .row { + margin-top: 2rem; + + button { + margin-top: 1em; + } +} + +table.export-table-template { + margin-top: 10px; + + thead td { + width: 20px; + background-color: #227447; + color: white; + border-bottom: 2px solid black; + font-size: 13px; + font-weight: bold; + padding: 10px 5px; + line-height: 12px; + } + + tbody td { + border-right: 1px solid #d4d4d4; + height: 30px; + } +} diff --git a/app/assets/templates/admin/invoices/accountingExportModal.html b/app/assets/templates/admin/invoices/accountingExportModal.html new file mode 100644 index 000000000..6d6b580fe --- /dev/null +++ b/app/assets/templates/admin/invoices/accountingExportModal.html @@ -0,0 +1,97 @@ + + + diff --git a/app/assets/templates/admin/invoices/avoirModal.html.erb b/app/assets/templates/admin/invoices/avoirModal.html.erb index a8e24049c..08f9cd531 100644 --- a/app/assets/templates/admin/invoices/avoirModal.html.erb +++ b/app/assets/templates/admin/invoices/avoirModal.html.erb @@ -23,7 +23,7 @@
- +
diff --git a/app/assets/templates/admin/invoices/index.html.erb b/app/assets/templates/admin/invoices/index.html.erb index 6ab3ab14e..a0c04a4ed 100644 --- a/app/assets/templates/admin/invoices/index.html.erb +++ b/app/assets/templates/admin/invoices/index.html.erb @@ -12,6 +12,8 @@
+ + {{ 'invoices.accounting_periods' | translate }}
@@ -116,7 +118,7 @@
-
@@ -409,8 +528,9 @@ - {{'invoices.VAT_disabled'}} - {{value.rate}} + {{'invoices.VAT_disabled'}} + {{'invoices.VAT_enabled'}} + {{value.rate}} {{value.date | amDateFormat:'L LT'}} {{value.user.name}}{{ 'invoices.deleted_user' }} diff --git a/app/assets/templates/admin/pricing/subscriptions.html.erb b/app/assets/templates/admin/pricing/subscriptions.html.erb index 602176e08..bcb98f4bc 100644 --- a/app/assets/templates/admin/pricing/subscriptions.html.erb +++ b/app/assets/templates/admin/pricing/subscriptions.html.erb @@ -36,8 +36,7 @@ + ng-init="group = getGroupFromId(groups, plan.group_id)"> {{getPlanType(plan.type)}} {{plan.base_name}} {{ plan.interval | planIntervalFilter:plan.interval_count }} @@ -47,4 +46,4 @@ - \ No newline at end of file + diff --git a/app/controllers/api/accounting_exports_controller.rb b/app/controllers/api/accounting_exports_controller.rb new file mode 100644 index 000000000..24d15492a --- /dev/null +++ b/app/controllers/api/accounting_exports_controller.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true + +# API Controller for exporting accounting data to external accounting softwares +class API::AccountingExportsController < API::ApiController + + before_action :authenticate_user! + + def export + authorize :accounting_export + + export = Export.where(category: 'accounting', export_type: 'accounting-software', key: params[:key]) + .where(extension: params[:extension], query: params[:query]) + .where('created_at > ?', Invoice.maximum('updated_at')) + .last + if export.nil? || !FileTest.exist?(export.file) + @export = Export.new( + category: 'accounting', + export_type: params[:type], + user: current_user, + extension: params[:extension], + query: params[:query], + key: params[:key] + ) + if @export.save + render json: { export_id: @export.id }, status: :ok + else + render json: @export.errors, status: :unprocessable_entity + end + else + send_file File.join(Rails.root, export.file), + type: 'text/csv', + disposition: 'attachment' + end + end +end diff --git a/app/controllers/api/exports_controller.rb b/app/controllers/api/exports_controller.rb index a785562e9..7008f432c 100644 --- a/app/controllers/api/exports_controller.rb +++ b/app/controllers/api/exports_controller.rb @@ -8,10 +8,17 @@ class API::ExportsController < API::ApiController def download authorize @export + mime_type = if @export.extension == 'xlsx' + 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' + elsif @export.extension == 'csv' + 'text/csv' + else + 'application/octet-stream' + end if FileTest.exist?(@export.file) send_file File.join(Rails.root, @export.file), - type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', + type: mime_type, disposition: 'attachment' else render text: I18n.t('errors.messages.export_not_found'), status: :not_found @@ -21,28 +28,14 @@ class API::ExportsController < API::ApiController def status authorize Export - export = Export.where(category: params[:category], export_type: params[:type], query: params[:query], key: params[:key]) - - if params[:category] == 'users' - case params[:type] - when 'subscriptions' - export = export.where('created_at > ?', Subscription.maximum('updated_at')) - when 'reservations' - export = export.where('created_at > ?', Reservation.maximum('updated_at')) - when 'members' - export = export.where('created_at > ?', User.with_role(:member).maximum('updated_at')) - else - raise ArgumentError, "Unknown export users/#{params[:type]}" - end - elsif params[:category] == 'availabilities' - case params[:type] - when 'index' - export = export.where('created_at > ?', [Availability.maximum('updated_at'), Reservation.maximum('updated_at')].max) - else - raise ArgumentError, "Unknown type availabilities/#{params[:type]}" - end - end - export = export.last + exports = Export.where( + category: params[:category], + export_type: params[:type], + query: params[:query], + key: params[:key], + extension: params[:extension] + ) + export = retrieve_last_export(exports, params[:category], params[:type]) if export.nil? || !FileTest.exist?(export.file) render json: { exists: false, id: nil }, status: :ok @@ -53,6 +46,39 @@ class API::ExportsController < API::ApiController private + def retrieve_last_export(export, category, type) + case category + when 'users' + case type + when 'subscriptions' + export = export.where('created_at > ?', Subscription.maximum('updated_at')) + when 'reservations' + export = export.where('created_at > ?', Reservation.maximum('updated_at')) + when 'members' + export = export.where('created_at > ?', User.with_role(:member).maximum('updated_at')) + else + raise ArgumentError, "Unknown export users/#{type}" + end + when 'availabilities' + case type + when 'index' + export = export.where('created_at > ?', [Availability.maximum('updated_at'), Reservation.maximum('updated_at')].max) + else + raise ArgumentError, "Unknown type availabilities/#{type}" + end + when 'accounting' + case type + when 'acd' + export = export.where('created_at > ?', Invoice.maximum('updated_at')) + else + raise ArgumentError, "Unknown type accounting/#{type}" + end + else + raise ArgumentError, "Unknown category #{category}" + end + export.last + end + def set_export @export = Export.find(params[:id]) end diff --git a/app/controllers/api/invoices_controller.rb b/app/controllers/api/invoices_controller.rb index 8fba607db..7ffbb56f5 100644 --- a/app/controllers/api/invoices_controller.rb +++ b/app/controllers/api/invoices_controller.rb @@ -51,10 +51,16 @@ class API::InvoicesController < API::ApiController end end + def first + authorize Invoice + invoice = Invoice.order(:created_at).first + @first = invoice&.created_at + end + private def avoir_params - params.require(:avoir).permit(:invoice_id, :avoir_date, :avoir_mode, :subscription_to_expire, :description, + params.require(:avoir).permit(:invoice_id, :avoir_date, :payment_method, :subscription_to_expire, :description, invoice_items_ids: []) end diff --git a/app/controllers/api/settings_controller.rb b/app/controllers/api/settings_controller.rb index 7f64a2aee..00c3eb465 100644 --- a/app/controllers/api/settings_controller.rb +++ b/app/controllers/api/settings_controller.rb @@ -18,6 +18,19 @@ class API::SettingsController < API::ApiController end end + def bulk_update + authorize Setting + + @settings = [] + params[:settings].each do |setting| + next if !setting[:name] || !setting[:value] + + db_setting = Setting.find_or_initialize_by(name: setting[:name]) + db_setting.save && db_setting.history_values.create(value: setting[:value], invoicing_profile: current_user.invoicing_profile) + @settings.push db_setting + end + end + def show @setting = Setting.find_or_create_by(name: params[:name]) @show_history = params[:history] == 'true' && current_user.admin? diff --git a/app/models/avoir.rb b/app/models/avoir.rb index 0621b2350..c4d976cfe 100644 --- a/app/models/avoir.rb +++ b/app/models/avoir.rb @@ -5,50 +5,12 @@ class Avoir < Invoice belongs_to :invoice - validates :avoir_mode, inclusion: { in: %w[stripe cheque transfer none cash wallet] } + validates :payment_method, inclusion: { in: %w[stripe cheque transfer none cash wallet] } attr_accessor :invoice_items_ids def generate_reference - pattern = Setting.find_by(name: 'invoice_reference').value - - # invoice number per day (dd..dd) - reference = pattern.gsub(/d+(?![^\[]*\])/) do |match| - pad_and_truncate(number_of_invoices('day'), match.to_s.length) - end - # invoice number per month (mm..mm) - reference.gsub!(/m+(?![^\[]*\])/) do |match| - pad_and_truncate(number_of_invoices('month'), match.to_s.length) - end - # invoice number per year (yy..yy) - reference.gsub!(/y+(?![^\[]*\])/) do |match| - pad_and_truncate(number_of_invoices('year'), match.to_s.length) - end - - # full year (YYYY) - reference.gsub!(/YYYY(?![^\[]*\])/, created_at.strftime('%Y')) - # year without century (YY) - reference.gsub!(/YY(?![^\[]*\])/, created_at.strftime('%y')) - - # abbreviated month name (MMM) - reference.gsub!(/MMM(?![^\[]*\])/, created_at.strftime('%^b')) - # month of the year, zero-padded (MM) - reference.gsub!(/MM(?![^\[]*\])/, created_at.strftime('%m')) - # month of the year, non zero-padded (M) - reference.gsub!(/M(?![^\[]*\])/, created_at.strftime('%-m')) - - # day of the month, zero-padded (DD) - reference.gsub!(/DD(?![^\[]*\])/, created_at.strftime('%d')) - # day of the month, non zero-padded (DD) - reference.gsub!(/DD(?![^\[]*\])/, created_at.strftime('%-d')) - - # information about refund/avoir (R[text]) - reference.gsub!(/R\[([^\]]+)\]/, '\1') - - # remove information about online selling (X[text]) - reference.gsub!(/X\[([^\]]+)\]/, ''.to_s) - - self.reference = reference + self.reference = InvoiceReferenceService.generate_reference(self, date: created_at, avoir: true) end def expire_subscription diff --git a/app/models/export.rb b/app/models/export.rb index 32ab8ed68..74056d3a9 100644 --- a/app/models/export.rb +++ b/app/models/export.rb @@ -21,7 +21,7 @@ class Export < ActiveRecord::Base end def filename - "#{export_type}-#{id}_#{created_at.strftime('%d%m%Y')}.xlsx" + "#{export_type}-#{id}_#{created_at.strftime('%d%m%Y')}.#{extension}" end private @@ -34,6 +34,8 @@ class Export < ActiveRecord::Base UsersExportWorker.perform_async(id) when 'availabilities' AvailabilitiesExportWorker.perform_async(id) + when 'accounting' + AccountingExportWorker.perform_async(id) else raise NoMethodError, "Unknown export service for #{category}/#{export_type}" end diff --git a/app/models/group.rb b/app/models/group.rb index ee2abfaad..3b7414441 100644 --- a/app/models/group.rb +++ b/app/models/group.rb @@ -1,3 +1,6 @@ +# frozen_string_literal: true + +# Group is way to bind users with prices. Different prices can be defined for each plan/reservable, for each group class Group < ActiveRecord::Base has_many :plans has_many :users @@ -12,15 +15,21 @@ class Group < ActiveRecord::Base friendly_id :name, use: :slugged validates :name, :slug, presence: true + validates :disabled, inclusion: { in: [false] }, if: :group_has_users? after_create :create_prices after_create :create_statistic_subtype after_update :update_statistic_subtype, if: :name_changed? + after_update :disable_plans, if: :disabled_changed? def destroyable? users.empty? and plans.empty? end + def group_has_users? + users.count.positive? + end + private def create_prices @@ -60,4 +69,10 @@ class Group < ActiveRecord::Base subtype.label = name subtype.save! end + + def disable_plans + plans.each do |plan| + plan.update_attributes(disabled: disabled) + end + end end diff --git a/app/models/history_value.rb b/app/models/history_value.rb index 3e3a60e8e..d4994aa92 100644 --- a/app/models/history_value.rb +++ b/app/models/history_value.rb @@ -25,14 +25,6 @@ class HistoryValue < ActiveRecord::Base private def compute_footprint - max_date = created_at || Time.current - previous = HistoryValue.where('created_at < ?', max_date) - .order('created_at DESC') - .limit(1) - - columns = HistoryValue.columns.map(&:name) - .delete_if { |c| %w[footprint updated_at].include? c } - - Checksum.text("#{columns.map { |c| self[c] }.join}#{previous.first ? previous.first.footprint : ''}") + FootprintService.compute_footprint(HistoryValue, self, 'created_at') end end diff --git a/app/models/invoice.rb b/app/models/invoice.rb index bc161ba9f..6221ca048 100644 --- a/app/models/invoice.rb +++ b/app/models/invoice.rb @@ -48,52 +48,7 @@ class Invoice < ActiveRecord::Base end def generate_reference - pattern = Setting.find_by(name: 'invoice_reference').value - - # invoice number per day (dd..dd) - reference = pattern.gsub(/d+(?![^\[]*\])/) do |match| - pad_and_truncate(number_of_invoices('day'), match.to_s.length) - end - # invoice number per month (mm..mm) - reference.gsub!(/m+(?![^\[]*\])/) do |match| - pad_and_truncate(number_of_invoices('month'), match.to_s.length) - end - # invoice number per year (yy..yy) - reference.gsub!(/y+(?![^\[]*\])/) do |match| - pad_and_truncate(number_of_invoices('year'), match.to_s.length) - end - - # full year (YYYY) - reference.gsub!(/YYYY(?![^\[]*\])/, Time.now.strftime('%Y')) - # year without century (YY) - reference.gsub!(/YY(?![^\[]*\])/, Time.now.strftime('%y')) - - # abreviated month name (MMM) - reference.gsub!(/MMM(?![^\[]*\])/, Time.now.strftime('%^b')) - # month of the year, zero-padded (MM) - reference.gsub!(/MM(?![^\[]*\])/, Time.now.strftime('%m')) - # month of the year, non zero-padded (M) - reference.gsub!(/M(?![^\[]*\])/, Time.now.strftime('%-m')) - - # day of the month, zero-padded (DD) - reference.gsub!(/DD(?![^\[]*\])/, Time.now.strftime('%d')) - # day of the month, non zero-padded (DD) - reference.gsub!(/DD(?![^\[]*\])/, Time.now.strftime('%-d')) - - # information about online selling (X[text]) - if paid_with_stripe? - reference.gsub!(/X\[([^\]]+)\]/, '\1') - else - reference.gsub!(/X\[([^\]]+)\]/, ''.to_s) - end - - # information about wallet (W[text]) - # reference.gsub!(/W\[([^\]]+)\]/, ''.to_s) - - # remove information about refunds (R[text]) - reference.gsub!(/R\[([^\]]+)\]/, ''.to_s) - - self.reference = reference + self.reference = InvoiceReferenceService.generate_reference(self) end def update_reference @@ -102,43 +57,7 @@ class Invoice < ActiveRecord::Base end def order_number - pattern = Setting.find_by(name: 'invoice_order-nb').value - - # global invoice number (nn..nn) - reference = pattern.gsub(/n+(?![^\[]*\])/) do |match| - pad_and_truncate(number_of_invoices('global'), match.to_s.length) - end - # invoice number per year (yy..yy) - reference.gsub!(/y+(?![^\[]*\])/) do |match| - pad_and_truncate(number_of_invoices('year'), match.to_s.length) - end - # invoice number per month (mm..mm) - reference.gsub!(/m+(?![^\[]*\])/) do |match| - pad_and_truncate(number_of_invoices('month'), match.to_s.length) - end - # invoice number per day (dd..dd) - reference.gsub!(/d+(?![^\[]*\])/) do |match| - pad_and_truncate(number_of_invoices('day'), match.to_s.length) - end - - # full year (YYYY) - reference.gsub!(/YYYY(?![^\[]*\])/, created_at.strftime('%Y')) - # year without century (YY) - reference.gsub!(/YY(?![^\[]*\])/, created_at.strftime('%y')) - - # abbreviated month name (MMM) - reference.gsub!(/MMM(?![^\[]*\])/, created_at.strftime('%^b')) - # month of the year, zero-padded (MM) - reference.gsub!(/MM(?![^\[]*\])/, created_at.strftime('%m')) - # month of the year, non zero-padded (M) - reference.gsub!(/M(?![^\[]*\])/, created_at.strftime('%-m')) - - # day of the month, zero-padded (DD) - reference.gsub!(/DD(?![^\[]*\])/, created_at.strftime('%d')) - # day of the month, non zero-padded (DD) - reference.gsub!(/DD(?![^\[]*\])/, created_at.strftime('%-d')) - - reference + InvoiceReferenceService.generate_order_number(self) end # for debug & used by rake task "fablab:maintenance:regenerate_invoices" @@ -148,7 +67,7 @@ class Invoice < ActiveRecord::Base end def build_avoir(attrs = {}) - raise Exception if refunded? === true || prevent_refund? + raise Exception if refunded? == true || prevent_refund? avoir = Avoir.new(dup.attributes) avoir.type = 'Avoir' @@ -189,7 +108,7 @@ class Invoice < ActiveRecord::Base def subscription_invoice? invoice_items.each do |ii| - return true if ii.subscription && !ii.subscription.expired? + return true if ii.subscription end false end @@ -230,6 +149,18 @@ class Invoice < ActiveRecord::Base total - (wallet_amount || 0) end + # return a summary of the payment means used + def payment_means + res = [] + res.push(means: :wallet, amount: wallet_amount) if wallet_transaction && wallet_amount.positive? + if paid_with_stripe? + res.push(means: :card, amount: amount_paid) + else + res.push(means: :other, amount: amount_paid) + end + res + end + def add_environment self.environment = Rails.env end @@ -244,16 +175,14 @@ class Invoice < ActiveRecord::Base end def set_wallet_transaction(amount, transaction_id) - if check_footprint - update_columns(wallet_amount: amount, wallet_transaction_id: transaction_id) - chain_record - else - raise InvalidFootprintError - end + raise InvalidFootprintError unless check_footprint + + update_columns(wallet_amount: amount, wallet_transaction_id: transaction_id) + chain_record end def paid_with_stripe? - stp_payment_intent_id? || stp_invoice_id? + stp_payment_intent_id? || stp_invoice_id? || payment_method == 'stripe' end private @@ -266,50 +195,8 @@ class Invoice < ActiveRecord::Base InvoiceWorker.perform_async(id, user&.subscription&.expired_at) end - ## - # Output the given integer with leading zeros. If the given value is longer than the given - # length, it will be truncated. - # @param value {Integer} the integer to pad - # @param length {Integer} the length of the resulting string. - ## - def pad_and_truncate(value, length) - value.to_s.rjust(length, '0').gsub(/^.*(.{#{length},}?)$/m, '\1') - end - - ## - # Returns the number of current invoices in the given range around the current date. - # If range is invalid or not specified, the total number of invoices is returned. - # @param range {String} 'day', 'month', 'year' - # @return {Integer} - ## - def number_of_invoices(range) - case range.to_s - when 'day' - start = DateTime.current.beginning_of_day - ending = DateTime.current.end_of_day - when 'month' - start = DateTime.current.beginning_of_month - ending = DateTime.current.end_of_month - when 'year' - start = DateTime.current.beginning_of_year - ending = DateTime.current.end_of_year - else - return id - end - return Invoice.count unless defined? start && defined? ending - - Invoice.where('created_at >= :start_date AND created_at < :end_date', start_date: start, end_date: ending).length - end - def compute_footprint - previous = Invoice.where('id < ?', id) - .order('id DESC') - .limit(1) - - columns = Invoice.columns.map(&:name) - .delete_if { |c| %w[footprint updated_at].include? c } - - Checksum.text("#{columns.map { |c| self[c] }.join}#{previous.first ? previous.first.footprint : ''}") + FootprintService.compute_footprint(Invoice, self) end def log_changes diff --git a/app/models/invoice_item.rb b/app/models/invoice_item.rb index 724b7271f..e6ff29e1d 100644 --- a/app/models/invoice_item.rb +++ b/app/models/invoice_item.rb @@ -21,17 +21,29 @@ class InvoiceItem < ActiveRecord::Base footprint == compute_footprint end + def amount_after_coupon + # deduct coupon discount + coupon_service = CouponService.new + coupon_service.ventilate(invoice.total, amount, invoice.coupon) + end + + # return the item amount, coupon discount deducted, if any, and VAT excluded, if applicable + def net_amount + # deduct VAT + vat_service = VatHistoryService.new + vat_rate = vat_service.invoice_vat(invoice) + Rational(amount_after_coupon / (vat_rate / 100.00 + 1)).round.to_f + end + + # return the VAT amount for this item + def vat + amount_after_coupon - net_amount + end + private def compute_footprint - previous = InvoiceItem.where('id < ?', id) - .order('id DESC') - .limit(1) - - columns = InvoiceItem.columns.map(&:name) - .delete_if { |c| %w[footprint updated_at].include? c } - - Checksum.text("#{columns.map { |c| self[c] }.join}#{previous.first ? previous.first.footprint : ''}") + FootprintService.compute_footprint(InvoiceItem, self) end def log_changes diff --git a/app/models/plan.rb b/app/models/plan.rb index 42ba0aca8..5ede79990 100644 --- a/app/models/plan.rb +++ b/app/models/plan.rb @@ -53,13 +53,13 @@ class Plan < ActiveRecord::Base def create_machines_prices Machine.all.each do |machine| - Price.create(priceable: machine, plan: self, group_id: self.group_id, amount: 0) + Price.create(priceable: machine, plan: self, group_id: group_id, amount: 0) end end def create_spaces_prices Space.all.each do |space| - Price.create(priceable: space, plan: self, group_id: self.group_id, amount: 0) + Price.create(priceable: space, plan: self, group_id: group_id, amount: 0) end end diff --git a/app/models/reservation.rb b/app/models/reservation.rb index aaea5ca77..c1632e462 100644 --- a/app/models/reservation.rb +++ b/app/models/reservation.rb @@ -202,11 +202,14 @@ class Reservation < ActiveRecord::Base end def save_with_payment(operator_profile_id, coupon_code = nil, payment_intent_id = nil) + method = InvoicingProfile.find(operator_profile_id)&.user&.admin? ? nil : 'stripe' + build_invoice( invoicing_profile: user.invoicing_profile, statistic_profile: user.statistic_profile, operator_profile_id: operator_profile_id, - stp_payment_intent_id: payment_intent_id + stp_payment_intent_id: payment_intent_id, + payment_method: method ) generate_invoice_items(true, coupon_code) diff --git a/app/models/setting.rb b/app/models/setting.rb index 517c2b912..a7b676e7b 100644 --- a/app/models/setting.rb +++ b/app/models/setting.rb @@ -40,7 +40,28 @@ class Setting < ActiveRecord::Base visibility_yearly visibility_others display_name_enable - machines_sort_by] } + machines_sort_by + accounting_journal_code + accounting_card_client_code + accounting_card_client_label + accounting_wallet_client_code + accounting_wallet_client_label + accounting_other_client_code + accounting_other_client_label + accounting_wallet_code + accounting_wallet_label + accounting_VAT_code + accounting_VAT_label + accounting_subscription_code + accounting_subscription_label + accounting_Machine_code + accounting_Machine_label + accounting_Training_code + accounting_Training_label + accounting_Event_code + accounting_Event_label + accounting_Space_code + accounting_Space_label] } after_update :update_stylesheet, :notify_privacy_policy_changed if :value_changed? diff --git a/app/models/subscription.rb b/app/models/subscription.rb index 4f6c6c9da..4d3160eb2 100644 --- a/app/models/subscription.rb +++ b/app/models/subscription.rb @@ -46,6 +46,7 @@ class Subscription < ActiveRecord::Base def generate_invoice(operator_profile_id, coupon_code = nil, payment_intent_id = nil) coupon_id = nil total = plan.amount + method = InvoicingProfile.find(operator_profile_id)&.user&.admin? ? nil : 'stripe' unless coupon_code.nil? @coupon = Coupon.find_by(code: coupon_code) @@ -64,7 +65,8 @@ class Subscription < ActiveRecord::Base total: total, coupon_id: coupon_id, operator_profile_id: operator_profile_id, - stp_payment_intent_id: payment_intent_id + stp_payment_intent_id: payment_intent_id, + payment_method: method ) invoice.invoice_items.push InvoiceItem.new( amount: plan.amount, diff --git a/app/pdfs/pdf/invoice.rb b/app/pdfs/pdf/invoice.rb index 0efde3216..5d0f8a5cc 100644 --- a/app/pdfs/pdf/invoice.rb +++ b/app/pdfs/pdf/invoice.rb @@ -121,6 +121,8 @@ class PDF::Invoice < Prawn::Document data = [[I18n.t('invoices.details'), I18n.t('invoices.amount')]] total_calc = 0 + total_ht = 0 + total_vat = 0 # going through invoice_items invoice.invoice_items.each do |item| @@ -184,6 +186,8 @@ class PDF::Invoice < Prawn::Document data += [[details, number_to_currency(price)]] total_calc += price + total_ht += item.net_amount + total_vat += item.vat end ## subtract the coupon, if any @@ -210,15 +214,13 @@ class PDF::Invoice < Prawn::Document # discount textual description literal_discount = cp.percent_off - if cp.type == 'amount_off' - literal_discount = number_to_currency(cp.amount_off / 100.00) - end + literal_discount = number_to_currency(cp.amount_off / 100.00) if cp.type == 'amount_off' # add a row for the coupon data += [[_t('invoices.coupon_CODE_discount_of_DISCOUNT', CODE: cp.code, DISCOUNT: literal_discount, - TYPE: cp.type), number_to_currency(-discount)] ] + TYPE: cp.type), number_to_currency(-discount)]] end # total verification @@ -226,20 +228,18 @@ class PDF::Invoice < Prawn::Document puts "ERROR: totals are NOT equals => expected: #{total}, computed: #{total_calc}" if total_calc != total # TVA - if Setting.find_by(name: 'invoice_VAT-active').value == 'true' + vat_service = VatHistoryService.new + vat_rate = vat_service.invoice_vat(invoice) + if vat_rate != 0 data += [[I18n.t('invoices.total_including_all_taxes'), number_to_currency(total)]] - - vat_service = VatHistoryService.new - vat_rate = vat_service.invoice_vat(invoice) - vat = total / (vat_rate / 100.00 + 1) - data += [[I18n.t('invoices.including_VAT_RATE', RATE: vat_rate), number_to_currency(total - vat)]] - data += [[I18n.t('invoices.including_total_excluding_taxes'), number_to_currency(vat)]] + data += [[I18n.t('invoices.including_VAT_RATE', RATE: vat_rate), number_to_currency(total_vat / 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)]] # checking the round number - rounded = sprintf('%.2f', vat).to_f + sprintf('%.2f', total - vat).to_f + rounded = sprintf('%.2f', total_vat / 100.00).to_f + sprintf('%.2f', total_ht / 100.00).to_f if rounded != sprintf('%.2f', total_calc).to_f - puts 'ERROR: rounding the numbers cause an invoice inconsistency. ' + + puts 'ERROR: rounding the numbers cause an invoice inconsistency. ' \ "Total expected: #{sprintf('%.2f', total_calc)}, total computed: #{rounded}" end else @@ -279,7 +279,7 @@ class PDF::Invoice < Prawn::Document move_down 20 if invoice.is_a?(Avoir) payment_verbose = I18n.t('invoices.refund_on_DATE', DATE:I18n.l(invoice.avoir_date.to_date)) + ' ' - case invoice.avoir_mode + case invoice.payment_method when 'stripe' payment_verbose += I18n.t('invoices.by_stripe_online_payment') when 'cheque' diff --git a/app/policies/accounting_export_policy.rb b/app/policies/accounting_export_policy.rb new file mode 100644 index 000000000..402696dea --- /dev/null +++ b/app/policies/accounting_export_policy.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +# Check the access policies for API::AccountingExportsController +class AccountingExportPolicy < ApplicationPolicy + def export? + user.admin? + end +end diff --git a/app/policies/invoice_policy.rb b/app/policies/invoice_policy.rb index 20489fa7c..4f22a18e6 100644 --- a/app/policies/invoice_policy.rb +++ b/app/policies/invoice_policy.rb @@ -14,4 +14,8 @@ class InvoicePolicy < ApplicationPolicy def list? user.admin? end + + def first? + user.admin? + end end diff --git a/app/policies/setting_policy.rb b/app/policies/setting_policy.rb index 5dc9e277d..aacdb6453 100644 --- a/app/policies/setting_policy.rb +++ b/app/policies/setting_policy.rb @@ -1,5 +1,8 @@ +# frozen_string_literal: true + +# Check the access policies for API::SettingsController class SettingPolicy < ApplicationPolicy - %w(update).each do |action| + %w[update bulk_update].each do |action| define_method "#{action}?" do user.admin? end diff --git a/app/services/accounting_export_service.rb b/app/services/accounting_export_service.rb index 19efe6dd8..144adb286 100644 --- a/app/services/accounting_export_service.rb +++ b/app/services/accounting_export_service.rb @@ -2,22 +2,36 @@ # Provides the routine to export the accounting data to an external accounting software class AccountingExportService - attr_reader :encoding, :format, :separator, :journal_code, :date_format, :columns, :vat_service + include ActionView::Helpers::NumberHelper - def initialize(columns, encoding = 'UTF-8', format = 'CSV', separator = ';', date_format = '%d/%m/%Y') + attr_reader :encoding, :format, :separator, :journal_code, :date_format, :columns, :decimal_separator, :label_max_length, + :export_zeros + + def initialize(columns, encoding: 'UTF-8', format: 'CSV', separator: ';') @encoding = encoding @format = format @separator = separator + @decimal_separator = ',' + @date_format = '%d/%m/%Y' + @label_max_length = 50 + @export_zeros = false @journal_code = Setting.find_by(name: 'accounting_journal_code')&.value || '' @date_format = date_format @columns = columns - @vat_service = VatHistoryService.new + end + + def set_options(decimal_separator: ',', date_format: '%d/%m/%Y', label_max_length: 50, export_zeros: false) + @decimal_separator = decimal_separator + @date_format = date_format + @label_max_length = label_max_length + @export_zeros = export_zeros end def export(start_date, end_date, file) # build CVS content content = header_row invoices = Invoice.where('created_at >= ? AND created_at <= ?', start_date, end_date).order('created_at ASC') + invoices = invoices.where('total > 0') unless export_zeros invoices.each do |i| content << generate_rows(i) end @@ -37,9 +51,12 @@ class AccountingExportService end def generate_rows(invoice) - "#{client_row(invoice)}\n" \ - "#{items_rows(invoice)}" \ - "#{vat_row(invoice)}\n" + rows = client_rows(invoice) + items_rows(invoice) + + vat = vat_row(invoice) + rows += "#{vat}\n" unless vat.nil? + + rows end # Generate the "subscription" and "reservation" rows associated with the provided invoice @@ -49,122 +66,78 @@ class AccountingExportService invoice.invoice_items.each do |item| rows << "#{reservation_row(invoice, item)}\n" end + elsif invoice.invoiced_type == 'WalletTransaction' + rows << "#{wallet_row(invoice)}\n" end rows end - # Generate the "client" row, which contains the debit to the client account, all taxes included - def client_row(invoice) - total = invoice.total / 100.00 - row = '' - columns.each do |column| - case column - when 'journal_code' - row << journal_code - when 'date' - row << invoice.created_at&.strftime(date_format) - when 'account_code' - row << account(invoice, :client) - when 'account_label' - row << account(invoice, :client, :label) - when 'piece' - row << invoice.reference - when 'line_label' - row << label(invoice.invoicing_profile.full_name) - when 'debit_origin' - row << debit_client(invoice, total) - when 'credit_origin' - row << credit_client(invoice, total) - when 'debit_euro' - row << debit_client(invoice, total) - when 'credit_euro' - row << credit_client(invoice, total) - when 'lettering' - row << '' - else - puts "Unsupported column: #{column}" - end - row << separator + # Generate the "client" rows, which contains the debit to the client account, all taxes included + def client_rows(invoice) + rows = '' + invoice.payment_means.each_with_index do |details, index| + rows << row( + invoice, + account(invoice, :client, means: details[:means]), + account(invoice, :client, means: details[:means], type: :label), + details[:amount] / 100.00, + line_label: index.zero? ? label(invoice) : '', + debit_method: :debit_client, + credit_method: :credit_client + ) + rows << "\n" end - row + rows end # Generate the "reservation" row, which contains the credit to the reservation account, all taxes excluded def reservation_row(invoice, item) - wo_taxes = (item.amount / (vat_service.invoice_vat(invoice) / 100.00 + 1)) / 100.00 - row = '' - columns.each do |column| - case column - when 'journal_code' - row << journal_code - when 'date' - row << invoice.created_at&.strftime(date_format) - when 'account_code' - row << account(invoice, :reservation) - when 'account_label' - row << account(invoice, :reservation, :label) - when 'piece' - row << invoice.reference - when 'line_label' - row << label(item.description) - when 'debit_origin' - row << debit(invoice, wo_taxes) - when 'credit_origin' - row << credit(invoice, wo_taxes) - when 'debit_euro' - row << debit(invoice, wo_taxes) - when 'credit_euro' - row << credit(invoice, wo_taxes) - when 'lettering' - row << '' - else - puts "Unsupported column: #{column}" - end - row << separator - end - row + row( + invoice, + account(invoice, :reservation), + account(invoice, :reservation, type: :label), + item.net_amount / 100.00 + ) end # Generate the "subscription" row, which contains the credit to the subscription account, all taxes excluded def subscription_row(invoice) subscription_item = invoice.invoice_items.select(&:subscription).first - wo_taxes = (subscription_item.amount / (vat_service.invoice_vat(invoice) / 100.00 + 1)) / 100.00 - row = '' - columns.each do |column| - case column - when 'journal_code' - row << journal_code - when 'date' - row << invoice.created_at&.strftime(date_format) - when 'account_code' - row << account(invoice, :subscription) - when 'account_label' - row << account(invoice, :subscription, :label) - when 'piece' - row << invoice.reference - when 'line_label' - row << label(subscription_item.description) - when 'debit_origin' - row << debit(invoice, wo_taxes) - when 'credit_origin' - row << credit(invoice, wo_taxes) - when 'debit_euro' - row << debit(invoice, wo_taxes) - when 'credit_euro' - row << credit(invoice, wo_taxes) - when 'lettering' - row << '' - else - puts "Unsupported column: #{column}" - end - row << separator - end - row + row( + invoice, + account(invoice, :subscription), + account(invoice, :subscription, type: :label), + subscription_item.net_amount / 100.00 + ) + end + + # Generate the "wallet" row, which contains the credit to the wallet account, all taxes excluded + # This applies to wallet crediting, when an Avoir is generated at this time + def wallet_row(invoice) + row( + invoice, + account(invoice, :wallet), + account(invoice, :wallet, type: :label), + invoice.invoice_items.first.net_amount / 100.00 + ) end # Generate the "VAT" row, which contains the credit to the VAT account, with VAT amount only def vat_row(invoice) - vat = (invoice.total - (invoice.total / (vat_service.invoice_vat(invoice) / 100.00 + 1))) / 100.00 + rate = VatHistoryService.new.invoice_vat(invoice) + # we do not render the VAT row if it was disabled for this invoice + return nil if rate.zero? + + row( + invoice, + account(invoice, :vat), + account(invoice, :vat, type: :label), + invoice.invoice_items.map(&:vat).map(&:to_i).reduce(:+) / 100.00 + ) + end + + # Generate a row of the export, filling the configured columns with the provided values + def row(invoice, account_code, account_label, amount, line_label: '', debit_method: :debit, credit_method: :credit) row = '' columns.each do |column| case column @@ -173,21 +146,21 @@ class AccountingExportService when 'date' row << invoice.created_at&.strftime(date_format) when 'account_code' - row << account(invoice, :vat) + row << account_code when 'account_label' - row << account(invoice, :vat, :label) + row << account_label when 'piece' row << invoice.reference when 'line_label' - row << I18n.t('accounting_export.VAT') + row << line_label when 'debit_origin' - row << debit(invoice, vat) + row << method(debit_method).call(invoice, amount) when 'credit_origin' - row << credit(invoice, vat) + row << method(credit_method).call(invoice, amount) when 'debit_euro' - row << debit(invoice, vat) + row << method(debit_method).call(invoice, amount) when 'credit_euro' - row << credit(invoice, vat) + row << method(credit_method).call(invoice, amount) when 'lettering' row << '' else @@ -199,40 +172,45 @@ class AccountingExportService end # Get the account code (or label) for the given invoice and the specified line type (client, vat, subscription or reservation) - def account(invoice, account, type = :code) - res = case account - when :client - Setting.find_by(name: "accounting_client_#{type}")&.value - when :vat - Setting.find_by(name: "accounting_VAT_#{type}")&.value - when :subscription - if invoice.subscription_invoice? - Setting.find_by(name: "accounting_subscription_#{type}")&.value - else - puts "WARN: Invoice #{invoice.id} has no subscription" - end - when :reservation - if invoice.invoiced_type == 'Reservation' - Setting.find_by(name: "accounting_#{invoice.invoiced.reservable_type}_#{type}")&.value - else - puts "WARN: Invoice #{invoice.id} has no reservation" - end - else - puts "Unsupported account #{account}" - end - res || '' + def account(invoice, account, type: :code, means: :other) + case account + when :client + Setting.find_by(name: "accounting_#{means}_client_#{type}")&.value + when :vat + Setting.find_by(name: "accounting_VAT_#{type}")&.value + when :subscription + if invoice.subscription_invoice? + Setting.find_by(name: "accounting_subscription_#{type}")&.value + else + puts "WARN: Invoice #{invoice.id} has no subscription" + end + when :reservation + if invoice.invoiced_type == 'Reservation' + Setting.find_by(name: "accounting_#{invoice.invoiced.reservable_type}_#{type}")&.value + else + puts "WARN: Invoice #{invoice.id} has no reservation" + end + when :wallet + if invoice.invoiced_type == 'WalletTransaction' + Setting.find_by(name: "accounting_wallet_#{type}")&.value + else + puts "WARN: Invoice #{invoice.id} is not a wallet credit" + end + else + puts "Unsupported account #{account}" + end || '' end # Fill the value of the "debit" column: if the invoice is a refund, returns the given amount, returns 0 otherwise def debit(invoice, amount) avoir = invoice.is_a? Avoir - avoir ? amount.to_s : '0' + avoir ? format_number(amount) : '0' end # Fill the value of the "credit" column: if the invoice is a refund, returns 0, otherwise, returns the given amount def credit(invoice, amount) avoir = invoice.is_a? Avoir - avoir ? '0' : amount.to_s + avoir ? '0' : format_number(amount) end # Fill the value of the "debit" column for the client row: if the invoice is a refund, returns 0, otherwise, returns the given amount @@ -245,9 +223,22 @@ class AccountingExportService debit(invoice, amount) end - # Format the given text to match the accounting software rules for the labels - def label(text) - res = text.tr separator, '' - res.truncate(50) + # 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 + + # Create a text from the given invoice, matching the accounting software rules for the labels + def label(invoice) + name = "#{invoice.invoicing_profile.last_name} #{invoice.invoicing_profile.first_name}".tr separator, '' + reference = invoice.reference + + items = invoice.subscription_invoice? ? [I18n.t('accounting_export.subscription')] : [] + items.push I18n.t("accounting_export.#{invoice.reservation.reservable_type}_reservation") if invoice.invoiced_type == 'Reservation' + items.push I18n.t('accounting_export.wallet') if invoice.invoiced_type == 'WalletTransaction' + + summary = items.join(' + ') + res = "#{reference}, #{summary}" + "#{name.truncate(label_max_length - res.length)}, #{res}" end end diff --git a/app/services/coupon_service.rb b/app/services/coupon_service.rb index a16b6d4f1..a81055bf8 100644 --- a/app/services/coupon_service.rb +++ b/app/services/coupon_service.rb @@ -1,3 +1,6 @@ +# frozen_string_literal: true + +# This class provides helper methods to deal with coupons class CouponService ## # Apply the provided coupon, if active, to the given price. Usability tests will be run depending on the @@ -54,4 +57,14 @@ class CouponService end price end + + ## + # Compute the total amount of the given invoice, without the applied coupon + # Invoice.total stores the amount payed by the customer, coupon deducted + # @param invoice {Invoice} invoice object, its total before discount will be computed + ## + def invoice_total_no_coupon(invoice) + total = (invoice.invoice_items.map(&:amount).map(&:to_i).reduce(:+) or 0) + total / 100.0 + end end diff --git a/app/services/footprint_service.rb b/app/services/footprint_service.rb new file mode 100644 index 000000000..c5c584b1b --- /dev/null +++ b/app/services/footprint_service.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +# Provides helper methods to compute footprints +class FootprintService + # Compute the footprint + # @param class_name Invoice|InvoiceItem|HistoryValue + # @param item an instance of the provided class + # @param sort the items in database by the provided criterion, to find the previous one + def self.compute_footprint(klass, item, sort_on = 'id') + raise TypeError unless item.is_a? klass + + previous = klass.where("#{sort_on} < ?", item[sort_on]) + .order("#{sort_on} DESC") + .limit(1) + + columns = klass.columns.map(&:name) + .delete_if { |c| %w[footprint updated_at].include? c } + + Checksum.text("#{columns.map { |c| item[c] }.join}#{previous.first ? previous.first.footprint : ''}") + end +end diff --git a/app/services/invoice_reference_service.rb b/app/services/invoice_reference_service.rb new file mode 100644 index 000000000..e756a7aa9 --- /dev/null +++ b/app/services/invoice_reference_service.rb @@ -0,0 +1,135 @@ +# frozen_string_literal: true + +# Provides methods to generate invoice references +class InvoiceReferenceService + class << self + def generate_reference(invoice, date: Time.now, avoir: false) + pattern = Setting.find_by(name: 'invoice_reference').value + + reference = replace_invoice_number_pattern(pattern, invoice) + reference = replace_date_pattern(reference, date) + + if avoir + # information about refund/avoir (R[text]) + reference.gsub!(/R\[([^\]]+)\]/, '\1') + + # remove information about online selling (X[text]) + reference.gsub!(/X\[([^\]]+)\]/, ''.to_s) + else + # information about online selling (X[text]) + if invoice.paid_with_stripe? + reference.gsub!(/X\[([^\]]+)\]/, '\1') + else + reference.gsub!(/X\[([^\]]+)\]/, ''.to_s) + end + + # remove information about refunds (R[text]) + reference.gsub!(/R\[([^\]]+)\]/, ''.to_s) + end + + reference + end + + def generate_order_number(invoice) + pattern = Setting.find_by(name: 'invoice_order-nb').value + + # global invoice number (nn..nn) + reference = pattern.gsub(/n+(?![^\[]*\])/) do |match| + pad_and_truncate(number_of_invoices(invoice, 'global'), match.to_s.length) + end + + reference = replace_invoice_number_pattern(reference, invoice) + replace_date_pattern(reference, invoice.created_at) + end + + private + + ## + # Output the given integer with leading zeros. If the given value is longer than the given + # length, it will be truncated. + # @param value {Integer} the integer to pad + # @param length {Integer} the length of the resulting string. + ## + def pad_and_truncate(value, length) + value.to_s.rjust(length, '0').gsub(/^.*(.{#{length},}?)$/m, '\1') + end + + ## + # Returns the number of current invoices in the given range around the current date. + # If range is invalid or not specified, the total number of invoices is returned. + # @param invoice {Invoice} + # @param range {String} 'day', 'month', 'year' + # @return {Integer} + ## + def number_of_invoices(invoice, range) + case range.to_s + when 'day' + start = DateTime.current.beginning_of_day + ending = DateTime.current.end_of_day + when 'month' + start = DateTime.current.beginning_of_month + ending = DateTime.current.end_of_month + when 'year' + start = DateTime.current.beginning_of_year + ending = DateTime.current.end_of_year + else + return invoice.id + end + return Invoice.count unless defined? start && defined? ending + + Invoice.where('created_at >= :start_date AND created_at < :end_date', start_date: start, end_date: ending).length + end + + ## + # Replace the date elements in the provided pattern with the date values, from the provided date + # @param reference {string} + # @param date {DateTime} + ## + def replace_date_pattern(reference, date) + copy = reference.dup + + # full year (YYYY) + copy.gsub!(/YYYY(?![^\[]*\])/, date.strftime('%Y')) + # year without century (YY) + copy.gsub!(/YY(?![^\[]*\])/, date.strftime('%y')) + + # abbreviated month name (MMM) + copy.gsub!(/MMM(?![^\[]*\])/, date.strftime('%^b')) + # month of the year, zero-padded (MM) + copy.gsub!(/MM(?![^\[]*\])/, date.strftime('%m')) + # month of the year, non zero-padded (M) + copy.gsub!(/M(?![^\[]*\])/, date.strftime('%-m')) + + # day of the month, zero-padded (DD) + copy.gsub!(/DD(?![^\[]*\])/, date.strftime('%d')) + # day of the month, non zero-padded (DD) + copy.gsub!(/DD(?![^\[]*\])/, date.strftime('%-d')) + + copy + end + + ## + # Replace the invoice number elements in the provided pattern with counts from the database + # @param reference {string} + # @param invoice {Invoice} + ## + def replace_invoice_number_pattern(reference, invoice) + copy = reference.dup + + # invoice number per year (yy..yy) + copy.gsub!(/y+(?![^\[]*\])/) do |match| + pad_and_truncate(number_of_invoices(invoice, 'year'), match.to_s.length) + end + # invoice number per month (mm..mm) + copy.gsub!(/m+(?![^\[]*\])/) do |match| + pad_and_truncate(number_of_invoices(invoice, 'month'), match.to_s.length) + end + # invoice number per day (dd..dd) + copy.gsub!(/d+(?![^\[]*\])/) do |match| + pad_and_truncate(number_of_invoices(invoice, 'day'), match.to_s.length) + end + + copy + end + end +end diff --git a/app/services/statistic_service.rb b/app/services/statistic_service.rb index 4a97fcfc3..abb7f883d 100644 --- a/app/services/statistic_service.rb +++ b/app/services/statistic_service.rb @@ -131,10 +131,12 @@ class StatisticService next if i.invoice.is_a?(Avoir) sub = i.subscription + next unless sub ca = i.amount.to_i / 100.0 - ca = CouponService.new.ventilate(get_invoice_total_no_coupon(i.invoice), ca, i.invoice.coupon) unless i.invoice.coupon_id.nil? + cs = CouponService.new + ca = cs.ventilate(cs.invoice_total_no_coupon(i.invoice), ca, i.invoice.coupon) unless i.invoice.coupon_id.nil? profile = sub.statistic_profile p = sub.plan result.push OpenStruct.new({ @@ -396,7 +398,8 @@ class StatisticService end end # subtract coupon discount from invoices and refunds - ca = CouponService.new.ventilate(get_invoice_total_no_coupon(invoice), ca, invoice.coupon) unless invoice.coupon_id.nil? + cs = CouponService.new + ca = cs.ventilate(cs.invoice_total_no_coupon(invoice), ca, invoice.coupon) unless invoice.coupon_id.nil? # divide the result by 100 to convert from centimes to monetary unit ca.zero? ? ca : ca / 100.0 end @@ -407,7 +410,8 @@ class StatisticService ca -= ii.amount.to_i end # subtract coupon discount from the refund - ca = CouponService.new.ventilate(get_invoice_total_no_coupon(invoice), ca, invoice.coupon) unless invoice.coupon_id.nil? + cs = CouponService.new + ca = cs.ventilate(cs.invoice_total_no_coupon(invoice), ca, invoice.coupon) unless invoice.coupon_id.nil? ca.zero? ? ca : ca / 100.0 end @@ -488,11 +492,6 @@ class StatisticService # user_subscription_ca.inject {|sum,x| sum.ca + x.ca } || 0 # end - def get_invoice_total_no_coupon(invoice) - total = (invoice.invoice_items.map(&:amount).map(&:to_i).reduce(:+) or 0) - total / 100.0 - end - def find_or_create_user_info_info_list(profile, list) found = list.select do |l| l.statistic_profile_id == profile.id diff --git a/app/services/statistics_export_service.rb b/app/services/statistics_export_service.rb index 58996a92e..9c51dad29 100644 --- a/app/services/statistics_export_service.rb +++ b/app/services/statistics_export_service.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'abstract_controller' require 'action_controller' require 'action_view' @@ -13,7 +15,7 @@ class StatisticsExportService # query all stats with range arguments query = MultiJson.load(export.query) - @results = Elasticsearch::Model.client.search({index: 'stats', scroll: '30s', body: query}) + @results = Elasticsearch::Model.client.search(index: 'stats', scroll: '30s', body: query) scroll_id = @results['_scroll_id'] while @results['hits']['hits'].size != @results['hits']['total'] scroll_res = Elasticsearch::Model.client.scroll(scroll: '30s', scroll_id: scroll_id) @@ -22,9 +24,9 @@ class StatisticsExportService end ids = @results['hits']['hits'].map { |u| u['_source']['userId'] } - @users = User.includes(:profile).where(:id => ids) + @users = User.includes(:profile).where(id: ids) - @indices = StatisticIndex.all.includes(:statistic_fields, :statistic_types => [:statistic_sub_types]) + @indices = StatisticIndex.all.includes(:statistic_fields, statistic_types: [:statistic_sub_types]) ActionController::Base.prepend_view_path './app/views/' # place data in view_assigns @@ -37,10 +39,10 @@ class StatisticsExportService content = av.render template: 'exports/statistics_global.xlsx.axlsx' # write content to file - File.open(export.file,"w+b") {|f| f.puts content } + File.open(export.file, 'w+b') { |f| f.puts content } end - %w(account event machine project subscription training space).each do |path| + %w[account event machine project subscription training space].each do |path| class_eval %{ def export_#{path}(export) @@ -76,7 +78,7 @@ class StatisticsExportService # write content to file File.open(export.file,"w+b") {|f| f.puts content } end - } + }, __FILE__, __LINE__ - 35 end -end \ No newline at end of file +end diff --git a/app/services/vat_history_service.rb b/app/services/vat_history_service.rb index c5b302621..320f62d69 100644 --- a/app/services/vat_history_service.rb +++ b/app/services/vat_history_service.rb @@ -26,13 +26,21 @@ class VatHistoryService private def vat_history - key_dates = [] - Setting.find_by(name: 'invoice_VAT-rate').history_values.each do |rate| - key_dates.push(date: rate.created_at, rate: rate.value.to_i) + chronology = [] + end_date = DateTime.now + Setting.find_by(name: 'invoice_VAT-active').history_values.order(created_at: 'DESC').each do |v| + chronology.push(start: v.created_at, end: end_date, enabled: v.value == 'true') + end_date = v.created_at end - Setting.find_by(name: 'invoice_VAT-active').history_values.each do |v| - key_dates.push(date: v.created_at, rate: 0) if v.value == 'false' + date_rates = [] + Setting.find_by(name: 'invoice_VAT-rate').history_values.order(created_at: 'ASC').each do |rate| + range = chronology.select { |p| rate.created_at.between?(p[:start], p[:end]) }.first + date = range[:enabled] ? rate.created_at : range[:end] + date_rates.push(date: date, rate: rate.value.to_i) end - key_dates.sort_by { |k| k[:date] } + chronology.reverse_each do |period| + date_rates.push(date: period[:start], rate: 0) unless period[:enabled] + end + date_rates.sort_by { |k| k[:date] } end -end \ No newline at end of file +end diff --git a/app/services/wallet_service.rb b/app/services/wallet_service.rb index 12d0a74e6..3dd88bc7c 100644 --- a/app/services/wallet_service.rb +++ b/app/services/wallet_service.rb @@ -51,7 +51,7 @@ class WalletService avoir.avoir_date = avoir_date avoir.created_at = avoir_date avoir.description = description - avoir.avoir_mode = 'wallet' + avoir.payment_method = 'wallet' avoir.subscription_to_expire = false avoir.invoicing_profile_id = wallet_transaction.wallet.user.invoicing_profile.id avoir.total = wallet_transaction.amount * 100.0 diff --git a/app/views/api/invoices/avoir.json.jbuilder b/app/views/api/invoices/avoir.json.jbuilder index 8dcf63f21..5ff6ea8fd 100644 --- a/app/views/api/invoices/avoir.json.jbuilder +++ b/app/views/api/invoices/avoir.json.jbuilder @@ -1,4 +1,4 @@ -json.extract! @avoir, :id, :created_at, :reference, :invoiced_type, :avoir_date, :avoir_mode, :invoice_id +json.extract! @avoir, :id, :created_at, :reference, :invoiced_type, :avoir_date, :payment_method, :invoice_id json.user_id @avoir.invoicing_profile.user_id json.total @avoir.total / 100.00 json.name @avoir.user.profile.full_name diff --git a/app/views/api/invoices/first.json.jbuilder b/app/views/api/invoices/first.json.jbuilder new file mode 100644 index 000000000..f0e28831b --- /dev/null +++ b/app/views/api/invoices/first.json.jbuilder @@ -0,0 +1 @@ +json.date @first diff --git a/app/views/api/settings/bulk_update.json.jbuilder b/app/views/api/settings/bulk_update.json.jbuilder new file mode 100644 index 000000000..db2ba1972 --- /dev/null +++ b/app/views/api/settings/bulk_update.json.jbuilder @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +json.settings @settings.each do |setting| + if setting[:errors] + json.error setting.errors.full_messages + json.id setting.id + json.name setting.name + else + json.partial! 'api/settings/setting', setting: setting + end +end diff --git a/app/views/api/settings/index.json.jbuilder b/app/views/api/settings/index.json.jbuilder index eef731fd6..a365b855a 100644 --- a/app/views/api/settings/index.json.jbuilder +++ b/app/views/api/settings/index.json.jbuilder @@ -1,3 +1,5 @@ +# frozen_string_literal: true + @settings.each do |setting| json.set! setting.name, setting.value end diff --git a/app/views/api/settings/update.json.jbuilder b/app/views/api/settings/update.json.jbuilder index c0c489dbb..cdb9acca8 100644 --- a/app/views/api/settings/update.json.jbuilder +++ b/app/views/api/settings/update.json.jbuilder @@ -1,3 +1,5 @@ +# frozen_string_literal: true + json.setting do json.partial! 'api/settings/setting', setting: @setting end diff --git a/app/views/notifications_mailer/notify_admin_export_complete.html.erb b/app/views/notifications_mailer/notify_admin_export_complete.html.erb index 9097d833c..03731e24e 100644 --- a/app/views/notifications_mailer/notify_admin_export_complete.html.erb +++ b/app/views/notifications_mailer/notify_admin_export_complete.html.erb @@ -5,6 +5,6 @@ <%= t(".body.#{@attached_object.category}_#{@attached_object.export_type}") %>.

- <%= t('.body.click_to_download') %> + <%= t('.body.click_to_download', TYPE: t(".body.file_type.#{@attached_object.extension}")) %> <%=link_to( t('.body.here'), "#{root_url}api/exports/#{@attached_object.id}/download", target: "_blank" )%> -

\ No newline at end of file +

diff --git a/app/workers/accounting_export_worker.rb b/app/workers/accounting_export_worker.rb new file mode 100644 index 000000000..a0bd28c22 --- /dev/null +++ b/app/workers/accounting_export_worker.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +# Asynchronously export the accounting data (Invoices & Avoirs) to an external accounting software +class AccountingExportWorker + include Sidekiq::Worker + + def perform(export_id) + export = Export.find(export_id) + + raise SecurityError, 'Not allowed to export' unless export.user.admin? + + data = JSON.parse(export.query) + service = AccountingExportService.new( + data['columns'], + encoding: data['encoding'], format: export.extension, separator: export.key + ) + service.set_options(date_format: data['date_format'], decimal_separator: data['decimal_separator'], + label_max_length: data['label_max_length'], export_zeros: data['export_invoices_at_zero']) + + service.export(data['start_date'], data['end_date'], export.file) + + NotificationCenter.call type: :notify_admin_export_complete, + receiver: export.user, + attached_object: export + end +end diff --git a/config/locales/app.admin.en.yml b/config/locales/app.admin.en.yml index ed9aa1521..a0273d529 100644 --- a/config/locales/app.admin.en.yml +++ b/config/locales/app.admin.en.yml @@ -322,6 +322,7 @@ en: total_amount: "Total amount" total_including_all_taxes: "Total incl. all taxes" VAT_disabled: "VAT disabled" + VAT_enabled: "VAT enabled" including_VAT: "Including VAT" including_total_excluding_taxes: "Including Total excl. taxes" including_amount_payed_on_ordering: "Including Amount payed on ordering" @@ -432,6 +433,76 @@ en: period_START_END_closed_success: "The accounting period from {{START}} to {{END}} has been successfully closed. Archive generation is running, you'll be notified when it's done." failed_to_close_period: "An error occurred, unable to close the accounting period" no_periods: "No closings for now" + accounting_codes: "Accounting codes" + accounting_journal_code: "Journal code" + general_journal_code: "Journal code" + accounting_card_client_code: "Card clients code" + card_client_code: "Accounting code for clients who paid by card" + accounting_card_client_label: "Card clients label" + card_client_label: "Account label for clients who paid by card" + accounting_wallet_client_code: "Wallet clients code" + wallet_client_code: "Accounting code for clients who paid by virtual wallet" + accounting_wallet_client_label: "Wallet clients label" + wallet_client_label: "Account label for clients who paid by virtual wallet" + accounting_other_client_code: "Other means client code" + other_client_code: "Accounting code for clients who paid using another payment means" + accounting_other_client_label: "Other means client label" + other_client_label: "Accounting label for clients who paid using another payment means" + accounting_wallet_code: "Wallet code" + general_wallet_code: "Accounting code for wallet credit" + accounting_wallet_label: "Wallet label" + general_wallet_label: "Account label for wallet credit" + accounting_vat_code: "VAT code" + general_vat_code: "Accounting code for VAT" + accounting_vat_label: "VAT label" + general_vat_label: "VAT account label" + accounting_subscription_code: "Subscriptions code" + general_subscription_code: "Accounting code for all subscriptions" + accounting_subscription_label: "Subscriptions label" + general_subscription_label: "Account label for all subscriptions" + accounting_Machine_code: "Machines code" + general_machine_code: "Accounting code for all machines" + accounting_Machine_label: "Machine label" + general_machine_label: "Account label for all machines" + accounting_Training_code: "Trainings code" + general_training_code: "Accounting code for all trainings" + accounting_Training_label: "Trainings label" + general_training_label: "Account label for all trainings" + accounting_Event_code: "Events code" + general_event_code: "Accounting code for all events" + accounting_Event_label: "Events label" + general_event_label: "Account label for all events" + accounting_Space_code: "Space code" + general_space_code: "Accounting code for all spaces" + accounting_Space_label: "Spaces label" + general_space_label: "Account label for all spaces" + codes_customization_success: "Customization of the accounting codes successfully saved." + export_accounting_data: "Export accounting data" + export_to: "Export to the accounting software" + export_is_running: "Export is running. You'll be notified when it's ready." + acd: "ACD" + export_form_date: "Export from" + export_to_date: "Export until" + format: "File format" + encoding: "Encoding" + separator: "Separator" + dateFormat: "Date format" + labelMaxLength: "Label (max)" + decimalSeparator: "Decimal separator" + exportInvoicesAtZero: "Export invoices equal to 0" + columns: "Columns" + exportColumns: + journal_code: "Journal code" + date: "Entry date" + account_code: "Account code" + account_label: "Account label" + piece: "Document" + line_label: "Entry label" + debit_origin: "Origin debit" + credit_origin: "Origin credit" + debit_euro: "Euro debit" + credit_euro: "Euro credit" + lettering: "Lettering" members: # management of users, labels, groups, and so on diff --git a/config/locales/app.admin.es.yml b/config/locales/app.admin.es.yml index cfa5fc853..71a9d7e5d 100644 --- a/config/locales/app.admin.es.yml +++ b/config/locales/app.admin.es.yml @@ -321,8 +321,9 @@ es: machine_booking-3D_printer: "Reserva de la máquina- Impresora 3D" total_amount: "Cantidad total" total_including_all_taxes: "Total incl. todos los impuestos" - VAT_disabled: "VAT disabled" - including_VAT: "IVA desactivado" + VAT_disabled: "IVA desactivado" + VAT_enabled: "IVA activado" + including_VAT: "Incluido IVA" including_total_excluding_taxes: "Incluido Total excl. impuestos" including_amount_payed_on_ordering: "Incluido el monto pagado en el pedido" settlement_by_debit_card_on_DATE_at_TIME_for_an_amount_of_AMOUNT: "Liquidación por tarjeta de débito el {{DATE}} a las {{TIME}}, por una cantidad de {{AMOUNT}}" @@ -432,6 +433,76 @@ es: period_START_END_closed_success: "The accounting period from {{START}} to {{END}} has been successfully closed. Archive generation is running, you'll be notified when it's done." # translation_missing failed_to_close_period: "An error occurred, unable to close the accounting period" # translation_missing no_periods: "No closings for now" # translation_missing + accounting_codes: "Accounting codes" # translation_missing + accounting_journal_code: "Journal code" # translation_missing + general_journal_code: "Journal code" # translation_missing + accounting_card_client_code: "Card clients code" # translation_missing + card_client_code: "Accounting code for clients who paid by card" # translation_missing + accounting_card_client_label: "Card clients label" # translation_missing + card_client_label: "Account label for clients who paid by card" # translation_missing + accounting_wallet_client_code: "Wallet clients code" # translation_missing + wallet_client_code: "Accounting code for clients who paid by virtual wallet" # translation_missing + accounting_wallet_client_label: "Wallet clients label" # translation_missing + wallet_client_label: "Account label for clients who paid by virtual wallet" # translation_missing + accounting_other_client_code: "Other means client code" # translation_missing + other_client_code: "Accounting code for clients who paid using another payment means" # translation_missing + accounting_other_client_label: "Other means client label" # translation_missing + other_client_label: "Accounting label for clients who paid using another payment means" # translation_missing + accounting_wallet_code: "Wallet code" # translation_missing + general_wallet_code: "Accounting code for wallet credit" # translation_missing + accounting_wallet_label: "Wallet label" # translation_missing + general_wallet_label: "Account label for wallet credit" # translation_missing + accounting_vat_code: "VAT code" # translation_missing + general_vat_code: "Accounting code for VAT" # translation_missing + accounting_vat_label: "VAT label" # translation_missing + general_vat_label: "VAT account label" # translation_missing + accounting_subscription_code: "Subscriptions code" # translation_missing + general_subscription_code: "Accounting code for all subscriptions" # translation_missing + accounting_subscription_label: "Subscriptions label" # translation_missing + general_subscription_label: "Account label for all subscriptions" # translation_missing + accounting_Machine_code: "Machines code" # translation_missing + general_machine_code: "Accounting code for all machines" # translation_missing + accounting_Machine_label: "Machine label" # translation_missing + general_machine_label: "Account label for all machines" # translation_missing + accounting_Training_code: "Trainings code" # translation_missing + general_training_code: "Accounting code for all trainings" # translation_missing + accounting_Training_label: "Trainings label" # translation_missing + general_training_label: "Account label for all trainings" # translation_missing + accounting_Event_code: "Events code" # translation_missing + general_event_code: "Accounting code for all events" # translation_missing + accounting_Event_label: "Events label" # translation_missing + general_event_label: "Account label for all events" # translation_missing + accounting_Space_code: "Space code" # translation_missing + general_space_code: "Accounting code for all spaces" # translation_missing + accounting_Space_label: "Spaces label" # translation_missing + general_space_label: "Account label for all spaces" # translation_missing + codes_customization_success: "Customization of accounting codes successfully saved." # angular interpolation # translation_missing + export_accounting_data: "Export accounting data" # translation_missing + export_to: "Export to the accounting software" # translation_missing + export_is_running: "Exportando, será notificado cuando esté listo." + acd: "ACD" # translation_missing + export_form_date: "Export from" # translation_missing + export_to_date: "Export until" # translation_missing + format: "File format" # translation_missing + encoding: "Encoding" # translation_missing + separator: "Separator" # translation_missing + dateFormat: "Date format" # translation_missing + labelMaxLength: "Label maximum length" # translation_missing + decimalSeparator: "Decimal separator" # translation_missing + exportInvoicesAtZero: "Export invoices equal to 0" # translation_missing + columns: "Columns" # translation_missing + exportColumns: # translation_missing + journal_code: "Journal code" # translation_missing + date: "Entry date" # translation_missing + account_code: "Account code" # translation_missing + account_label: "Account label" # translation_missing + piece: "Document" # translation_missing + line_label: "Entry label" # translation_missing + debit_origin: "Origin debit" # translation_missing + credit_origin: "Origin credit" # translation_missing + debit_euro: "Euro debit" # translation_missing + credit_euro: "Euro credit" # translation_missing + lettering: "Lettering" # translation_missing members: # management of users, labels, groups, and so on diff --git a/config/locales/app.admin.fr.yml b/config/locales/app.admin.fr.yml index d0f0db776..5f9ab3d65 100644 --- a/config/locales/app.admin.fr.yml +++ b/config/locales/app.admin.fr.yml @@ -322,6 +322,7 @@ fr: total_amount: "Montant total" total_including_all_taxes: "Total TTC" VAT_disabled: "TVA désactivée" + VAT_enabled: "TVA activée" including_VAT: "Dont TVA" including_total_excluding_taxes: "Dont total HT" including_amount_payed_on_ordering: "Dont montant payé à la commande" @@ -432,6 +433,76 @@ fr: period_START_END_closed_success: "La période comptable du {{START}} au {{END}} a bien été clôturée. La génération de l'archive est en cours, vous serez prévenu lorsque celle-ci sera terminée." failed_to_close_period: "Une erreur est survenue, impossible de clôturer la période comptable" no_periods: "Aucune clôture pour le moment" + accounting_codes: "Codes comptables" + accounting_journal_code: "Code journal" + general_journal_code: "Code journal" + accounting_card_client_code: "Code clients par carte" + card_client_code: "Code comptable pour les clients ayant réglé par carte bancaire" + accounting_card_client_label: "Libellé clients par carte" + card_client_label: "Libellé du compte pour les clients ayant réglé par carte bancaire" + accounting_wallet_client_code: "Code clients par porte-monnaie" + wallet_client_code: "Code comptable pour les clients ayant réglé par porte-monnaie virtuel" + accounting_wallet_client_label: "Libellé clients par porte-monnaie" + wallet_client_label: "Libellé du compte pour les clients ayant réglé par porte-monnaie virtuel" + accounting_other_client_code: "Code clients autre moyen" + other_client_code: "Code comptable pour les clients ayant avec un autre moyen de paiement" + accounting_other_client_label: "Libellé clients autre moyen" + other_client_label: "Libellé du compte pour les clients ayant réglé avec un autre moyen de paiement" + accounting_wallet_code: "Code porte-monnaie" + general_wallet_code: "Code comptable pour le crédit du porte-monnaie" + accounting_wallet_label: "Libellé porte-monnaie" + general_wallet_label: "Libellé du compte pour le crédit du porte-monnaie" + accounting_vat_code: "Code TVA" + general_vat_code: "Code comptable pour la TVA" + accounting_vat_label: "Libellé TVA" + general_vat_label: "Libellé du compte TVA" + accounting_subscription_code: "Code abonnements" + general_subscription_code: "Code comptable pour tous les abonnements" + accounting_subscription_label: "Libellé abonnements" + general_subscription_label: "Libellé du compte pour tous les abonnements" + accounting_Machine_code: "Code machines" + general_machine_code: "Code comptable pour toutes les machines" + accounting_Machine_label: "Libellé machine" + general_machine_label: "Libellé du compte pour toutes les machines" + accounting_Training_code: "Code formations" + general_training_code: "Code comptable pour toutes les formations" + accounting_Training_label: "Libellé formations" + general_training_label: "Libellé du compte pour toutes les formations" + accounting_Event_code: "Code évènements" + general_event_code: "Code comptable pour tous les évènements" + accounting_Event_label: "Libellé évènements" + general_event_label: "Libellé du compte pour tous les évènements" + accounting_Space_code: "Code espaces" + general_space_code: "Code comptable pour tous les espaces" + accounting_Space_label: "Libellé espaces" + general_space_label: "Libellé du compte pour tous les espaces" + codes_customization_success: "La personnalisation des codes comptables a bien été enregistrée." + export_accounting_data: "Exporter les données comptables" + export_to: "Exporter vers le logiciel comptable" + export_is_running: "L'export est en cours. Vous serez notifié lorsqu'il sera prêt." + acd: "ACD" + export_form_date: "Exporter depuis le" + export_to_date: "Exporter jusqu'au" + format: "Format de fichier" + encoding: "Encodage" + separator: "Séparateur" + dateFormat: "Format de date" + labelMaxLength: "Étiquette (max)" + decimalSeparator: "Séparateur décimal" + exportInvoicesAtZero: "Exporter les factures à 0" + columns: "Colonnes" + exportColumns: + journal_code: "Code journal" + date: "Date écriture" + account_code: "Code compte" + account_label: "Intitulé compte" + piece: "Pièce" + line_label: "Libellé écriture" + debit_origin: "Débit origine" + credit_origin: "Crédit origine" + debit_euro: "Débit euro" + credit_euro: "Crédit euro" + lettering: "Lettrage" members: # gestion des utilisateurs, des groupes, des étiquettes, etc. diff --git a/config/locales/app.admin.pt.yml b/config/locales/app.admin.pt.yml index f128161a0..be77652d3 100755 --- a/config/locales/app.admin.pt.yml +++ b/config/locales/app.admin.pt.yml @@ -321,8 +321,9 @@ pt: machine_booking-3D_printer: "Reserva de máquina - 3D printer" total_amount: "Montante total" total_including_all_taxes: "Total incluindo todas as taxas" - VAT_disabled: "VAT desativado" - including_VAT: "Incluindo VAT" + VAT_disabled: "IVA desativado" + VAT_enabled: "IVA activado" + including_VAT: "Incluindo IVA" including_total_excluding_taxes: "Incluindo o total de taxas excluidas" including_amount_payed_on_ordering: "Incluindo o valor pago na compra" settlement_by_debit_card_on_DATE_at_TIME_for_an_amount_of_AMOUNT: "Pagamento por cartão de débito em {{DATE}} ás {{TIME}}, no valor de {{AMOUNT}}" @@ -432,6 +433,76 @@ pt: period_START_END_closed_success: "The accounting period from {{START}} to {{END}} has been successfully closed. Archive generation is running, you'll be notified when it's done." # translation_missing failed_to_close_period: "An error occurred, unable to close the accounting period" # translation_missing no_periods: "No closings for now" # translation_missing + accounting_codes: "Accounting codes" # translation_missing + accounting_journal_code: "Journal code" # translation_missing + general_journal_code: "Journal code" # translation_missing + accounting_card_client_code: "Card clients code" # translation_missing + card_client_code: "Accounting code for clients who paid by card" # translation_missing + accounting_card_client_label: "Card clients label" # translation_missing + card_client_label: "Account label for clients who paid by card" # translation_missing + accounting_wallet_client_code: "Wallet clients code" # translation_missing + wallet_client_code: "Accounting code for clients who paid by virtual wallet" # translation_missing + accounting_wallet_client_label: "Wallet clients label" # translation_missing + wallet_client_label: "Account label for clients who paid by virtual wallet" # translation_missing + accounting_other_client_code: "Other payment client code" # translation_missing + other_client_code: "Accounting code for clients who paid using another payment means" # translation_missing + accounting_other_client_label: "Other payment client label" # translation_missing + other_client_label: "Accounting label for clients who paid using another payment means" # translation_missing + accounting_wallet_code: "Wallet code" # translation_missing + general_wallet_code: "Accounting code for wallet credit" # translation_missing + accounting_wallet_label: "Wallet label" # translation_missing + general_wallet_label: "Account label for wallet credit" # translation_missing + accounting_vat_code: "VAT code" # translation_missing + general_vat_code: "Accounting code for VAT" # translation_missing + accounting_vat_label: "VAT label" # translation_missing + general_vat_label: "VAT account label" # translation_missing + accounting_subscription_code: "Subscriptions code" # translation_missing + general_subscription_code: "Accounting code for all subscriptions" # translation_missing + accounting_subscription_label: "Subscriptions label" # translation_missing + general_subscription_label: "Account label for all subscriptions" # translation_missing + accounting_Machine_code: "Machines code" # translation_missing + general_machine_code: "Accounting code for all machines" # translation_missing + accounting_Machine_label: "Machine label" # translation_missing + general_machine_label: "Account label for all machines" # translation_missing + accounting_Training_code: "Trainings code" # translation_missing + general_training_code: "Accounting code for all trainings" # translation_missing + accounting_Training_label: "Trainings label" # translation_missing + general_training_label: "Account label for all trainings" # translation_missing + accounting_Event_code: "Events code" # translation_missing + general_event_code: "Accounting code for all events" # translation_missing + accounting_Event_label: "Events label" # translation_missing + general_event_label: "Account label for all events" # translation_missing + accounting_Space_code: "Space code" # translation_missing + general_space_code: "Accounting code for all spaces" # translation_missing + accounting_Space_label: "Spaces label" # translation_missing + general_space_label: "Account label for all spaces" # translation_missing + codes_customization_success: "Customization of accounting codes successfully saved." # angular interpolation # translation_missing + export_accounting_data: "Export accounting data" # translation_missing + export_to: "Export to the accounting software" # translation_missing + export_is_running: "A Exportação está em andamento. Você será notificado quando terminar." + acd: "ACD" # translation_missing + export_form_date: "Export from" # translation_missing + export_to_date: "Export until" # translation_missing + format: "File format" # translation_missing + encoding: "Encoding" # translation_missing + separator: "Separator" # translation_missing + dateFormat: "Date format" # translation_missing + labelMaxLength: "Label maximum length" # translation_missing + decimalSeparator: "Decimal separator" # translation_missing + exportInvoicesAtZero: "Export invoices equal to 0" # translation_missing + columns: "Columns" # translation_missing + exportColumns: # translation_missing + journal_code: "Journal code" # translation_missing + date: "Entry date" # translation_missing + account_code: "Account code" # translation_missing + account_label: "Account label" # translation_missing + piece: "Document" # translation_missing + line_label: "Entry label" # translation_missing + debit_origin: "Origin debit" # translation_missing + credit_origin: "Origin credit" # translation_missing + debit_euro: "Euro debit" # translation_missing + credit_euro: "Euro credit" # translation_missing + lettering: "Lettering" # translation_missing members: # management of users, labels, groups, and so on diff --git a/config/locales/app.shared.en.yml b/config/locales/app.shared.en.yml index 80ca87980..a35125a9d 100644 --- a/config/locales/app.shared.en.yml +++ b/config/locales/app.shared.en.yml @@ -101,6 +101,7 @@ en: unlimited: "Unlimited" payment_card_error: "A problem occurred with your payment card:" online_payment_disabled: "Online payment is not available. Please contact the Fablab reception directly." + unexpected_error_occurred: "An unexpected error occurred" messages: you_will_lose_any_unsaved_modification_if_you_quit_this_page: "You will lose any unsaved modification if you quit this page" diff --git a/config/locales/app.shared.es.yml b/config/locales/app.shared.es.yml index 25e9b208d..4b61ef146 100644 --- a/config/locales/app.shared.es.yml +++ b/config/locales/app.shared.es.yml @@ -101,6 +101,7 @@ es: unlimited: "Ilimitado" payment_card_error: "Hubo un problema con su tarjeta:" online_payment_disabled: "El pago en línea no está disponible. Póngase en contacto directamente con la recepción de Fablab." + unexpected_error_occurred: "Ocurrió un error inesperado" messages: you_will_lose_any_unsaved_modification_if_you_quit_this_page: "Si cierra la página se perderán todas las modificaciones que no se hayan guardado" diff --git a/config/locales/app.shared.fr.yml b/config/locales/app.shared.fr.yml index 24bbf383f..f5de76751 100644 --- a/config/locales/app.shared.fr.yml +++ b/config/locales/app.shared.fr.yml @@ -101,6 +101,7 @@ fr: unlimited: "Illimité" payment_card_error: "Un problème est survenu avec votre carte bancaire :" online_payment_disabled: "Le payment par carte bancaire n'est pas disponible. Merci de contacter directement l'accueil du Fablab." + unexpected_error_occurred: "Une erreur inattendue est survenue" messages: you_will_lose_any_unsaved_modification_if_you_quit_this_page: "Vous perdrez les modifications non enregistrées si vous quittez cette page" diff --git a/config/locales/app.shared.pt.yml b/config/locales/app.shared.pt.yml index c82c8d90f..fd38215bd 100755 --- a/config/locales/app.shared.pt.yml +++ b/config/locales/app.shared.pt.yml @@ -101,6 +101,7 @@ pt: unlimited: "Ilimitado" payment_card_error: "A problem occurred with your payment card:" # translation_missing online_payment_disabled: "El pago en línea no está disponible. Póngase en contacto directamente con la recepción de Fablab." + unexpected_error_occurred: "Um erro inesperado ocorreu" messages: you_will_lose_any_unsaved_modification_if_you_quit_this_page: "Você irá perder todas as modificações não salvas se sair desta página" diff --git a/config/locales/en.yml b/config/locales/en.yml index 92280f134..6d815010c 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -113,8 +113,8 @@ en: by_cheque: "by cheque" by_transfer: "by transfer" by_cash: "by cash" - no_refund: "No refund" by_wallet: "by wallet" + no_refund: "No refund" settlement_by_debit_card: "Settlement by debit card" settlement_done_at_the_reception: "Settlement done at the reception" settlement_by_wallet: "Settlement by wallet" @@ -126,6 +126,26 @@ en: subscription_of_NAME_extended_starting_from_STARTDATE_until_ENDDATE: "Subscription of %{NAME} extended (Free days) starting from %{STARTDATE} until %{ENDDATE}" and: 'and' + accounting_export: + journal_code: "Journal code" + date: "Entry date" + account_code: "Account code" + account_label: "Account label" + piece: "Document" + line_label: "Entry label" + debit_origin: "Origin debit" + credit_origin: "Origin credit" + debit_euro: "Euro debit" + credit_euro: "Euro credit" + lettering: "Lettering" + VAT: 'VAT' + subscription: "subscr." + Machine_reservation: "machine reserv." + Training_reservation: "training reserv." + Event_reservation: "event reserv." + Space_reservation: "space reserv." + wallet: "wallet" + trainings: # training availabilities i_ve_reserved: "I've reserved" @@ -307,6 +327,7 @@ en: users_subscriptions: "of the subscriptions' list" users_reservations: "of the reservations' list" availabilities_index: "of the reservations availabilities" + accounting_acd: "of the accounting data to ACD" is_over: "is over." download_here: "Download here" notify_member_about_coupon: diff --git a/config/locales/es.yml b/config/locales/es.yml index 26bf513ea..f2d3210b9 100644 --- a/config/locales/es.yml +++ b/config/locales/es.yml @@ -126,6 +126,26 @@ es: subscription_of_NAME_extended_starting_from_STARTDATE_until_ENDDATE: "Subscripción de %{NAME} extendida (Free days) empezando desde %{STARTDATE} hasta %{ENDDATE}" and: 'y' + accounting_export: + journal_code: "Journal code" # translation_missing + date: "Entry date" # translation_missing + account_code: "Account code" # translation_missing + account_label: "Account label" # translation_missing + piece: "Document" # translation_missing + line_label: "Entry label" # translation_missing + debit_origin: "Origin debit" # translation_missing + credit_origin: "Origin credit" # translation_missing + debit_euro: "Euro debit" # translation_missing + credit_euro: "Euro credit" # translation_missing + lettering: "Lettering" # translation_missing + VAT: 'IVA' + subscription: "subscr." # translation_missing + Machine_reservation: "machine reserv." # translation_missing + Training_reservation: "training reserv." # translation_missing + Event_reservation: "event reserv." # translation_missing + Space_reservation: "space reserv." # translation_missing + wallet: "wallet" # translation_missing + trainings: # training availabilities i_ve_reserved: "he reservado" @@ -307,6 +327,7 @@ es: users_subscriptions: "de la lista de suscripciones" users_reservations: "de la lista de reservas" availabilities_index: "de las reservas disponibles" + accounting_acd: "de los datos contables para ACD" is_over: "se ha acabado." download_here: "Descargar aquí" notify_member_about_coupon: diff --git a/config/locales/fr.yml b/config/locales/fr.yml index e011c9103..677dc2b25 100644 --- a/config/locales/fr.yml +++ b/config/locales/fr.yml @@ -126,6 +126,26 @@ fr: subscription_of_NAME_extended_starting_from_STARTDATE_until_ENDDATE: "Prolongement Abonnement (Jours gratuits) de %{NAME} à compter du %{STARTDATE} jusqu'au %{ENDDATE}" and: 'et' + accounting_export: + journal_code: "Code journal" + date: "Date écriture" + account_code: "Code compte" + account_label: "Intitulé compte" + piece: "Pièce" + line_label: "Libellé écriture" + debit_origin: "Débit origine" + credit_origin: "Crédit origine" + debit_euro: "Débit euro" + credit_euro: "Crédit euro" + lettering: "Lettrage" + VAT: 'TVA' + subscription: "abo." + Machine_reservation: "réserv. machine" + Training_reservation: "réserv. formation" + Event_reservation: "réserv. évènement" + Space_reservation: "réserv. espace" + wallet: "porte-monnaie" + trainings: # disponibilités formations i_ve_reserved: "J'ai réservé" @@ -307,6 +327,7 @@ fr: users_subscriptions: "de la liste des abonnements" users_reservations: "de la liste des réservations" availabilities_index: "des disponibilités de réservations" + accounting_acd: "des données comptables pour ACD" is_over: "est terminé." download_here: "Téléchargez ici" notify_member_about_coupon: diff --git a/config/locales/mails.en.yml b/config/locales/mails.en.yml index c37b278e5..eb53b018e 100644 --- a/config/locales/mails.en.yml +++ b/config/locales/mails.en.yml @@ -267,8 +267,12 @@ en: users_subscriptions: "of the subscriptions' list" users_reservations: "of the reservations' list" availabilities_index: "of the reservations availabilities" + accounting_accounting-software: "of the accounting data" click_to_download: "Excel file generated successfully. To download it, click" here: "here" + file_type: + xlsx: "Excel" + csv: "CSV" notify_member_about_coupon: subject: "Coupon" diff --git a/config/locales/mails.es.yml b/config/locales/mails.es.yml index c001b39a1..3fe911bed 100644 --- a/config/locales/mails.es.yml +++ b/config/locales/mails.es.yml @@ -266,8 +266,12 @@ es: users_subscriptions: "de la lista de suscripciones" users_reservations: "de la lista de reservas" availabilities_index: "de las reservas disponibles" + accounting_accounting-software: "de los datos contables" click_to_download: " archivo Excel generado correctamente. Para descargarlo, haga clic " here: "aquí" + file_type: + xlsx: "Excel" + csv: "CSV" notify_member_about_coupon: subject: "Cupón" diff --git a/config/locales/mails.fr.yml b/config/locales/mails.fr.yml index f74d337e0..a21e6b179 100644 --- a/config/locales/mails.fr.yml +++ b/config/locales/mails.fr.yml @@ -267,8 +267,12 @@ fr: users_subscriptions: "de la liste des abonnements" users_reservations: "de la liste des réservations" availabilities_index: "des disponibilités de réservations" - click_to_download: "La génération est terminée. Pour télécharger le fichier Excel, cliquez" + accounting_accounting-software: "des données comptables" + click_to_download: "La génération est terminée. Pour télécharger le fichier %{TYPE}, cliquez" here: "ici" + file_type: + xlsx: "Excel" + csv: "CSV" notify_member_about_coupon: subject: "Code promo" diff --git a/config/locales/mails.pt.yml b/config/locales/mails.pt.yml index eba1cb9de..b6a6ea03c 100755 --- a/config/locales/mails.pt.yml +++ b/config/locales/mails.pt.yml @@ -267,8 +267,12 @@ pt: users_subscriptions: "da lista de assinaturas" users_reservations: "da lista de reservas" availabilities_index: "as reservas disponíveis" + accounting_accounting-software: "de dados contábeis" click_to_download: "Arquivo do Excel gerado com êxito. Para fazer o download, clique" here: "aqui" + file_type: + xlsx: "Excel" + csv: "CSV" notify_member_about_coupon: subject: "Cupom" diff --git a/config/locales/pt.yml b/config/locales/pt.yml index 481575605..3833f961c 100755 --- a/config/locales/pt.yml +++ b/config/locales/pt.yml @@ -126,6 +126,26 @@ pt: subscription_of_NAME_extended_starting_from_STARTDATE_until_ENDDATE: "Assinatura de %{NAME} estendida (dias livres) a partir de% STARTDATE até %{ENDDATE}" and: 'e' + accounting_export: + journal_code: "Journal code" # translation_missing + date: "Entry date" # translation_missing + account_code: "Account code" # translation_missing + account_label: "Account label" # translation_missing + piece: "Document" # translation_missing + line_label: "Entry label" # translation_missing + debit_origin: "Origin debit" # translation_missing + credit_origin: "Origin credit" # translation_missing + debit_euro: "Euro debit" # translation_missing + credit_euro: "Euro credit" # translation_missing + lettering: "Lettering" # translation_missing + VAT: 'IVA' + subscription: "subscr." # translation_missing + Machine_reservation: "machine reserv." # translation_missing + Training_reservation: "training reserv." # translation_missing + Event_reservation: "event reserv." # translation_missing + Space_reservation: "space reserv." # translation_missing + wallet: "wallet" # translation_missing + trainings: # training availabilities i_ve_reserved: "Eu reservei" @@ -307,6 +327,7 @@ pt: users_subscriptions: "da lista de assinaturas" users_reservations: "da lista de reservas" availabilities_index: "de reservas disponíveis" + accounting_acd: "de dados contábeis para ACD" is_over: "está finalizado." download_here: "Baixe aqui" notify_member_about_coupon: diff --git a/config/routes.rb b/config/routes.rb index 4568eeaa5..1afda02ad 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -43,7 +43,9 @@ Rails.application.routes.draw do resources :themes resources :licences resources :admins, only: %i[index create destroy] - resources :settings, only: %i[show update index], param: :name + resources :settings, only: %i[show update index], param: :name do + patch '/bulk_update', action: 'bulk_update', on: :collection + end resources :users, only: %i[index create] resources :members, only: %i[index show create update destroy] do get '/export_subscriptions', action: 'export_subscriptions', on: :collection @@ -106,6 +108,7 @@ Rails.application.routes.draw do resources :invoices, only: %i[index show create] do get 'download', action: 'download', on: :member post 'list', action: 'list', on: :collection + get 'first', action: 'first', on: :collection end # for admin @@ -135,6 +138,8 @@ Rails.application.routes.draw do get 'last_closing_end', on: :collection get 'archive', action: 'download_archive', on: :member end + # export accounting data to csv or equivalent + post 'accounting/export' => 'accounting_exports#export' # i18n # regex allows using dots in URL for 'state' diff --git a/db/migrate/20190730085826_add_extension_to_export.rb b/db/migrate/20190730085826_add_extension_to_export.rb new file mode 100644 index 000000000..40e15a140 --- /dev/null +++ b/db/migrate/20190730085826_add_extension_to_export.rb @@ -0,0 +1,5 @@ +class AddExtensionToExport < ActiveRecord::Migration + def change + add_column :exports, :extension, :string, default: 'xlsx' + end +end diff --git a/db/migrate/20190917123631_rename_avoir_mode_to_payment_method_from_invoices.rb b/db/migrate/20190917123631_rename_avoir_mode_to_payment_method_from_invoices.rb new file mode 100644 index 000000000..405ff2191 --- /dev/null +++ b/db/migrate/20190917123631_rename_avoir_mode_to_payment_method_from_invoices.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +# From this migration, this column will also be used to save the means of payment used to charge the customer +# This is due to Strong Customer Authentication changes, that don't store any more an stp_invoice_id in table +# "invoices". The new stp_payment_intent_id is not populated if the invoice total = 0 but we must know if the +# payment was made on site or online. +class RenameAvoirModeToPaymentMethodFromInvoices < ActiveRecord::Migration + def change + rename_column :invoices, :avoir_mode, :payment_method + end +end diff --git a/db/schema.rb b/db/schema.rb index 77d133009..b8826f52f 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -11,7 +11,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20190910141336) do +ActiveRecord::Schema.define(version: 20190917123631) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -202,10 +202,11 @@ ActiveRecord::Schema.define(version: 20190910141336) do t.string "category" t.string "export_type" t.string "query" - t.datetime "created_at", null: false - t.datetime "updated_at", null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false t.integer "user_id" t.string "key" + t.string "extension", default: "xlsx" end add_index "exports", ["user_id"], name: "index_exports_on_user_id", using: :btree @@ -267,7 +268,7 @@ ActiveRecord::Schema.define(version: 20190910141336) do t.datetime "created_at" t.datetime "updated_at" t.string "reference" - t.string "avoir_mode" + t.string "payment_method" t.datetime "avoir_date" t.integer "invoice_id" t.string "type" diff --git a/lib/tasks/fablab/maintenance.rake b/lib/tasks/fablab/maintenance.rake index 1d83138d3..e9b15205b 100644 --- a/lib/tasks/fablab/maintenance.rake +++ b/lib/tasks/fablab/maintenance.rake @@ -3,7 +3,7 @@ # Maintenance tasks namespace :fablab do namespace :maintenance do - desc 'Regenerate the invoices PDF' + desc 'Regenerate the invoices (invoices & avoirs) PDF' task :regenerate_invoices, %i[year month] => :environment do |_task, args| year = args.year || Time.current.year month = args.month || Time.current.month diff --git a/package.json b/package.json index d493b0ad6..685e0d12a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "fab-manager", - "version": "4.1.1", + "version": "4.2.0-dev", "description": "FabManager is the FabLab management solution. It is web-based, open-source and totally free.", "keywords": [ "fablab", diff --git a/test/fixtures/history_values.yml b/test/fixtures/history_values.yml index 13a45b4e7..0940b6d94 100644 --- a/test/fixtures/history_values.yml +++ b/test/fixtures/history_values.yml @@ -369,3 +369,192 @@ value_history_38: created_at: 2018-12-31 10:22:25.116369000 Z updated_at: 2019-06-12 09:21:38.606818000 Z footprint: 085164a7288540c9beb0a6243856016fc36aae54bfb7d5d41af354650277d1ea + +value_history_39: + id: 39 + setting_id: 39 + invoicing_profile_id: 1 + value: '530' + created_at: 2019-09-20 11:02:32.125400000 Z + updated_at: 2019-09-20 11:02:32.125400000 Z + footprint: + +value_history_40: + id: 40 + setting_id: 40 + invoicing_profile_id: 1 + value: '5801' + created_at: 2019-09-20 11:02:32.125400000 Z + updated_at: 2019-09-20 11:02:32.125400000 Z + footprint: + +value_history_41: + id: 41 + setting_id: 41 + invoicing_profile_id: 1 + value: 'Client card' + created_at: 2019-09-20 11:02:32.125400000 Z + updated_at: 2019-09-20 11:02:32.125400000 Z + footprint: + +value_history_42: + id: 42 + setting_id: 42 + invoicing_profile_id: 1 + value: '5802' + created_at: 2019-09-20 11:02:32.125400000 Z + updated_at: 2019-09-20 11:02:32.125400000 Z + footprint: + +value_history_43: + id: 43 + setting_id: 43 + invoicing_profile_id: 1 + value: 'Client wallet' + created_at: 2019-09-20 11:02:32.125400000 Z + updated_at: 2019-09-20 11:02:32.125400000 Z + footprint: + +value_history_44: + id: 44 + setting_id: 44 + invoicing_profile_id: 1 + value: '5803' + created_at: 2019-09-20 11:02:32.125400000 Z + updated_at: 2019-09-20 11:02:32.125400000 Z + footprint: + +value_history_45: + id: 45 + setting_id: 45 + invoicing_profile_id: 1 + value: 'Client other' + created_at: 2019-09-20 11:02:32.125400000 Z + updated_at: 2019-09-20 11:02:32.125400000 Z + footprint: + +value_history_46: + id: 46 + setting_id: 46 + invoicing_profile_id: 1 + value: '4091' + created_at: 2019-09-20 11:02:32.125400000 Z + updated_at: 2019-09-20 11:02:32.125400000 Z + footprint: + +value_history_47: + id: 47 + setting_id: 47 + invoicing_profile_id: 1 + value: 'Wallet credit' + created_at: 2019-09-20 11:02:32.125400000 Z + updated_at: 2019-09-20 11:02:32.125400000 Z + footprint: + +value_history_48: + id: 48 + setting_id: 48 + invoicing_profile_id: 1 + value: '445' + created_at: 2019-09-20 11:02:32.125400000 Z + updated_at: 2019-09-20 11:02:32.125400000 Z + footprint: + +value_history_49: + id: 49 + setting_id: 49 + invoicing_profile_id: 1 + value: 'VAT' + created_at: 2019-09-20 11:02:32.125400000 Z + updated_at: 2019-09-20 11:02:32.125400000 Z + footprint: + +value_history_50: + id: 50 + setting_id: 50 + invoicing_profile_id: 1 + value: '7061' + created_at: 2019-09-20 11:02:32.125400000 Z + updated_at: 2019-09-20 11:02:32.125400000 Z + footprint: + +value_history_51: + id: 51 + setting_id: 51 + invoicing_profile_id: 1 + value: 'Subscription' + created_at: 2019-09-20 11:02:32.125400000 Z + updated_at: 2019-09-20 11:02:32.125400000 Z + footprint: + +value_history_52: + id: 52 + setting_id: 52 + invoicing_profile_id: 1 + value: '7062' + created_at: 2019-09-20 11:02:32.125400000 Z + updated_at: 2019-09-20 11:02:32.125400000 Z + footprint: + +value_history_53: + id: 53 + setting_id: 53 + invoicing_profile_id: 1 + value: 'Machine reservation' + created_at: 2019-09-20 11:02:32.125400000 Z + updated_at: 2019-09-20 11:02:32.125400000 Z + footprint: + +value_history_54: + id: 54 + setting_id: 54 + invoicing_profile_id: 1 + value: '7063' + created_at: 2019-09-20 11:02:32.125400000 Z + updated_at: 2019-09-20 11:02:32.125400000 Z + footprint: + +value_history_55: + id: 55 + setting_id: 55 + invoicing_profile_id: 1 + value: 'Training reservation' + created_at: 2019-09-20 11:02:32.125400000 Z + updated_at: 2019-09-20 11:02:32.125400000 Z + footprint: + +value_history_56: + id: 56 + setting_id: 56 + invoicing_profile_id: 1 + value: '7064' + created_at: 2019-09-20 11:02:32.125400000 Z + updated_at: 2019-09-20 11:02:32.125400000 Z + footprint: + +value_history_57: + id: 57 + setting_id: 57 + invoicing_profile_id: 1 + value: 'Event reservation' + created_at: 2019-09-20 11:02:32.125400000 Z + updated_at: 2019-09-20 11:02:32.125400000 Z + footprint: + +value_history_58: + id: 58 + setting_id: 58 + invoicing_profile_id: 1 + value: '7065' + created_at: 2019-09-20 11:02:32.125400000 Z + updated_at: 2019-09-20 11:02:32.125400000 Z + footprint: + +value_history_59: + id: 59 + setting_id: 59 + invoicing_profile_id: 1 + value: 'Space reservation' + created_at: 2019-09-20 11:02:32.125400000 Z + updated_at: 2019-09-20 11:02:32.125400000 Z + footprint: diff --git a/test/fixtures/invoices.yml b/test/fixtures/invoices.yml index a959ef801..5d083af67 100644 --- a/test/fixtures/invoices.yml +++ b/test/fixtures/invoices.yml @@ -10,7 +10,7 @@ invoice_1: invoicing_profile_id: 3 statistic_profile_id: 3 reference: 1604001/VL - avoir_mode: + payment_method: avoir_date: invoice_id: type: @@ -31,7 +31,7 @@ invoice_2: invoicing_profile_id: 4 statistic_profile_id: 4 reference: '1604002' - avoir_mode: + payment_method: avoir_date: invoice_id: type: @@ -52,7 +52,7 @@ invoice_3: invoicing_profile_id: 7 statistic_profile_id: 7 reference: '1203001' - avoir_mode: + payment_method: avoir_date: invoice_id: type: @@ -74,7 +74,7 @@ invoice_4: invoicing_profile_id: 7 statistic_profile_id: 7 reference: '1203002' - avoir_mode: + payment_method: avoir_date: invoice_id: type: @@ -95,7 +95,7 @@ invoice_5: invoicing_profile_id: 3 statistic_profile_id: 3 reference: '1506031' - avoir_mode: + payment_method: avoir_date: invoice_id: type: diff --git a/test/fixtures/settings.yml b/test/fixtures/settings.yml index b1b535776..d8473a39c 100644 --- a/test/fixtures/settings.yml +++ b/test/fixtures/settings.yml @@ -220,3 +220,129 @@ setting_38: name: privacy_draft created_at: 2019-06-12 13:25:08.125640000 Z updated_at: 2019-06-12 13:25:08.125640000 Z + +setting_39: + id: 39 + name: accounting_journal_code + created_at: 2019-09-20 11:02:32.125400000 Z + updated_at: 2019-09-20 11:02:32.125400000 Z + +setting_40: + id: 40 + name: accounting_card_client_code + created_at: 2019-09-20 11:02:32.125400000 Z + updated_at: 2019-09-20 11:02:32.125400000 Z + +setting_41: + id: 41 + name: accounting_card_client_label + created_at: 2019-09-20 11:02:32.125400000 Z + updated_at: 2019-09-20 11:02:32.125400000 Z + +setting_42: + id: 42 + name: accounting_wallet_client_code + created_at: 2019-09-20 11:02:32.125400000 Z + updated_at: 2019-09-20 11:02:32.125400000 Z + +setting_43: + id: 43 + name: accounting_wallet_client_label + created_at: 2019-09-20 11:02:32.125400000 Z + updated_at: 2019-09-20 11:02:32.125400000 Z + +setting_44: + id: 44 + name: accounting_other_client_code + created_at: 2019-09-20 11:02:32.125400000 Z + updated_at: 2019-09-20 11:02:32.125400000 Z + +setting_45: + id: 45 + name: accounting_other_client_label + created_at: 2019-09-20 11:02:32.125400000 Z + updated_at: 2019-09-20 11:02:32.125400000 Z + +setting_46: + id: 46 + name: accounting_wallet_code + created_at: 2019-09-20 11:02:32.125400000 Z + updated_at: 2019-09-20 11:02:32.125400000 Z + +setting_47: + id: 47 + name: accounting_wallet_label + created_at: 2019-09-20 11:02:32.125400000 Z + updated_at: 2019-09-20 11:02:32.125400000 Z + +setting_48: + id: 48 + name: accounting_VAT_code + created_at: 2019-09-20 11:02:32.125400000 Z + updated_at: 2019-09-20 11:02:32.125400000 Z + +setting_49: + id: 49 + name: accounting_VAT_label + created_at: 2019-09-20 11:02:32.125400000 Z + updated_at: 2019-09-20 11:02:32.125400000 Z + +setting_50: + id: 50 + name: accounting_subscription_code + created_at: 2019-09-20 11:02:32.125400000 Z + updated_at: 2019-09-20 11:02:32.125400000 Z + +setting_51: + id: 51 + name: accounting_subscription_label + created_at: 2019-09-20 11:02:32.125400000 Z + updated_at: 2019-09-20 11:02:32.125400000 Z + +setting_52: + id: 52 + name: accounting_Machine_label + created_at: 2019-09-20 11:02:32.125400000 Z + updated_at: 2019-09-20 11:02:32.125400000 Z + +setting_53: + id: 53 + name: accounting_Training_code + created_at: 2019-09-20 11:02:32.125400000 Z + updated_at: 2019-09-20 11:02:32.125400000 Z + +setting_54: + id: 54 + name: accounting_Training_label + created_at: 2019-09-20 11:02:32.125400000 Z + updated_at: 2019-09-20 11:02:32.125400000 Z + +setting_55: + id: 55 + name: accounting_Event_code + created_at: 2019-09-20 11:02:32.125400000 Z + updated_at: 2019-09-20 11:02:32.125400000 Z + +setting_56: + id: 56 + name: accounting_Event_label + created_at: 2019-09-20 11:02:32.125400000 Z + updated_at: 2019-09-20 11:02:32.125400000 Z + +setting_57: + id: 57 + name: accounting_Space_code + created_at: 2019-09-20 11:02:32.125400000 Z + updated_at: 2019-09-20 11:02:32.125400000 Z + +setting_58: + id: 58 + name: accounting_Machine_code + created_at: 2019-09-20 11:02:32.125400000 Z + updated_at: 2019-09-20 11:02:32.125400000 Z + +setting_59: + id: 59 + name: accounting_Space_label + created_at: 2019-09-20 11:02:32.125400000 Z + updated_at: 2019-09-20 11:02:32.125400000 Z \ No newline at end of file diff --git a/test/integration/credits/machine_test.rb b/test/integration/credits/machine_test.rb index bef98ad8b..2728d08b0 100644 --- a/test/integration/credits/machine_test.rb +++ b/test/integration/credits/machine_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Credits class TrainingTest < ActionDispatch::IntegrationTest @@ -12,15 +14,15 @@ module Credits # First, we create a new credit post '/api/credits', - { - credit: { - creditable_id: 5, - creditable_type: 'Machine', - hours: 1, - plan_id: 1, - } - }.to_json, - default_headers + { + credit: { + creditable_id: 5, + creditable_type: 'Machine', + hours: 1, + plan_id: 1 + } + }.to_json, + default_headers # Check response format & status assert_equal 201, response.status, response.body @@ -37,15 +39,15 @@ module Credits test 'update a credit' do put '/api/credits/13', - { - credit: { - creditable_id: 4, - creditable_type: 'Machine', - hours: 5, - plan_id: 3, - } - }.to_json, - default_headers + { + credit: { + creditable_id: 4, + creditable_type: 'Machine', + hours: 5, + plan_id: 3 + } + }.to_json, + default_headers # Check response format & status assert_equal 200, response.status, response.body diff --git a/test/integration/exports/accounting_export_test.rb b/test/integration/exports/accounting_export_test.rb new file mode 100644 index 000000000..63c065219 --- /dev/null +++ b/test/integration/exports/accounting_export_test.rb @@ -0,0 +1,141 @@ +# frozen_string_literal: true + +module Exports; end + +class Exports::AccountingExportTest < ActionDispatch::IntegrationTest + + setup do + admin = User.with_role(:admin).first + login_as(admin, scope: :user) + end + + test 'creation modification reservation and re-modification scenario' do + + # First, we create a new export + post '/api/accounting/export', + { + query: { + columns: %w[journal_code date account_code account_label piece line_label debit_origin credit_origin debit_euro credit_euro lettering], + encoding: 'ISO-8859-1', + date_format: '%d/%m/%Y', + start_date: '2012-03-12T00:00:00.000Z', + end_date: DateTime.now.utc.iso8601, + label_max_length: 50, + decimal_separator: ',', + export_invoices_at_zero: false + }.to_json.to_s, + extension: 'csv', + type: 'acd', + key: ';' + }.to_json, + default_headers + + # Check response format & status + assert_equal 200, response.status, response.body + assert_equal Mime::JSON, response.content_type + + # Check the export was created correctly + res = json_response(response.body) + e = Export.where(id: res[:export_id]).first + assert_not_nil e, 'Export was not created in database' + + # Run the worker + worker = AccountingExportWorker.new + worker.perform(e.id) + + # notification + assert_not_empty Notification.where(attached_object: e) + + # resulting CSV file + assert FileTest.exist?(e.file), 'CSV file was not generated' + require 'csv' + data = CSV.read(e.file, headers: true, col_sep: e.key) + + # test values + # first line = client line + journal_code = Setting.find_by(name: 'accounting_journal_code').value + assert_equal journal_code, data[0][I18n.t('accounting_export.journal_code')], 'Wrong journal code' + + first_invoice = Invoice.first + entry_date = first_invoice.created_at.to_date + assert_equal entry_date, DateTime.parse(data[0][I18n.t('accounting_export.date')]), 'Wrong date' + + if first_invoice.paid_with_stripe? + card_client_code = Setting.find_by(name: 'accounting_card_client_code').value + assert_equal card_client_code, data[0][I18n.t('accounting_export.account_code')], 'Account code for card client is wrong' + + card_client_label = Setting.find_by(name: 'accounting_card_client_label').value + assert_equal card_client_label, data[0][I18n.t('accounting_export.account_label')], 'Account label for card client is wrong' + else + STDERR.puts "WARNING: unable to test accurately accounting export: invoice #{first_invoice.id} was not paid by card" + end + + assert_equal first_invoice.reference, data[0][I18n.t('accounting_export.piece')], 'Piece (invoice reference) is wrong' + + if first_invoice.subscription_invoice? + assert_match I18n.t('accounting_export.subscription'), + data[0][I18n.t('accounting_export.line_label')], + 'Line label does not contains the reference to the invoiced item' + else + STDERR.puts "WARNING: unable to test accurately accounting export: invoice #{first_invoice.id} does not have a subscription" + end + + if first_invoice.wallet_transaction_id.nil? + assert_equal first_invoice.total / 100.00, data[0][I18n.t('accounting_export.debit_origin')].to_f, 'Origin debit amount does not match' + assert_equal first_invoice.total / 100.00, data[0][I18n.t('accounting_export.debit_euro')].to_f, 'Euro debit amount does not match' + else + STDERR.puts "WARNING: unable to test accurately accounting export: invoice #{first_invoice.id} is using wallet" + end + + assert_equal 0, data[0][I18n.t('accounting_export.credit_origin')].to_f, 'Credit origin amount does not match' + assert_equal 0, data[0][I18n.t('accounting_export.credit_euro')].to_f, 'Credit euro amount does not match' + + # second line = sold item line + assert_equal journal_code, data[1][I18n.t('accounting_export.journal_code')], 'Wrong journal code' + assert_equal entry_date, DateTime.parse(data[1][I18n.t('accounting_export.date')]), 'Wrong date' + + if first_invoice.subscription_invoice? + subscription_code = Setting.find_by(name: 'accounting_subscription_code').value + assert_equal subscription_code, data[1][I18n.t('accounting_export.account_code')], 'Account code for subscription is wrong' + + subscription_label = Setting.find_by(name: 'accounting_subscription_label').value + assert_equal subscription_label, data[1][I18n.t('accounting_export.account_label')], 'Account label for subscription is wrong' + end + + assert_equal first_invoice.reference, data[1][I18n.t('accounting_export.piece')], 'Piece (invoice reference) is wrong' + assert_nil data[1][I18n.t('accounting_export.line_label')], 'Line label should be empty for non client lines' + + item = first_invoice.invoice_items.first + assert_equal item.amount / 100.00, data[1][I18n.t('accounting_export.credit_origin')].to_f, 'Origin credit amount does not match' + assert_equal item.amount / 100.00, data[1][I18n.t('accounting_export.credit_euro')].to_f, 'Euro credit amount does not match' + + assert_equal 0, data[1][I18n.t('accounting_export.debit_origin')].to_f, 'Debit origin amount does not match' + assert_equal 0, data[1][I18n.t('accounting_export.debit_euro')].to_f, 'Debit euro amount does not match' + + # test with another invoice + last_invoice = Invoice.last + client_row = data[data.length - 2] + item_row = data[data.length - 1] + + if last_invoice.invoiced_type == 'Reservation' && last_invoice.invoiced.reservable_type == 'Machine' + assert_match I18n.t('accounting_export.Machine_reservation'), + client_row[I18n.t('accounting_export.line_label')], + 'Line label does not contains the reference to the invoiced item' + + machine_code = Setting.find_by(name: 'accounting_Machine_code').value + assert_equal machine_code, item_row[I18n.t('accounting_export.account_code')], 'Account code for machine reservation is wrong' + + machine_label = Setting.find_by(name: 'accounting_Machine_label').value + assert_equal machine_label, item_row[I18n.t('accounting_export.account_label')], 'Account label for machine reservation is wrong' + + else + STDERR.puts "WARNING: unable to test accurately accounting export: invoice #{last_invoice.id} is not a Machine reservation" + end + + + # Clean CSV file + require 'fileutils' + FileUtils.rm(e.file) + end +end + diff --git a/test/integration/invoices/as_admin_test.rb b/test/integration/invoices/as_admin_test.rb index 53a920a3a..bee8de4ec 100644 --- a/test/integration/invoices/as_admin_test.rb +++ b/test/integration/invoices/as_admin_test.rb @@ -37,7 +37,7 @@ class InvoicesTest < ActionDispatch::IntegrationTest post '/api/invoices', { avoir: { avoir_date: date, - avoir_mode: 'cash', + payment_method: 'cash', description: 'Lorem ipsum', invoice_id: 4, invoice_items_ids: [4], @@ -54,7 +54,7 @@ class InvoicesTest < ActionDispatch::IntegrationTest assert_dates_equal date, refund[:avoir_date] assert_dates_equal date, refund[:date] - assert_equal 'cash', refund[:avoir_mode] + assert_equal 'cash', refund[:payment_method] assert_equal false, refund[:has_avoir] assert_equal 4, refund[:invoice_id] assert_equal 4, refund[:items][0][:invoice_item_id] @@ -70,7 +70,7 @@ class InvoicesTest < ActionDispatch::IntegrationTest post '/api/invoices', { avoir: { avoir_date: date, - avoir_mode: 'cash', + payment_method: 'cash', description: 'Unable to refund', invoice_id: 5, invoice_items_ids: [5],