mirror of
https://github.com/LaCasemate/fab-manager.git
synced 2025-03-01 23:29:23 +01:00
Merge branch 'dev' for release 3.1.0
This commit is contained in:
commit
cff2dddb2e
@ -24,3 +24,5 @@ Style/ClassAndModuleChildren:
|
|||||||
EnforcedStyle: compact
|
EnforcedStyle: compact
|
||||||
Style/AndOr:
|
Style/AndOr:
|
||||||
EnforcedStyle: conditionals
|
EnforcedStyle: conditionals
|
||||||
|
Style/FormatString:
|
||||||
|
EnforcedStyle: sprintf
|
||||||
|
13
CHANGELOG.md
13
CHANGELOG.md
@ -1,4 +1,15 @@
|
|||||||
# Changelog Fab Manager
|
# Changelog Fab Manager
|
||||||
|
|
||||||
|
## v3.1.0 2019 April 8
|
||||||
|
|
||||||
|
- Asynchronously generate accounting archives
|
||||||
|
- Improved end-user message when closing an accounting period
|
||||||
|
- Improved date checks before closing an accounting period
|
||||||
|
- Paginate list of coupons
|
||||||
|
- Allow filtering coupons list
|
||||||
|
- Fix a bug: when VAT has changed during fab-manager's lifecycle, this may not be reflected in archives
|
||||||
|
- Fix a bug: using a quote in event category's name results in angular $parse:syntax Error
|
||||||
|
|
||||||
## v3.0.1 2019 April 1st
|
## v3.0.1 2019 April 1st
|
||||||
|
|
||||||
- Insert archive generation datetime in chained.sha256
|
- Insert archive generation datetime in chained.sha256
|
||||||
@ -20,6 +31,7 @@
|
|||||||
- Rebranded product from "La Casemate"
|
- Rebranded product from "La Casemate"
|
||||||
- Refactored some pieces of Ruby code, according to style guide
|
- Refactored some pieces of Ruby code, according to style guide
|
||||||
- Added asterisks on required fields in sign-up form
|
- Added asterisks on required fields in sign-up form
|
||||||
|
- [TODO DEPLOY] /!\ Before deploying, you must check (and eventually) correct your VAT history using the rails console. Missing rates can be added later but dates and rates (including date of activation, disabling) MUST be correct. These values are very likely wrong if your installation was made prior to 2.8.0 with VAT enabled. Other cases must be checked too.
|
||||||
- [TODO DEPLOY] (dev) if applicable, you must first downgrade bundler to v1 `gem uninstall bundler --version=2.0.1 && gem install bundler --version=1.7.3 && bundle install`
|
- [TODO DEPLOY] (dev) if applicable, you must first downgrade bundler to v1 `gem uninstall bundler --version=2.0.1 && gem install bundler --version=1.7.3 && bundle install`
|
||||||
- [TODO DEPLOY] if you have changed your VAT rate in the past, add its history into database. You can use a rate of "0" to disable VAT. Eg. `rake fablab:setup:add_vat_rate[20,2017-01-01]`
|
- [TODO DEPLOY] if you have changed your VAT rate in the past, add its history into database. You can use a rate of "0" to disable VAT. Eg. `rake fablab:setup:add_vat_rate[20,2017-01-01]`
|
||||||
- [TODO DEPLOY] `rake fablab:setup:set_environment_to_invoices`
|
- [TODO DEPLOY] `rake fablab:setup:set_environment_to_invoices`
|
||||||
@ -27,6 +39,7 @@
|
|||||||
- [TODO DEPLOY] `rake fablab:setup:chain_invoices_records`
|
- [TODO DEPLOY] `rake fablab:setup:chain_invoices_records`
|
||||||
- [TODO DEPLOY] `rake fablab:setup:chain_history_values_records`
|
- [TODO DEPLOY] `rake fablab:setup:chain_history_values_records`
|
||||||
- [TODO DEPLOY] add `DISK_SPACE_MB_ALERT` and `SUPERADMIN_EMAIL` environment variables (see [doc/environment.md](doc/environment.md) for configuration details)
|
- [TODO DEPLOY] add `DISK_SPACE_MB_ALERT` and `SUPERADMIN_EMAIL` environment variables (see [doc/environment.md](doc/environment.md) for configuration details)
|
||||||
|
- [TODO DEPLOY] add the `accounting` volume to the fab-manager's image in [docker-compose.yml](docker/docker-compose.yml)
|
||||||
|
|
||||||
## v2.8.4 2019 March 18
|
## v2.8.4 2019 March 18
|
||||||
|
|
||||||
|
@ -419,6 +419,8 @@ Application.Controllers.controller('InvoicesController', ['$scope', '$state', 'I
|
|||||||
$uibModal.open({
|
$uibModal.open({
|
||||||
templateUrl: '<%= asset_path "admin/invoices/closePeriodModal.html" %>',
|
templateUrl: '<%= asset_path "admin/invoices/closePeriodModal.html" %>',
|
||||||
controller: 'ClosePeriodModalController',
|
controller: 'ClosePeriodModalController',
|
||||||
|
backdrop: 'static',
|
||||||
|
keyboard: false,
|
||||||
size: 'lg',
|
size: 'lg',
|
||||||
resolve: {
|
resolve: {
|
||||||
periods() { return AccountingPeriod.query().$promise; },
|
periods() { return AccountingPeriod.query().$promise; },
|
||||||
@ -676,8 +678,8 @@ Application.Controllers.controller('AvoirModalController', ['$scope', '$uibModal
|
|||||||
/**
|
/**
|
||||||
* Controller used in the modal window allowing an admin to close an accounting period
|
* 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',
|
Application.Controllers.controller('ClosePeriodModalController', ['$scope', '$uibModalInstance', '$window', '$sce', 'Invoice', 'AccountingPeriod', 'periods', 'lastClosingEnd','dialogs', 'growl', '_t',
|
||||||
function ($scope, $uibModalInstance, $window, 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 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 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();
|
const MAX_END = moment.utc(lastClosingEnd.last_end_date).add(1, 'year').subtract(1, 'day').toDate();
|
||||||
@ -734,9 +736,15 @@ Application.Controllers.controller('ClosePeriodModalController', ['$scope', '$ui
|
|||||||
object () {
|
object () {
|
||||||
return {
|
return {
|
||||||
title: _t('invoices.confirmation_required'),
|
title: _t('invoices.confirmation_required'),
|
||||||
msg: _t(
|
msg: $sce.trustAsHtml(
|
||||||
'invoices.confirm_close_START_END',
|
_t(
|
||||||
{ START: moment.utc($scope.period.start_at).format('LL'), END: moment.utc($scope.period.end_at).format('LL') }
|
'invoices.confirm_close_START_END',
|
||||||
|
{ START: moment.utc($scope.period.start_at).format('LL'), END: moment.utc($scope.period.end_at).format('LL') }
|
||||||
|
)
|
||||||
|
+ '<br/><br/><strong>'
|
||||||
|
+ _t('invoices.period_must_match_fiscal_year')
|
||||||
|
+ '</strong><br/><br/>'
|
||||||
|
+ _t('invoices.this_may_take_a_while')
|
||||||
)
|
)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -53,6 +53,7 @@ Application.Controllers.controller('EditPricingController', ['$scope', '$state',
|
|||||||
|
|
||||||
// List of coupons
|
// List of coupons
|
||||||
$scope.coupons = couponsPromise;
|
$scope.coupons = couponsPromise;
|
||||||
|
$scope.couponsPage = 1;
|
||||||
|
|
||||||
// List of spaces
|
// List of spaces
|
||||||
$scope.spaces = spacesPromise;
|
$scope.spaces = spacesPromise;
|
||||||
@ -81,6 +82,20 @@ Application.Controllers.controller('EditPricingController', ['$scope', '$state',
|
|||||||
'all'
|
'all'
|
||||||
];
|
];
|
||||||
|
|
||||||
|
// Default: we do not filter coupons
|
||||||
|
$scope.filter = {
|
||||||
|
coupon: 'all',
|
||||||
|
};
|
||||||
|
|
||||||
|
// Available status for filtering coupons
|
||||||
|
$scope.couponStatus = [
|
||||||
|
'all',
|
||||||
|
'disabled',
|
||||||
|
'expired',
|
||||||
|
'sold_out',
|
||||||
|
'active'
|
||||||
|
];
|
||||||
|
|
||||||
$scope.findTrainingsPricing = function (trainingsPricings, trainingId, groupId) {
|
$scope.findTrainingsPricing = function (trainingsPricings, trainingId, groupId) {
|
||||||
for (let trainingsPricing of Array.from(trainingsPricings)) {
|
for (let trainingsPricing of Array.from(trainingsPricings)) {
|
||||||
if ((trainingsPricing.training_id === trainingId) && (trainingsPricing.group_id === groupId)) {
|
if ((trainingsPricing.training_id === trainingId) && (trainingsPricing.group_id === groupId)) {
|
||||||
@ -565,6 +580,26 @@ Application.Controllers.controller('EditPricingController', ['$scope', '$state',
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load the next 10 coupons
|
||||||
|
*/
|
||||||
|
$scope.loadMore = function() {
|
||||||
|
$scope.couponsPage++;
|
||||||
|
Coupon.query({ page: $scope.couponsPage, filter: $scope.filter.coupon }, function (data) {
|
||||||
|
$scope.coupons = $scope.coupons.concat(data);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reset the list of coupons according to the newly selected filter
|
||||||
|
*/
|
||||||
|
$scope.updateCouponFilter = function() {
|
||||||
|
$scope.couponsPage = 1;
|
||||||
|
Coupon.query({ page: $scope.couponsPage, filter: $scope.filter.coupon }, function (data) {
|
||||||
|
$scope.coupons = data;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/* PRIVATE SCOPE */
|
/* PRIVATE SCOPE */
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -786,7 +786,7 @@ angular.module('application.router', ['ui.router'])
|
|||||||
machineCreditsPromise: ['Credit', function (Credit) { return Credit.query({ creditable_type: 'Machine' }).$promise; }],
|
machineCreditsPromise: ['Credit', function (Credit) { return Credit.query({ creditable_type: 'Machine' }).$promise; }],
|
||||||
machinesPromise: ['Machine', function (Machine) { return Machine.query().$promise; }],
|
machinesPromise: ['Machine', function (Machine) { return Machine.query().$promise; }],
|
||||||
trainingCreditsPromise: ['Credit', function (Credit) { return Credit.query({ creditable_type: 'Training' }).$promise; }],
|
trainingCreditsPromise: ['Credit', function (Credit) { return Credit.query({ creditable_type: 'Training' }).$promise; }],
|
||||||
couponsPromise: ['Coupon', function (Coupon) { return Coupon.query().$promise; }],
|
couponsPromise: ['Coupon', function (Coupon) { return Coupon.query({ page: 1, filter: 'all' }).$promise; }],
|
||||||
spacesPromise: ['Space', function (Space) { return Space.query().$promise; }],
|
spacesPromise: ['Space', function (Space) { return Space.query().$promise; }],
|
||||||
spacesPricesPromise: ['Price', function (Price) { return Price.query({ priceable_type: 'Space', plan_id: 'null' }).$promise; }],
|
spacesPricesPromise: ['Price', function (Price) { return Price.query({ priceable_type: 'Space', plan_id: 'null' }).$promise; }],
|
||||||
spacesCreditsPromise: ['Credit', function (Credit) { return Credit.query({ creditable_type: 'Space' }).$promise; }]
|
spacesCreditsPromise: ['Credit', function (Credit) { return Credit.query({ creditable_type: 'Space' }).$promise; }]
|
||||||
|
@ -204,6 +204,10 @@ table.closings-table {
|
|||||||
margin-left: 2em;
|
margin-left: 2em;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
& > span.no-pointer {
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
tbody .show-more {
|
tbody .show-more {
|
||||||
@ -268,3 +272,7 @@ table.scrollable-3-cols {
|
|||||||
.period-info-title {
|
.period-info-title {
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
input.form-control.as-writable {
|
||||||
|
background-color: white;
|
||||||
|
}
|
||||||
|
@ -29,7 +29,7 @@
|
|||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
<span class="input-group-addon"><i class="fa fa-calendar"></i></span>
|
<span class="input-group-addon"><i class="fa fa-calendar"></i></span>
|
||||||
<input type="text"
|
<input type="text"
|
||||||
class="form-control"
|
class="form-control as-writable"
|
||||||
name="end_at"
|
name="end_at"
|
||||||
ng-model="period.end_at"
|
ng-model="period.end_at"
|
||||||
uib-datepicker-popup="{{datePicker.format}}"
|
uib-datepicker-popup="{{datePicker.format}}"
|
||||||
@ -40,7 +40,8 @@
|
|||||||
init-date="period.end_at"
|
init-date="period.end_at"
|
||||||
placeholder="{{datePicker.format}}"
|
placeholder="{{datePicker.format}}"
|
||||||
ng-click="toggleDatePicker($event)"
|
ng-click="toggleDatePicker($event)"
|
||||||
required/>
|
required
|
||||||
|
readonly/>
|
||||||
</div>
|
</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" 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>
|
<span class="help-block error" ng-show="errors.end_at">{{ errors.end_at[0] }}</span>
|
||||||
@ -65,7 +66,8 @@
|
|||||||
<td>{{period.end_at | amDateFormat:'L'}}</td>
|
<td>{{period.end_at | amDateFormat:'L'}}</td>
|
||||||
<td class="actions">
|
<td class="actions">
|
||||||
<span class="show-more" uib-popover-template="'<%= asset_path 'admin/invoices/_period.html' %>'"><i class="fa fa-info-circle"></i></span>
|
<span class="show-more" uib-popover-template="'<%= asset_path 'admin/invoices/_period.html' %>'"><i class="fa fa-info-circle"></i></span>
|
||||||
<span class="download-archive" ng-click="downloadArchive(period)"><i class="fa fa-archive"></i></span>
|
<span class="download-archive" ng-click="downloadArchive(period)" ng-show="period.archive_ready"><i class="fa fa-archive"></i></span>
|
||||||
|
<span class="no-pointer" ng-hide="period.archive_ready"><i class="fa fa-spinner fa-pulse"></i></span>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
|
@ -1,6 +1,20 @@
|
|||||||
<h2 translate>{{ 'pricing.list_of_the_coupons' }}</h2>
|
<h2 translate>{{ 'pricing.list_of_the_coupons' }}</h2>
|
||||||
|
|
||||||
<button type="button" class="btn btn-warning m-t-lg m-b" ui-sref="app.admin.coupons_new" translate>{{ 'pricing.add_a_new_coupon' }}</button>
|
<div class="m-t-lg m-b">
|
||||||
|
<button type="button" class="btn btn-warning" ui-sref="app.admin.coupons_new">
|
||||||
|
<i class="fa fa-plus m-r"></i>
|
||||||
|
<span translate>{{ 'pricing.add_a_new_coupon' }}</span>
|
||||||
|
</button>
|
||||||
|
<div class="form-group pull-right">
|
||||||
|
<div class="input-group">
|
||||||
|
<span class="input-group-addon"><i class="fa fa-filter"></i></span>
|
||||||
|
<select ng-model="filter.coupon" class="form-control" ng-change="updateCouponFilter()">
|
||||||
|
<option ng-repeat="status in couponStatus" value="{{status}}" translate>{{ 'pricing.'+status }}</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<table class="table">
|
<table class="table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
@ -27,4 +41,8 @@
|
|||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
|
<div class="text-center">
|
||||||
|
<button class="btn btn-warning" ng-click="loadMore()" ng-hide="coupons.length === 0 || coupons.length >= coupons[0].total"><i class="fa fa-search-plus" aria-hidden="true"></i> {{ 'pricing.display_more_coupons' | translate }}</button>
|
||||||
|
</div>
|
||||||
|
@ -97,7 +97,7 @@
|
|||||||
<h1 class="m-b">{{event.title}}</h1>
|
<h1 class="m-b">{{event.title}}</h1>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-xs-3">
|
<div class="col-xs-3">
|
||||||
<span class="v-middle badge text-xs" ng-class="'bg-{{event.category.name | lowercase}}'">{{event.category.name}}</span>
|
<span class="v-middle badge text-xs" ng-class="'bg-{{event.category.slug}}'">{{event.category.name}}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p class="event-description" ng-bind-html="event.description | simpleText | humanize : 500 | breakFilter"></p>
|
<p class="event-description" ng-bind-html="event.description | simpleText | humanize : 500 | breakFilter"></p>
|
||||||
|
@ -6,8 +6,12 @@ class API::CouponsController < API::ApiController
|
|||||||
before_action :authenticate_user!
|
before_action :authenticate_user!
|
||||||
before_action :set_coupon, only: %i[show update destroy]
|
before_action :set_coupon, only: %i[show update destroy]
|
||||||
|
|
||||||
|
# Number of notifications added to the page when the user clicks on 'load next notifications'
|
||||||
|
COUPONS_PER_PAGE = 10
|
||||||
|
|
||||||
def index
|
def index
|
||||||
@coupons = Coupon.all
|
@coupons = Coupon.method(params[:filter]).call.page(params[:page]).per(COUPONS_PER_PAGE).order('created_at DESC')
|
||||||
|
@total = Coupon.method(params[:filter]).call.length
|
||||||
end
|
end
|
||||||
|
|
||||||
def show; end
|
def show; end
|
||||||
|
@ -1,67 +1,70 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
# Various helpers methods
|
||||||
module ApplicationHelper
|
module ApplicationHelper
|
||||||
|
|
||||||
include Twitter::Autolink
|
include Twitter::Autolink
|
||||||
require 'message_format'
|
require 'message_format'
|
||||||
|
|
||||||
## machine/spaces availabilities are divided in multiple slots of 60 minutes
|
## machine/spaces availabilities are divided in multiple slots of 60 minutes
|
||||||
SLOT_DURATION ||= 60
|
SLOT_DURATION ||= 60
|
||||||
|
|
||||||
##
|
##
|
||||||
# Verify if the provided attribute is in the provided attributes array, whatever it exists or not
|
# Verify if the provided attribute is in the provided attributes array, whatever it exists or not
|
||||||
# @param attributes {Array|nil}
|
# @param attributes {Array|nil}
|
||||||
# @param attribute {String}
|
# @param attribute {String}
|
||||||
##
|
##
|
||||||
def attribute_requested?(attributes, attribute)
|
def attribute_requested?(attributes, attribute)
|
||||||
attributes.try(:include?, attribute)
|
attributes.try(:include?, attribute)
|
||||||
end
|
end
|
||||||
|
|
||||||
def bootstrap_class_for flash_type
|
def bootstrap_class_for flash_type
|
||||||
{ flash: 'alert-success', alert: 'alert-danger', notice: 'alert-info' }[flash_type.to_sym] || flash_type.to_s
|
{ flash: 'alert-success', alert: 'alert-danger', notice: 'alert-info' }[flash_type.to_sym] || flash_type.to_s
|
||||||
end
|
end
|
||||||
|
|
||||||
def flash_messages(opts = {})
|
def flash_messages(_opts = {})
|
||||||
flash.each do |msg_type, message|
|
flash.each do |msg_type, message|
|
||||||
concat(content_tag(:div, message, class: "flash-message alert #{bootstrap_class_for(msg_type)} fade in") do
|
concat(content_tag(:div, message, class: "flash-message alert #{bootstrap_class_for(msg_type)} fade in") do
|
||||||
concat content_tag(:button, 'x', class: 'close', data: { dismiss: 'alert' })
|
concat content_tag(:button, 'x', class: 'close', data: { dismiss: 'alert' })
|
||||||
concat message
|
concat message
|
||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
nil
|
nil
|
||||||
end
|
end
|
||||||
|
|
||||||
def print_slot(starting, ending)
|
def print_slot(starting, ending)
|
||||||
"#{starting.strftime('%H:%M')} - #{ending.strftime('%H:%M')}"
|
"#{starting.strftime('%H:%M')} - #{ending.strftime('%H:%M')}"
|
||||||
end
|
end
|
||||||
|
|
||||||
def class_exists?(class_name)
|
def class_exists?(class_name)
|
||||||
klass = Module.const_get(class_name)
|
klass = Module.const_get(class_name)
|
||||||
return klass.is_a?(Class)
|
klass.is_a?(Class)
|
||||||
rescue NameError
|
rescue NameError
|
||||||
return false
|
false
|
||||||
end
|
end
|
||||||
|
|
||||||
##
|
##
|
||||||
# Allow to treat a rails i18n key as a MessageFormat interpolated pattern. Used in ruby views (API/mails)
|
# Allow to treat a rails i18n key as a MessageFormat interpolated pattern. Used in ruby views (API/mails)
|
||||||
# @param key {String} Ruby-on-Rails I18n key (from config/locales/xx.yml)
|
# @param key {String} Ruby-on-Rails I18n key (from config/locales/xx.yml)
|
||||||
# @param interpolations {Hash} list of variables to interpolate, following ICU MessageFormat syntax
|
# @param interpolations {Hash} list of variables to interpolate, following ICU MessageFormat syntax
|
||||||
##
|
##
|
||||||
def _t(key, interpolations)
|
def _t(key, interpolations)
|
||||||
message = MessageFormat.new(I18n.t(scope_key_by_partial(key)), I18n.locale.to_s)
|
message = MessageFormat.new(I18n.t(scope_key_by_partial(key)), I18n.locale.to_s)
|
||||||
text = message.format(interpolations)
|
text = message.format(interpolations)
|
||||||
if html_safe_translation_key?(key)
|
if html_safe_translation_key?(key)
|
||||||
text.html_safe
|
text.html_safe
|
||||||
else
|
else
|
||||||
text
|
text
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def bool_to_sym(bool)
|
def bool_to_sym(bool)
|
||||||
if (bool) then return :true else return :false end
|
bool ? :true : :false # rubocop:disable Lint/BooleanSymbol
|
||||||
end
|
end
|
||||||
|
|
||||||
def amount_to_f(amount)
|
def amount_to_f(amount)
|
||||||
amount / 100.00
|
amount / 100.00
|
||||||
end
|
end
|
||||||
|
|
||||||
##
|
##
|
||||||
# Retrieve an item in the given array of items
|
# Retrieve an item in the given array of items
|
||||||
@ -69,46 +72,45 @@ module ApplicationHelper
|
|||||||
# this can be overridden by passing a third parameter to specify the
|
# this can be overridden by passing a third parameter to specify the
|
||||||
# property to match
|
# property to match
|
||||||
##
|
##
|
||||||
def get_item(array, id, key = nil)
|
def get_item(array, id, key = nil)
|
||||||
array.each do |i|
|
array.each do |i|
|
||||||
if key.nil?
|
if key.nil?
|
||||||
return i if i.id == id
|
return i if i.id == id
|
||||||
else
|
elsif i[key] == id
|
||||||
return i if i[key] == id
|
return i
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
nil
|
nil
|
||||||
end
|
end
|
||||||
|
|
||||||
##
|
##
|
||||||
# Apply a correction for a future DateTime due to change in Daylight Saving Time (DST) period
|
# Apply a correction for a future DateTime due to change in Daylight Saving Time (DST) period
|
||||||
# @param reference {ActiveSupport::TimeWithZone}
|
# @param reference {ActiveSupport::TimeWithZone}
|
||||||
# @param datetime {DateTime}
|
# @param datetime {DateTime}
|
||||||
# Inspired by https://stackoverflow.com/a/12065605
|
# Inspired by https://stackoverflow.com/a/12065605
|
||||||
##
|
##
|
||||||
def dst_correction(reference, datetime)
|
def dst_correction(reference, datetime)
|
||||||
res = datetime.in_time_zone(reference.time_zone.tzinfo.name)
|
res = datetime.in_time_zone(reference.time_zone.tzinfo.name)
|
||||||
res = res - 1.hour if res.dst? && !reference.dst?
|
res -= 1.hour if res.dst? && !reference.dst?
|
||||||
res = res + 1.hour if reference.dst? && !res.dst?
|
res += 1.hour if reference.dst? && !res.dst?
|
||||||
res
|
res
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
||||||
private
|
private
|
||||||
## inspired by gems/actionview-4.2.5/lib/action_view/helpers/translation_helper.rb
|
|
||||||
def scope_key_by_partial(key)
|
|
||||||
if key.to_s.first == "."
|
|
||||||
if @virtual_path
|
|
||||||
@virtual_path.gsub(%r{/_?}, ".") + key.to_s
|
|
||||||
else
|
|
||||||
raise "Cannot use t(#{key.inspect}) shortcut because path is not available"
|
|
||||||
end
|
|
||||||
else
|
|
||||||
key
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def html_safe_translation_key?(key)
|
## inspired by gems/actionview-4.2.5/lib/action_view/helpers/translation_helper.rb
|
||||||
key.to_s =~ /(\b|_|\.)html$/
|
def scope_key_by_partial(key)
|
||||||
end
|
if key.to_s.first == '.'
|
||||||
|
raise "Cannot use t(#{key.inspect}) shortcut because path is not available" unless @virtual_path
|
||||||
|
|
||||||
|
@virtual_path.gsub(%r{/_?}, '.') + key.to_s
|
||||||
|
else
|
||||||
|
key
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def html_safe_translation_key?(key)
|
||||||
|
key.to_s =~ /(\b|_|\.)html$/
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
@ -1,3 +1,6 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
# Helpers methods about calendar availabilities
|
||||||
module AvailabilityHelper
|
module AvailabilityHelper
|
||||||
MACHINE_COLOR = '#e4cd78'
|
MACHINE_COLOR = '#e4cd78'
|
||||||
TRAINING_COLOR = '#bd7ae9'
|
TRAINING_COLOR = '#bd7ae9'
|
||||||
@ -9,14 +12,14 @@ module AvailabilityHelper
|
|||||||
|
|
||||||
def availability_border_color(availability)
|
def availability_border_color(availability)
|
||||||
case availability.available_type
|
case availability.available_type
|
||||||
when 'machines'
|
when 'machines'
|
||||||
MACHINE_COLOR
|
MACHINE_COLOR
|
||||||
when 'training'
|
when 'training'
|
||||||
TRAINING_COLOR
|
TRAINING_COLOR
|
||||||
when 'space'
|
when 'space'
|
||||||
SPACE_COLOR
|
SPACE_COLOR
|
||||||
else
|
else
|
||||||
EVENT_COLOR
|
EVENT_COLOR
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -45,14 +48,14 @@ module AvailabilityHelper
|
|||||||
IS_COMPLETED
|
IS_COMPLETED
|
||||||
else
|
else
|
||||||
case availability.available_type
|
case availability.available_type
|
||||||
when 'training'
|
when 'training'
|
||||||
TRAINING_COLOR
|
TRAINING_COLOR
|
||||||
when 'event'
|
when 'event'
|
||||||
EVENT_COLOR
|
EVENT_COLOR
|
||||||
when 'space'
|
when 'space'
|
||||||
SPACE_COLOR
|
SPACE_COLOR
|
||||||
else
|
else
|
||||||
'#000'
|
'#000'
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -1,3 +1,6 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
# Helpers methods about uploading files
|
||||||
module UploadHelper
|
module UploadHelper
|
||||||
|
|
||||||
def delete_empty_dirs
|
def delete_empty_dirs
|
||||||
|
@ -1,3 +1,7 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
# Abuse is a report made by a visitor (not especially a logged user) who has signaled a content that seems abusive to his eyes.
|
||||||
|
# It is currently used with projects.
|
||||||
class Abuse < ActiveRecord::Base
|
class Abuse < ActiveRecord::Base
|
||||||
include NotifyWith::NotificationAttachedObject
|
include NotifyWith::NotificationAttachedObject
|
||||||
|
|
||||||
|
@ -10,10 +10,12 @@ class AccountingPeriod < ActiveRecord::Base
|
|||||||
before_destroy { false }
|
before_destroy { false }
|
||||||
before_update { false }
|
before_update { false }
|
||||||
before_create :compute_totals
|
before_create :compute_totals
|
||||||
after_create :archive_closed_data
|
after_commit :archive_closed_data, on: [:create]
|
||||||
|
|
||||||
validates :start_at, :end_at, :closed_at, :closed_by, presence: true
|
validates :start_at, :end_at, :closed_at, :closed_by, presence: true
|
||||||
validates_with DateRangeValidator
|
validates_with DateRangeValidator
|
||||||
|
validates_with DurationValidator
|
||||||
|
validates_with PastPeriodValidator
|
||||||
validates_with PeriodOverlapValidator
|
validates_with PeriodOverlapValidator
|
||||||
validates_with PeriodIntegrityValidator
|
validates_with PeriodIntegrityValidator
|
||||||
|
|
||||||
@ -61,11 +63,15 @@ class AccountingPeriod < ActiveRecord::Base
|
|||||||
first_rate = @vat_rates.first
|
first_rate = @vat_rates.first
|
||||||
return first_rate[:rate] if date < first_rate[:date]
|
return first_rate[:rate] if date < first_rate[:date]
|
||||||
|
|
||||||
@vat_rates.each do |h|
|
@vat_rates.each_index do |i|
|
||||||
return h[:rate] if h[:date] <= date
|
return @vat_rates[i][:rate] if date >= @vat_rates[i][:date] && (@vat_rates[i + 1].nil? || date < @vat_rates[i + 1][:date])
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def previous_period
|
||||||
|
AccountingPeriod.where('closed_at < ?', closed_at).order(closed_at: :desc).limit(1).last
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def vat_history
|
def vat_history
|
||||||
@ -79,47 +85,8 @@ class AccountingPeriod < ActiveRecord::Base
|
|||||||
key_dates.sort_by { |k| k[:date] }
|
key_dates.sort_by { |k| k[:date] }
|
||||||
end
|
end
|
||||||
|
|
||||||
def to_json_archive(invoices, previous_file, last_checksum)
|
|
||||||
code_checksum = Checksum.code
|
|
||||||
ApplicationController.new.view_context.render(
|
|
||||||
partial: 'archive/accounting',
|
|
||||||
locals: {
|
|
||||||
invoices: invoices_with_vat(invoices),
|
|
||||||
period_total: period_total,
|
|
||||||
perpetual_total: perpetual_total,
|
|
||||||
period_footprint: footprint,
|
|
||||||
code_checksum: code_checksum,
|
|
||||||
last_archive_checksum: last_checksum,
|
|
||||||
previous_file: previous_file,
|
|
||||||
software_version: Version.current,
|
|
||||||
date: Time.now.iso8601
|
|
||||||
},
|
|
||||||
formats: [:json],
|
|
||||||
handlers: [:jbuilder]
|
|
||||||
)
|
|
||||||
end
|
|
||||||
|
|
||||||
def previous_period
|
|
||||||
AccountingPeriod.where('closed_at < ?', closed_at).order(closed_at: :desc).limit(1).last
|
|
||||||
end
|
|
||||||
|
|
||||||
def archive_closed_data
|
def archive_closed_data
|
||||||
data = invoices.includes(:invoice_items)
|
ArchiveWorker.perform_async(id)
|
||||||
previous_file = previous_period&.archive_file
|
|
||||||
last_archive_checksum = previous_file ? Checksum.file(previous_file) : nil
|
|
||||||
json_data = to_json_archive(data, previous_file, last_archive_checksum)
|
|
||||||
current_archive_checksum = Checksum.text(json_data)
|
|
||||||
date = DateTime.iso8601
|
|
||||||
chained = Checksum.text("#{current_archive_checksum}#{last_archive_checksum}#{date}")
|
|
||||||
|
|
||||||
Zip::OutputStream.open(archive_file) do |io|
|
|
||||||
io.put_next_entry(archive_json_file)
|
|
||||||
io.write(json_data)
|
|
||||||
io.put_next_entry('checksum.sha256')
|
|
||||||
io.write("#{current_archive_checksum}\t#{archive_json_file}")
|
|
||||||
io.put_next_entry('chained.sha256')
|
|
||||||
io.write("#{chained}\t#{date}")
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def price_without_taxe(invoice)
|
def price_without_taxe(invoice)
|
||||||
|
@ -16,6 +16,20 @@ class Coupon < ActiveRecord::Base
|
|||||||
validates_with CouponDiscountValidator
|
validates_with CouponDiscountValidator
|
||||||
validates_with CouponExpirationValidator
|
validates_with CouponExpirationValidator
|
||||||
|
|
||||||
|
scope :disabled, -> { where(active: false) }
|
||||||
|
scope :expired, -> { where('valid_until IS NOT NULL AND valid_until < ?', DateTime.now) }
|
||||||
|
scope :sold_out, lambda {
|
||||||
|
joins(:invoices).select('coupons.*, COUNT(invoices.id) as invoices_count').group('coupons.id')
|
||||||
|
.where.not(max_usages: nil).having('COUNT(invoices.id) >= coupons.max_usages')
|
||||||
|
}
|
||||||
|
scope :active, lambda {
|
||||||
|
joins('LEFT OUTER JOIN invoices ON invoices.coupon_id = coupons.id')
|
||||||
|
.select('coupons.*, COUNT(invoices.id) as invoices_count')
|
||||||
|
.group('coupons.id')
|
||||||
|
.where('active = true AND (valid_until IS NULL OR valid_until >= ?)', DateTime.now)
|
||||||
|
.having('COUNT(invoices.id) < coupons.max_usages OR coupons.max_usages IS NULL')
|
||||||
|
}
|
||||||
|
|
||||||
def safe_destroy
|
def safe_destroy
|
||||||
if invoices.size.zero?
|
if invoices.size.zero?
|
||||||
destroy
|
destroy
|
||||||
|
@ -1,3 +1,6 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
# Export is a reference to a file asynchronously generated by the system and downloadable by the user
|
||||||
class Export < ActiveRecord::Base
|
class Export < ActiveRecord::Base
|
||||||
require 'fileutils'
|
require 'fileutils'
|
||||||
|
|
||||||
@ -13,25 +16,26 @@ class Export < ActiveRecord::Base
|
|||||||
dir = "exports/#{category}/#{export_type}"
|
dir = "exports/#{category}/#{export_type}"
|
||||||
|
|
||||||
# create directories if they doesn't exists (exports & type & id)
|
# create directories if they doesn't exists (exports & type & id)
|
||||||
FileUtils::mkdir_p dir
|
FileUtils.mkdir_p dir
|
||||||
"#{dir}/#{self.filename}"
|
"#{dir}/#{filename}"
|
||||||
end
|
end
|
||||||
|
|
||||||
def filename
|
def filename
|
||||||
"#{export_type}-#{self.id}_#{self.created_at.strftime('%d%m%Y')}.xlsx"
|
"#{export_type}-#{id}_#{created_at.strftime('%d%m%Y')}.xlsx"
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def generate_and_send_export
|
def generate_and_send_export
|
||||||
case category
|
case category
|
||||||
when 'statistics'
|
when 'statistics'
|
||||||
StatisticsExportWorker.perform_async(self.id)
|
StatisticsExportWorker.perform_async(id)
|
||||||
when 'users'
|
when 'users'
|
||||||
UsersExportWorker.perform_async(self.id)
|
UsersExportWorker.perform_async(id)
|
||||||
when 'availabilities'
|
when 'availabilities'
|
||||||
AvailabilitiesExportWorker.perform_async(self.id)
|
AvailabilitiesExportWorker.perform_async(id)
|
||||||
else
|
else
|
||||||
raise NoMethodError, "Unknown export service for #{category}/#{export_type}"
|
raise NoMethodError, "Unknown export service for #{category}/#{export_type}"
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -44,6 +44,7 @@ class NotificationType
|
|||||||
notify_member_reservation_reminder
|
notify_member_reservation_reminder
|
||||||
notify_admin_free_disk_space
|
notify_admin_free_disk_space
|
||||||
notify_admin_close_period_reminder
|
notify_admin_close_period_reminder
|
||||||
|
notify_admin_archive_complete
|
||||||
]
|
]
|
||||||
# deprecated:
|
# deprecated:
|
||||||
# - notify_member_subscribed_plan_is_changed
|
# - notify_member_subscribed_plan_is_changed
|
||||||
|
14
app/validators/duration_validator.rb
Normal file
14
app/validators/duration_validator.rb
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
# 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
|
||||||
|
# 0.day means that (the_start == the_end), so it's a one day period
|
||||||
|
return if diff.days >= 0.day && diff.days <= 1.year
|
||||||
|
|
||||||
|
record.errors[:end_at] << I18n.t('errors.messages.invalid_duration', DAYS: diff)
|
||||||
|
end
|
||||||
|
end
|
12
app/validators/past_period_validator.rb
Normal file
12
app/validators/past_period_validator.rb
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
# Validates the current period is strictly in the past
|
||||||
|
class PastPeriodValidator < ActiveModel::Validator
|
||||||
|
def validate(record)
|
||||||
|
the_end = record.end_at
|
||||||
|
|
||||||
|
return if the_end.present? && the_end < Date.today
|
||||||
|
|
||||||
|
record.errors[:end_at] << I18n.t('errors.messages.must_be_in_the_past')
|
||||||
|
end
|
||||||
|
end
|
@ -6,4 +6,5 @@ json.array!(@accounting_periods) do |ap|
|
|||||||
json.perpetual_total ap.perpetual_total / 100.0
|
json.perpetual_total ap.perpetual_total / 100.0
|
||||||
json.chained_footprint ap.check_footprint
|
json.chained_footprint ap.check_footprint
|
||||||
json.user_name "#{ap.first_name} #{ap.last_name}"
|
json.user_name "#{ap.first_name} #{ap.last_name}"
|
||||||
|
json.archive_ready FileTest.exist?(ap.archive_file)
|
||||||
end
|
end
|
||||||
|
@ -1,3 +1,6 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
json.array!(@coupons) do |coupon|
|
json.array!(@coupons) do |coupon|
|
||||||
json.partial! 'api/coupons/coupon', coupon: coupon
|
json.partial! 'api/coupons/coupon', coupon: coupon
|
||||||
|
json.total @total
|
||||||
end
|
end
|
||||||
|
@ -12,6 +12,7 @@ if event.category
|
|||||||
json.category do
|
json.category do
|
||||||
json.id event.category.id
|
json.id event.category.id
|
||||||
json.name event.category.name
|
json.name event.category.name
|
||||||
|
json.slug event.category.slug
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
json.event_theme_ids event.event_theme_ids
|
json.event_theme_ids event.event_theme_ids
|
||||||
|
@ -0,0 +1,7 @@
|
|||||||
|
json.title notification.notification_type
|
||||||
|
json.description t('.archive_complete',
|
||||||
|
START: notification.attached_object.start_at,
|
||||||
|
END: notification.attached_object.end_at,
|
||||||
|
ID: notification.attached_object.id
|
||||||
|
)
|
||||||
|
json.url notification_url(notification, format: :json)
|
@ -0,0 +1,12 @@
|
|||||||
|
<%= render 'notifications_mailer/shared/hello', recipient: @recipient %>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
<%= t('.body.archive_complete', START: @attached_object.start_at, END: @attached_object.end_at) %>
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<%= t('.body.click_to_download') %>
|
||||||
|
<%=link_to( t('.body.here'), "#{root_url}api/accounting_periods/#{@attached_object.id}/archive", target: "_blank" )%>
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<%= t('.body.save_on_secured') %>
|
||||||
|
</p>
|
54
app/workers/archive_worker.rb
Normal file
54
app/workers/archive_worker.rb
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
# Will generate a ZIP archive file containing all invoicing data for the given period.
|
||||||
|
# This file will be asynchronously generated by sidekiq and a notification will be sent to the requesting user when it's done.
|
||||||
|
class ArchiveWorker
|
||||||
|
include Sidekiq::Worker
|
||||||
|
|
||||||
|
def perform(accounting_period_id)
|
||||||
|
period = AccountingPeriod.find(accounting_period_id)
|
||||||
|
|
||||||
|
data = period.invoices.includes(:invoice_items).order(id: :asc)
|
||||||
|
previous_file = period.previous_period&.archive_file
|
||||||
|
last_archive_checksum = previous_file ? Checksum.file(previous_file) : nil
|
||||||
|
json_data = to_json_archive(period, data, previous_file, last_archive_checksum)
|
||||||
|
current_archive_checksum = Checksum.text(json_data)
|
||||||
|
date = DateTime.iso8601
|
||||||
|
chained = Checksum.text("#{current_archive_checksum}#{last_archive_checksum}#{date}")
|
||||||
|
|
||||||
|
Zip::OutputStream.open(period.archive_file) do |io|
|
||||||
|
io.put_next_entry(period.archive_json_file)
|
||||||
|
io.write(json_data)
|
||||||
|
io.put_next_entry('checksum.sha256')
|
||||||
|
io.write("#{current_archive_checksum}\t#{period.archive_json_file}")
|
||||||
|
io.put_next_entry('chained.sha256')
|
||||||
|
io.write("#{chained}\t#{date}")
|
||||||
|
end
|
||||||
|
|
||||||
|
NotificationCenter.call type: :notify_admin_archive_complete,
|
||||||
|
receiver: User.find(period.closed_by),
|
||||||
|
attached_object: period
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def to_json_archive(period, invoices, previous_file, last_checksum)
|
||||||
|
code_checksum = Checksum.code
|
||||||
|
ApplicationController.new.view_context.render(
|
||||||
|
partial: 'archive/accounting',
|
||||||
|
locals: {
|
||||||
|
invoices: period.invoices_with_vat(invoices),
|
||||||
|
period_total: period.period_total,
|
||||||
|
perpetual_total: period.perpetual_total,
|
||||||
|
period_footprint: period.footprint,
|
||||||
|
code_checksum: code_checksum,
|
||||||
|
last_archive_checksum: last_checksum,
|
||||||
|
previous_file: previous_file,
|
||||||
|
software_version: Version.current,
|
||||||
|
date: Time.now.iso8601
|
||||||
|
},
|
||||||
|
formats: [:json],
|
||||||
|
handlers: [:jbuilder]
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
@ -1,27 +1,25 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
# Will generate an excel file containing all the users.
|
||||||
|
# This file will be asynchronously generated by sidekiq and a notification will be sent to the requesting user when it's done.
|
||||||
class UsersExportWorker
|
class UsersExportWorker
|
||||||
include Sidekiq::Worker
|
include Sidekiq::Worker
|
||||||
|
|
||||||
def perform(export_id)
|
def perform(export_id)
|
||||||
export = Export.find(export_id)
|
export = Export.find(export_id)
|
||||||
|
|
||||||
unless export.user.admin?
|
raise SecurityError, 'Not allowed to export' unless export.user.admin?
|
||||||
raise SecurityError, 'Not allowed to export'
|
raise KeyError, 'Wrong worker called' unless export.category == 'users'
|
||||||
end
|
|
||||||
|
|
||||||
unless export.category == 'users'
|
|
||||||
raise KeyError, 'Wrong worker called'
|
|
||||||
end
|
|
||||||
|
|
||||||
service = UsersExportService.new
|
service = UsersExportService.new
|
||||||
method_name = "export_#{export.export_type}"
|
method_name = "export_#{export.export_type}"
|
||||||
|
|
||||||
if %w(members subscriptions reservations).include?(export.export_type) and service.respond_to?(method_name)
|
return unless %w[members subscriptions reservations].include?(export.export_type) && service.respond_to?(method_name)
|
||||||
service.public_send(method_name, export)
|
|
||||||
|
|
||||||
NotificationCenter.call type: :notify_admin_export_complete,
|
service.public_send(method_name, export)
|
||||||
receiver: export.user,
|
|
||||||
attached_object: export
|
|
||||||
end
|
|
||||||
|
|
||||||
|
NotificationCenter.call type: :notify_admin_export_complete,
|
||||||
|
receiver: export.user,
|
||||||
|
attached_object: export
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -221,10 +221,12 @@ en:
|
|||||||
nb_of_usages: "Number of usages"
|
nb_of_usages: "Number of usages"
|
||||||
status: "Status"
|
status: "Status"
|
||||||
add_a_new_coupon: "Add a new coupon"
|
add_a_new_coupon: "Add a new coupon"
|
||||||
|
display_more_coupons: "Display the next coupons"
|
||||||
disabled: "Disabled"
|
disabled: "Disabled"
|
||||||
expired: "Expired"
|
expired: "Expired"
|
||||||
sold_out: "Sold out"
|
sold_out: "Sold out"
|
||||||
active: "Active"
|
active: "Active"
|
||||||
|
all: "Display all"
|
||||||
confirmation_required: "Confirmation required"
|
confirmation_required: "Confirmation required"
|
||||||
do_you_really_want_to_delete_this_coupon: "Do you really want to delete this coupon?"
|
do_you_really_want_to_delete_this_coupon: "Do you really want to delete this coupon?"
|
||||||
coupon_was_successfully_deleted: "Coupon was successfully deleted."
|
coupon_was_successfully_deleted: "Coupon was successfully deleted."
|
||||||
@ -421,8 +423,10 @@ en:
|
|||||||
perpetual_total: "Perpetual total"
|
perpetual_total: "Perpetual total"
|
||||||
integrity: "Integrity check"
|
integrity: "Integrity check"
|
||||||
confirmation_required: "Confirmation required"
|
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_START_END_closed_success: "The accounting period from {{START}} to {{END}} has been successfully closed"
|
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. 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"
|
failed_to_close_period: "An error occurred, unable to close the accounting period"
|
||||||
no_periods: "No closings for now"
|
no_periods: "No closings for now"
|
||||||
|
|
||||||
|
@ -221,10 +221,12 @@ es:
|
|||||||
nb_of_usages: "Número de usos"
|
nb_of_usages: "Número de usos"
|
||||||
status: "Estado"
|
status: "Estado"
|
||||||
add_a_new_coupon: "Añadir un nuevo cupón"
|
add_a_new_coupon: "Añadir un nuevo cupón"
|
||||||
|
display_more_coupons: "Display the next coupons" # translation_missing
|
||||||
disabled: "Desactivado"
|
disabled: "Desactivado"
|
||||||
expired: "Expirado"
|
expired: "Expirado"
|
||||||
sold_out: "Agotado"
|
sold_out: "Agotado"
|
||||||
active: "Activo"
|
active: "Activo"
|
||||||
|
all: "Display all" # translation_missing
|
||||||
confirmation_required: "Confirmación requerida"
|
confirmation_required: "Confirmación requerida"
|
||||||
do_you_really_want_to_delete_this_coupon: "¿Desea realmente eliminar este cupón?"
|
do_you_really_want_to_delete_this_coupon: "¿Desea realmente eliminar este cupón?"
|
||||||
coupon_was_successfully_deleted: "El cupón se eliminó correctamente."
|
coupon_was_successfully_deleted: "El cupón se eliminó correctamente."
|
||||||
@ -421,8 +423,10 @@ es:
|
|||||||
perpetual_total: "Perpetual total" # translation_missing
|
perpetual_total: "Perpetual total" # translation_missing
|
||||||
integrity: "Verificación de integridad"
|
integrity: "Verificación de integridad"
|
||||||
confirmation_required: "Confirmation required" # translation_missing
|
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_START_END_closed_success: "The accounting period from {{START}} to {{END}} has been successfully closed" # 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. 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
|
failed_to_close_period: "An error occurred, unable to close the accounting period" # translation_missing
|
||||||
no_periods: "No closings for now" # translation_missing
|
no_periods: "No closings for now" # translation_missing
|
||||||
|
|
||||||
|
@ -221,10 +221,12 @@ fr:
|
|||||||
nb_of_usages: "Nombre d'utilisations"
|
nb_of_usages: "Nombre d'utilisations"
|
||||||
status: "Statut"
|
status: "Statut"
|
||||||
add_a_new_coupon: "Ajouter un code promotionnel"
|
add_a_new_coupon: "Ajouter un code promotionnel"
|
||||||
|
display_more_coupons: "Afficher les codes suivants"
|
||||||
disabled: "Désactivé"
|
disabled: "Désactivé"
|
||||||
expired: "Expiré"
|
expired: "Expiré"
|
||||||
sold_out: "Épuisé"
|
sold_out: "Épuisé"
|
||||||
active: "Actif"
|
active: "Actif"
|
||||||
|
all: "Afficher tous"
|
||||||
confirmation_required: "Confirmation requise"
|
confirmation_required: "Confirmation requise"
|
||||||
do_you_really_want_to_delete_this_coupon: "Êtes-vous sûr(e) de vouloir supprimer ce code promotionnel ?"
|
do_you_really_want_to_delete_this_coupon: "Êtes-vous sûr(e) de vouloir supprimer ce code promotionnel ?"
|
||||||
coupon_was_successfully_deleted: "Le code promotionnel a bien été supprimé."
|
coupon_was_successfully_deleted: "Le code promotionnel a bien été supprimé."
|
||||||
@ -421,8 +423,10 @@ fr:
|
|||||||
perpetual_total: "Total perpétuel"
|
perpetual_total: "Total perpétuel"
|
||||||
integrity: "Contrôle d'intégrité"
|
integrity: "Contrôle d'intégrité"
|
||||||
confirmation_required: "Confirmation requise"
|
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_START_END_closed_success: "La période comptable du {{START}} au {{END}} a bien été clôturée"
|
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. 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"
|
failed_to_close_period: "Une erreur est survenue, impossible de clôturer la période comptable"
|
||||||
no_periods: "Aucune clôture pour le moment"
|
no_periods: "Aucune clôture pour le moment"
|
||||||
|
|
||||||
|
@ -221,10 +221,12 @@ pt:
|
|||||||
nb_of_usages: "Número de usos"
|
nb_of_usages: "Número de usos"
|
||||||
status: "Status"
|
status: "Status"
|
||||||
add_a_new_coupon: "Adicionar novo cupom"
|
add_a_new_coupon: "Adicionar novo cupom"
|
||||||
|
display_more_coupons: "Display the next coupons" # translation_missing
|
||||||
disabled: "Desabilitado"
|
disabled: "Desabilitado"
|
||||||
expired: "Expirado"
|
expired: "Expirado"
|
||||||
sold_out: "Esgotado"
|
sold_out: "Esgotado"
|
||||||
active: "Ativo"
|
active: "Ativo"
|
||||||
|
all: "Display all" # translation_missing
|
||||||
confirmation_required: "Confirmação obrigatória"
|
confirmation_required: "Confirmação obrigatória"
|
||||||
do_you_really_want_to_delete_this_coupon: "Você realmente deseja deletar este cupom?"
|
do_you_really_want_to_delete_this_coupon: "Você realmente deseja deletar este cupom?"
|
||||||
coupon_was_successfully_deleted: "O cupom foi deletado com sucesso."
|
coupon_was_successfully_deleted: "O cupom foi deletado com sucesso."
|
||||||
@ -421,8 +423,10 @@ pt:
|
|||||||
perpetual_total: "Perpetual total" # translation_missing
|
perpetual_total: "Perpetual total" # translation_missing
|
||||||
integrity: "Verificação de integridade"
|
integrity: "Verificação de integridade"
|
||||||
confirmation_required: "Confirmation required" # translation_missing
|
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_START_END_closed_success: "The accounting period from {{START}} to {{END}} has been successfully closed" # 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. 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
|
failed_to_close_period: "An error occurred, unable to close the accounting period" # translation_missing
|
||||||
no_periods: "No closings for now" # translation_missing
|
no_periods: "No closings for now" # translation_missing
|
||||||
|
|
||||||
|
@ -41,6 +41,8 @@ en:
|
|||||||
in_closed_period: "can't be within a closed accounting period"
|
in_closed_period: "can't be within a closed accounting period"
|
||||||
invalid_footprint: "invoice's checksum is invalid"
|
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}"
|
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."
|
||||||
|
must_be_in_the_past: "The period must be strictly prior to today's date."
|
||||||
|
|
||||||
activemodel:
|
activemodel:
|
||||||
errors:
|
errors:
|
||||||
@ -314,6 +316,8 @@ en:
|
|||||||
notify_admin_close_period_reminder:
|
notify_admin_close_period_reminder:
|
||||||
warning_last_closed_period_over_1_year: "Please remind to periodically close your accounting periods. Last closed period ended at %{LAST_END}"
|
warning_last_closed_period_over_1_year: "Please remind to periodically close your accounting periods. Last closed period ended at %{LAST_END}"
|
||||||
warning_no_closed_periods: "Please remind to periodically close your accounting periods. You have to close periods from %{FIRST_DATE}"
|
warning_no_closed_periods: "Please remind to periodically close your accounting periods. You have to close periods from %{FIRST_DATE}"
|
||||||
|
notify_admin_archive_complete:
|
||||||
|
archive_complete: "Data archiving from %{START} to %{END} is done. <a href='api/accounting_periods/%{ID}/archive' target='_blank'>click here to download</a>. Remember to save it on an external secured media."
|
||||||
|
|
||||||
statistics:
|
statistics:
|
||||||
# statistics tools for admins
|
# statistics tools for admins
|
||||||
|
@ -41,6 +41,8 @@ es:
|
|||||||
in_closed_period: "can't be within a closed accounting period" # missing translation
|
in_closed_period: "can't be within a closed accounting period" # missing translation
|
||||||
invalid_footprint: "invoice's checksum is invalid" # 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
|
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
|
||||||
|
must_be_in_the_past: "The period must be strictly prior to today's date." # missing translation
|
||||||
|
|
||||||
activemodel:
|
activemodel:
|
||||||
errors:
|
errors:
|
||||||
@ -314,6 +316,8 @@ es:
|
|||||||
notify_admin_close_period_reminder:
|
notify_admin_close_period_reminder:
|
||||||
warning_last_closed_period_over_1_year: "Please remind to periodically close your accounting periods. Last closed period ended at %{LAST_END}" # missing translation
|
warning_last_closed_period_over_1_year: "Please remind to periodically close your accounting periods. Last closed period ended at %{LAST_END}" # missing translation
|
||||||
warning_no_closed_periods: "Please remind to periodically close your accounting periods. You have to close periods from %{FIRST_DATE}" # missing translation
|
warning_no_closed_periods: "Please remind to periodically close your accounting periods. You have to close periods from %{FIRST_DATE}" # missing translation
|
||||||
|
notify_admin_archive_complete: # missing translation
|
||||||
|
archive_complete: "Data archiving from %{START} to %{END} is done. <a href='api/accounting_periods/%{ID}/archive' target='_blank'>click here to download</a>. Remember to save it on an external secured media." # missing translation
|
||||||
|
|
||||||
statistics:
|
statistics:
|
||||||
# statistics tools for admins
|
# statistics tools for admins
|
||||||
|
@ -41,6 +41,8 @@ fr:
|
|||||||
in_closed_period: "ne peut pas être dans une période comptable fermée"
|
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"
|
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}"
|
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."
|
||||||
|
must_be_in_the_past: "La période doit être strictement antérieure à la date du jour."
|
||||||
|
|
||||||
activemodel:
|
activemodel:
|
||||||
errors:
|
errors:
|
||||||
@ -314,6 +316,8 @@ fr:
|
|||||||
notify_admin_close_period_reminder:
|
notify_admin_close_period_reminder:
|
||||||
warning_last_closed_period_over_1_year: "Pensez à clôturer régulièrement vos périodes comptables. Les comptes sont actuellement clôturés jusqu'au %{LAST_END}"
|
warning_last_closed_period_over_1_year: "Pensez à clôturer régulièrement vos périodes comptables. Les comptes sont actuellement clôturés jusqu'au %{LAST_END}"
|
||||||
warning_no_closed_periods: "Pensez à clôturer régulièrement vos périodes comptables. Vous devez clôturer des périodes depuis le %{FIRST_DATE}"
|
warning_no_closed_periods: "Pensez à clôturer régulièrement vos périodes comptables. Vous devez clôturer des périodes depuis le %{FIRST_DATE}"
|
||||||
|
notify_admin_archive_complete:
|
||||||
|
archive_complete: "L'archivage des données du %{START} au %{END} est terminé. <a href='api/accounting_periods/%{ID}/archive' target='_blank'>Cliquez ici pour la télécharger</a>. Pensez à l'enregistrer sur un support externe sécurisé."
|
||||||
|
|
||||||
statistics:
|
statistics:
|
||||||
# outil de statistiques pour les administrateurs
|
# outil de statistiques pour les administrateurs
|
||||||
|
@ -286,5 +286,13 @@ en:
|
|||||||
warning_last_closed_period_over_1_year: "Please remind to periodically close your accounting periods. Last closed period ended at %{LAST_END}."
|
warning_last_closed_period_over_1_year: "Please remind to periodically close your accounting periods. Last closed period ended at %{LAST_END}."
|
||||||
warning_no_closed_periods: "Please remind to periodically close your accounting periods. You have to close periods from %{FIRST_DATE}."
|
warning_no_closed_periods: "Please remind to periodically close your accounting periods. You have to close periods from %{FIRST_DATE}."
|
||||||
|
|
||||||
|
notify_admin_archive_complete:
|
||||||
|
subject: "Archiving completed"
|
||||||
|
body:
|
||||||
|
archive_complete: "You have closed the accounting period from %{START} to %{END}. Archiving of data is now complete."
|
||||||
|
click_to_download: "To download the ZIP archive, click"
|
||||||
|
here: "here."
|
||||||
|
save_on_secured: "Remember that you must save this archive on a secured external support, which may be requested by the tax authorities during a check."
|
||||||
|
|
||||||
shared:
|
shared:
|
||||||
hello: "Hello %{user_name}"
|
hello: "Hello %{user_name}"
|
||||||
|
@ -285,5 +285,13 @@ es:
|
|||||||
warning_last_closed_period_over_1_year: "Please remind to periodically close your accounting periods. Last closed period ended at %{LAST_END}."
|
warning_last_closed_period_over_1_year: "Please remind to periodically close your accounting periods. Last closed period ended at %{LAST_END}."
|
||||||
warning_no_closed_periods: "Please remind to periodically close your accounting periods. You have to close periods from %{FIRST_DATE}."
|
warning_no_closed_periods: "Please remind to periodically close your accounting periods. You have to close periods from %{FIRST_DATE}."
|
||||||
|
|
||||||
|
notify_admin_archive_complete: #translation_missing
|
||||||
|
subject: "Archiving completed"
|
||||||
|
body:
|
||||||
|
archive_complete: "You have closed the accounting period from %{START} to %{END}. Archiving of data is now complete."
|
||||||
|
click_to_download: "To download the ZIP archive, click"
|
||||||
|
here: "here."
|
||||||
|
save_on_secured: "Remember that you must save this archive on a secured external support, which may be requested by the tax authorities during a check."
|
||||||
|
|
||||||
shared:
|
shared:
|
||||||
hello: "¡Hola %{user_name}!"
|
hello: "¡Hola %{user_name}!"
|
||||||
|
@ -286,5 +286,13 @@ fr:
|
|||||||
warning_last_closed_period_over_1_year: "Pensez à clôturer régulièrement vos périodes comptables. Les comptes sont actuellement clôturés jusqu'au %{LAST_END}."
|
warning_last_closed_period_over_1_year: "Pensez à clôturer régulièrement vos périodes comptables. Les comptes sont actuellement clôturés jusqu'au %{LAST_END}."
|
||||||
warning_no_closed_periods: "Pensez à clôturer régulièrement vos périodes comptables. Vous devez clôturer des périodes depuis le %{FIRST_DATE}."
|
warning_no_closed_periods: "Pensez à clôturer régulièrement vos périodes comptables. Vous devez clôturer des périodes depuis le %{FIRST_DATE}."
|
||||||
|
|
||||||
|
notify_admin_archive_complete:
|
||||||
|
subject: "Archivage terminé"
|
||||||
|
body:
|
||||||
|
archive_complete: "Vous avez clôturé la période comptable du %{START} au %{END}. L'archivage des données est maintenant terminé."
|
||||||
|
click_to_download: "Pour télécharger l'archive ZIP, cliquez"
|
||||||
|
here: "ici."
|
||||||
|
save_on_secured: "N'oubliez pas que vous devez obligatoirement enregistrer cette archive sur un support externe sécurisé, qui peut vous être demandé par l'administration fiscale lors d'un contrôle."
|
||||||
|
|
||||||
shared:
|
shared:
|
||||||
hello: "Bonjour %{user_name}"
|
hello: "Bonjour %{user_name}"
|
||||||
|
@ -286,5 +286,13 @@ pt:
|
|||||||
warning_last_closed_period_over_1_year: "Please remind to periodically close your accounting periods. Last closed period ended at %{LAST_END}."
|
warning_last_closed_period_over_1_year: "Please remind to periodically close your accounting periods. Last closed period ended at %{LAST_END}."
|
||||||
warning_no_closed_periods: "Please remind to periodically close your accounting periods. You have to close periods from %{FIRST_DATE}."
|
warning_no_closed_periods: "Please remind to periodically close your accounting periods. You have to close periods from %{FIRST_DATE}."
|
||||||
|
|
||||||
|
notify_admin_archive_complete: #translation_missing
|
||||||
|
subject: "Archiving completed"
|
||||||
|
body:
|
||||||
|
archive_complete: "You have closed the accounting period from %{START} to %{END}. Archiving of data is now complete."
|
||||||
|
click_to_download: "To download the ZIP archive, click"
|
||||||
|
here: "here."
|
||||||
|
save_on_secured: "Remember that you must save this archive on a secured external support, which may be requested by the tax authorities during a check."
|
||||||
|
|
||||||
shared:
|
shared:
|
||||||
hello: "Olá %{user_name}"
|
hello: "Olá %{user_name}"
|
||||||
|
@ -41,6 +41,8 @@ pt:
|
|||||||
in_closed_period: "can't be within a closed accounting period" # missing translation
|
in_closed_period: "can't be within a closed accounting period" # missing translation
|
||||||
invalid_footprint: "invoice's checksum is invalid" # 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
|
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
|
||||||
|
must_be_in_the_past: "The period must be strictly prior to today's date." # missing translation
|
||||||
|
|
||||||
activemodel:
|
activemodel:
|
||||||
errors:
|
errors:
|
||||||
@ -314,6 +316,8 @@ pt:
|
|||||||
notify_admin_close_period_reminder:
|
notify_admin_close_period_reminder:
|
||||||
warning_last_closed_period_over_1_year: "Please remind to periodically close your accounting periods. Last closed period ended at %{LAST_END}" # missing translation
|
warning_last_closed_period_over_1_year: "Please remind to periodically close your accounting periods. Last closed period ended at %{LAST_END}" # missing translation
|
||||||
warning_no_closed_periods: "Please remind to periodically close your accounting periods. You have to close periods from %{FIRST_DATE}" # missing translation
|
warning_no_closed_periods: "Please remind to periodically close your accounting periods. You have to close periods from %{FIRST_DATE}" # missing translation
|
||||||
|
notify_admin_archive_complete: # missing translation
|
||||||
|
archive_complete: "Data archiving from %{START} to %{END} is done. <a href='api/accounting_periods/%{ID}/archive' target='_blank'>click here to download</a>. Remember to save it on an external secured media." # missing translation
|
||||||
|
|
||||||
statistics:
|
statistics:
|
||||||
# statistics tools for admins
|
# statistics tools for admins
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "fab-manager",
|
"name": "fab-manager",
|
||||||
"version": "3.0.1",
|
"version": "3.1.0",
|
||||||
"description": "FabManager is the FabLab management solution. It is web-based, open-source and totally free.",
|
"description": "FabManager is the FabLab management solution. It is web-based, open-source and totally free.",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"fablab",
|
"fablab",
|
||||||
|
@ -30,42 +30,67 @@ class AccountingPeriodTest < ActionDispatch::IntegrationTest
|
|||||||
assert_dates_equal end_at.to_date, period[:end_at]
|
assert_dates_equal end_at.to_date, period[:end_at]
|
||||||
|
|
||||||
# Check archive file was created
|
# Check archive file was created
|
||||||
assert FileTest.exists? accounting_period.archive_file
|
assert_archive accounting_period
|
||||||
|
|
||||||
# Extract archive
|
|
||||||
require 'tmpdir'
|
|
||||||
require 'fileutils'
|
|
||||||
dest = "#{Dir.tmpdir}/accounting/#{accounting_period.id}"
|
|
||||||
FileUtils.mkdir_p "#{dest}/accounting"
|
|
||||||
Zip::File.open(accounting_period.archive_file) do |zip_file|
|
|
||||||
# Handle entries one by one
|
|
||||||
zip_file.each do |entry|
|
|
||||||
# Extract to file/directory/symlink
|
|
||||||
entry.extract("#{dest}/#{entry.name}")
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
# Check archive matches
|
|
||||||
require 'checksum'
|
|
||||||
sumfile = File.read("#{dest}/checksum.sha256").split("\t")
|
|
||||||
assert_equal sumfile[0], Checksum.file("#{dest}/#{sumfile[1]}"), 'archive checksum does not match'
|
|
||||||
|
|
||||||
archive = File.read("#{dest}/#{sumfile[1]}")
|
|
||||||
archive_json = JSON.parse(archive)
|
|
||||||
invoices = Invoice.where(
|
|
||||||
'created_at >= :start_date AND created_at <= :end_date',
|
|
||||||
start_date: start_at.to_datetime, end_date: end_at.to_datetime
|
|
||||||
)
|
|
||||||
|
|
||||||
assert_equal invoices.count, archive_json['invoices'].count
|
|
||||||
assert_equal accounting_period.footprint, archive_json['period_footprint']
|
|
||||||
|
|
||||||
require 'version'
|
|
||||||
assert_equal Version.current, archive_json['software']['version']
|
|
||||||
|
|
||||||
# we clean up the files before quitting
|
|
||||||
FileUtils.rm_rf(dest)
|
|
||||||
FileUtils.rm_rf(accounting_period.archive_folder)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test 'admin tries to close 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 close 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
|
||||||
|
|
||||||
|
test 'admin tries to close today' do
|
||||||
|
start_at = Date.today.beginning_of_day.iso8601
|
||||||
|
end_at = Date.today.end_of_day.iso8601
|
||||||
|
|
||||||
|
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.must_be_in_the_past')}/, response.body)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
@ -1,20 +1,22 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
require 'coveralls'
|
require 'coveralls'
|
||||||
Coveralls.wear!('rails')
|
Coveralls.wear!('rails')
|
||||||
|
|
||||||
ENV['RAILS_ENV'] ||= 'test'
|
ENV['RAILS_ENV'] ||= 'test'
|
||||||
require File.expand_path('../../config/environment', __FILE__)
|
require File.expand_path('../config/environment', __dir__)
|
||||||
require 'rails/test_help'
|
require 'rails/test_help'
|
||||||
require 'vcr'
|
require 'vcr'
|
||||||
require 'sidekiq/testing'
|
require 'sidekiq/testing'
|
||||||
require 'minitest/reporters'
|
require 'minitest/reporters'
|
||||||
|
|
||||||
VCR.configure do |config|
|
VCR.configure do |config|
|
||||||
config.cassette_library_dir = "test/vcr_cassettes"
|
config.cassette_library_dir = 'test/vcr_cassettes'
|
||||||
config.hook_into :webmock
|
config.hook_into :webmock
|
||||||
end
|
end
|
||||||
|
|
||||||
Sidekiq::Testing.fake!
|
Sidekiq::Testing.fake!
|
||||||
Minitest::Reporters.use! [Minitest::Reporters::DefaultReporter.new({ color: true })]
|
Minitest::Reporters.use! [Minitest::Reporters::DefaultReporter.new(color: true)]
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@ -33,30 +35,35 @@ class ActiveSupport::TestCase
|
|||||||
end
|
end
|
||||||
|
|
||||||
def stripe_card_token(error: nil)
|
def stripe_card_token(error: nil)
|
||||||
number = "4242424242424242"
|
number = '4242424242424242'
|
||||||
exp_month = 4
|
exp_month = 4
|
||||||
exp_year = DateTime.now.next_year.year
|
exp_year = DateTime.now.next_year.year
|
||||||
cvc = "314"
|
cvc = '314'
|
||||||
|
|
||||||
case error
|
case error
|
||||||
when /card_declined/
|
when /card_declined/
|
||||||
number = "4000000000000002"
|
number = '4000000000000002'
|
||||||
when /incorrect_number/
|
when /incorrect_number/
|
||||||
number = "4242424242424241"
|
number = '4242424242424241'
|
||||||
when /invalid_expiry_month/
|
when /invalid_expiry_month/
|
||||||
exp_month = 15
|
exp_month = 15
|
||||||
when /invalid_expiry_year/
|
when /invalid_expiry_year/
|
||||||
exp_year = 1964
|
exp_year = 1964
|
||||||
when /invalid_cvc/
|
when /invalid_cvc/
|
||||||
cvc = "99"
|
cvc = '99'
|
||||||
|
else
|
||||||
|
number = (rand * 100_000).floor
|
||||||
|
exp_year = (rand * 1000).floor
|
||||||
|
cvc = (rand * 100).floor
|
||||||
end
|
end
|
||||||
|
|
||||||
Stripe::Token.create(card: {
|
Stripe::Token.create(
|
||||||
number: number,
|
card: {
|
||||||
|
number: number,
|
||||||
exp_month: exp_month,
|
exp_month: exp_month,
|
||||||
exp_year: exp_year,
|
exp_year: exp_year,
|
||||||
cvc: cvc
|
cvc: cvc
|
||||||
},
|
}
|
||||||
).id
|
).id
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -85,13 +92,11 @@ class ActiveSupport::TestCase
|
|||||||
end
|
end
|
||||||
|
|
||||||
# check that the VAT was correctly applied if it was configured
|
# check that the VAT was correctly applied if it was configured
|
||||||
if line.include? I18n.t('invoices.including_total_excluding_taxes')
|
ht_amount = parse_amount_from_invoice_line(line) if line.include? I18n.t('invoices.including_total_excluding_taxes')
|
||||||
ht_amount = parse_amount_from_invoice_line(line)
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
if Setting.find_by(name: 'invoice_VAT-active').value == 'true'
|
if Setting.find_by(name: 'invoice_VAT-active').value == 'true'
|
||||||
vat_rate = Setting.find_by({name: 'invoice_VAT-rate'}).value.to_f
|
vat_rate = Setting.find_by(name: 'invoice_VAT-rate').value.to_f
|
||||||
computed_ht = sprintf('%.2f', (invoice.total / (vat_rate / 100 + 1)) / 100.0).to_f
|
computed_ht = sprintf('%.2f', (invoice.total / (vat_rate / 100 + 1)) / 100.0).to_f
|
||||||
|
|
||||||
assert_equal computed_ht, ht_amount, 'Total excluding taxes rendered in the PDF file is not computed correctly'
|
assert_equal computed_ht, ht_amount, 'Total excluding taxes rendered in the PDF file is not computed correctly'
|
||||||
@ -101,7 +106,6 @@ class ActiveSupport::TestCase
|
|||||||
File.delete(invoice.file)
|
File.delete(invoice.file)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
||||||
# Force the statistics export generation worker to run NOW and check the resulting file generated.
|
# Force the statistics export generation worker to run NOW and check the resulting file generated.
|
||||||
# Delete the file afterwards.
|
# Delete the file afterwards.
|
||||||
# @param export {Export}
|
# @param export {Export}
|
||||||
@ -120,6 +124,50 @@ class ActiveSupport::TestCase
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def assert_archive(accounting_period)
|
||||||
|
assert_not_nil accounting_period, 'AccountingPeriod was not created'
|
||||||
|
|
||||||
|
archive_worker = ArchiveWorker.new
|
||||||
|
archive_worker.perform(accounting_period.id)
|
||||||
|
|
||||||
|
assert FileTest.exist?(accounting_period.archive_file), 'ZIP archive was not generated'
|
||||||
|
|
||||||
|
# Extract archive
|
||||||
|
require 'tmpdir'
|
||||||
|
require 'fileutils'
|
||||||
|
dest = "#{Dir.tmpdir}/accounting/#{accounting_period.id}"
|
||||||
|
FileUtils.mkdir_p "#{dest}/accounting"
|
||||||
|
Zip::File.open(accounting_period.archive_file) do |zip_file|
|
||||||
|
# Handle entries one by one
|
||||||
|
zip_file.each do |entry|
|
||||||
|
# Extract to file/directory/symlink
|
||||||
|
entry.extract("#{dest}/#{entry.name}")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Check archive matches
|
||||||
|
require 'checksum'
|
||||||
|
sumfile = File.read("#{dest}/checksum.sha256").split("\t")
|
||||||
|
assert_equal sumfile[0], Checksum.file("#{dest}/#{sumfile[1]}"), 'archive checksum does not match'
|
||||||
|
|
||||||
|
archive = File.read("#{dest}/#{sumfile[1]}")
|
||||||
|
archive_json = JSON.parse(archive)
|
||||||
|
invoices = Invoice.where(
|
||||||
|
'created_at >= :start_date AND created_at <= :end_date',
|
||||||
|
start_date: accounting_period.start_at.to_datetime, end_date: accounting_period.end_at.to_datetime
|
||||||
|
)
|
||||||
|
|
||||||
|
assert_equal invoices.count, archive_json['invoices'].count
|
||||||
|
assert_equal accounting_period.footprint, archive_json['period_footprint']
|
||||||
|
|
||||||
|
require 'version'
|
||||||
|
assert_equal Version.current, archive_json['software']['version']
|
||||||
|
|
||||||
|
# we clean up the files before quitting
|
||||||
|
FileUtils.rm_rf(dest)
|
||||||
|
FileUtils.rm_rf(accounting_period.archive_folder)
|
||||||
|
end
|
||||||
|
|
||||||
def assert_dates_equal(expected, actual, msg = nil)
|
def assert_dates_equal(expected, actual, msg = nil)
|
||||||
assert_not_nil actual, msg
|
assert_not_nil actual, msg
|
||||||
assert_equal expected.to_date, actual.to_date, msg
|
assert_equal expected.to_date, actual.to_date, msg
|
||||||
|
Loading…
x
Reference in New Issue
Block a user