1
0
mirror of https://github.com/LaCasemate/fab-manager.git synced 2025-02-19 13:54:25 +01:00

validates accounting periods on creation + prevent refunding on closed periods (client only)

This commit is contained in:
Sylvain 2019-01-08 17:32:45 +01:00
parent 84aa0c75ff
commit 9fac706da8
12 changed files with 91 additions and 9 deletions

View File

@ -17,8 +17,8 @@
/**
* Controller used in the admin invoices listing page
*/
Application.Controllers.controller('InvoicesController', ['$scope', '$state', 'Invoice', 'AccountingPeriod', 'invoices', '$uibModal', 'growl', '$filter', 'Setting', 'settings', '_t',
function ($scope, $state, Invoice, AccountingPeriod, invoices, $uibModal, growl, $filter, Setting, settings, _t) {
Application.Controllers.controller('InvoicesController', ['$scope', '$state', 'Invoice', 'AccountingPeriod', 'invoices', 'closedPeriods', '$uibModal', 'growl', '$filter', 'Setting', 'settings', '_t',
function ($scope, $state, Invoice, AccountingPeriod, invoices, closedPeriods, $uibModal, growl, $filter, Setting, settings, _t) {
/* PRIVATE STATIC CONSTANTS */
// number of invoices loaded each time we click on 'load more...'
@ -110,7 +110,8 @@ Application.Controllers.controller('InvoicesController', ['$scope', '$state', 'I
templateUrl: '<%= asset_path "admin/invoices/avoirModal.html" %>',
controller: 'AvoirModalController',
resolve: {
invoice () { return invoice; }
invoice () { return invoice; },
closedPeriods() { return closedPeriods; }
}
});
@ -387,6 +388,10 @@ Application.Controllers.controller('InvoicesController', ['$scope', '$state', 'I
return invoiceSearch(true);
};
/**
* Open a modal allowing the user to close an accounting period and to
* view all periods already closed.
*/
$scope.closeAnAccountingPeriod = function() {
// open modal
$uibModal.open({
@ -394,7 +399,7 @@ Application.Controllers.controller('InvoicesController', ['$scope', '$state', 'I
controller: 'ClosePeriodModalController',
size: 'lg',
resolve: {
periods() { return AccountingPeriod.query().$promise; },
periods() { return closedPeriods; },
lastClosingEnd() { return AccountingPeriod.lastClosingEnd().$promise; },
}
});
@ -509,8 +514,8 @@ Application.Controllers.controller('InvoicesController', ['$scope', '$state', 'I
/**
* Controller used in the invoice refunding modal window
*/
Application.Controllers.controller('AvoirModalController', ['$scope', '$uibModalInstance', 'invoice', 'Invoice', 'growl', '_t',
function ($scope, $uibModalInstance, invoice, Invoice, growl, _t) {
Application.Controllers.controller('AvoirModalController', ['$scope', '$uibModalInstance', 'invoice', 'closedPeriods', 'Invoice', 'growl', '_t',
function ($scope, $uibModalInstance, invoice, closedPeriods, Invoice, growl, _t) {
/* PUBLIC SCOPE */
// invoice linked to the current refund
@ -589,6 +594,20 @@ Application.Controllers.controller('AvoirModalController', ['$scope', '$uibModal
*/
$scope.cancel = function () { $uibModalInstance.dismiss('cancel'); };
/**
* Test if the given date is within a closed accounting period
* @param date {Date} date to test
* @returns {boolean} true if closed, false otherwise
*/
$scope.isDateClosed = function(date) {
for (const period of closedPeriods) {
if (moment(date).isBetween(moment.utc(period.start_at).startOf('day'), moment.utc(period.end_at).endOf('day'), null, '[]')) {
return true;
}
}
return false;
}
/* PRIVATE SCOPE */
/**
@ -624,11 +643,17 @@ Application.Controllers.controller('ClosePeriodModalController', ['$scope', '$ui
const LAST_CLOSING = moment.utc(lastClosingEnd.last_end_date).toDate();
/* PUBLIC SCOPE */
// date pickers values are bound to these variables
$scope.period = {
start_at: LAST_CLOSING,
end_at: YESTERDAY
};
// any form errors will come here
$scope.errors = {};
// existing closed periods, provided by the API
$scope.accountingPeriods = periods;
// AngularUI-Bootstrap datepickers parameters to define the period to close
@ -681,7 +706,7 @@ Application.Controllers.controller('ClosePeriodModalController', ['$scope', '$ui
}
, function(error) {
growl.error(_t('invoices.failed_to_close_period'));
console.error(error);
$scope.errors = error.data;
});
}
);

View File

@ -885,6 +885,7 @@ angular.module('application.router', ['ui.router'])
query: { number: '', customer: '', date: null, order_by: '-reference', page: 1, size: 20 }
}).$promise;
}],
closedPeriods: [ 'AccountingPeriod', function(AccountingPeriod) { return AccountingPeriod.query().$promise; }],
translations: ['Translations', function (Translations) { return Translations.query('app.admin.invoices').$promise; }]
}
})

View File

@ -616,4 +616,8 @@ padding: 10px;
& > i.fileinput-exists {
margin-right: 5px;
}
}
}
.help-block.error {
color: #ff565d;
}

View File

@ -14,6 +14,7 @@
uib-datepicker-popup="{{datePicker.format}}"
datepicker-options="datePicker.options"
is-open="datePicker.opened"
date-disabled="isDateClosed(date, mode)"
placeholder="{{datePicker.format}}"
ng-click="openDatePicker($event)"
required/>

View File

@ -22,6 +22,7 @@
required/>
</div>
<span class="help-block" ng-show="closePeriodForm.start_at.$dirty && closePeriodForm.start_at.$error.required" translate>{{ 'invoices.start_date_is_required' }}</span>
<span class="help-block error" ng-show="errors.start_at">{{ errors.start_at[0] }}</span>
</div>
<div class="form-group col-md-6" ng-class="{'has-error': closePeriodForm.end_at.$dirty && closePeriodForm.end_at.$invalid }">
<label translate>{{ 'invoices.close_until_date' }}</label>
@ -42,6 +43,7 @@
required/>
</div>
<span class="help-block" ng-show="closePeriodForm.end_at.$dirty && closePeriodForm.end_at.$error.required" translate>{{ 'invoices.end_date_is_required' }}</span>
<span class="help-block error" ng-show="errors.end_at">{{ errors.end_at[0] }}</span>
</div>
</form>
<div>

View File

@ -1,7 +1,15 @@
# frozen_string_literal: true
# AccountingPeriod is a period of N days (N > 0) which as been closed by an admin
# to prevent writing new accounting lines (invoices & refunds) during this period of time.
class AccountingPeriod < ActiveRecord::Base
before_destroy { false }
before_update { false }
validates :start_at, :end_at, :closed_at, :closed_by, presence: true
validates_with DateRangeValidator
validates_with PeriodOverlapValidator
def delete
false
end

View File

@ -0,0 +1,12 @@
# frozen_string_literal: true
# Validates that start_at is same or before end_at in the given record
class DateRangeValidator < ActiveModel::Validator
def validate(record)
the_end = record.start_at
the_start = record.end_at
return unless the_end.present? && the_end >= the_start
record.errors[:end_at] << "The end date can't be before the start date. Pick a date after #{the_start}"
end
end

View File

@ -0,0 +1,21 @@
# frozen_string_literal: true
# Validates the current accounting period does not overlap an existing one
class PeriodOverlapValidator < ActiveModel::Validator
def validate(record)
the_end = record.end_at
the_start = record.start_at
AccountingPeriod.all.each do |period|
if the_start >= period.start_at && the_start <= period.end_at
record.errors[:start_at] << I18n.t('errors.messages.cannot_overlap')
end
if the_end >= period.start_at && the_end <= period.end_at
record.errors[:end_at] << I18n.t('errors.messages.cannot_overlap')
end
if period.start_at >= the_start && period.end_at <= the_end
record.errors[:end_at] << I18n.t('errors.messages.cannot_encompass')
end
end
end
end

View File

@ -36,6 +36,8 @@ en:
cannot_be_blank_at_same_time: "cannot be blank when %{field} is blank too"
cannot_be_in_the_past: "cannot be in the past"
cannot_be_before_previous_value: "cannot be before the previous value"
cannot_overlap: "can't overlap an existing accounting period"
cannot_encompass: "can't encompass an existing accounting period"
activemodel:
errors:

View File

@ -36,6 +36,8 @@ es:
cannot_be_blank_at_same_time: "no puede estar vacío cuando %{field} también está vacío"
cannot_be_in_the_past: "no puede estar en el pasado"
cannot_be_before_previous_value: "No puede estar antes del valor anterior."
cannot_overlap: "can't overlap an existing accounting period" # missing translation
cannot_encompass: "can't encompass an existing accounting period" # missing translation
activemodel:
errors:

View File

@ -36,6 +36,8 @@ fr:
cannot_be_blank_at_same_time: "ou %{field} doit être rempli(e)"
cannot_be_in_the_past: "ne peut pas être dans le passé"
cannot_be_before_previous_value: "ne peut pas être antérieur(e) à la valeur précédente"
cannot_overlap: "ne peut pas chevaucher une période comptable existante"
cannot_encompass: "ne peut pas englober une période comptable existante"
activemodel:
errors:
@ -360,4 +362,4 @@ fr:
group:
# nom du groupe utilisateur pour les administrateurs
admins: 'Administrateurs'
admins: 'Administrateurs'

View File

@ -36,6 +36,8 @@ pt:
cannot_be_blank_at_same_time: "Não pode ficar em branco quando %{field} estiver em branco também"
cannot_be_in_the_past: "não pode ser no passado"
cannot_be_before_previous_value: "não pode ser antes do valor anterior"
cannot_overlap: "can't overlap an existing accounting period" # missing translation
cannot_encompass: "can't encompass an existing accounting period" # missing translation
activemodel:
errors: