From 722d5d36e7bee34b6e7922e7e9820a25fb7983fb Mon Sep 17 00:00:00 2001 From: Sylvain Date: Wed, 3 Apr 2019 13:04:19 +0200 Subject: [PATCH] check server-side that periods match length requirements + explain requirements to user before closing --- .../controllers/admin/invoices.js.erb | 16 +++++--- app/models/accounting_period.rb | 1 + app/validators/duration_validator.rb | 13 ++++++ config/locales/app.admin.en.yml | 4 +- config/locales/app.admin.es.yml | 4 +- config/locales/app.admin.fr.yml | 4 +- config/locales/app.admin.pt.yml | 4 +- config/locales/en.yml | 1 + config/locales/es.yml | 1 + config/locales/fr.yml | 1 + config/locales/pt.yml | 1 + test/integration/accounting_period_test.rb | 40 +++++++++++++++++++ 12 files changed, 81 insertions(+), 9 deletions(-) create mode 100644 app/validators/duration_validator.rb diff --git a/app/assets/javascripts/controllers/admin/invoices.js.erb b/app/assets/javascripts/controllers/admin/invoices.js.erb index cd0bd622f..33dde1fb4 100644 --- a/app/assets/javascripts/controllers/admin/invoices.js.erb +++ b/app/assets/javascripts/controllers/admin/invoices.js.erb @@ -676,8 +676,8 @@ Application.Controllers.controller('AvoirModalController', ['$scope', '$uibModal /** * Controller used in the modal window allowing an admin to close an accounting period */ -Application.Controllers.controller('ClosePeriodModalController', ['$scope', '$uibModalInstance', '$window', 'Invoice', 'AccountingPeriod', 'periods', 'lastClosingEnd','dialogs', 'growl', '_t', - function ($scope, $uibModalInstance, $window, Invoice, AccountingPeriod, periods, lastClosingEnd, dialogs, growl, _t) { +Application.Controllers.controller('ClosePeriodModalController', ['$scope', '$uibModalInstance', '$window', '$sce', 'Invoice', 'AccountingPeriod', 'periods', 'lastClosingEnd','dialogs', 'growl', '_t', + function ($scope, $uibModalInstance, $window, $sce, Invoice, AccountingPeriod, periods, lastClosingEnd, dialogs, growl, _t) { const YESTERDAY = moment.utc({ h: 0, m: 0, s: 0, ms: 0 }).subtract(1, 'day').toDate(); const LAST_CLOSING = moment.utc(lastClosingEnd.last_end_date).toDate(); const MAX_END = moment.utc(lastClosingEnd.last_end_date).add(1, 'year').subtract(1, 'day').toDate(); @@ -734,9 +734,15 @@ Application.Controllers.controller('ClosePeriodModalController', ['$scope', '$ui object () { return { title: _t('invoices.confirmation_required'), - msg: _t( - 'invoices.confirm_close_START_END', - { START: moment.utc($scope.period.start_at).format('LL'), END: moment.utc($scope.period.end_at).format('LL') } + msg: $sce.trustAsHtml( + _t( + 'invoices.confirm_close_START_END', + { START: moment.utc($scope.period.start_at).format('LL'), END: moment.utc($scope.period.end_at).format('LL') } + ) + + '

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

' + + _t('invoices.this_may_take_a_while') ) }; } diff --git a/app/models/accounting_period.rb b/app/models/accounting_period.rb index 5178e6255..0ee4e389c 100644 --- a/app/models/accounting_period.rb +++ b/app/models/accounting_period.rb @@ -14,6 +14,7 @@ class AccountingPeriod < ActiveRecord::Base validates :start_at, :end_at, :closed_at, :closed_by, presence: true validates_with DateRangeValidator + validates_with DurationValidator validates_with PeriodOverlapValidator validates_with PeriodIntegrityValidator diff --git a/app/validators/duration_validator.rb b/app/validators/duration_validator.rb new file mode 100644 index 000000000..fa30e9a37 --- /dev/null +++ b/app/validators/duration_validator.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +# Validates that the duration between start_at and end_at is between 1 day and 1 year +class DurationValidator < ActiveModel::Validator + def validate(record) + the_end = record.end_at + the_start = record.start_at + diff = (the_end - the_start).to_i + return if diff.days >= 1.day && diff.days <= 1.year + + record.errors[:end_at] << I18n.t('errors.messages.invalid_duration', DAYS: diff) + end +end diff --git a/config/locales/app.admin.en.yml b/config/locales/app.admin.en.yml index 1e96f1610..c9dee138c 100644 --- a/config/locales/app.admin.en.yml +++ b/config/locales/app.admin.en.yml @@ -421,7 +421,9 @@ en: perpetual_total: "Perpetual total" integrity: "Integrity check" confirmation_required: "Confirmation required" - confirm_close_START_END: "Do you really want to close the accounting period between {{START}} and {{END}}? Any subsequent changes will be impossible. This operation will take some time to complete" + confirm_close_START_END: "Do you really want to close the accounting period between {{START}} and {{END}}? Any subsequent changes will be impossible." + period_must_match_fiscal_year: "A closing must occur at the end of a minimum annual period, or per financial year when it is not calendar-based." + this_may_take_a_while: "This operation will take some time to complete." period_START_END_closed_success: "The accounting period from {{START}} to {{END}} has been successfully closed" failed_to_close_period: "An error occurred, unable to close the accounting period" no_periods: "No closings for now" diff --git a/config/locales/app.admin.es.yml b/config/locales/app.admin.es.yml index fe4266d0e..825a1dfdb 100644 --- a/config/locales/app.admin.es.yml +++ b/config/locales/app.admin.es.yml @@ -421,7 +421,9 @@ es: perpetual_total: "Perpetual total" # translation_missing integrity: "Verificación de integridad" confirmation_required: "Confirmation required" # translation_missing - confirm_close_START_END: "Do you really want to close the accounting period between {{START}} and {{END}}? Any subsequent changes will be impossible. This operation will take some time to complete" # translation_missing + confirm_close_START_END: "Do you really want to close the accounting period between {{START}} and {{END}}? Any subsequent changes will be impossible." # translation_missing + period_must_match_fiscal_year: "A closing must occur at the end of a minimum annual period, or per financial year when it is not calendar-based." # translation_missing + this_may_take_a_while: "This operation will take some time to complete." # translation_missing period_START_END_closed_success: "The accounting period from {{START}} to {{END}} has been successfully closed" # 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 diff --git a/config/locales/app.admin.fr.yml b/config/locales/app.admin.fr.yml index 508e1c7ba..d04470e92 100644 --- a/config/locales/app.admin.fr.yml +++ b/config/locales/app.admin.fr.yml @@ -421,7 +421,9 @@ fr: perpetual_total: "Total perpétuel" integrity: "Contrôle d'intégrité" confirmation_required: "Confirmation requise" - confirm_close_START_END: "Êtes-vous sur de vouloir clôturer la période comptable du {{START}} au {{END}} ? Toute modification ultérieure sera impossible. Cette opération va prendre un certain temps." + confirm_close_START_END: "Êtes-vous sur de vouloir clôturer la période comptable du {{START}} au {{END}} ? Toute modification ultérieure sera impossible." + period_must_match_fiscal_year: "Une clôture doit intervenir à l'issue d'une période au minimum annuelle, ou par exercice lorsque celui-ci n'est pas calé sur l'année civile." + this_may_take_a_while: "Cette opération va prendre un certain temps." period_START_END_closed_success: "La période comptable du {{START}} au {{END}} a bien été clôturé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" diff --git a/config/locales/app.admin.pt.yml b/config/locales/app.admin.pt.yml index 4d0f916a3..6291094bf 100755 --- a/config/locales/app.admin.pt.yml +++ b/config/locales/app.admin.pt.yml @@ -421,7 +421,9 @@ pt: perpetual_total: "Perpetual total" # translation_missing integrity: "Verificação de integridade" confirmation_required: "Confirmation required" # translation_missing - confirm_close_START_END: "Do you really want to close the accounting period between {{START}} and {{END}}? Any subsequent changes will be impossible. This operation will take some time to complete." # translation_missing + confirm_close_START_END: "Do you really want to close the accounting period between {{START}} and {{END}}? Any subsequent changes will be impossible" # translation_missing + period_must_match_fiscal_year: "A closing must occur at the end of a minimum annual period, or per financial year when it is not calendar-based." # translation_missing + this_may_take_a_while: "This operation will take some time to complete." # translation_missing period_START_END_closed_success: "The accounting period from {{START}} to {{END}} has been successfully closed" # 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 diff --git a/config/locales/en.yml b/config/locales/en.yml index 0690a80e2..39ca81d2c 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -41,6 +41,7 @@ en: in_closed_period: "can't be within a closed accounting period" invalid_footprint: "invoice's checksum is invalid" end_before_start: "The end date can't be before the start date. Pick a date after %{START}" + invalid_duration: "The allowed duration must be between 1 day and 1 year. Your period is %{DAYS} days long." activemodel: errors: diff --git a/config/locales/es.yml b/config/locales/es.yml index 7b63df33d..3d5096355 100644 --- a/config/locales/es.yml +++ b/config/locales/es.yml @@ -41,6 +41,7 @@ es: in_closed_period: "can't be within a closed accounting period" # missing translation invalid_footprint: "invoice's checksum is invalid" # missing translation end_before_start: "The end date can't be before the start date. Pick a date after %{START}" # missing translation + invalid_duration: "The allowed duration must be between 1 day and 1 year. Your period is %{DAYS} days long." # missing translation activemodel: errors: diff --git a/config/locales/fr.yml b/config/locales/fr.yml index aedcbcee7..cd6c3760a 100644 --- a/config/locales/fr.yml +++ b/config/locales/fr.yml @@ -41,6 +41,7 @@ fr: in_closed_period: "ne peut pas être dans une période comptable fermée" invalid_footprint: "la somme de contrôle de la facture est invalide" end_before_start: "La date de fin ne peut pas être antérieure à la date de début. Choisissez une date après le %{START}" + invalid_duration: "La durée doit être comprise entre 1 jour et 1 an. Votre période dure %{DAYS} jours." activemodel: errors: diff --git a/config/locales/pt.yml b/config/locales/pt.yml index 00ef49273..298be6c00 100755 --- a/config/locales/pt.yml +++ b/config/locales/pt.yml @@ -41,6 +41,7 @@ pt: in_closed_period: "can't be within a closed accounting period" # missing translation invalid_footprint: "invoice's checksum is invalid" # missing translation end_before_start: "The end date can't be before the start date. Pick a date after %{START}" # missing translation + invalid_duration: "The allowed duration must be between 1 day and 1 year. Your period is %{DAYS} days long." # missing translation activemodel: errors: diff --git a/test/integration/accounting_period_test.rb b/test/integration/accounting_period_test.rb index 1c0948673..98aaf777e 100644 --- a/test/integration/accounting_period_test.rb +++ b/test/integration/accounting_period_test.rb @@ -68,4 +68,44 @@ class AccountingPeriodTest < ActionDispatch::IntegrationTest FileUtils.rm_rf(accounting_period.archive_folder) end + test 'admin tries to closes a too long period' do + start_at = '2012-01-01T00:00:00.000Z' + end_at = '2014-12-31T00:00:00.000Z' + diff = (end_at.to_date - start_at.to_date).to_i + + post '/api/accounting_periods', + { + accounting_period: { + start_at: start_at, + end_at: end_at + } + }.to_json, default_headers + + # Check response format & status + assert_equal 422, response.status, response.body + assert_equal Mime::JSON, response.content_type + + # check the error + assert_match(/#{I18n.t('errors.messages.invalid_duration', DAYS: diff)}/, response.body) + end + + test 'admin tries to closes an overlapping period' do + start_at = '2014-12-01T00:00:00.000Z' + end_at = '2015-02-27T00:00:00.000Z' + + post '/api/accounting_periods', + { + accounting_period: { + start_at: start_at, + end_at: end_at + } + }.to_json, default_headers + + # Check response format & status + assert_equal 422, response.status, response.body + assert_equal Mime::JSON, response.content_type + + # check the error + assert_match(/#{I18n.t('errors.messages.cannot_overlap')}/, response.body) + end end