mirror of
https://github.com/LaCasemate/fab-manager.git
synced 2024-11-28 09:24:24 +01:00
Merge branch 'dev' into feature/docker-alpine-image
This commit is contained in:
commit
8853465365
@ -12,6 +12,7 @@ postgresql
|
||||
elasticsearch
|
||||
redis
|
||||
|
||||
|
||||
# Ignore public assets
|
||||
public/uploads
|
||||
public/assets
|
||||
|
@ -1,9 +1,9 @@
|
||||
Metrics/LineLength:
|
||||
Max: 140
|
||||
Metrics/MethodLength:
|
||||
Max: 30
|
||||
Max: 35
|
||||
Metrics/CyclomaticComplexity:
|
||||
Max: 9
|
||||
Max: 13
|
||||
Metrics/PerceivedComplexity:
|
||||
Max: 9
|
||||
Metrics/AbcSize:
|
||||
@ -16,6 +16,8 @@ Metrics/BlockLength:
|
||||
- 'lib/tasks/**/*.rake'
|
||||
- 'config/routes.rb'
|
||||
- 'app/pdfs/pdf/*.rb'
|
||||
Metrics/ParameterLists:
|
||||
CountKeywordArgs: false
|
||||
Style/BracesAroundHashParameters:
|
||||
EnforcedStyle: context_dependent
|
||||
Style/RegexpLiteral:
|
||||
|
14
CHANGELOG.md
14
CHANGELOG.md
@ -1,10 +1,18 @@
|
||||
# Changelog Fab Manager
|
||||
|
||||
- Ability to configure and export the accounting data to the ACD accounting software
|
||||
- Compute the VAT per item in each invoices, instead of globally
|
||||
- Use Alpine Linux to build the Docker image (#147)
|
||||
- Fix a bug: invoices with total = 0, are marked as paid on site even if paid by card
|
||||
- Fix a bug: after disabling a group, its associated plans are hidden from the interface
|
||||
- Fix a bug: in case of unexpected server error during stripe payment process, the confirm button is not unlocked
|
||||
- [TODO DEPLOY] `rake db:migrate`
|
||||
|
||||
## v4.1.1 2019 september 20
|
||||
|
||||
- fix a bug: api/reservations#index was using user_id instead of statistic_profile_id
|
||||
- fix a bug: event_service#date_range method, test on all_day was never truthy
|
||||
- fix a bug: sidekiq 5 does not have delay_for method anymore, uses perform_in instead
|
||||
- Fix a bug: api/reservations#index was using user_id instead of statistic_profile_id
|
||||
- Fix a bug: event_service#date_range method, test on all_day was never truthy
|
||||
- Fix a bug: sidekiq 5 does not have delay_for method anymore, uses perform_in instead
|
||||
|
||||
## v4.1.0 2019 September 12
|
||||
|
||||
|
2
Procfile
2
Procfile
@ -1,3 +1,3 @@
|
||||
web: bundle exec rails server puma -p $PORT -b0.0.0.0
|
||||
worker: bundle exec sidekiq -C ./config/sidekiq.yml
|
||||
mail: bundle exec mailcatcher --foreground
|
||||
mail: bundle exec mailcatcher --foreground --http-ip=0.0.0.0
|
||||
|
@ -76,6 +76,94 @@ Application.Controllers.controller('InvoicesController', ['$scope', '$state', 'I
|
||||
}
|
||||
};
|
||||
|
||||
// Accounting codes
|
||||
$scope.settings = {
|
||||
journalCode: {
|
||||
name: 'accounting_journal_code',
|
||||
value: settings['accounting_journal_code']
|
||||
},
|
||||
cardClientCode: {
|
||||
name: 'accounting_card_client_code',
|
||||
value: settings['accounting_card_client_code']
|
||||
},
|
||||
cardClientLabel: {
|
||||
name: 'accounting_card_client_label',
|
||||
value: settings['accounting_card_client_label']
|
||||
},
|
||||
walletClientCode: {
|
||||
name: 'accounting_wallet_client_code',
|
||||
value: settings['accounting_wallet_client_code']
|
||||
},
|
||||
walletClientLabel: {
|
||||
name: 'accounting_wallet_client_label',
|
||||
value: settings['accounting_wallet_client_label']
|
||||
},
|
||||
otherClientCode: {
|
||||
name: 'accounting_other_client_code',
|
||||
value: settings['accounting_other_client_code']
|
||||
},
|
||||
otherClientLabel: {
|
||||
name: 'accounting_other_client_label',
|
||||
value: settings['accounting_other_client_label']
|
||||
},
|
||||
walletCode: {
|
||||
name: 'accounting_wallet_code',
|
||||
value: settings['accounting_wallet_code']
|
||||
},
|
||||
walletLabel: {
|
||||
name: 'accounting_wallet_label',
|
||||
value: settings['accounting_wallet_label']
|
||||
},
|
||||
vatCode: {
|
||||
name: 'accounting_VAT_code',
|
||||
value: settings['accounting_VAT_code']
|
||||
},
|
||||
vatLabel: {
|
||||
name: 'accounting_VAT_label',
|
||||
value: settings['accounting_VAT_label']
|
||||
},
|
||||
subscriptionCode: {
|
||||
name: 'accounting_subscription_code',
|
||||
value: settings['accounting_subscription_code']
|
||||
},
|
||||
subscriptionLabel: {
|
||||
name: 'accounting_subscription_label',
|
||||
value: settings['accounting_subscription_label']
|
||||
},
|
||||
machineCode: {
|
||||
name: 'accounting_Machine_code',
|
||||
value: settings['accounting_Machine_code']
|
||||
},
|
||||
machineLabel: {
|
||||
name: 'accounting_Machine_label',
|
||||
value: settings['accounting_Machine_label']
|
||||
},
|
||||
trainingCode: {
|
||||
name: 'accounting_Training_code',
|
||||
value: settings['accounting_Training_code']
|
||||
},
|
||||
trainingLabel: {
|
||||
name: 'accounting_Training_label',
|
||||
value: settings['accounting_Training_label']
|
||||
},
|
||||
eventCode: {
|
||||
name: 'accounting_Event_code',
|
||||
value: settings['accounting_Event_code']
|
||||
},
|
||||
eventLabel: {
|
||||
name: 'accounting_Event_label',
|
||||
value: settings['accounting_Event_label']
|
||||
},
|
||||
spaceCode: {
|
||||
name: 'accounting_Space_code',
|
||||
value: settings['accounting_Space_code']
|
||||
},
|
||||
spaceLabel: {
|
||||
name: 'accounting_Space_label',
|
||||
value: settings['accounting_Space_label']
|
||||
}
|
||||
};
|
||||
|
||||
// Placeholding date for the invoice creation
|
||||
$scope.today = moment();
|
||||
|
||||
@ -325,9 +413,7 @@ Application.Controllers.controller('InvoicesController', ['$scope', '$state', 'I
|
||||
$scope.history.push({ date: rate.created_at, rate: rate.value, user: rate.user })
|
||||
});
|
||||
activeHistory.setting.history.forEach(function (v) {
|
||||
if (v.value === 'false') {
|
||||
$scope.history.push({ date: v.created_at, rate: 0, user: v.user })
|
||||
}
|
||||
$scope.history.push({ date: v.created_at, enabled: v.value === 'true', user: v.user })
|
||||
});
|
||||
}
|
||||
|
||||
@ -432,6 +518,14 @@ Application.Controllers.controller('InvoicesController', ['$scope', '$state', 'I
|
||||
});
|
||||
}
|
||||
|
||||
$scope.toggleExportModal = function() {
|
||||
$uibModal.open({
|
||||
templateUrl: '<%= asset_path "admin/invoices/accountingExportModal.html" %>',
|
||||
controller: 'AccountingExportModalController',
|
||||
size: 'xl'
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Test if the given date is within a closed accounting period
|
||||
* @param date {Date} date to test
|
||||
@ -446,6 +540,20 @@ Application.Controllers.controller('InvoicesController', ['$scope', '$state', 'I
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback to bulk save all settings in the page to the database with their values
|
||||
*/
|
||||
$scope.save = function() {
|
||||
Setting.bulkUpdate(
|
||||
{ settings: Object.values($scope.settings) },
|
||||
function () { growl.success(_t('invoices.codes_customization_success')); },
|
||||
function (error) {
|
||||
growl.error('unexpected_error_occurred');
|
||||
console.error(error);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/* PRIVATE SCOPE */
|
||||
|
||||
/**
|
||||
@ -791,7 +899,7 @@ Application.Controllers.controller('ClosePeriodModalController', ['$scope', '$ui
|
||||
};
|
||||
|
||||
/**
|
||||
* Cancel the refund, dismiss the modal window
|
||||
* Just dismiss the modal window
|
||||
*/
|
||||
$scope.cancel = function () { $uibModalInstance.dismiss('cancel'); };
|
||||
|
||||
@ -803,3 +911,136 @@ Application.Controllers.controller('ClosePeriodModalController', ['$scope', '$ui
|
||||
}
|
||||
}
|
||||
]);
|
||||
|
||||
Application.Controllers.controller('AccountingExportModalController', ['$scope', '$uibModalInstance', 'Invoice', 'Export', 'CSRF', 'growl', '_t',
|
||||
function ($scope, $uibModalInstance, Invoice, Export, CSRF, growl, _t) {
|
||||
// Retrieve Anti-CSRF tokens from cookies
|
||||
CSRF.setMetaTags();
|
||||
|
||||
const SETTINGS = {
|
||||
acd: {
|
||||
format: 'csv',
|
||||
encoding: 'ISO-8859-1',
|
||||
separator: ';',
|
||||
dateFormat: '%d/%m/%Y',
|
||||
labelMaxLength: 50,
|
||||
decimalSeparator: ',',
|
||||
exportInvoicesAtZero: false,
|
||||
columns: ['journal_code', 'date', 'account_code', 'account_label', 'piece', 'line_label', 'debit_origin', 'credit_origin', 'debit_euro', 'credit_euro', 'lettering']
|
||||
}
|
||||
};
|
||||
|
||||
/* PUBLIC SCOPE */
|
||||
|
||||
// API URL where the form will be posted
|
||||
$scope.actionUrl = '/api/accounting/export';
|
||||
|
||||
// Form action on the above URL
|
||||
$scope.method = 'post';
|
||||
|
||||
// Anti-CSRF token to inject into the download form
|
||||
$scope.csrfToken = angular.element('meta[name="csrf-token"]')[0].content;
|
||||
|
||||
// API request body to generate the export
|
||||
$scope.query = null;
|
||||
|
||||
// binding to radio button "export to"
|
||||
$scope.exportTarget = {
|
||||
software: null,
|
||||
startDate: null,
|
||||
endDate: null,
|
||||
settings: null
|
||||
};
|
||||
|
||||
// AngularUI-Bootstrap datepicker parameters to define export dates range
|
||||
$scope.datePicker = {
|
||||
format: Fablab.uibDateFormat,
|
||||
opened: { // default: datePickers are not shown
|
||||
start: false,
|
||||
end: false
|
||||
},
|
||||
options: {
|
||||
startingDay: Fablab.weekStartingDay
|
||||
}
|
||||
};
|
||||
|
||||
// Date of the first invoice
|
||||
$scope.firstInvoice = null;
|
||||
|
||||
/**
|
||||
* Validate the export
|
||||
*/
|
||||
$scope.ok = function () {
|
||||
const statusQry = mkQuery();
|
||||
$scope.query = statusQry;
|
||||
|
||||
Export.status(statusQry).then(function (res) {
|
||||
if (!res.data.exists) {
|
||||
growl.success(_t('invoices.export_is_running'));
|
||||
}
|
||||
$uibModalInstance.close(res);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Callback to open/close one of the datepickers
|
||||
* @param event {Object} see https://docs.angularjs.org/guide/expression#-event-
|
||||
* @param picker {string} start | end
|
||||
*/
|
||||
$scope.toggleDatePicker = function(event, picker) {
|
||||
event.preventDefault();
|
||||
$scope.datePicker.opened[picker] = !$scope.datePicker.opened[picker];
|
||||
};
|
||||
|
||||
/**
|
||||
* Will fill the export settings, according to the selected software
|
||||
*/
|
||||
$scope.fillSettings = function() {
|
||||
$scope.exportTarget.settings = SETTINGS[$scope.exportTarget.software];
|
||||
};
|
||||
|
||||
/**
|
||||
* Just dismiss the modal window
|
||||
*/
|
||||
$scope.cancel = function () { $uibModalInstance.dismiss('cancel'); };
|
||||
|
||||
/* PRIVATE SCOPE */
|
||||
|
||||
/**
|
||||
* Kind of constructor: these actions will be realized first when the controller is loaded
|
||||
*/
|
||||
const initialize = function () {
|
||||
// if the invoice was payed with stripe, allow to refund through stripe
|
||||
Invoice.first(function (data) {
|
||||
$scope.firstInvoice = data.date;
|
||||
$scope.exportTarget.startDate = data.date;
|
||||
$scope.exportTarget.endDate = moment().toISOString();
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Prepare the query for the export API
|
||||
* @returns {{extension: *, query: *, category: string, type: *, key: *}}
|
||||
*/
|
||||
const mkQuery = function() {
|
||||
return {
|
||||
category: 'accounting',
|
||||
type: $scope.exportTarget.software,
|
||||
extension: $scope.exportTarget.settings.format,
|
||||
key: $scope.exportTarget.settings.separator,
|
||||
query: JSON.stringify({
|
||||
columns: $scope.exportTarget.settings.columns,
|
||||
encoding: $scope.exportTarget.settings.encoding,
|
||||
date_format: $scope.exportTarget.settings.dateFormat,
|
||||
start_date: $scope.exportTarget.startDate,
|
||||
end_date: $scope.exportTarget.endDate,
|
||||
label_max_length: $scope.exportTarget.settings.labelMaxLength,
|
||||
decimal_separator: $scope.exportTarget.settings.decimalSeparator,
|
||||
export_invoices_at_zero: $scope.exportTarget.settings.exportInvoicesAtZero
|
||||
})
|
||||
};
|
||||
}
|
||||
|
||||
// !!! MUST BE CALLED AT THE END of the controller
|
||||
return initialize();
|
||||
}]);
|
||||
|
@ -63,7 +63,7 @@ Application.Directives.directive('stripeForm', ['Payment', 'growl', '_t',
|
||||
Payment.confirm({ payment_method_id: paymentMethod.id, cart_items: $scope.cartItems }, function (response) {
|
||||
// Handle server response (see Step 3)
|
||||
handleServerResponse(response, button);
|
||||
}, function(error) { handleServerResponse({ error }) });
|
||||
}, function(error) { handleServerResponse({ error }, button) });
|
||||
}
|
||||
});
|
||||
});
|
||||
@ -89,7 +89,7 @@ Application.Directives.directive('stripeForm', ['Payment', 'growl', '_t',
|
||||
// The PaymentIntent can be confirmed again on the server
|
||||
Payment.confirm({ payment_intent_id: result.paymentIntent.id, cart_items: $scope.cartItems }, function(confirmResult) {
|
||||
handleServerResponse(confirmResult, confirmButton);
|
||||
}, function(error) { handleServerResponse({ error }) });
|
||||
}, function(error) { handleServerResponse({ error }, confirmButton) });
|
||||
}
|
||||
});
|
||||
} else {
|
||||
|
@ -899,15 +899,13 @@ angular.module('application.router', ['ui.router'])
|
||||
resolve: {
|
||||
settings: ['Setting', function (Setting) {
|
||||
return Setting.query({
|
||||
names: `['invoice_legals', \
|
||||
'invoice_text', \
|
||||
'invoice_VAT-rate', \
|
||||
'invoice_VAT-active', \
|
||||
'invoice_order-nb', \
|
||||
'invoice_code-value', \
|
||||
'invoice_code-active', \
|
||||
'invoice_reference', \
|
||||
'invoice_logo']` }).$promise;
|
||||
names: `['invoice_legals', 'invoice_text', 'invoice_VAT-rate', 'invoice_VAT-active', 'invoice_order-nb', 'invoice_code-value', \
|
||||
'invoice_code-active', 'invoice_reference', 'invoice_logo', 'accounting_journal_code', 'accounting_card_client_code', \
|
||||
'accounting_card_client_label', 'accounting_wallet_client_code', 'accounting_wallet_client_label', \
|
||||
'accounting_other_client_code', 'accounting_other_client_label', 'accounting_wallet_code', 'accounting_wallet_label', \
|
||||
'accounting_VAT_code', 'accounting_VAT_label', 'accounting_subscription_code', 'accounting_subscription_label', \
|
||||
'accounting_Machine_code', 'accounting_Machine_label', 'accounting_Training_code', 'accounting_Training_label', \
|
||||
'accounting_Event_code', 'accounting_Event_label', 'accounting_Space_code', 'accounting_Space_label']` }).$promise;
|
||||
}],
|
||||
invoices: [ 'Invoice', function (Invoice) {
|
||||
return Invoice.list({
|
||||
|
@ -10,6 +10,10 @@ Application.Services.factory('Invoice', ['$resource', function ($resource) {
|
||||
url: '/api/invoices/list',
|
||||
method: 'POST',
|
||||
isArray: true
|
||||
},
|
||||
first: {
|
||||
url: '/api/invoices/first',
|
||||
method: 'GET'
|
||||
}
|
||||
}
|
||||
);
|
||||
|
@ -9,6 +9,10 @@ Application.Services.factory('Setting', ['$resource', function ($resource) {
|
||||
return angular.toJson({ setting: data });
|
||||
}
|
||||
},
|
||||
bulkUpdate: {
|
||||
url: '/api/settings/bulk_update',
|
||||
method: 'PATCH'
|
||||
},
|
||||
query: {
|
||||
isArray: false
|
||||
}
|
||||
|
@ -65,6 +65,10 @@
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.modal-xl {
|
||||
width: 900px;
|
||||
}
|
||||
|
||||
// component card
|
||||
.card {
|
||||
position: relative;
|
||||
|
@ -276,3 +276,31 @@ table.scrollable-3-cols {
|
||||
input.form-control.as-writable {
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
.accounting-codes .row {
|
||||
margin-top: 2rem;
|
||||
|
||||
button {
|
||||
margin-top: 1em;
|
||||
}
|
||||
}
|
||||
|
||||
table.export-table-template {
|
||||
margin-top: 10px;
|
||||
|
||||
thead td {
|
||||
width: 20px;
|
||||
background-color: #227447;
|
||||
color: white;
|
||||
border-bottom: 2px solid black;
|
||||
font-size: 13px;
|
||||
font-weight: bold;
|
||||
padding: 10px 5px;
|
||||
line-height: 12px;
|
||||
}
|
||||
|
||||
tbody td {
|
||||
border-right: 1px solid #d4d4d4;
|
||||
height: 30px;
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,97 @@
|
||||
<div class="modal-header">
|
||||
<h3 class="text-center red" translate>{{ 'invoices.export_accounting_data' }}</h3>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form role="form" name="exportForm">
|
||||
<div class="row">
|
||||
<div class="form-group col-md-6">
|
||||
<label for="start_date" translate>{{ 'invoices.export_form_date' }}</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-addon"><i class="fa fa-calendar"></i></span>
|
||||
<input type="text"
|
||||
class="form-control"
|
||||
name="start_date"
|
||||
id="start_date"
|
||||
ng-model="exportTarget.startDate"
|
||||
uib-datepicker-popup="{{datePicker.format}}"
|
||||
datepicker-options="datePicker.options"
|
||||
is-open="datePicker.opened.start"
|
||||
min-date="firstInvoice"
|
||||
placeholder="{{datePicker.format}}"
|
||||
ng-click="toggleDatePicker($event, 'start')"
|
||||
required/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group col-md-6">
|
||||
<label for="end_date" translate>{{ 'invoices.export_to_date' }}</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-addon"><i class="fa fa-calendar"></i></span>
|
||||
<input type="text"
|
||||
class="form-control"
|
||||
name="end_date"
|
||||
id="end_date"
|
||||
ng-model="exportTarget.endDate"
|
||||
uib-datepicker-popup="{{datePicker.format}}"
|
||||
datepicker-options="datePicker.options"
|
||||
is-open="datePicker.opened.end"
|
||||
min-date="exportTarget.startDate || firstInvoice"
|
||||
placeholder="{{datePicker.format}}"
|
||||
ng-click="toggleDatePicker($event, 'end')"
|
||||
required/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<h4 class="control-label m-l" translate>{{ 'invoices.export_to' }}</h4>
|
||||
<div class="form-group m-l-lg">
|
||||
<label for="acd">
|
||||
<input type="radio" name="acd" id="acd" ng-model="exportTarget.software" ng-value="'acd'" ng-click="fillSettings()" required/>
|
||||
{{ 'invoices.acd' | translate }}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row" ng-show="exportTarget.settings">
|
||||
<div class="col-md-4 font-bold" translate>{{ 'invoices.format' }}</div>
|
||||
<div class="col-md-8">{{ exportTarget.settings.format }}</div>
|
||||
<div class="col-md-4 font-bold" translate>{{ 'invoices.encoding' }}</div>
|
||||
<div class="col-md-8">{{ exportTarget.settings.encoding }}</div>
|
||||
<div class="col-md-4 font-bold" translate>{{ 'invoices.separator' }}</div>
|
||||
<div class="col-md-8">{{ exportTarget.settings.separator }}</div>
|
||||
<div class="col-md-4 font-bold" translate>{{ 'invoices.dateFormat' }}</div>
|
||||
<div class="col-md-8">
|
||||
<a href="https://apidock.com/ruby/DateTime/strftime" class="help-cursor" target="_blank">{{ exportTarget.settings.dateFormat }}</a>
|
||||
</div>
|
||||
<div class="col-md-4 font-bold" translate>{{ 'invoices.labelMaxLength' }}</div>
|
||||
<div class="col-md-8">{{ exportTarget.settings.labelMaxLength }}</div>
|
||||
<div class="col-md-4 font-bold" translate>{{ 'invoices.decimalSeparator' }}</div>
|
||||
<div class="col-md-8">{{ exportTarget.settings.decimalSeparator }}</div>
|
||||
<div class="col-md-4 font-bold" translate>{{ 'invoices.exportInvoicesAtZero' }}</div>
|
||||
<div class="col-md-8" translate>{{ exportTarget.settings.exportInvoicesAtZero ? 'yes' : 'no' }}</div>
|
||||
<div class="col-md-4 font-bold" translate>{{ 'invoices.columns' }}</div>
|
||||
<table class="col-md-12 export-table-template">
|
||||
<thead>
|
||||
<tr>
|
||||
<td ng-repeat="column in exportTarget.settings.columns" translate>{{ 'invoices.exportColumns.' + column }}</td>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td ng-repeat="column in exportTarget.settings.columns"></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<form role="form" ng-submit="ok()" name="exportFormParams" method="post" action="{{ actionUrl }}" class="inline">
|
||||
<input name="authenticity_token" type="hidden" ng-value="csrfToken"/>
|
||||
<input name="_method" type="hidden" ng-value="method"/>
|
||||
<input name="extension" type="hidden" ng-value="query.extension"/>
|
||||
<input name="type" type="hidden" ng-value="exportTarget.software"/>
|
||||
<input name="key" type="hidden" ng-value="query.key"/>
|
||||
<input name="query" type="hidden" ng-value="query.query"/>
|
||||
<input type="submit" class="btn btn-warning" value="{{ 'confirm' | translate }}" formtarget="export-frame"/>
|
||||
</form>
|
||||
<button class="btn btn-default" ng-click="cancel()" translate>{{ 'cancel' }}</button>
|
||||
</div>
|
@ -23,7 +23,7 @@
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label translate>{{ 'invoices.refund_mode' }}</label>
|
||||
<select class="form-control m-t-sm" name="avoir_mode" ng-model="avoir.avoir_mode" ng-options="mode.value as mode.name for mode in avoirModes" required></select>
|
||||
<select class="form-control m-t-sm" name="payment_method" ng-model="avoir.payment_method" ng-options="mode.value as mode.name for mode in avoirModes" required></select>
|
||||
</div>
|
||||
<div class="form-group" ng-if="invoice.is_subscription_invoice">
|
||||
<label translate>{{ 'invoices.do_you_want_to_disable_the_user_s_subscription' }}</label>
|
||||
|
@ -12,6 +12,8 @@
|
||||
</div>
|
||||
<div class="col-xs-12 col-sm-12 col-md-3 b-t hide-b-md">
|
||||
<section class="heading-actions wrapper">
|
||||
<a class="btn btn-default rounded m-t-sm" ng-click="toggleExportModal()"><i class="fa fa-book"></i></a>
|
||||
<iframe name="export-frame" height="0" width="0" class="none" id="accounting-export-frame"></iframe>
|
||||
<a class="btn btn-lg btn-default rounded m-t-sm text-sm" ng-click="closeAnAccountingPeriod()"><i class="fa fa-calendar-check-o"></i> {{ 'invoices.accounting_periods' | translate }}</a>
|
||||
</section>
|
||||
</div>
|
||||
@ -116,7 +118,7 @@
|
||||
|
||||
<uib-tab heading="{{ 'invoices.invoicing_settings' | translate }}">
|
||||
<form class="invoice-placeholder">
|
||||
<div class="invoice-logo" style="background-image: url({{invoice.logo}});">
|
||||
<div class="invoice-logo">
|
||||
<img src="data:image/png;base64," data-src="holder.js/100%x100%/text:/font:FontAwesome/icon" bs-holder ng-if="!invoice.logo" class="img-responsive">
|
||||
<img base-sixty-four-image="invoice.logo" ng-if="invoice.logo && invoice.logo.base64">
|
||||
<div class="tools-box">
|
||||
@ -201,6 +203,123 @@
|
||||
</div>
|
||||
</form>
|
||||
</uib-tab>
|
||||
|
||||
|
||||
|
||||
|
||||
<uib-tab heading="{{ 'invoices.accounting_codes' | translate }}">
|
||||
<div class="panel panel-default m-t-md accounting-codes">
|
||||
<div class="panel-body">
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<label for="journalCode" translate>{{ 'invoices.accounting_journal_code' }}</label>
|
||||
<input type="text" id="journalCode" ng-model="settings.journalCode.value" class="form-control" placeholder="{{ 'invoices.general_journal_code' | translate }}"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<label for="cardClientCode" translate>{{ 'invoices.accounting_card_client_code' }}</label>
|
||||
<input type="text" id="cardClientCode" ng-model="settings.cardClientCode.value" class="form-control" placeholder="{{ 'invoices.card_client_code' | translate }}" />
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label for="cardClientLabel" translate>{{ 'invoices.accounting_card_client_label' }}</label>
|
||||
<input type="text" id="cardClientLabel" ng-model="settings.cardClientLabel.value" class="form-control" placeholder="{{ 'invoices.card_client_label' | translate }}"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<label for="walletClientCode" translate>{{ 'invoices.accounting_wallet_client_code' }}</label>
|
||||
<input type="text" id="walletClientCode" ng-model="settings.walletClientCode.value" class="form-control" placeholder="{{ 'invoices.wallet_client_code' | translate }}" />
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label for="walletClientLabel" translate>{{ 'invoices.accounting_wallet_client_label' }}</label>
|
||||
<input type="text" id="walletClientLabel" ng-model="settings.walletClientLabel.value" class="form-control" placeholder="{{ 'invoices.wallet_client_label' | translate }}"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<label for="otherClientCode" translate>{{ 'invoices.accounting_other_client_code' }}</label>
|
||||
<input type="text" id="otherClientCode" ng-model="settings.otherClientCode.value" class="form-control" placeholder="{{ 'invoices.other_client_code' | translate }}" />
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label for="otherClientLabel" translate>{{ 'invoices.accounting_other_client_label' }}</label>
|
||||
<input type="text" id="otherClientLabel" ng-model="settings.otherClientLabel.value" class="form-control" placeholder="{{ 'invoices.other_client_label' | translate }}"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<label for="walletCode" translate>{{ 'invoices.accounting_wallet_code' }}</label>
|
||||
<input type="text" id="walletCode" ng-model="settings.walletCode.value" class="form-control" placeholder="{{ 'invoices.general_wallet_code' | translate }}" />
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label for="walletLabel" translate>{{ 'invoices.accounting_wallet_label' }}</label>
|
||||
<input type="text" id="walletLabel" ng-model="settings.walletLabel.value" class="form-control" placeholder="{{ 'invoices.general_wallet_label' | translate }}"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<label for="vatCode" translate>{{ 'invoices.accounting_vat_code' }}</label>
|
||||
<input type="text" id="vatCode" ng-model="settings.vatCode.value" class="form-control" placeholder="{{ 'invoices.general_vat_code' | translate }}"/>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label for="vatLabel" translate>{{ 'invoices.accounting_vat_label' }}</label>
|
||||
<input type="text" id="vatLabel" ng-model="settings.vatLabel.value" class="form-control" placeholder="{{ 'invoices.general_vat_label' | translate }}"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<label for="subscriptionCode" translate>{{ 'invoices.accounting_subscription_code' }}</label>
|
||||
<input type="text" id="subscriptionCode" ng-model="settings.subscriptionCode.value" class="form-control" placeholder="{{ 'invoices.general_subscription_code' | translate }}" />
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label for="subscriptionLabel" translate>{{ 'invoices.accounting_subscription_label' }}</label>
|
||||
<input type="text" id="subscriptionLabel" ng-model="settings.subscriptionLabel.value" class="form-control" placeholder="{{ 'invoices.general_subscription_label' | translate }}"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<label for="machineCode" translate>{{ 'invoices.accounting_Machine_code' }}</label>
|
||||
<input type="text" id="machineCode" ng-model="settings.machineCode.value" class="form-control" placeholder="{{ 'invoices.general_machine_code' | translate }}"/>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label for="machineLabel" translate>{{ 'invoices.accounting_Machine_label' }}</label>
|
||||
<input type="text" id="machineLabel" ng-model="settings.machineLabel.value" class="form-control" placeholder="{{ 'invoices.general_machine_label' | translate }}"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<label for="trainingCode" translate>{{ 'invoices.accounting_Training_code' }}</label>
|
||||
<input type="text" id="trainingCode" ng-model="settings.trainingCode.value" class="form-control" placeholder="{{ 'invoices.general_training_code' | translate }}" />
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label for="trainingLabel" translate>{{ 'invoices.accounting_Training_label' }}</label>
|
||||
<input type="text" id="trainingLabel" ng-model="settings.trainingLabel.value" class="form-control" placeholder="{{ 'invoices.general_training_label' | translate }}"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<label for="eventCode" translate>{{ 'invoices.accounting_Event_code' }}</label>
|
||||
<input type="text" id="eventCode" ng-model="settings.eventCode.value" class="form-control" placeholder="{{ 'invoices.general_event_code' | translate }}"/>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label for="eventLabel" translate>{{ 'invoices.accounting_Event_label' }}</label>
|
||||
<input type="text" id="eventLabel" ng-model="settings.eventLabel.value" class="form-control" placeholder="{{ 'invoices.general_event_label' | translate }}"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<label for="spaceCode" translate>{{ 'invoices.accounting_Space_code' }}</label>
|
||||
<input type="text" id="spaceCode" ng-model="settings.spaceCode.value" class="form-control" placeholder="{{ 'invoices.general_space_code' | translate }}" />
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label for="spaceLabel" translate>{{ 'invoices.accounting_Space_label' }}</label>
|
||||
<input type="text" id="spaceLabel" ng-model="settings.spaceLabel.value" class="form-control" placeholder="{{ 'invoices.general_space_label' | translate }}"/>
|
||||
</div>
|
||||
</div>
|
||||
<button name="button" class="btn btn-warning m-t-lg" ng-click="save()" translate>{{ 'save' }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</uib-tab>
|
||||
</uib-tabset>
|
||||
</div>
|
||||
</div>
|
||||
@ -409,8 +528,9 @@
|
||||
<tbody>
|
||||
<tr ng-repeat="value in history | orderBy:'-date'">
|
||||
<td>
|
||||
<span class="no-user-label" ng-show="value.rate === 0" translate>{{'invoices.VAT_disabled'}}</span>
|
||||
<span ng-hide="value.rate === 0">{{value.rate}}</span>
|
||||
<span class="no-user-label" ng-show="value.enabled === false" translate>{{'invoices.VAT_disabled'}}</span>
|
||||
<span class="no-user-label" ng-show="value.enabled === true" translate>{{'invoices.VAT_enabled'}}</span>
|
||||
<span ng-show="value.rate">{{value.rate}}</span>
|
||||
</td>
|
||||
<td>{{value.date | amDateFormat:'L LT'}}</td>
|
||||
<td>{{value.user.name}}<span class="no-user-label" ng-hide="value.user" translate>{{ 'invoices.deleted_user' }}</span></td>
|
||||
|
@ -36,8 +36,7 @@
|
||||
<tbody>
|
||||
<tr ng-repeat="plan in plans | filterDisabled:planFiltering | orderBy:orderPlans"
|
||||
ng-class="{'disabled-line' : plan.disabled && planFiltering === 'all'}"
|
||||
ng-init="group = getGroupFromId(groups, plan.group_id)"
|
||||
ng-hide="group.disabled">
|
||||
ng-init="group = getGroupFromId(groups, plan.group_id)">
|
||||
<td>{{getPlanType(plan.type)}}</td>
|
||||
<td>{{plan.base_name}}</td>
|
||||
<td>{{ plan.interval | planIntervalFilter:plan.interval_count }}</td>
|
||||
@ -47,4 +46,4 @@
|
||||
<td><button type="button" class="btn btn-default" ui-sref="app.admin.plans.edit({id:plan.id})"><i class="fa fa-pencil-square-o"></i></button> <button type="button" class="btn btn-danger" ng-click="deletePlan(plans, plan.id)"><i class="fa fa-trash"></i></button></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</table>
|
||||
|
35
app/controllers/api/accounting_exports_controller.rb
Normal file
35
app/controllers/api/accounting_exports_controller.rb
Normal file
@ -0,0 +1,35 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# API Controller for exporting accounting data to external accounting softwares
|
||||
class API::AccountingExportsController < API::ApiController
|
||||
|
||||
before_action :authenticate_user!
|
||||
|
||||
def export
|
||||
authorize :accounting_export
|
||||
|
||||
export = Export.where(category: 'accounting', export_type: 'accounting-software', key: params[:key])
|
||||
.where(extension: params[:extension], query: params[:query])
|
||||
.where('created_at > ?', Invoice.maximum('updated_at'))
|
||||
.last
|
||||
if export.nil? || !FileTest.exist?(export.file)
|
||||
@export = Export.new(
|
||||
category: 'accounting',
|
||||
export_type: params[:type],
|
||||
user: current_user,
|
||||
extension: params[:extension],
|
||||
query: params[:query],
|
||||
key: params[:key]
|
||||
)
|
||||
if @export.save
|
||||
render json: { export_id: @export.id }, status: :ok
|
||||
else
|
||||
render json: @export.errors, status: :unprocessable_entity
|
||||
end
|
||||
else
|
||||
send_file File.join(Rails.root, export.file),
|
||||
type: 'text/csv',
|
||||
disposition: 'attachment'
|
||||
end
|
||||
end
|
||||
end
|
@ -8,10 +8,17 @@ class API::ExportsController < API::ApiController
|
||||
|
||||
def download
|
||||
authorize @export
|
||||
mime_type = if @export.extension == 'xlsx'
|
||||
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
|
||||
elsif @export.extension == 'csv'
|
||||
'text/csv'
|
||||
else
|
||||
'application/octet-stream'
|
||||
end
|
||||
|
||||
if FileTest.exist?(@export.file)
|
||||
send_file File.join(Rails.root, @export.file),
|
||||
type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||
type: mime_type,
|
||||
disposition: 'attachment'
|
||||
else
|
||||
render text: I18n.t('errors.messages.export_not_found'), status: :not_found
|
||||
@ -21,28 +28,14 @@ class API::ExportsController < API::ApiController
|
||||
def status
|
||||
authorize Export
|
||||
|
||||
export = Export.where(category: params[:category], export_type: params[:type], query: params[:query], key: params[:key])
|
||||
|
||||
if params[:category] == 'users'
|
||||
case params[:type]
|
||||
when 'subscriptions'
|
||||
export = export.where('created_at > ?', Subscription.maximum('updated_at'))
|
||||
when 'reservations'
|
||||
export = export.where('created_at > ?', Reservation.maximum('updated_at'))
|
||||
when 'members'
|
||||
export = export.where('created_at > ?', User.with_role(:member).maximum('updated_at'))
|
||||
else
|
||||
raise ArgumentError, "Unknown export users/#{params[:type]}"
|
||||
end
|
||||
elsif params[:category] == 'availabilities'
|
||||
case params[:type]
|
||||
when 'index'
|
||||
export = export.where('created_at > ?', [Availability.maximum('updated_at'), Reservation.maximum('updated_at')].max)
|
||||
else
|
||||
raise ArgumentError, "Unknown type availabilities/#{params[:type]}"
|
||||
end
|
||||
end
|
||||
export = export.last
|
||||
exports = Export.where(
|
||||
category: params[:category],
|
||||
export_type: params[:type],
|
||||
query: params[:query],
|
||||
key: params[:key],
|
||||
extension: params[:extension]
|
||||
)
|
||||
export = retrieve_last_export(exports, params[:category], params[:type])
|
||||
|
||||
if export.nil? || !FileTest.exist?(export.file)
|
||||
render json: { exists: false, id: nil }, status: :ok
|
||||
@ -53,6 +46,39 @@ class API::ExportsController < API::ApiController
|
||||
|
||||
private
|
||||
|
||||
def retrieve_last_export(export, category, type)
|
||||
case category
|
||||
when 'users'
|
||||
case type
|
||||
when 'subscriptions'
|
||||
export = export.where('created_at > ?', Subscription.maximum('updated_at'))
|
||||
when 'reservations'
|
||||
export = export.where('created_at > ?', Reservation.maximum('updated_at'))
|
||||
when 'members'
|
||||
export = export.where('created_at > ?', User.with_role(:member).maximum('updated_at'))
|
||||
else
|
||||
raise ArgumentError, "Unknown export users/#{type}"
|
||||
end
|
||||
when 'availabilities'
|
||||
case type
|
||||
when 'index'
|
||||
export = export.where('created_at > ?', [Availability.maximum('updated_at'), Reservation.maximum('updated_at')].max)
|
||||
else
|
||||
raise ArgumentError, "Unknown type availabilities/#{type}"
|
||||
end
|
||||
when 'accounting'
|
||||
case type
|
||||
when 'acd'
|
||||
export = export.where('created_at > ?', Invoice.maximum('updated_at'))
|
||||
else
|
||||
raise ArgumentError, "Unknown type accounting/#{type}"
|
||||
end
|
||||
else
|
||||
raise ArgumentError, "Unknown category #{category}"
|
||||
end
|
||||
export.last
|
||||
end
|
||||
|
||||
def set_export
|
||||
@export = Export.find(params[:id])
|
||||
end
|
||||
|
@ -51,10 +51,16 @@ class API::InvoicesController < API::ApiController
|
||||
end
|
||||
end
|
||||
|
||||
def first
|
||||
authorize Invoice
|
||||
invoice = Invoice.order(:created_at).first
|
||||
@first = invoice&.created_at
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def avoir_params
|
||||
params.require(:avoir).permit(:invoice_id, :avoir_date, :avoir_mode, :subscription_to_expire, :description,
|
||||
params.require(:avoir).permit(:invoice_id, :avoir_date, :payment_method, :subscription_to_expire, :description,
|
||||
invoice_items_ids: [])
|
||||
end
|
||||
|
||||
|
@ -18,6 +18,19 @@ class API::SettingsController < API::ApiController
|
||||
end
|
||||
end
|
||||
|
||||
def bulk_update
|
||||
authorize Setting
|
||||
|
||||
@settings = []
|
||||
params[:settings].each do |setting|
|
||||
next if !setting[:name] || !setting[:value]
|
||||
|
||||
db_setting = Setting.find_or_initialize_by(name: setting[:name])
|
||||
db_setting.save && db_setting.history_values.create(value: setting[:value], invoicing_profile: current_user.invoicing_profile)
|
||||
@settings.push db_setting
|
||||
end
|
||||
end
|
||||
|
||||
def show
|
||||
@setting = Setting.find_or_create_by(name: params[:name])
|
||||
@show_history = params[:history] == 'true' && current_user.admin?
|
||||
|
@ -5,50 +5,12 @@
|
||||
class Avoir < Invoice
|
||||
belongs_to :invoice
|
||||
|
||||
validates :avoir_mode, inclusion: { in: %w[stripe cheque transfer none cash wallet] }
|
||||
validates :payment_method, inclusion: { in: %w[stripe cheque transfer none cash wallet] }
|
||||
|
||||
attr_accessor :invoice_items_ids
|
||||
|
||||
def generate_reference
|
||||
pattern = Setting.find_by(name: 'invoice_reference').value
|
||||
|
||||
# invoice number per day (dd..dd)
|
||||
reference = pattern.gsub(/d+(?![^\[]*\])/) do |match|
|
||||
pad_and_truncate(number_of_invoices('day'), match.to_s.length)
|
||||
end
|
||||
# invoice number per month (mm..mm)
|
||||
reference.gsub!(/m+(?![^\[]*\])/) do |match|
|
||||
pad_and_truncate(number_of_invoices('month'), match.to_s.length)
|
||||
end
|
||||
# invoice number per year (yy..yy)
|
||||
reference.gsub!(/y+(?![^\[]*\])/) do |match|
|
||||
pad_and_truncate(number_of_invoices('year'), match.to_s.length)
|
||||
end
|
||||
|
||||
# full year (YYYY)
|
||||
reference.gsub!(/YYYY(?![^\[]*\])/, created_at.strftime('%Y'))
|
||||
# year without century (YY)
|
||||
reference.gsub!(/YY(?![^\[]*\])/, created_at.strftime('%y'))
|
||||
|
||||
# abbreviated month name (MMM)
|
||||
reference.gsub!(/MMM(?![^\[]*\])/, created_at.strftime('%^b'))
|
||||
# month of the year, zero-padded (MM)
|
||||
reference.gsub!(/MM(?![^\[]*\])/, created_at.strftime('%m'))
|
||||
# month of the year, non zero-padded (M)
|
||||
reference.gsub!(/M(?![^\[]*\])/, created_at.strftime('%-m'))
|
||||
|
||||
# day of the month, zero-padded (DD)
|
||||
reference.gsub!(/DD(?![^\[]*\])/, created_at.strftime('%d'))
|
||||
# day of the month, non zero-padded (DD)
|
||||
reference.gsub!(/DD(?![^\[]*\])/, created_at.strftime('%-d'))
|
||||
|
||||
# information about refund/avoir (R[text])
|
||||
reference.gsub!(/R\[([^\]]+)\]/, '\1')
|
||||
|
||||
# remove information about online selling (X[text])
|
||||
reference.gsub!(/X\[([^\]]+)\]/, ''.to_s)
|
||||
|
||||
self.reference = reference
|
||||
self.reference = InvoiceReferenceService.generate_reference(self, date: created_at, avoir: true)
|
||||
end
|
||||
|
||||
def expire_subscription
|
||||
|
@ -21,7 +21,7 @@ class Export < ActiveRecord::Base
|
||||
end
|
||||
|
||||
def filename
|
||||
"#{export_type}-#{id}_#{created_at.strftime('%d%m%Y')}.xlsx"
|
||||
"#{export_type}-#{id}_#{created_at.strftime('%d%m%Y')}.#{extension}"
|
||||
end
|
||||
|
||||
private
|
||||
@ -34,6 +34,8 @@ class Export < ActiveRecord::Base
|
||||
UsersExportWorker.perform_async(id)
|
||||
when 'availabilities'
|
||||
AvailabilitiesExportWorker.perform_async(id)
|
||||
when 'accounting'
|
||||
AccountingExportWorker.perform_async(id)
|
||||
else
|
||||
raise NoMethodError, "Unknown export service for #{category}/#{export_type}"
|
||||
end
|
||||
|
@ -1,3 +1,6 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# Group is way to bind users with prices. Different prices can be defined for each plan/reservable, for each group
|
||||
class Group < ActiveRecord::Base
|
||||
has_many :plans
|
||||
has_many :users
|
||||
@ -12,15 +15,21 @@ class Group < ActiveRecord::Base
|
||||
friendly_id :name, use: :slugged
|
||||
|
||||
validates :name, :slug, presence: true
|
||||
validates :disabled, inclusion: { in: [false] }, if: :group_has_users?
|
||||
|
||||
after_create :create_prices
|
||||
after_create :create_statistic_subtype
|
||||
after_update :update_statistic_subtype, if: :name_changed?
|
||||
after_update :disable_plans, if: :disabled_changed?
|
||||
|
||||
def destroyable?
|
||||
users.empty? and plans.empty?
|
||||
end
|
||||
|
||||
def group_has_users?
|
||||
users.count.positive?
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def create_prices
|
||||
@ -60,4 +69,10 @@ class Group < ActiveRecord::Base
|
||||
subtype.label = name
|
||||
subtype.save!
|
||||
end
|
||||
|
||||
def disable_plans
|
||||
plans.each do |plan|
|
||||
plan.update_attributes(disabled: disabled)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -25,14 +25,6 @@ class HistoryValue < ActiveRecord::Base
|
||||
private
|
||||
|
||||
def compute_footprint
|
||||
max_date = created_at || Time.current
|
||||
previous = HistoryValue.where('created_at < ?', max_date)
|
||||
.order('created_at DESC')
|
||||
.limit(1)
|
||||
|
||||
columns = HistoryValue.columns.map(&:name)
|
||||
.delete_if { |c| %w[footprint updated_at].include? c }
|
||||
|
||||
Checksum.text("#{columns.map { |c| self[c] }.join}#{previous.first ? previous.first.footprint : ''}")
|
||||
FootprintService.compute_footprint(HistoryValue, self, 'created_at')
|
||||
end
|
||||
end
|
||||
|
@ -48,52 +48,7 @@ class Invoice < ActiveRecord::Base
|
||||
end
|
||||
|
||||
def generate_reference
|
||||
pattern = Setting.find_by(name: 'invoice_reference').value
|
||||
|
||||
# invoice number per day (dd..dd)
|
||||
reference = pattern.gsub(/d+(?![^\[]*\])/) do |match|
|
||||
pad_and_truncate(number_of_invoices('day'), match.to_s.length)
|
||||
end
|
||||
# invoice number per month (mm..mm)
|
||||
reference.gsub!(/m+(?![^\[]*\])/) do |match|
|
||||
pad_and_truncate(number_of_invoices('month'), match.to_s.length)
|
||||
end
|
||||
# invoice number per year (yy..yy)
|
||||
reference.gsub!(/y+(?![^\[]*\])/) do |match|
|
||||
pad_and_truncate(number_of_invoices('year'), match.to_s.length)
|
||||
end
|
||||
|
||||
# full year (YYYY)
|
||||
reference.gsub!(/YYYY(?![^\[]*\])/, Time.now.strftime('%Y'))
|
||||
# year without century (YY)
|
||||
reference.gsub!(/YY(?![^\[]*\])/, Time.now.strftime('%y'))
|
||||
|
||||
# abreviated month name (MMM)
|
||||
reference.gsub!(/MMM(?![^\[]*\])/, Time.now.strftime('%^b'))
|
||||
# month of the year, zero-padded (MM)
|
||||
reference.gsub!(/MM(?![^\[]*\])/, Time.now.strftime('%m'))
|
||||
# month of the year, non zero-padded (M)
|
||||
reference.gsub!(/M(?![^\[]*\])/, Time.now.strftime('%-m'))
|
||||
|
||||
# day of the month, zero-padded (DD)
|
||||
reference.gsub!(/DD(?![^\[]*\])/, Time.now.strftime('%d'))
|
||||
# day of the month, non zero-padded (DD)
|
||||
reference.gsub!(/DD(?![^\[]*\])/, Time.now.strftime('%-d'))
|
||||
|
||||
# information about online selling (X[text])
|
||||
if paid_with_stripe?
|
||||
reference.gsub!(/X\[([^\]]+)\]/, '\1')
|
||||
else
|
||||
reference.gsub!(/X\[([^\]]+)\]/, ''.to_s)
|
||||
end
|
||||
|
||||
# information about wallet (W[text])
|
||||
# reference.gsub!(/W\[([^\]]+)\]/, ''.to_s)
|
||||
|
||||
# remove information about refunds (R[text])
|
||||
reference.gsub!(/R\[([^\]]+)\]/, ''.to_s)
|
||||
|
||||
self.reference = reference
|
||||
self.reference = InvoiceReferenceService.generate_reference(self)
|
||||
end
|
||||
|
||||
def update_reference
|
||||
@ -102,43 +57,7 @@ class Invoice < ActiveRecord::Base
|
||||
end
|
||||
|
||||
def order_number
|
||||
pattern = Setting.find_by(name: 'invoice_order-nb').value
|
||||
|
||||
# global invoice number (nn..nn)
|
||||
reference = pattern.gsub(/n+(?![^\[]*\])/) do |match|
|
||||
pad_and_truncate(number_of_invoices('global'), match.to_s.length)
|
||||
end
|
||||
# invoice number per year (yy..yy)
|
||||
reference.gsub!(/y+(?![^\[]*\])/) do |match|
|
||||
pad_and_truncate(number_of_invoices('year'), match.to_s.length)
|
||||
end
|
||||
# invoice number per month (mm..mm)
|
||||
reference.gsub!(/m+(?![^\[]*\])/) do |match|
|
||||
pad_and_truncate(number_of_invoices('month'), match.to_s.length)
|
||||
end
|
||||
# invoice number per day (dd..dd)
|
||||
reference.gsub!(/d+(?![^\[]*\])/) do |match|
|
||||
pad_and_truncate(number_of_invoices('day'), match.to_s.length)
|
||||
end
|
||||
|
||||
# full year (YYYY)
|
||||
reference.gsub!(/YYYY(?![^\[]*\])/, created_at.strftime('%Y'))
|
||||
# year without century (YY)
|
||||
reference.gsub!(/YY(?![^\[]*\])/, created_at.strftime('%y'))
|
||||
|
||||
# abbreviated month name (MMM)
|
||||
reference.gsub!(/MMM(?![^\[]*\])/, created_at.strftime('%^b'))
|
||||
# month of the year, zero-padded (MM)
|
||||
reference.gsub!(/MM(?![^\[]*\])/, created_at.strftime('%m'))
|
||||
# month of the year, non zero-padded (M)
|
||||
reference.gsub!(/M(?![^\[]*\])/, created_at.strftime('%-m'))
|
||||
|
||||
# day of the month, zero-padded (DD)
|
||||
reference.gsub!(/DD(?![^\[]*\])/, created_at.strftime('%d'))
|
||||
# day of the month, non zero-padded (DD)
|
||||
reference.gsub!(/DD(?![^\[]*\])/, created_at.strftime('%-d'))
|
||||
|
||||
reference
|
||||
InvoiceReferenceService.generate_order_number(self)
|
||||
end
|
||||
|
||||
# for debug & used by rake task "fablab:maintenance:regenerate_invoices"
|
||||
@ -148,7 +67,7 @@ class Invoice < ActiveRecord::Base
|
||||
end
|
||||
|
||||
def build_avoir(attrs = {})
|
||||
raise Exception if refunded? === true || prevent_refund?
|
||||
raise Exception if refunded? == true || prevent_refund?
|
||||
|
||||
avoir = Avoir.new(dup.attributes)
|
||||
avoir.type = 'Avoir'
|
||||
@ -189,7 +108,7 @@ class Invoice < ActiveRecord::Base
|
||||
|
||||
def subscription_invoice?
|
||||
invoice_items.each do |ii|
|
||||
return true if ii.subscription && !ii.subscription.expired?
|
||||
return true if ii.subscription
|
||||
end
|
||||
false
|
||||
end
|
||||
@ -230,6 +149,18 @@ class Invoice < ActiveRecord::Base
|
||||
total - (wallet_amount || 0)
|
||||
end
|
||||
|
||||
# return a summary of the payment means used
|
||||
def payment_means
|
||||
res = []
|
||||
res.push(means: :wallet, amount: wallet_amount) if wallet_transaction && wallet_amount.positive?
|
||||
if paid_with_stripe?
|
||||
res.push(means: :card, amount: amount_paid)
|
||||
else
|
||||
res.push(means: :other, amount: amount_paid)
|
||||
end
|
||||
res
|
||||
end
|
||||
|
||||
def add_environment
|
||||
self.environment = Rails.env
|
||||
end
|
||||
@ -244,16 +175,14 @@ class Invoice < ActiveRecord::Base
|
||||
end
|
||||
|
||||
def set_wallet_transaction(amount, transaction_id)
|
||||
if check_footprint
|
||||
update_columns(wallet_amount: amount, wallet_transaction_id: transaction_id)
|
||||
chain_record
|
||||
else
|
||||
raise InvalidFootprintError
|
||||
end
|
||||
raise InvalidFootprintError unless check_footprint
|
||||
|
||||
update_columns(wallet_amount: amount, wallet_transaction_id: transaction_id)
|
||||
chain_record
|
||||
end
|
||||
|
||||
def paid_with_stripe?
|
||||
stp_payment_intent_id? || stp_invoice_id?
|
||||
stp_payment_intent_id? || stp_invoice_id? || payment_method == 'stripe'
|
||||
end
|
||||
|
||||
private
|
||||
@ -266,50 +195,8 @@ class Invoice < ActiveRecord::Base
|
||||
InvoiceWorker.perform_async(id, user&.subscription&.expired_at)
|
||||
end
|
||||
|
||||
##
|
||||
# Output the given integer with leading zeros. If the given value is longer than the given
|
||||
# length, it will be truncated.
|
||||
# @param value {Integer} the integer to pad
|
||||
# @param length {Integer} the length of the resulting string.
|
||||
##
|
||||
def pad_and_truncate(value, length)
|
||||
value.to_s.rjust(length, '0').gsub(/^.*(.{#{length},}?)$/m, '\1')
|
||||
end
|
||||
|
||||
##
|
||||
# Returns the number of current invoices in the given range around the current date.
|
||||
# If range is invalid or not specified, the total number of invoices is returned.
|
||||
# @param range {String} 'day', 'month', 'year'
|
||||
# @return {Integer}
|
||||
##
|
||||
def number_of_invoices(range)
|
||||
case range.to_s
|
||||
when 'day'
|
||||
start = DateTime.current.beginning_of_day
|
||||
ending = DateTime.current.end_of_day
|
||||
when 'month'
|
||||
start = DateTime.current.beginning_of_month
|
||||
ending = DateTime.current.end_of_month
|
||||
when 'year'
|
||||
start = DateTime.current.beginning_of_year
|
||||
ending = DateTime.current.end_of_year
|
||||
else
|
||||
return id
|
||||
end
|
||||
return Invoice.count unless defined? start && defined? ending
|
||||
|
||||
Invoice.where('created_at >= :start_date AND created_at < :end_date', start_date: start, end_date: ending).length
|
||||
end
|
||||
|
||||
def compute_footprint
|
||||
previous = Invoice.where('id < ?', id)
|
||||
.order('id DESC')
|
||||
.limit(1)
|
||||
|
||||
columns = Invoice.columns.map(&:name)
|
||||
.delete_if { |c| %w[footprint updated_at].include? c }
|
||||
|
||||
Checksum.text("#{columns.map { |c| self[c] }.join}#{previous.first ? previous.first.footprint : ''}")
|
||||
FootprintService.compute_footprint(Invoice, self)
|
||||
end
|
||||
|
||||
def log_changes
|
||||
|
@ -21,17 +21,29 @@ class InvoiceItem < ActiveRecord::Base
|
||||
footprint == compute_footprint
|
||||
end
|
||||
|
||||
def amount_after_coupon
|
||||
# deduct coupon discount
|
||||
coupon_service = CouponService.new
|
||||
coupon_service.ventilate(invoice.total, amount, invoice.coupon)
|
||||
end
|
||||
|
||||
# return the item amount, coupon discount deducted, if any, and VAT excluded, if applicable
|
||||
def net_amount
|
||||
# deduct VAT
|
||||
vat_service = VatHistoryService.new
|
||||
vat_rate = vat_service.invoice_vat(invoice)
|
||||
Rational(amount_after_coupon / (vat_rate / 100.00 + 1)).round.to_f
|
||||
end
|
||||
|
||||
# return the VAT amount for this item
|
||||
def vat
|
||||
amount_after_coupon - net_amount
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def compute_footprint
|
||||
previous = InvoiceItem.where('id < ?', id)
|
||||
.order('id DESC')
|
||||
.limit(1)
|
||||
|
||||
columns = InvoiceItem.columns.map(&:name)
|
||||
.delete_if { |c| %w[footprint updated_at].include? c }
|
||||
|
||||
Checksum.text("#{columns.map { |c| self[c] }.join}#{previous.first ? previous.first.footprint : ''}")
|
||||
FootprintService.compute_footprint(InvoiceItem, self)
|
||||
end
|
||||
|
||||
def log_changes
|
||||
|
@ -53,13 +53,13 @@ class Plan < ActiveRecord::Base
|
||||
|
||||
def create_machines_prices
|
||||
Machine.all.each do |machine|
|
||||
Price.create(priceable: machine, plan: self, group_id: self.group_id, amount: 0)
|
||||
Price.create(priceable: machine, plan: self, group_id: group_id, amount: 0)
|
||||
end
|
||||
end
|
||||
|
||||
def create_spaces_prices
|
||||
Space.all.each do |space|
|
||||
Price.create(priceable: space, plan: self, group_id: self.group_id, amount: 0)
|
||||
Price.create(priceable: space, plan: self, group_id: group_id, amount: 0)
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -202,11 +202,14 @@ class Reservation < ActiveRecord::Base
|
||||
end
|
||||
|
||||
def save_with_payment(operator_profile_id, coupon_code = nil, payment_intent_id = nil)
|
||||
method = InvoicingProfile.find(operator_profile_id)&.user&.admin? ? nil : 'stripe'
|
||||
|
||||
build_invoice(
|
||||
invoicing_profile: user.invoicing_profile,
|
||||
statistic_profile: user.statistic_profile,
|
||||
operator_profile_id: operator_profile_id,
|
||||
stp_payment_intent_id: payment_intent_id
|
||||
stp_payment_intent_id: payment_intent_id,
|
||||
payment_method: method
|
||||
)
|
||||
generate_invoice_items(true, coupon_code)
|
||||
|
||||
|
@ -40,7 +40,28 @@ class Setting < ActiveRecord::Base
|
||||
visibility_yearly
|
||||
visibility_others
|
||||
display_name_enable
|
||||
machines_sort_by] }
|
||||
machines_sort_by
|
||||
accounting_journal_code
|
||||
accounting_card_client_code
|
||||
accounting_card_client_label
|
||||
accounting_wallet_client_code
|
||||
accounting_wallet_client_label
|
||||
accounting_other_client_code
|
||||
accounting_other_client_label
|
||||
accounting_wallet_code
|
||||
accounting_wallet_label
|
||||
accounting_VAT_code
|
||||
accounting_VAT_label
|
||||
accounting_subscription_code
|
||||
accounting_subscription_label
|
||||
accounting_Machine_code
|
||||
accounting_Machine_label
|
||||
accounting_Training_code
|
||||
accounting_Training_label
|
||||
accounting_Event_code
|
||||
accounting_Event_label
|
||||
accounting_Space_code
|
||||
accounting_Space_label] }
|
||||
|
||||
after_update :update_stylesheet, :notify_privacy_policy_changed if :value_changed?
|
||||
|
||||
|
@ -46,6 +46,7 @@ class Subscription < ActiveRecord::Base
|
||||
def generate_invoice(operator_profile_id, coupon_code = nil, payment_intent_id = nil)
|
||||
coupon_id = nil
|
||||
total = plan.amount
|
||||
method = InvoicingProfile.find(operator_profile_id)&.user&.admin? ? nil : 'stripe'
|
||||
|
||||
unless coupon_code.nil?
|
||||
@coupon = Coupon.find_by(code: coupon_code)
|
||||
@ -64,7 +65,8 @@ class Subscription < ActiveRecord::Base
|
||||
total: total,
|
||||
coupon_id: coupon_id,
|
||||
operator_profile_id: operator_profile_id,
|
||||
stp_payment_intent_id: payment_intent_id
|
||||
stp_payment_intent_id: payment_intent_id,
|
||||
payment_method: method
|
||||
)
|
||||
invoice.invoice_items.push InvoiceItem.new(
|
||||
amount: plan.amount,
|
||||
|
@ -121,6 +121,8 @@ class PDF::Invoice < Prawn::Document
|
||||
data = [[I18n.t('invoices.details'), I18n.t('invoices.amount')]]
|
||||
|
||||
total_calc = 0
|
||||
total_ht = 0
|
||||
total_vat = 0
|
||||
# going through invoice_items
|
||||
invoice.invoice_items.each do |item|
|
||||
|
||||
@ -184,6 +186,8 @@ class PDF::Invoice < Prawn::Document
|
||||
|
||||
data += [[details, number_to_currency(price)]]
|
||||
total_calc += price
|
||||
total_ht += item.net_amount
|
||||
total_vat += item.vat
|
||||
end
|
||||
|
||||
## subtract the coupon, if any
|
||||
@ -210,15 +214,13 @@ class PDF::Invoice < Prawn::Document
|
||||
|
||||
# discount textual description
|
||||
literal_discount = cp.percent_off
|
||||
if cp.type == 'amount_off'
|
||||
literal_discount = number_to_currency(cp.amount_off / 100.00)
|
||||
end
|
||||
literal_discount = number_to_currency(cp.amount_off / 100.00) if cp.type == 'amount_off'
|
||||
|
||||
# add a row for the coupon
|
||||
data += [[_t('invoices.coupon_CODE_discount_of_DISCOUNT',
|
||||
CODE: cp.code,
|
||||
DISCOUNT: literal_discount,
|
||||
TYPE: cp.type), number_to_currency(-discount)] ]
|
||||
TYPE: cp.type), number_to_currency(-discount)]]
|
||||
end
|
||||
|
||||
# total verification
|
||||
@ -226,20 +228,18 @@ class PDF::Invoice < Prawn::Document
|
||||
puts "ERROR: totals are NOT equals => expected: #{total}, computed: #{total_calc}" if total_calc != total
|
||||
|
||||
# TVA
|
||||
if Setting.find_by(name: 'invoice_VAT-active').value == 'true'
|
||||
vat_service = VatHistoryService.new
|
||||
vat_rate = vat_service.invoice_vat(invoice)
|
||||
if vat_rate != 0
|
||||
data += [[I18n.t('invoices.total_including_all_taxes'), number_to_currency(total)]]
|
||||
|
||||
vat_service = VatHistoryService.new
|
||||
vat_rate = vat_service.invoice_vat(invoice)
|
||||
vat = total / (vat_rate / 100.00 + 1)
|
||||
data += [[I18n.t('invoices.including_VAT_RATE', RATE: vat_rate), number_to_currency(total - vat)]]
|
||||
data += [[I18n.t('invoices.including_total_excluding_taxes'), number_to_currency(vat)]]
|
||||
data += [[I18n.t('invoices.including_VAT_RATE', RATE: vat_rate), number_to_currency(total_vat / 100.00)]]
|
||||
data += [[I18n.t('invoices.including_total_excluding_taxes'), number_to_currency(total_ht / 100.00)]]
|
||||
data += [[I18n.t('invoices.including_amount_payed_on_ordering'), number_to_currency(total)]]
|
||||
|
||||
# checking the round number
|
||||
rounded = sprintf('%.2f', vat).to_f + sprintf('%.2f', total - vat).to_f
|
||||
rounded = sprintf('%.2f', total_vat / 100.00).to_f + sprintf('%.2f', total_ht / 100.00).to_f
|
||||
if rounded != sprintf('%.2f', total_calc).to_f
|
||||
puts 'ERROR: rounding the numbers cause an invoice inconsistency. ' +
|
||||
puts 'ERROR: rounding the numbers cause an invoice inconsistency. ' \
|
||||
"Total expected: #{sprintf('%.2f', total_calc)}, total computed: #{rounded}"
|
||||
end
|
||||
else
|
||||
@ -279,7 +279,7 @@ class PDF::Invoice < Prawn::Document
|
||||
move_down 20
|
||||
if invoice.is_a?(Avoir)
|
||||
payment_verbose = I18n.t('invoices.refund_on_DATE', DATE:I18n.l(invoice.avoir_date.to_date)) + ' '
|
||||
case invoice.avoir_mode
|
||||
case invoice.payment_method
|
||||
when 'stripe'
|
||||
payment_verbose += I18n.t('invoices.by_stripe_online_payment')
|
||||
when 'cheque'
|
||||
|
8
app/policies/accounting_export_policy.rb
Normal file
8
app/policies/accounting_export_policy.rb
Normal file
@ -0,0 +1,8 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# Check the access policies for API::AccountingExportsController
|
||||
class AccountingExportPolicy < ApplicationPolicy
|
||||
def export?
|
||||
user.admin?
|
||||
end
|
||||
end
|
@ -14,4 +14,8 @@ class InvoicePolicy < ApplicationPolicy
|
||||
def list?
|
||||
user.admin?
|
||||
end
|
||||
|
||||
def first?
|
||||
user.admin?
|
||||
end
|
||||
end
|
||||
|
@ -1,5 +1,8 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# Check the access policies for API::SettingsController
|
||||
class SettingPolicy < ApplicationPolicy
|
||||
%w(update).each do |action|
|
||||
%w[update bulk_update].each do |action|
|
||||
define_method "#{action}?" do
|
||||
user.admin?
|
||||
end
|
||||
|
@ -2,22 +2,36 @@
|
||||
|
||||
# Provides the routine to export the accounting data to an external accounting software
|
||||
class AccountingExportService
|
||||
attr_reader :encoding, :format, :separator, :journal_code, :date_format, :columns, :vat_service
|
||||
include ActionView::Helpers::NumberHelper
|
||||
|
||||
def initialize(columns, encoding = 'UTF-8', format = 'CSV', separator = ';', date_format = '%d/%m/%Y')
|
||||
attr_reader :encoding, :format, :separator, :journal_code, :date_format, :columns, :decimal_separator, :label_max_length,
|
||||
:export_zeros
|
||||
|
||||
def initialize(columns, encoding: 'UTF-8', format: 'CSV', separator: ';')
|
||||
@encoding = encoding
|
||||
@format = format
|
||||
@separator = separator
|
||||
@decimal_separator = ','
|
||||
@date_format = '%d/%m/%Y'
|
||||
@label_max_length = 50
|
||||
@export_zeros = false
|
||||
@journal_code = Setting.find_by(name: 'accounting_journal_code')&.value || ''
|
||||
@date_format = date_format
|
||||
@columns = columns
|
||||
@vat_service = VatHistoryService.new
|
||||
end
|
||||
|
||||
def set_options(decimal_separator: ',', date_format: '%d/%m/%Y', label_max_length: 50, export_zeros: false)
|
||||
@decimal_separator = decimal_separator
|
||||
@date_format = date_format
|
||||
@label_max_length = label_max_length
|
||||
@export_zeros = export_zeros
|
||||
end
|
||||
|
||||
def export(start_date, end_date, file)
|
||||
# build CVS content
|
||||
content = header_row
|
||||
invoices = Invoice.where('created_at >= ? AND created_at <= ?', start_date, end_date).order('created_at ASC')
|
||||
invoices = invoices.where('total > 0') unless export_zeros
|
||||
invoices.each do |i|
|
||||
content << generate_rows(i)
|
||||
end
|
||||
@ -37,9 +51,12 @@ class AccountingExportService
|
||||
end
|
||||
|
||||
def generate_rows(invoice)
|
||||
"#{client_row(invoice)}\n" \
|
||||
"#{items_rows(invoice)}" \
|
||||
"#{vat_row(invoice)}\n"
|
||||
rows = client_rows(invoice) + items_rows(invoice)
|
||||
|
||||
vat = vat_row(invoice)
|
||||
rows += "#{vat}\n" unless vat.nil?
|
||||
|
||||
rows
|
||||
end
|
||||
|
||||
# Generate the "subscription" and "reservation" rows associated with the provided invoice
|
||||
@ -49,122 +66,78 @@ class AccountingExportService
|
||||
invoice.invoice_items.each do |item|
|
||||
rows << "#{reservation_row(invoice, item)}\n"
|
||||
end
|
||||
elsif invoice.invoiced_type == 'WalletTransaction'
|
||||
rows << "#{wallet_row(invoice)}\n"
|
||||
end
|
||||
rows
|
||||
end
|
||||
|
||||
# Generate the "client" row, which contains the debit to the client account, all taxes included
|
||||
def client_row(invoice)
|
||||
total = invoice.total / 100.00
|
||||
row = ''
|
||||
columns.each do |column|
|
||||
case column
|
||||
when 'journal_code'
|
||||
row << journal_code
|
||||
when 'date'
|
||||
row << invoice.created_at&.strftime(date_format)
|
||||
when 'account_code'
|
||||
row << account(invoice, :client)
|
||||
when 'account_label'
|
||||
row << account(invoice, :client, :label)
|
||||
when 'piece'
|
||||
row << invoice.reference
|
||||
when 'line_label'
|
||||
row << label(invoice.invoicing_profile.full_name)
|
||||
when 'debit_origin'
|
||||
row << debit_client(invoice, total)
|
||||
when 'credit_origin'
|
||||
row << credit_client(invoice, total)
|
||||
when 'debit_euro'
|
||||
row << debit_client(invoice, total)
|
||||
when 'credit_euro'
|
||||
row << credit_client(invoice, total)
|
||||
when 'lettering'
|
||||
row << ''
|
||||
else
|
||||
puts "Unsupported column: #{column}"
|
||||
end
|
||||
row << separator
|
||||
# Generate the "client" rows, which contains the debit to the client account, all taxes included
|
||||
def client_rows(invoice)
|
||||
rows = ''
|
||||
invoice.payment_means.each_with_index do |details, index|
|
||||
rows << row(
|
||||
invoice,
|
||||
account(invoice, :client, means: details[:means]),
|
||||
account(invoice, :client, means: details[:means], type: :label),
|
||||
details[:amount] / 100.00,
|
||||
line_label: index.zero? ? label(invoice) : '',
|
||||
debit_method: :debit_client,
|
||||
credit_method: :credit_client
|
||||
)
|
||||
rows << "\n"
|
||||
end
|
||||
row
|
||||
rows
|
||||
end
|
||||
|
||||
# Generate the "reservation" row, which contains the credit to the reservation account, all taxes excluded
|
||||
def reservation_row(invoice, item)
|
||||
wo_taxes = (item.amount / (vat_service.invoice_vat(invoice) / 100.00 + 1)) / 100.00
|
||||
row = ''
|
||||
columns.each do |column|
|
||||
case column
|
||||
when 'journal_code'
|
||||
row << journal_code
|
||||
when 'date'
|
||||
row << invoice.created_at&.strftime(date_format)
|
||||
when 'account_code'
|
||||
row << account(invoice, :reservation)
|
||||
when 'account_label'
|
||||
row << account(invoice, :reservation, :label)
|
||||
when 'piece'
|
||||
row << invoice.reference
|
||||
when 'line_label'
|
||||
row << label(item.description)
|
||||
when 'debit_origin'
|
||||
row << debit(invoice, wo_taxes)
|
||||
when 'credit_origin'
|
||||
row << credit(invoice, wo_taxes)
|
||||
when 'debit_euro'
|
||||
row << debit(invoice, wo_taxes)
|
||||
when 'credit_euro'
|
||||
row << credit(invoice, wo_taxes)
|
||||
when 'lettering'
|
||||
row << ''
|
||||
else
|
||||
puts "Unsupported column: #{column}"
|
||||
end
|
||||
row << separator
|
||||
end
|
||||
row
|
||||
row(
|
||||
invoice,
|
||||
account(invoice, :reservation),
|
||||
account(invoice, :reservation, type: :label),
|
||||
item.net_amount / 100.00
|
||||
)
|
||||
end
|
||||
|
||||
# Generate the "subscription" row, which contains the credit to the subscription account, all taxes excluded
|
||||
def subscription_row(invoice)
|
||||
subscription_item = invoice.invoice_items.select(&:subscription).first
|
||||
wo_taxes = (subscription_item.amount / (vat_service.invoice_vat(invoice) / 100.00 + 1)) / 100.00
|
||||
row = ''
|
||||
columns.each do |column|
|
||||
case column
|
||||
when 'journal_code'
|
||||
row << journal_code
|
||||
when 'date'
|
||||
row << invoice.created_at&.strftime(date_format)
|
||||
when 'account_code'
|
||||
row << account(invoice, :subscription)
|
||||
when 'account_label'
|
||||
row << account(invoice, :subscription, :label)
|
||||
when 'piece'
|
||||
row << invoice.reference
|
||||
when 'line_label'
|
||||
row << label(subscription_item.description)
|
||||
when 'debit_origin'
|
||||
row << debit(invoice, wo_taxes)
|
||||
when 'credit_origin'
|
||||
row << credit(invoice, wo_taxes)
|
||||
when 'debit_euro'
|
||||
row << debit(invoice, wo_taxes)
|
||||
when 'credit_euro'
|
||||
row << credit(invoice, wo_taxes)
|
||||
when 'lettering'
|
||||
row << ''
|
||||
else
|
||||
puts "Unsupported column: #{column}"
|
||||
end
|
||||
row << separator
|
||||
end
|
||||
row
|
||||
row(
|
||||
invoice,
|
||||
account(invoice, :subscription),
|
||||
account(invoice, :subscription, type: :label),
|
||||
subscription_item.net_amount / 100.00
|
||||
)
|
||||
end
|
||||
|
||||
# Generate the "wallet" row, which contains the credit to the wallet account, all taxes excluded
|
||||
# This applies to wallet crediting, when an Avoir is generated at this time
|
||||
def wallet_row(invoice)
|
||||
row(
|
||||
invoice,
|
||||
account(invoice, :wallet),
|
||||
account(invoice, :wallet, type: :label),
|
||||
invoice.invoice_items.first.net_amount / 100.00
|
||||
)
|
||||
end
|
||||
|
||||
# Generate the "VAT" row, which contains the credit to the VAT account, with VAT amount only
|
||||
def vat_row(invoice)
|
||||
vat = (invoice.total - (invoice.total / (vat_service.invoice_vat(invoice) / 100.00 + 1))) / 100.00
|
||||
rate = VatHistoryService.new.invoice_vat(invoice)
|
||||
# we do not render the VAT row if it was disabled for this invoice
|
||||
return nil if rate.zero?
|
||||
|
||||
row(
|
||||
invoice,
|
||||
account(invoice, :vat),
|
||||
account(invoice, :vat, type: :label),
|
||||
invoice.invoice_items.map(&:vat).map(&:to_i).reduce(:+) / 100.00
|
||||
)
|
||||
end
|
||||
|
||||
# Generate a row of the export, filling the configured columns with the provided values
|
||||
def row(invoice, account_code, account_label, amount, line_label: '', debit_method: :debit, credit_method: :credit)
|
||||
row = ''
|
||||
columns.each do |column|
|
||||
case column
|
||||
@ -173,21 +146,21 @@ class AccountingExportService
|
||||
when 'date'
|
||||
row << invoice.created_at&.strftime(date_format)
|
||||
when 'account_code'
|
||||
row << account(invoice, :vat)
|
||||
row << account_code
|
||||
when 'account_label'
|
||||
row << account(invoice, :vat, :label)
|
||||
row << account_label
|
||||
when 'piece'
|
||||
row << invoice.reference
|
||||
when 'line_label'
|
||||
row << I18n.t('accounting_export.VAT')
|
||||
row << line_label
|
||||
when 'debit_origin'
|
||||
row << debit(invoice, vat)
|
||||
row << method(debit_method).call(invoice, amount)
|
||||
when 'credit_origin'
|
||||
row << credit(invoice, vat)
|
||||
row << method(credit_method).call(invoice, amount)
|
||||
when 'debit_euro'
|
||||
row << debit(invoice, vat)
|
||||
row << method(debit_method).call(invoice, amount)
|
||||
when 'credit_euro'
|
||||
row << credit(invoice, vat)
|
||||
row << method(credit_method).call(invoice, amount)
|
||||
when 'lettering'
|
||||
row << ''
|
||||
else
|
||||
@ -199,40 +172,45 @@ class AccountingExportService
|
||||
end
|
||||
|
||||
# Get the account code (or label) for the given invoice and the specified line type (client, vat, subscription or reservation)
|
||||
def account(invoice, account, type = :code)
|
||||
res = case account
|
||||
when :client
|
||||
Setting.find_by(name: "accounting_client_#{type}")&.value
|
||||
when :vat
|
||||
Setting.find_by(name: "accounting_VAT_#{type}")&.value
|
||||
when :subscription
|
||||
if invoice.subscription_invoice?
|
||||
Setting.find_by(name: "accounting_subscription_#{type}")&.value
|
||||
else
|
||||
puts "WARN: Invoice #{invoice.id} has no subscription"
|
||||
end
|
||||
when :reservation
|
||||
if invoice.invoiced_type == 'Reservation'
|
||||
Setting.find_by(name: "accounting_#{invoice.invoiced.reservable_type}_#{type}")&.value
|
||||
else
|
||||
puts "WARN: Invoice #{invoice.id} has no reservation"
|
||||
end
|
||||
else
|
||||
puts "Unsupported account #{account}"
|
||||
end
|
||||
res || ''
|
||||
def account(invoice, account, type: :code, means: :other)
|
||||
case account
|
||||
when :client
|
||||
Setting.find_by(name: "accounting_#{means}_client_#{type}")&.value
|
||||
when :vat
|
||||
Setting.find_by(name: "accounting_VAT_#{type}")&.value
|
||||
when :subscription
|
||||
if invoice.subscription_invoice?
|
||||
Setting.find_by(name: "accounting_subscription_#{type}")&.value
|
||||
else
|
||||
puts "WARN: Invoice #{invoice.id} has no subscription"
|
||||
end
|
||||
when :reservation
|
||||
if invoice.invoiced_type == 'Reservation'
|
||||
Setting.find_by(name: "accounting_#{invoice.invoiced.reservable_type}_#{type}")&.value
|
||||
else
|
||||
puts "WARN: Invoice #{invoice.id} has no reservation"
|
||||
end
|
||||
when :wallet
|
||||
if invoice.invoiced_type == 'WalletTransaction'
|
||||
Setting.find_by(name: "accounting_wallet_#{type}")&.value
|
||||
else
|
||||
puts "WARN: Invoice #{invoice.id} is not a wallet credit"
|
||||
end
|
||||
else
|
||||
puts "Unsupported account #{account}"
|
||||
end || ''
|
||||
end
|
||||
|
||||
# Fill the value of the "debit" column: if the invoice is a refund, returns the given amount, returns 0 otherwise
|
||||
def debit(invoice, amount)
|
||||
avoir = invoice.is_a? Avoir
|
||||
avoir ? amount.to_s : '0'
|
||||
avoir ? format_number(amount) : '0'
|
||||
end
|
||||
|
||||
# Fill the value of the "credit" column: if the invoice is a refund, returns 0, otherwise, returns the given amount
|
||||
def credit(invoice, amount)
|
||||
avoir = invoice.is_a? Avoir
|
||||
avoir ? '0' : amount.to_s
|
||||
avoir ? '0' : format_number(amount)
|
||||
end
|
||||
|
||||
# Fill the value of the "debit" column for the client row: if the invoice is a refund, returns 0, otherwise, returns the given amount
|
||||
@ -245,9 +223,22 @@ class AccountingExportService
|
||||
debit(invoice, amount)
|
||||
end
|
||||
|
||||
# Format the given text to match the accounting software rules for the labels
|
||||
def label(text)
|
||||
res = text.tr separator, ''
|
||||
res.truncate(50)
|
||||
# Format the given number as a string, using the configured separator
|
||||
def format_number(num)
|
||||
number_to_currency(num, unit: '', separator: decimal_separator, delimiter: '', precision: 2)
|
||||
end
|
||||
|
||||
# Create a text from the given invoice, matching the accounting software rules for the labels
|
||||
def label(invoice)
|
||||
name = "#{invoice.invoicing_profile.last_name} #{invoice.invoicing_profile.first_name}".tr separator, ''
|
||||
reference = invoice.reference
|
||||
|
||||
items = invoice.subscription_invoice? ? [I18n.t('accounting_export.subscription')] : []
|
||||
items.push I18n.t("accounting_export.#{invoice.reservation.reservable_type}_reservation") if invoice.invoiced_type == 'Reservation'
|
||||
items.push I18n.t('accounting_export.wallet') if invoice.invoiced_type == 'WalletTransaction'
|
||||
|
||||
summary = items.join(' + ')
|
||||
res = "#{reference}, #{summary}"
|
||||
"#{name.truncate(label_max_length - res.length)}, #{res}"
|
||||
end
|
||||
end
|
||||
|
@ -1,3 +1,6 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# This class provides helper methods to deal with coupons
|
||||
class CouponService
|
||||
##
|
||||
# Apply the provided coupon, if active, to the given price. Usability tests will be run depending on the
|
||||
@ -54,4 +57,14 @@ class CouponService
|
||||
end
|
||||
price
|
||||
end
|
||||
|
||||
##
|
||||
# Compute the total amount of the given invoice, without the applied coupon
|
||||
# Invoice.total stores the amount payed by the customer, coupon deducted
|
||||
# @param invoice {Invoice} invoice object, its total before discount will be computed
|
||||
##
|
||||
def invoice_total_no_coupon(invoice)
|
||||
total = (invoice.invoice_items.map(&:amount).map(&:to_i).reduce(:+) or 0)
|
||||
total / 100.0
|
||||
end
|
||||
end
|
||||
|
21
app/services/footprint_service.rb
Normal file
21
app/services/footprint_service.rb
Normal file
@ -0,0 +1,21 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# Provides helper methods to compute footprints
|
||||
class FootprintService
|
||||
# Compute the footprint
|
||||
# @param class_name Invoice|InvoiceItem|HistoryValue
|
||||
# @param item an instance of the provided class
|
||||
# @param sort the items in database by the provided criterion, to find the previous one
|
||||
def self.compute_footprint(klass, item, sort_on = 'id')
|
||||
raise TypeError unless item.is_a? klass
|
||||
|
||||
previous = klass.where("#{sort_on} < ?", item[sort_on])
|
||||
.order("#{sort_on} DESC")
|
||||
.limit(1)
|
||||
|
||||
columns = klass.columns.map(&:name)
|
||||
.delete_if { |c| %w[footprint updated_at].include? c }
|
||||
|
||||
Checksum.text("#{columns.map { |c| item[c] }.join}#{previous.first ? previous.first.footprint : ''}")
|
||||
end
|
||||
end
|
135
app/services/invoice_reference_service.rb
Normal file
135
app/services/invoice_reference_service.rb
Normal file
@ -0,0 +1,135 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# Provides methods to generate invoice references
|
||||
class InvoiceReferenceService
|
||||
class << self
|
||||
def generate_reference(invoice, date: Time.now, avoir: false)
|
||||
pattern = Setting.find_by(name: 'invoice_reference').value
|
||||
|
||||
reference = replace_invoice_number_pattern(pattern, invoice)
|
||||
reference = replace_date_pattern(reference, date)
|
||||
|
||||
if avoir
|
||||
# information about refund/avoir (R[text])
|
||||
reference.gsub!(/R\[([^\]]+)\]/, '\1')
|
||||
|
||||
# remove information about online selling (X[text])
|
||||
reference.gsub!(/X\[([^\]]+)\]/, ''.to_s)
|
||||
else
|
||||
# information about online selling (X[text])
|
||||
if invoice.paid_with_stripe?
|
||||
reference.gsub!(/X\[([^\]]+)\]/, '\1')
|
||||
else
|
||||
reference.gsub!(/X\[([^\]]+)\]/, ''.to_s)
|
||||
end
|
||||
|
||||
# remove information about refunds (R[text])
|
||||
reference.gsub!(/R\[([^\]]+)\]/, ''.to_s)
|
||||
end
|
||||
|
||||
reference
|
||||
end
|
||||
|
||||
def generate_order_number(invoice)
|
||||
pattern = Setting.find_by(name: 'invoice_order-nb').value
|
||||
|
||||
# global invoice number (nn..nn)
|
||||
reference = pattern.gsub(/n+(?![^\[]*\])/) do |match|
|
||||
pad_and_truncate(number_of_invoices(invoice, 'global'), match.to_s.length)
|
||||
end
|
||||
|
||||
reference = replace_invoice_number_pattern(reference, invoice)
|
||||
replace_date_pattern(reference, invoice.created_at)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
##
|
||||
# Output the given integer with leading zeros. If the given value is longer than the given
|
||||
# length, it will be truncated.
|
||||
# @param value {Integer} the integer to pad
|
||||
# @param length {Integer} the length of the resulting string.
|
||||
##
|
||||
def pad_and_truncate(value, length)
|
||||
value.to_s.rjust(length, '0').gsub(/^.*(.{#{length},}?)$/m, '\1')
|
||||
end
|
||||
|
||||
##
|
||||
# Returns the number of current invoices in the given range around the current date.
|
||||
# If range is invalid or not specified, the total number of invoices is returned.
|
||||
# @param invoice {Invoice}
|
||||
# @param range {String} 'day', 'month', 'year'
|
||||
# @return {Integer}
|
||||
##
|
||||
def number_of_invoices(invoice, range)
|
||||
case range.to_s
|
||||
when 'day'
|
||||
start = DateTime.current.beginning_of_day
|
||||
ending = DateTime.current.end_of_day
|
||||
when 'month'
|
||||
start = DateTime.current.beginning_of_month
|
||||
ending = DateTime.current.end_of_month
|
||||
when 'year'
|
||||
start = DateTime.current.beginning_of_year
|
||||
ending = DateTime.current.end_of_year
|
||||
else
|
||||
return invoice.id
|
||||
end
|
||||
return Invoice.count unless defined? start && defined? ending
|
||||
|
||||
Invoice.where('created_at >= :start_date AND created_at < :end_date', start_date: start, end_date: ending).length
|
||||
end
|
||||
|
||||
##
|
||||
# Replace the date elements in the provided pattern with the date values, from the provided date
|
||||
# @param reference {string}
|
||||
# @param date {DateTime}
|
||||
##
|
||||
def replace_date_pattern(reference, date)
|
||||
copy = reference.dup
|
||||
|
||||
# full year (YYYY)
|
||||
copy.gsub!(/YYYY(?![^\[]*\])/, date.strftime('%Y'))
|
||||
# year without century (YY)
|
||||
copy.gsub!(/YY(?![^\[]*\])/, date.strftime('%y'))
|
||||
|
||||
# abbreviated month name (MMM)
|
||||
copy.gsub!(/MMM(?![^\[]*\])/, date.strftime('%^b'))
|
||||
# month of the year, zero-padded (MM)
|
||||
copy.gsub!(/MM(?![^\[]*\])/, date.strftime('%m'))
|
||||
# month of the year, non zero-padded (M)
|
||||
copy.gsub!(/M(?![^\[]*\])/, date.strftime('%-m'))
|
||||
|
||||
# day of the month, zero-padded (DD)
|
||||
copy.gsub!(/DD(?![^\[]*\])/, date.strftime('%d'))
|
||||
# day of the month, non zero-padded (DD)
|
||||
copy.gsub!(/DD(?![^\[]*\])/, date.strftime('%-d'))
|
||||
|
||||
copy
|
||||
end
|
||||
|
||||
##
|
||||
# Replace the invoice number elements in the provided pattern with counts from the database
|
||||
# @param reference {string}
|
||||
# @param invoice {Invoice}
|
||||
##
|
||||
def replace_invoice_number_pattern(reference, invoice)
|
||||
copy = reference.dup
|
||||
|
||||
# invoice number per year (yy..yy)
|
||||
copy.gsub!(/y+(?![^\[]*\])/) do |match|
|
||||
pad_and_truncate(number_of_invoices(invoice, 'year'), match.to_s.length)
|
||||
end
|
||||
# invoice number per month (mm..mm)
|
||||
copy.gsub!(/m+(?![^\[]*\])/) do |match|
|
||||
pad_and_truncate(number_of_invoices(invoice, 'month'), match.to_s.length)
|
||||
end
|
||||
# invoice number per day (dd..dd)
|
||||
copy.gsub!(/d+(?![^\[]*\])/) do |match|
|
||||
pad_and_truncate(number_of_invoices(invoice, 'day'), match.to_s.length)
|
||||
end
|
||||
|
||||
copy
|
||||
end
|
||||
end
|
||||
end
|
@ -131,10 +131,12 @@ class StatisticService
|
||||
next if i.invoice.is_a?(Avoir)
|
||||
|
||||
sub = i.subscription
|
||||
|
||||
next unless sub
|
||||
|
||||
ca = i.amount.to_i / 100.0
|
||||
ca = CouponService.new.ventilate(get_invoice_total_no_coupon(i.invoice), ca, i.invoice.coupon) unless i.invoice.coupon_id.nil?
|
||||
cs = CouponService.new
|
||||
ca = cs.ventilate(cs.invoice_total_no_coupon(i.invoice), ca, i.invoice.coupon) unless i.invoice.coupon_id.nil?
|
||||
profile = sub.statistic_profile
|
||||
p = sub.plan
|
||||
result.push OpenStruct.new({
|
||||
@ -396,7 +398,8 @@ class StatisticService
|
||||
end
|
||||
end
|
||||
# subtract coupon discount from invoices and refunds
|
||||
ca = CouponService.new.ventilate(get_invoice_total_no_coupon(invoice), ca, invoice.coupon) unless invoice.coupon_id.nil?
|
||||
cs = CouponService.new
|
||||
ca = cs.ventilate(cs.invoice_total_no_coupon(invoice), ca, invoice.coupon) unless invoice.coupon_id.nil?
|
||||
# divide the result by 100 to convert from centimes to monetary unit
|
||||
ca.zero? ? ca : ca / 100.0
|
||||
end
|
||||
@ -407,7 +410,8 @@ class StatisticService
|
||||
ca -= ii.amount.to_i
|
||||
end
|
||||
# subtract coupon discount from the refund
|
||||
ca = CouponService.new.ventilate(get_invoice_total_no_coupon(invoice), ca, invoice.coupon) unless invoice.coupon_id.nil?
|
||||
cs = CouponService.new
|
||||
ca = cs.ventilate(cs.invoice_total_no_coupon(invoice), ca, invoice.coupon) unless invoice.coupon_id.nil?
|
||||
ca.zero? ? ca : ca / 100.0
|
||||
end
|
||||
|
||||
@ -488,11 +492,6 @@ class StatisticService
|
||||
# user_subscription_ca.inject {|sum,x| sum.ca + x.ca } || 0
|
||||
# end
|
||||
|
||||
def get_invoice_total_no_coupon(invoice)
|
||||
total = (invoice.invoice_items.map(&:amount).map(&:to_i).reduce(:+) or 0)
|
||||
total / 100.0
|
||||
end
|
||||
|
||||
def find_or_create_user_info_info_list(profile, list)
|
||||
found = list.select do |l|
|
||||
l.statistic_profile_id == profile.id
|
||||
|
@ -1,3 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'abstract_controller'
|
||||
require 'action_controller'
|
||||
require 'action_view'
|
||||
@ -13,7 +15,7 @@ class StatisticsExportService
|
||||
# query all stats with range arguments
|
||||
query = MultiJson.load(export.query)
|
||||
|
||||
@results = Elasticsearch::Model.client.search({index: 'stats', scroll: '30s', body: query})
|
||||
@results = Elasticsearch::Model.client.search(index: 'stats', scroll: '30s', body: query)
|
||||
scroll_id = @results['_scroll_id']
|
||||
while @results['hits']['hits'].size != @results['hits']['total']
|
||||
scroll_res = Elasticsearch::Model.client.scroll(scroll: '30s', scroll_id: scroll_id)
|
||||
@ -22,9 +24,9 @@ class StatisticsExportService
|
||||
end
|
||||
|
||||
ids = @results['hits']['hits'].map { |u| u['_source']['userId'] }
|
||||
@users = User.includes(:profile).where(:id => ids)
|
||||
@users = User.includes(:profile).where(id: ids)
|
||||
|
||||
@indices = StatisticIndex.all.includes(:statistic_fields, :statistic_types => [:statistic_sub_types])
|
||||
@indices = StatisticIndex.all.includes(:statistic_fields, statistic_types: [:statistic_sub_types])
|
||||
|
||||
ActionController::Base.prepend_view_path './app/views/'
|
||||
# place data in view_assigns
|
||||
@ -37,10 +39,10 @@ class StatisticsExportService
|
||||
|
||||
content = av.render template: 'exports/statistics_global.xlsx.axlsx'
|
||||
# write content to file
|
||||
File.open(export.file,"w+b") {|f| f.puts content }
|
||||
File.open(export.file, 'w+b') { |f| f.puts content }
|
||||
end
|
||||
|
||||
%w(account event machine project subscription training space).each do |path|
|
||||
%w[account event machine project subscription training space].each do |path|
|
||||
class_eval %{
|
||||
def export_#{path}(export)
|
||||
|
||||
@ -76,7 +78,7 @@ class StatisticsExportService
|
||||
# write content to file
|
||||
File.open(export.file,"w+b") {|f| f.puts content }
|
||||
end
|
||||
}
|
||||
}, __FILE__, __LINE__ - 35
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
|
@ -26,13 +26,21 @@ class VatHistoryService
|
||||
private
|
||||
|
||||
def vat_history
|
||||
key_dates = []
|
||||
Setting.find_by(name: 'invoice_VAT-rate').history_values.each do |rate|
|
||||
key_dates.push(date: rate.created_at, rate: rate.value.to_i)
|
||||
chronology = []
|
||||
end_date = DateTime.now
|
||||
Setting.find_by(name: 'invoice_VAT-active').history_values.order(created_at: 'DESC').each do |v|
|
||||
chronology.push(start: v.created_at, end: end_date, enabled: v.value == 'true')
|
||||
end_date = v.created_at
|
||||
end
|
||||
Setting.find_by(name: 'invoice_VAT-active').history_values.each do |v|
|
||||
key_dates.push(date: v.created_at, rate: 0) if v.value == 'false'
|
||||
date_rates = []
|
||||
Setting.find_by(name: 'invoice_VAT-rate').history_values.order(created_at: 'ASC').each do |rate|
|
||||
range = chronology.select { |p| rate.created_at.between?(p[:start], p[:end]) }.first
|
||||
date = range[:enabled] ? rate.created_at : range[:end]
|
||||
date_rates.push(date: date, rate: rate.value.to_i)
|
||||
end
|
||||
key_dates.sort_by { |k| k[:date] }
|
||||
chronology.reverse_each do |period|
|
||||
date_rates.push(date: period[:start], rate: 0) unless period[:enabled]
|
||||
end
|
||||
date_rates.sort_by { |k| k[:date] }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -51,7 +51,7 @@ class WalletService
|
||||
avoir.avoir_date = avoir_date
|
||||
avoir.created_at = avoir_date
|
||||
avoir.description = description
|
||||
avoir.avoir_mode = 'wallet'
|
||||
avoir.payment_method = 'wallet'
|
||||
avoir.subscription_to_expire = false
|
||||
avoir.invoicing_profile_id = wallet_transaction.wallet.user.invoicing_profile.id
|
||||
avoir.total = wallet_transaction.amount * 100.0
|
||||
|
@ -1,4 +1,4 @@
|
||||
json.extract! @avoir, :id, :created_at, :reference, :invoiced_type, :avoir_date, :avoir_mode, :invoice_id
|
||||
json.extract! @avoir, :id, :created_at, :reference, :invoiced_type, :avoir_date, :payment_method, :invoice_id
|
||||
json.user_id @avoir.invoicing_profile.user_id
|
||||
json.total @avoir.total / 100.00
|
||||
json.name @avoir.user.profile.full_name
|
||||
|
1
app/views/api/invoices/first.json.jbuilder
Normal file
1
app/views/api/invoices/first.json.jbuilder
Normal file
@ -0,0 +1 @@
|
||||
json.date @first
|
11
app/views/api/settings/bulk_update.json.jbuilder
Normal file
11
app/views/api/settings/bulk_update.json.jbuilder
Normal file
@ -0,0 +1,11 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
json.settings @settings.each do |setting|
|
||||
if setting[:errors]
|
||||
json.error setting.errors.full_messages
|
||||
json.id setting.id
|
||||
json.name setting.name
|
||||
else
|
||||
json.partial! 'api/settings/setting', setting: setting
|
||||
end
|
||||
end
|
@ -1,3 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
@settings.each do |setting|
|
||||
json.set! setting.name, setting.value
|
||||
end
|
||||
|
@ -1,3 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
json.setting do
|
||||
json.partial! 'api/settings/setting', setting: @setting
|
||||
end
|
||||
|
@ -5,6 +5,6 @@
|
||||
<%= t(".body.#{@attached_object.category}_#{@attached_object.export_type}") %>.
|
||||
</p>
|
||||
<p>
|
||||
<%= t('.body.click_to_download') %>
|
||||
<%= t('.body.click_to_download', TYPE: t(".body.file_type.#{@attached_object.extension}")) %>
|
||||
<%=link_to( t('.body.here'), "#{root_url}api/exports/#{@attached_object.id}/download", target: "_blank" )%>
|
||||
</p>
|
||||
</p>
|
||||
|
26
app/workers/accounting_export_worker.rb
Normal file
26
app/workers/accounting_export_worker.rb
Normal file
@ -0,0 +1,26 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# Asynchronously export the accounting data (Invoices & Avoirs) to an external accounting software
|
||||
class AccountingExportWorker
|
||||
include Sidekiq::Worker
|
||||
|
||||
def perform(export_id)
|
||||
export = Export.find(export_id)
|
||||
|
||||
raise SecurityError, 'Not allowed to export' unless export.user.admin?
|
||||
|
||||
data = JSON.parse(export.query)
|
||||
service = AccountingExportService.new(
|
||||
data['columns'],
|
||||
encoding: data['encoding'], format: export.extension, separator: export.key
|
||||
)
|
||||
service.set_options(date_format: data['date_format'], decimal_separator: data['decimal_separator'],
|
||||
label_max_length: data['label_max_length'], export_zeros: data['export_invoices_at_zero'])
|
||||
|
||||
service.export(data['start_date'], data['end_date'], export.file)
|
||||
|
||||
NotificationCenter.call type: :notify_admin_export_complete,
|
||||
receiver: export.user,
|
||||
attached_object: export
|
||||
end
|
||||
end
|
@ -322,6 +322,7 @@ en:
|
||||
total_amount: "Total amount"
|
||||
total_including_all_taxes: "Total incl. all taxes"
|
||||
VAT_disabled: "VAT disabled"
|
||||
VAT_enabled: "VAT enabled"
|
||||
including_VAT: "Including VAT"
|
||||
including_total_excluding_taxes: "Including Total excl. taxes"
|
||||
including_amount_payed_on_ordering: "Including Amount payed on ordering"
|
||||
@ -432,6 +433,76 @@ en:
|
||||
period_START_END_closed_success: "The accounting period from {{START}} to {{END}} has been successfully closed. Archive generation is running, you'll be notified when it's done."
|
||||
failed_to_close_period: "An error occurred, unable to close the accounting period"
|
||||
no_periods: "No closings for now"
|
||||
accounting_codes: "Accounting codes"
|
||||
accounting_journal_code: "Journal code"
|
||||
general_journal_code: "Journal code"
|
||||
accounting_card_client_code: "Card clients code"
|
||||
card_client_code: "Accounting code for clients who paid by card"
|
||||
accounting_card_client_label: "Card clients label"
|
||||
card_client_label: "Account label for clients who paid by card"
|
||||
accounting_wallet_client_code: "Wallet clients code"
|
||||
wallet_client_code: "Accounting code for clients who paid by virtual wallet"
|
||||
accounting_wallet_client_label: "Wallet clients label"
|
||||
wallet_client_label: "Account label for clients who paid by virtual wallet"
|
||||
accounting_other_client_code: "Other means client code"
|
||||
other_client_code: "Accounting code for clients who paid using another payment means"
|
||||
accounting_other_client_label: "Other means client label"
|
||||
other_client_label: "Accounting label for clients who paid using another payment means"
|
||||
accounting_wallet_code: "Wallet code"
|
||||
general_wallet_code: "Accounting code for wallet credit"
|
||||
accounting_wallet_label: "Wallet label"
|
||||
general_wallet_label: "Account label for wallet credit"
|
||||
accounting_vat_code: "VAT code"
|
||||
general_vat_code: "Accounting code for VAT"
|
||||
accounting_vat_label: "VAT label"
|
||||
general_vat_label: "VAT account label"
|
||||
accounting_subscription_code: "Subscriptions code"
|
||||
general_subscription_code: "Accounting code for all subscriptions"
|
||||
accounting_subscription_label: "Subscriptions label"
|
||||
general_subscription_label: "Account label for all subscriptions"
|
||||
accounting_Machine_code: "Machines code"
|
||||
general_machine_code: "Accounting code for all machines"
|
||||
accounting_Machine_label: "Machine label"
|
||||
general_machine_label: "Account label for all machines"
|
||||
accounting_Training_code: "Trainings code"
|
||||
general_training_code: "Accounting code for all trainings"
|
||||
accounting_Training_label: "Trainings label"
|
||||
general_training_label: "Account label for all trainings"
|
||||
accounting_Event_code: "Events code"
|
||||
general_event_code: "Accounting code for all events"
|
||||
accounting_Event_label: "Events label"
|
||||
general_event_label: "Account label for all events"
|
||||
accounting_Space_code: "Space code"
|
||||
general_space_code: "Accounting code for all spaces"
|
||||
accounting_Space_label: "Spaces label"
|
||||
general_space_label: "Account label for all spaces"
|
||||
codes_customization_success: "Customization of the accounting codes successfully saved."
|
||||
export_accounting_data: "Export accounting data"
|
||||
export_to: "Export to the accounting software"
|
||||
export_is_running: "Export is running. You'll be notified when it's ready."
|
||||
acd: "ACD"
|
||||
export_form_date: "Export from"
|
||||
export_to_date: "Export until"
|
||||
format: "File format"
|
||||
encoding: "Encoding"
|
||||
separator: "Separator"
|
||||
dateFormat: "Date format"
|
||||
labelMaxLength: "Label (max)"
|
||||
decimalSeparator: "Decimal separator"
|
||||
exportInvoicesAtZero: "Export invoices equal to 0"
|
||||
columns: "Columns"
|
||||
exportColumns:
|
||||
journal_code: "Journal code"
|
||||
date: "Entry date"
|
||||
account_code: "Account code"
|
||||
account_label: "Account label"
|
||||
piece: "Document"
|
||||
line_label: "Entry label"
|
||||
debit_origin: "Origin debit"
|
||||
credit_origin: "Origin credit"
|
||||
debit_euro: "Euro debit"
|
||||
credit_euro: "Euro credit"
|
||||
lettering: "Lettering"
|
||||
|
||||
members:
|
||||
# management of users, labels, groups, and so on
|
||||
|
@ -321,8 +321,9 @@ es:
|
||||
machine_booking-3D_printer: "Reserva de la máquina- Impresora 3D"
|
||||
total_amount: "Cantidad total"
|
||||
total_including_all_taxes: "Total incl. todos los impuestos"
|
||||
VAT_disabled: "VAT disabled"
|
||||
including_VAT: "IVA desactivado"
|
||||
VAT_disabled: "IVA desactivado"
|
||||
VAT_enabled: "IVA activado"
|
||||
including_VAT: "Incluido IVA"
|
||||
including_total_excluding_taxes: "Incluido Total excl. impuestos"
|
||||
including_amount_payed_on_ordering: "Incluido el monto pagado en el pedido"
|
||||
settlement_by_debit_card_on_DATE_at_TIME_for_an_amount_of_AMOUNT: "Liquidación por tarjeta de débito el {{DATE}} a las {{TIME}}, por una cantidad de {{AMOUNT}}"
|
||||
@ -432,6 +433,76 @@ es:
|
||||
period_START_END_closed_success: "The accounting period from {{START}} to {{END}} has been successfully closed. Archive generation is running, you'll be notified when it's done." # translation_missing
|
||||
failed_to_close_period: "An error occurred, unable to close the accounting period" # translation_missing
|
||||
no_periods: "No closings for now" # translation_missing
|
||||
accounting_codes: "Accounting codes" # translation_missing
|
||||
accounting_journal_code: "Journal code" # translation_missing
|
||||
general_journal_code: "Journal code" # translation_missing
|
||||
accounting_card_client_code: "Card clients code" # translation_missing
|
||||
card_client_code: "Accounting code for clients who paid by card" # translation_missing
|
||||
accounting_card_client_label: "Card clients label" # translation_missing
|
||||
card_client_label: "Account label for clients who paid by card" # translation_missing
|
||||
accounting_wallet_client_code: "Wallet clients code" # translation_missing
|
||||
wallet_client_code: "Accounting code for clients who paid by virtual wallet" # translation_missing
|
||||
accounting_wallet_client_label: "Wallet clients label" # translation_missing
|
||||
wallet_client_label: "Account label for clients who paid by virtual wallet" # translation_missing
|
||||
accounting_other_client_code: "Other means client code" # translation_missing
|
||||
other_client_code: "Accounting code for clients who paid using another payment means" # translation_missing
|
||||
accounting_other_client_label: "Other means client label" # translation_missing
|
||||
other_client_label: "Accounting label for clients who paid using another payment means" # translation_missing
|
||||
accounting_wallet_code: "Wallet code" # translation_missing
|
||||
general_wallet_code: "Accounting code for wallet credit" # translation_missing
|
||||
accounting_wallet_label: "Wallet label" # translation_missing
|
||||
general_wallet_label: "Account label for wallet credit" # translation_missing
|
||||
accounting_vat_code: "VAT code" # translation_missing
|
||||
general_vat_code: "Accounting code for VAT" # translation_missing
|
||||
accounting_vat_label: "VAT label" # translation_missing
|
||||
general_vat_label: "VAT account label" # translation_missing
|
||||
accounting_subscription_code: "Subscriptions code" # translation_missing
|
||||
general_subscription_code: "Accounting code for all subscriptions" # translation_missing
|
||||
accounting_subscription_label: "Subscriptions label" # translation_missing
|
||||
general_subscription_label: "Account label for all subscriptions" # translation_missing
|
||||
accounting_Machine_code: "Machines code" # translation_missing
|
||||
general_machine_code: "Accounting code for all machines" # translation_missing
|
||||
accounting_Machine_label: "Machine label" # translation_missing
|
||||
general_machine_label: "Account label for all machines" # translation_missing
|
||||
accounting_Training_code: "Trainings code" # translation_missing
|
||||
general_training_code: "Accounting code for all trainings" # translation_missing
|
||||
accounting_Training_label: "Trainings label" # translation_missing
|
||||
general_training_label: "Account label for all trainings" # translation_missing
|
||||
accounting_Event_code: "Events code" # translation_missing
|
||||
general_event_code: "Accounting code for all events" # translation_missing
|
||||
accounting_Event_label: "Events label" # translation_missing
|
||||
general_event_label: "Account label for all events" # translation_missing
|
||||
accounting_Space_code: "Space code" # translation_missing
|
||||
general_space_code: "Accounting code for all spaces" # translation_missing
|
||||
accounting_Space_label: "Spaces label" # translation_missing
|
||||
general_space_label: "Account label for all spaces" # translation_missing
|
||||
codes_customization_success: "Customization of accounting codes successfully saved." # angular interpolation # translation_missing
|
||||
export_accounting_data: "Export accounting data" # translation_missing
|
||||
export_to: "Export to the accounting software" # translation_missing
|
||||
export_is_running: "Exportando, será notificado cuando esté listo."
|
||||
acd: "ACD" # translation_missing
|
||||
export_form_date: "Export from" # translation_missing
|
||||
export_to_date: "Export until" # translation_missing
|
||||
format: "File format" # translation_missing
|
||||
encoding: "Encoding" # translation_missing
|
||||
separator: "Separator" # translation_missing
|
||||
dateFormat: "Date format" # translation_missing
|
||||
labelMaxLength: "Label maximum length" # translation_missing
|
||||
decimalSeparator: "Decimal separator" # translation_missing
|
||||
exportInvoicesAtZero: "Export invoices equal to 0" # translation_missing
|
||||
columns: "Columns" # translation_missing
|
||||
exportColumns: # translation_missing
|
||||
journal_code: "Journal code" # translation_missing
|
||||
date: "Entry date" # translation_missing
|
||||
account_code: "Account code" # translation_missing
|
||||
account_label: "Account label" # translation_missing
|
||||
piece: "Document" # translation_missing
|
||||
line_label: "Entry label" # translation_missing
|
||||
debit_origin: "Origin debit" # translation_missing
|
||||
credit_origin: "Origin credit" # translation_missing
|
||||
debit_euro: "Euro debit" # translation_missing
|
||||
credit_euro: "Euro credit" # translation_missing
|
||||
lettering: "Lettering" # translation_missing
|
||||
|
||||
members:
|
||||
# management of users, labels, groups, and so on
|
||||
|
@ -322,6 +322,7 @@ fr:
|
||||
total_amount: "Montant total"
|
||||
total_including_all_taxes: "Total TTC"
|
||||
VAT_disabled: "TVA désactivée"
|
||||
VAT_enabled: "TVA activée"
|
||||
including_VAT: "Dont TVA"
|
||||
including_total_excluding_taxes: "Dont total HT"
|
||||
including_amount_payed_on_ordering: "Dont montant payé à la commande"
|
||||
@ -432,6 +433,76 @@ fr:
|
||||
period_START_END_closed_success: "La période comptable du {{START}} au {{END}} a bien été clôturée. La génération de l'archive est en cours, vous serez prévenu lorsque celle-ci sera terminée."
|
||||
failed_to_close_period: "Une erreur est survenue, impossible de clôturer la période comptable"
|
||||
no_periods: "Aucune clôture pour le moment"
|
||||
accounting_codes: "Codes comptables"
|
||||
accounting_journal_code: "Code journal"
|
||||
general_journal_code: "Code journal"
|
||||
accounting_card_client_code: "Code clients par carte"
|
||||
card_client_code: "Code comptable pour les clients ayant réglé par carte bancaire"
|
||||
accounting_card_client_label: "Libellé clients par carte"
|
||||
card_client_label: "Libellé du compte pour les clients ayant réglé par carte bancaire"
|
||||
accounting_wallet_client_code: "Code clients par porte-monnaie"
|
||||
wallet_client_code: "Code comptable pour les clients ayant réglé par porte-monnaie virtuel"
|
||||
accounting_wallet_client_label: "Libellé clients par porte-monnaie"
|
||||
wallet_client_label: "Libellé du compte pour les clients ayant réglé par porte-monnaie virtuel"
|
||||
accounting_other_client_code: "Code clients autre moyen"
|
||||
other_client_code: "Code comptable pour les clients ayant avec un autre moyen de paiement"
|
||||
accounting_other_client_label: "Libellé clients autre moyen"
|
||||
other_client_label: "Libellé du compte pour les clients ayant réglé avec un autre moyen de paiement"
|
||||
accounting_wallet_code: "Code porte-monnaie"
|
||||
general_wallet_code: "Code comptable pour le crédit du porte-monnaie"
|
||||
accounting_wallet_label: "Libellé porte-monnaie"
|
||||
general_wallet_label: "Libellé du compte pour le crédit du porte-monnaie"
|
||||
accounting_vat_code: "Code TVA"
|
||||
general_vat_code: "Code comptable pour la TVA"
|
||||
accounting_vat_label: "Libellé TVA"
|
||||
general_vat_label: "Libellé du compte TVA"
|
||||
accounting_subscription_code: "Code abonnements"
|
||||
general_subscription_code: "Code comptable pour tous les abonnements"
|
||||
accounting_subscription_label: "Libellé abonnements"
|
||||
general_subscription_label: "Libellé du compte pour tous les abonnements"
|
||||
accounting_Machine_code: "Code machines"
|
||||
general_machine_code: "Code comptable pour toutes les machines"
|
||||
accounting_Machine_label: "Libellé machine"
|
||||
general_machine_label: "Libellé du compte pour toutes les machines"
|
||||
accounting_Training_code: "Code formations"
|
||||
general_training_code: "Code comptable pour toutes les formations"
|
||||
accounting_Training_label: "Libellé formations"
|
||||
general_training_label: "Libellé du compte pour toutes les formations"
|
||||
accounting_Event_code: "Code évènements"
|
||||
general_event_code: "Code comptable pour tous les évènements"
|
||||
accounting_Event_label: "Libellé évènements"
|
||||
general_event_label: "Libellé du compte pour tous les évènements"
|
||||
accounting_Space_code: "Code espaces"
|
||||
general_space_code: "Code comptable pour tous les espaces"
|
||||
accounting_Space_label: "Libellé espaces"
|
||||
general_space_label: "Libellé du compte pour tous les espaces"
|
||||
codes_customization_success: "La personnalisation des codes comptables a bien été enregistrée."
|
||||
export_accounting_data: "Exporter les données comptables"
|
||||
export_to: "Exporter vers le logiciel comptable"
|
||||
export_is_running: "L'export est en cours. Vous serez notifié lorsqu'il sera prêt."
|
||||
acd: "ACD"
|
||||
export_form_date: "Exporter depuis le"
|
||||
export_to_date: "Exporter jusqu'au"
|
||||
format: "Format de fichier"
|
||||
encoding: "Encodage"
|
||||
separator: "Séparateur"
|
||||
dateFormat: "Format de date"
|
||||
labelMaxLength: "Étiquette (max)"
|
||||
decimalSeparator: "Séparateur décimal"
|
||||
exportInvoicesAtZero: "Exporter les factures à 0"
|
||||
columns: "Colonnes"
|
||||
exportColumns:
|
||||
journal_code: "Code journal"
|
||||
date: "Date écriture"
|
||||
account_code: "Code compte"
|
||||
account_label: "Intitulé compte"
|
||||
piece: "Pièce"
|
||||
line_label: "Libellé écriture"
|
||||
debit_origin: "Débit origine"
|
||||
credit_origin: "Crédit origine"
|
||||
debit_euro: "Débit euro"
|
||||
credit_euro: "Crédit euro"
|
||||
lettering: "Lettrage"
|
||||
|
||||
members:
|
||||
# gestion des utilisateurs, des groupes, des étiquettes, etc.
|
||||
|
@ -321,8 +321,9 @@ pt:
|
||||
machine_booking-3D_printer: "Reserva de máquina - 3D printer"
|
||||
total_amount: "Montante total"
|
||||
total_including_all_taxes: "Total incluindo todas as taxas"
|
||||
VAT_disabled: "VAT desativado"
|
||||
including_VAT: "Incluindo VAT"
|
||||
VAT_disabled: "IVA desativado"
|
||||
VAT_enabled: "IVA activado"
|
||||
including_VAT: "Incluindo IVA"
|
||||
including_total_excluding_taxes: "Incluindo o total de taxas excluidas"
|
||||
including_amount_payed_on_ordering: "Incluindo o valor pago na compra"
|
||||
settlement_by_debit_card_on_DATE_at_TIME_for_an_amount_of_AMOUNT: "Pagamento por cartão de débito em {{DATE}} ás {{TIME}}, no valor de {{AMOUNT}}"
|
||||
@ -432,6 +433,76 @@ pt:
|
||||
period_START_END_closed_success: "The accounting period from {{START}} to {{END}} has been successfully closed. Archive generation is running, you'll be notified when it's done." # translation_missing
|
||||
failed_to_close_period: "An error occurred, unable to close the accounting period" # translation_missing
|
||||
no_periods: "No closings for now" # translation_missing
|
||||
accounting_codes: "Accounting codes" # translation_missing
|
||||
accounting_journal_code: "Journal code" # translation_missing
|
||||
general_journal_code: "Journal code" # translation_missing
|
||||
accounting_card_client_code: "Card clients code" # translation_missing
|
||||
card_client_code: "Accounting code for clients who paid by card" # translation_missing
|
||||
accounting_card_client_label: "Card clients label" # translation_missing
|
||||
card_client_label: "Account label for clients who paid by card" # translation_missing
|
||||
accounting_wallet_client_code: "Wallet clients code" # translation_missing
|
||||
wallet_client_code: "Accounting code for clients who paid by virtual wallet" # translation_missing
|
||||
accounting_wallet_client_label: "Wallet clients label" # translation_missing
|
||||
wallet_client_label: "Account label for clients who paid by virtual wallet" # translation_missing
|
||||
accounting_other_client_code: "Other payment client code" # translation_missing
|
||||
other_client_code: "Accounting code for clients who paid using another payment means" # translation_missing
|
||||
accounting_other_client_label: "Other payment client label" # translation_missing
|
||||
other_client_label: "Accounting label for clients who paid using another payment means" # translation_missing
|
||||
accounting_wallet_code: "Wallet code" # translation_missing
|
||||
general_wallet_code: "Accounting code for wallet credit" # translation_missing
|
||||
accounting_wallet_label: "Wallet label" # translation_missing
|
||||
general_wallet_label: "Account label for wallet credit" # translation_missing
|
||||
accounting_vat_code: "VAT code" # translation_missing
|
||||
general_vat_code: "Accounting code for VAT" # translation_missing
|
||||
accounting_vat_label: "VAT label" # translation_missing
|
||||
general_vat_label: "VAT account label" # translation_missing
|
||||
accounting_subscription_code: "Subscriptions code" # translation_missing
|
||||
general_subscription_code: "Accounting code for all subscriptions" # translation_missing
|
||||
accounting_subscription_label: "Subscriptions label" # translation_missing
|
||||
general_subscription_label: "Account label for all subscriptions" # translation_missing
|
||||
accounting_Machine_code: "Machines code" # translation_missing
|
||||
general_machine_code: "Accounting code for all machines" # translation_missing
|
||||
accounting_Machine_label: "Machine label" # translation_missing
|
||||
general_machine_label: "Account label for all machines" # translation_missing
|
||||
accounting_Training_code: "Trainings code" # translation_missing
|
||||
general_training_code: "Accounting code for all trainings" # translation_missing
|
||||
accounting_Training_label: "Trainings label" # translation_missing
|
||||
general_training_label: "Account label for all trainings" # translation_missing
|
||||
accounting_Event_code: "Events code" # translation_missing
|
||||
general_event_code: "Accounting code for all events" # translation_missing
|
||||
accounting_Event_label: "Events label" # translation_missing
|
||||
general_event_label: "Account label for all events" # translation_missing
|
||||
accounting_Space_code: "Space code" # translation_missing
|
||||
general_space_code: "Accounting code for all spaces" # translation_missing
|
||||
accounting_Space_label: "Spaces label" # translation_missing
|
||||
general_space_label: "Account label for all spaces" # translation_missing
|
||||
codes_customization_success: "Customization of accounting codes successfully saved." # angular interpolation # translation_missing
|
||||
export_accounting_data: "Export accounting data" # translation_missing
|
||||
export_to: "Export to the accounting software" # translation_missing
|
||||
export_is_running: "A Exportação está em andamento. Você será notificado quando terminar."
|
||||
acd: "ACD" # translation_missing
|
||||
export_form_date: "Export from" # translation_missing
|
||||
export_to_date: "Export until" # translation_missing
|
||||
format: "File format" # translation_missing
|
||||
encoding: "Encoding" # translation_missing
|
||||
separator: "Separator" # translation_missing
|
||||
dateFormat: "Date format" # translation_missing
|
||||
labelMaxLength: "Label maximum length" # translation_missing
|
||||
decimalSeparator: "Decimal separator" # translation_missing
|
||||
exportInvoicesAtZero: "Export invoices equal to 0" # translation_missing
|
||||
columns: "Columns" # translation_missing
|
||||
exportColumns: # translation_missing
|
||||
journal_code: "Journal code" # translation_missing
|
||||
date: "Entry date" # translation_missing
|
||||
account_code: "Account code" # translation_missing
|
||||
account_label: "Account label" # translation_missing
|
||||
piece: "Document" # translation_missing
|
||||
line_label: "Entry label" # translation_missing
|
||||
debit_origin: "Origin debit" # translation_missing
|
||||
credit_origin: "Origin credit" # translation_missing
|
||||
debit_euro: "Euro debit" # translation_missing
|
||||
credit_euro: "Euro credit" # translation_missing
|
||||
lettering: "Lettering" # translation_missing
|
||||
|
||||
members:
|
||||
# management of users, labels, groups, and so on
|
||||
|
@ -101,6 +101,7 @@ en:
|
||||
unlimited: "Unlimited"
|
||||
payment_card_error: "A problem occurred with your payment card:"
|
||||
online_payment_disabled: "Online payment is not available. Please contact the Fablab reception directly."
|
||||
unexpected_error_occurred: "An unexpected error occurred"
|
||||
|
||||
messages:
|
||||
you_will_lose_any_unsaved_modification_if_you_quit_this_page: "You will lose any unsaved modification if you quit this page"
|
||||
|
@ -101,6 +101,7 @@ es:
|
||||
unlimited: "Ilimitado"
|
||||
payment_card_error: "Hubo un problema con su tarjeta:"
|
||||
online_payment_disabled: "El pago en línea no está disponible. Póngase en contacto directamente con la recepción de Fablab."
|
||||
unexpected_error_occurred: "Ocurrió un error inesperado"
|
||||
|
||||
messages:
|
||||
you_will_lose_any_unsaved_modification_if_you_quit_this_page: "Si cierra la página se perderán todas las modificaciones que no se hayan guardado"
|
||||
|
@ -101,6 +101,7 @@ fr:
|
||||
unlimited: "Illimité"
|
||||
payment_card_error: "Un problème est survenu avec votre carte bancaire :"
|
||||
online_payment_disabled: "Le payment par carte bancaire n'est pas disponible. Merci de contacter directement l'accueil du Fablab."
|
||||
unexpected_error_occurred: "Une erreur inattendue est survenue"
|
||||
|
||||
messages:
|
||||
you_will_lose_any_unsaved_modification_if_you_quit_this_page: "Vous perdrez les modifications non enregistrées si vous quittez cette page"
|
||||
|
@ -101,6 +101,7 @@ pt:
|
||||
unlimited: "Ilimitado"
|
||||
payment_card_error: "A problem occurred with your payment card:" # translation_missing
|
||||
online_payment_disabled: "El pago en línea no está disponible. Póngase en contacto directamente con la recepción de Fablab."
|
||||
unexpected_error_occurred: "Um erro inesperado ocorreu"
|
||||
|
||||
messages:
|
||||
you_will_lose_any_unsaved_modification_if_you_quit_this_page: "Você irá perder todas as modificações não salvas se sair desta página"
|
||||
|
@ -113,8 +113,8 @@ en:
|
||||
by_cheque: "by cheque"
|
||||
by_transfer: "by transfer"
|
||||
by_cash: "by cash"
|
||||
no_refund: "No refund"
|
||||
by_wallet: "by wallet"
|
||||
no_refund: "No refund"
|
||||
settlement_by_debit_card: "Settlement by debit card"
|
||||
settlement_done_at_the_reception: "Settlement done at the reception"
|
||||
settlement_by_wallet: "Settlement by wallet"
|
||||
@ -126,6 +126,26 @@ en:
|
||||
subscription_of_NAME_extended_starting_from_STARTDATE_until_ENDDATE: "Subscription of %{NAME} extended (Free days) starting from %{STARTDATE} until %{ENDDATE}"
|
||||
and: 'and'
|
||||
|
||||
accounting_export:
|
||||
journal_code: "Journal code"
|
||||
date: "Entry date"
|
||||
account_code: "Account code"
|
||||
account_label: "Account label"
|
||||
piece: "Document"
|
||||
line_label: "Entry label"
|
||||
debit_origin: "Origin debit"
|
||||
credit_origin: "Origin credit"
|
||||
debit_euro: "Euro debit"
|
||||
credit_euro: "Euro credit"
|
||||
lettering: "Lettering"
|
||||
VAT: 'VAT'
|
||||
subscription: "subscr."
|
||||
Machine_reservation: "machine reserv."
|
||||
Training_reservation: "training reserv."
|
||||
Event_reservation: "event reserv."
|
||||
Space_reservation: "space reserv."
|
||||
wallet: "wallet"
|
||||
|
||||
trainings:
|
||||
# training availabilities
|
||||
i_ve_reserved: "I've reserved"
|
||||
@ -307,6 +327,7 @@ en:
|
||||
users_subscriptions: "of the subscriptions' list"
|
||||
users_reservations: "of the reservations' list"
|
||||
availabilities_index: "of the reservations availabilities"
|
||||
accounting_acd: "of the accounting data to ACD"
|
||||
is_over: "is over."
|
||||
download_here: "Download here"
|
||||
notify_member_about_coupon:
|
||||
|
@ -126,6 +126,26 @@ es:
|
||||
subscription_of_NAME_extended_starting_from_STARTDATE_until_ENDDATE: "Subscripción de %{NAME} extendida (Free days) empezando desde %{STARTDATE} hasta %{ENDDATE}"
|
||||
and: 'y'
|
||||
|
||||
accounting_export:
|
||||
journal_code: "Journal code" # translation_missing
|
||||
date: "Entry date" # translation_missing
|
||||
account_code: "Account code" # translation_missing
|
||||
account_label: "Account label" # translation_missing
|
||||
piece: "Document" # translation_missing
|
||||
line_label: "Entry label" # translation_missing
|
||||
debit_origin: "Origin debit" # translation_missing
|
||||
credit_origin: "Origin credit" # translation_missing
|
||||
debit_euro: "Euro debit" # translation_missing
|
||||
credit_euro: "Euro credit" # translation_missing
|
||||
lettering: "Lettering" # translation_missing
|
||||
VAT: 'IVA'
|
||||
subscription: "subscr." # translation_missing
|
||||
Machine_reservation: "machine reserv." # translation_missing
|
||||
Training_reservation: "training reserv." # translation_missing
|
||||
Event_reservation: "event reserv." # translation_missing
|
||||
Space_reservation: "space reserv." # translation_missing
|
||||
wallet: "wallet" # translation_missing
|
||||
|
||||
trainings:
|
||||
# training availabilities
|
||||
i_ve_reserved: "he reservado"
|
||||
@ -307,6 +327,7 @@ es:
|
||||
users_subscriptions: "de la lista de suscripciones"
|
||||
users_reservations: "de la lista de reservas"
|
||||
availabilities_index: "de las reservas disponibles"
|
||||
accounting_acd: "de los datos contables para ACD"
|
||||
is_over: "se ha acabado."
|
||||
download_here: "Descargar aquí"
|
||||
notify_member_about_coupon:
|
||||
|
@ -126,6 +126,26 @@ fr:
|
||||
subscription_of_NAME_extended_starting_from_STARTDATE_until_ENDDATE: "Prolongement Abonnement (Jours gratuits) de %{NAME} à compter du %{STARTDATE} jusqu'au %{ENDDATE}"
|
||||
and: 'et'
|
||||
|
||||
accounting_export:
|
||||
journal_code: "Code journal"
|
||||
date: "Date écriture"
|
||||
account_code: "Code compte"
|
||||
account_label: "Intitulé compte"
|
||||
piece: "Pièce"
|
||||
line_label: "Libellé écriture"
|
||||
debit_origin: "Débit origine"
|
||||
credit_origin: "Crédit origine"
|
||||
debit_euro: "Débit euro"
|
||||
credit_euro: "Crédit euro"
|
||||
lettering: "Lettrage"
|
||||
VAT: 'TVA'
|
||||
subscription: "abo."
|
||||
Machine_reservation: "réserv. machine"
|
||||
Training_reservation: "réserv. formation"
|
||||
Event_reservation: "réserv. évènement"
|
||||
Space_reservation: "réserv. espace"
|
||||
wallet: "porte-monnaie"
|
||||
|
||||
trainings:
|
||||
# disponibilités formations
|
||||
i_ve_reserved: "J'ai réservé"
|
||||
@ -307,6 +327,7 @@ fr:
|
||||
users_subscriptions: "de la liste des abonnements"
|
||||
users_reservations: "de la liste des réservations"
|
||||
availabilities_index: "des disponibilités de réservations"
|
||||
accounting_acd: "des données comptables pour ACD"
|
||||
is_over: "est terminé."
|
||||
download_here: "Téléchargez ici"
|
||||
notify_member_about_coupon:
|
||||
|
@ -267,8 +267,12 @@ en:
|
||||
users_subscriptions: "of the subscriptions' list"
|
||||
users_reservations: "of the reservations' list"
|
||||
availabilities_index: "of the reservations availabilities"
|
||||
accounting_accounting-software: "of the accounting data"
|
||||
click_to_download: "Excel file generated successfully. To download it, click"
|
||||
here: "here"
|
||||
file_type:
|
||||
xlsx: "Excel"
|
||||
csv: "CSV"
|
||||
|
||||
notify_member_about_coupon:
|
||||
subject: "Coupon"
|
||||
|
@ -266,8 +266,12 @@ es:
|
||||
users_subscriptions: "de la lista de suscripciones"
|
||||
users_reservations: "de la lista de reservas"
|
||||
availabilities_index: "de las reservas disponibles"
|
||||
accounting_accounting-software: "de los datos contables"
|
||||
click_to_download: " archivo Excel generado correctamente. Para descargarlo, haga clic "
|
||||
here: "aquí"
|
||||
file_type:
|
||||
xlsx: "Excel"
|
||||
csv: "CSV"
|
||||
|
||||
notify_member_about_coupon:
|
||||
subject: "Cupón"
|
||||
|
@ -267,8 +267,12 @@ fr:
|
||||
users_subscriptions: "de la liste des abonnements"
|
||||
users_reservations: "de la liste des réservations"
|
||||
availabilities_index: "des disponibilités de réservations"
|
||||
click_to_download: "La génération est terminée. Pour télécharger le fichier Excel, cliquez"
|
||||
accounting_accounting-software: "des données comptables"
|
||||
click_to_download: "La génération est terminée. Pour télécharger le fichier %{TYPE}, cliquez"
|
||||
here: "ici"
|
||||
file_type:
|
||||
xlsx: "Excel"
|
||||
csv: "CSV"
|
||||
|
||||
notify_member_about_coupon:
|
||||
subject: "Code promo"
|
||||
|
@ -267,8 +267,12 @@ pt:
|
||||
users_subscriptions: "da lista de assinaturas"
|
||||
users_reservations: "da lista de reservas"
|
||||
availabilities_index: "as reservas disponíveis"
|
||||
accounting_accounting-software: "de dados contábeis"
|
||||
click_to_download: "Arquivo do Excel gerado com êxito. Para fazer o download, clique"
|
||||
here: "aqui"
|
||||
file_type:
|
||||
xlsx: "Excel"
|
||||
csv: "CSV"
|
||||
|
||||
notify_member_about_coupon:
|
||||
subject: "Cupom"
|
||||
|
@ -126,6 +126,26 @@ pt:
|
||||
subscription_of_NAME_extended_starting_from_STARTDATE_until_ENDDATE: "Assinatura de %{NAME} estendida (dias livres) a partir de% STARTDATE até %{ENDDATE}"
|
||||
and: 'e'
|
||||
|
||||
accounting_export:
|
||||
journal_code: "Journal code" # translation_missing
|
||||
date: "Entry date" # translation_missing
|
||||
account_code: "Account code" # translation_missing
|
||||
account_label: "Account label" # translation_missing
|
||||
piece: "Document" # translation_missing
|
||||
line_label: "Entry label" # translation_missing
|
||||
debit_origin: "Origin debit" # translation_missing
|
||||
credit_origin: "Origin credit" # translation_missing
|
||||
debit_euro: "Euro debit" # translation_missing
|
||||
credit_euro: "Euro credit" # translation_missing
|
||||
lettering: "Lettering" # translation_missing
|
||||
VAT: 'IVA'
|
||||
subscription: "subscr." # translation_missing
|
||||
Machine_reservation: "machine reserv." # translation_missing
|
||||
Training_reservation: "training reserv." # translation_missing
|
||||
Event_reservation: "event reserv." # translation_missing
|
||||
Space_reservation: "space reserv." # translation_missing
|
||||
wallet: "wallet" # translation_missing
|
||||
|
||||
trainings:
|
||||
# training availabilities
|
||||
i_ve_reserved: "Eu reservei"
|
||||
@ -307,6 +327,7 @@ pt:
|
||||
users_subscriptions: "da lista de assinaturas"
|
||||
users_reservations: "da lista de reservas"
|
||||
availabilities_index: "de reservas disponíveis"
|
||||
accounting_acd: "de dados contábeis para ACD"
|
||||
is_over: "está finalizado."
|
||||
download_here: "Baixe aqui"
|
||||
notify_member_about_coupon:
|
||||
|
@ -43,7 +43,9 @@ Rails.application.routes.draw do
|
||||
resources :themes
|
||||
resources :licences
|
||||
resources :admins, only: %i[index create destroy]
|
||||
resources :settings, only: %i[show update index], param: :name
|
||||
resources :settings, only: %i[show update index], param: :name do
|
||||
patch '/bulk_update', action: 'bulk_update', on: :collection
|
||||
end
|
||||
resources :users, only: %i[index create]
|
||||
resources :members, only: %i[index show create update destroy] do
|
||||
get '/export_subscriptions', action: 'export_subscriptions', on: :collection
|
||||
@ -106,6 +108,7 @@ Rails.application.routes.draw do
|
||||
resources :invoices, only: %i[index show create] do
|
||||
get 'download', action: 'download', on: :member
|
||||
post 'list', action: 'list', on: :collection
|
||||
get 'first', action: 'first', on: :collection
|
||||
end
|
||||
|
||||
# for admin
|
||||
@ -135,6 +138,8 @@ Rails.application.routes.draw do
|
||||
get 'last_closing_end', on: :collection
|
||||
get 'archive', action: 'download_archive', on: :member
|
||||
end
|
||||
# export accounting data to csv or equivalent
|
||||
post 'accounting/export' => 'accounting_exports#export'
|
||||
|
||||
# i18n
|
||||
# regex allows using dots in URL for 'state'
|
||||
|
5
db/migrate/20190730085826_add_extension_to_export.rb
Normal file
5
db/migrate/20190730085826_add_extension_to_export.rb
Normal file
@ -0,0 +1,5 @@
|
||||
class AddExtensionToExport < ActiveRecord::Migration
|
||||
def change
|
||||
add_column :exports, :extension, :string, default: 'xlsx'
|
||||
end
|
||||
end
|
@ -0,0 +1,11 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# From this migration, this column will also be used to save the means of payment used to charge the customer
|
||||
# This is due to Strong Customer Authentication changes, that don't store any more an stp_invoice_id in table
|
||||
# "invoices". The new stp_payment_intent_id is not populated if the invoice total = 0 but we must know if the
|
||||
# payment was made on site or online.
|
||||
class RenameAvoirModeToPaymentMethodFromInvoices < ActiveRecord::Migration
|
||||
def change
|
||||
rename_column :invoices, :avoir_mode, :payment_method
|
||||
end
|
||||
end
|
@ -11,7 +11,7 @@
|
||||
#
|
||||
# It's strongly recommended that you check this file into your version control system.
|
||||
|
||||
ActiveRecord::Schema.define(version: 20190910141336) do
|
||||
ActiveRecord::Schema.define(version: 20190917123631) do
|
||||
|
||||
# These are extensions that must be enabled in order to support this database
|
||||
enable_extension "plpgsql"
|
||||
@ -202,10 +202,11 @@ ActiveRecord::Schema.define(version: 20190910141336) do
|
||||
t.string "category"
|
||||
t.string "export_type"
|
||||
t.string "query"
|
||||
t.datetime "created_at", null: false
|
||||
t.datetime "updated_at", null: false
|
||||
t.datetime "created_at", null: false
|
||||
t.datetime "updated_at", null: false
|
||||
t.integer "user_id"
|
||||
t.string "key"
|
||||
t.string "extension", default: "xlsx"
|
||||
end
|
||||
|
||||
add_index "exports", ["user_id"], name: "index_exports_on_user_id", using: :btree
|
||||
@ -267,7 +268,7 @@ ActiveRecord::Schema.define(version: 20190910141336) do
|
||||
t.datetime "created_at"
|
||||
t.datetime "updated_at"
|
||||
t.string "reference"
|
||||
t.string "avoir_mode"
|
||||
t.string "payment_method"
|
||||
t.datetime "avoir_date"
|
||||
t.integer "invoice_id"
|
||||
t.string "type"
|
||||
|
@ -3,7 +3,7 @@
|
||||
# Maintenance tasks
|
||||
namespace :fablab do
|
||||
namespace :maintenance do
|
||||
desc 'Regenerate the invoices PDF'
|
||||
desc 'Regenerate the invoices (invoices & avoirs) PDF'
|
||||
task :regenerate_invoices, %i[year month] => :environment do |_task, args|
|
||||
year = args.year || Time.current.year
|
||||
month = args.month || Time.current.month
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "fab-manager",
|
||||
"version": "4.1.1",
|
||||
"version": "4.2.0-dev",
|
||||
"description": "FabManager is the FabLab management solution. It is web-based, open-source and totally free.",
|
||||
"keywords": [
|
||||
"fablab",
|
||||
|
189
test/fixtures/history_values.yml
vendored
189
test/fixtures/history_values.yml
vendored
@ -369,3 +369,192 @@ value_history_38:
|
||||
created_at: 2018-12-31 10:22:25.116369000 Z
|
||||
updated_at: 2019-06-12 09:21:38.606818000 Z
|
||||
footprint: 085164a7288540c9beb0a6243856016fc36aae54bfb7d5d41af354650277d1ea
|
||||
|
||||
value_history_39:
|
||||
id: 39
|
||||
setting_id: 39
|
||||
invoicing_profile_id: 1
|
||||
value: '530'
|
||||
created_at: 2019-09-20 11:02:32.125400000 Z
|
||||
updated_at: 2019-09-20 11:02:32.125400000 Z
|
||||
footprint:
|
||||
|
||||
value_history_40:
|
||||
id: 40
|
||||
setting_id: 40
|
||||
invoicing_profile_id: 1
|
||||
value: '5801'
|
||||
created_at: 2019-09-20 11:02:32.125400000 Z
|
||||
updated_at: 2019-09-20 11:02:32.125400000 Z
|
||||
footprint:
|
||||
|
||||
value_history_41:
|
||||
id: 41
|
||||
setting_id: 41
|
||||
invoicing_profile_id: 1
|
||||
value: 'Client card'
|
||||
created_at: 2019-09-20 11:02:32.125400000 Z
|
||||
updated_at: 2019-09-20 11:02:32.125400000 Z
|
||||
footprint:
|
||||
|
||||
value_history_42:
|
||||
id: 42
|
||||
setting_id: 42
|
||||
invoicing_profile_id: 1
|
||||
value: '5802'
|
||||
created_at: 2019-09-20 11:02:32.125400000 Z
|
||||
updated_at: 2019-09-20 11:02:32.125400000 Z
|
||||
footprint:
|
||||
|
||||
value_history_43:
|
||||
id: 43
|
||||
setting_id: 43
|
||||
invoicing_profile_id: 1
|
||||
value: 'Client wallet'
|
||||
created_at: 2019-09-20 11:02:32.125400000 Z
|
||||
updated_at: 2019-09-20 11:02:32.125400000 Z
|
||||
footprint:
|
||||
|
||||
value_history_44:
|
||||
id: 44
|
||||
setting_id: 44
|
||||
invoicing_profile_id: 1
|
||||
value: '5803'
|
||||
created_at: 2019-09-20 11:02:32.125400000 Z
|
||||
updated_at: 2019-09-20 11:02:32.125400000 Z
|
||||
footprint:
|
||||
|
||||
value_history_45:
|
||||
id: 45
|
||||
setting_id: 45
|
||||
invoicing_profile_id: 1
|
||||
value: 'Client other'
|
||||
created_at: 2019-09-20 11:02:32.125400000 Z
|
||||
updated_at: 2019-09-20 11:02:32.125400000 Z
|
||||
footprint:
|
||||
|
||||
value_history_46:
|
||||
id: 46
|
||||
setting_id: 46
|
||||
invoicing_profile_id: 1
|
||||
value: '4091'
|
||||
created_at: 2019-09-20 11:02:32.125400000 Z
|
||||
updated_at: 2019-09-20 11:02:32.125400000 Z
|
||||
footprint:
|
||||
|
||||
value_history_47:
|
||||
id: 47
|
||||
setting_id: 47
|
||||
invoicing_profile_id: 1
|
||||
value: 'Wallet credit'
|
||||
created_at: 2019-09-20 11:02:32.125400000 Z
|
||||
updated_at: 2019-09-20 11:02:32.125400000 Z
|
||||
footprint:
|
||||
|
||||
value_history_48:
|
||||
id: 48
|
||||
setting_id: 48
|
||||
invoicing_profile_id: 1
|
||||
value: '445'
|
||||
created_at: 2019-09-20 11:02:32.125400000 Z
|
||||
updated_at: 2019-09-20 11:02:32.125400000 Z
|
||||
footprint:
|
||||
|
||||
value_history_49:
|
||||
id: 49
|
||||
setting_id: 49
|
||||
invoicing_profile_id: 1
|
||||
value: 'VAT'
|
||||
created_at: 2019-09-20 11:02:32.125400000 Z
|
||||
updated_at: 2019-09-20 11:02:32.125400000 Z
|
||||
footprint:
|
||||
|
||||
value_history_50:
|
||||
id: 50
|
||||
setting_id: 50
|
||||
invoicing_profile_id: 1
|
||||
value: '7061'
|
||||
created_at: 2019-09-20 11:02:32.125400000 Z
|
||||
updated_at: 2019-09-20 11:02:32.125400000 Z
|
||||
footprint:
|
||||
|
||||
value_history_51:
|
||||
id: 51
|
||||
setting_id: 51
|
||||
invoicing_profile_id: 1
|
||||
value: 'Subscription'
|
||||
created_at: 2019-09-20 11:02:32.125400000 Z
|
||||
updated_at: 2019-09-20 11:02:32.125400000 Z
|
||||
footprint:
|
||||
|
||||
value_history_52:
|
||||
id: 52
|
||||
setting_id: 52
|
||||
invoicing_profile_id: 1
|
||||
value: '7062'
|
||||
created_at: 2019-09-20 11:02:32.125400000 Z
|
||||
updated_at: 2019-09-20 11:02:32.125400000 Z
|
||||
footprint:
|
||||
|
||||
value_history_53:
|
||||
id: 53
|
||||
setting_id: 53
|
||||
invoicing_profile_id: 1
|
||||
value: 'Machine reservation'
|
||||
created_at: 2019-09-20 11:02:32.125400000 Z
|
||||
updated_at: 2019-09-20 11:02:32.125400000 Z
|
||||
footprint:
|
||||
|
||||
value_history_54:
|
||||
id: 54
|
||||
setting_id: 54
|
||||
invoicing_profile_id: 1
|
||||
value: '7063'
|
||||
created_at: 2019-09-20 11:02:32.125400000 Z
|
||||
updated_at: 2019-09-20 11:02:32.125400000 Z
|
||||
footprint:
|
||||
|
||||
value_history_55:
|
||||
id: 55
|
||||
setting_id: 55
|
||||
invoicing_profile_id: 1
|
||||
value: 'Training reservation'
|
||||
created_at: 2019-09-20 11:02:32.125400000 Z
|
||||
updated_at: 2019-09-20 11:02:32.125400000 Z
|
||||
footprint:
|
||||
|
||||
value_history_56:
|
||||
id: 56
|
||||
setting_id: 56
|
||||
invoicing_profile_id: 1
|
||||
value: '7064'
|
||||
created_at: 2019-09-20 11:02:32.125400000 Z
|
||||
updated_at: 2019-09-20 11:02:32.125400000 Z
|
||||
footprint:
|
||||
|
||||
value_history_57:
|
||||
id: 57
|
||||
setting_id: 57
|
||||
invoicing_profile_id: 1
|
||||
value: 'Event reservation'
|
||||
created_at: 2019-09-20 11:02:32.125400000 Z
|
||||
updated_at: 2019-09-20 11:02:32.125400000 Z
|
||||
footprint:
|
||||
|
||||
value_history_58:
|
||||
id: 58
|
||||
setting_id: 58
|
||||
invoicing_profile_id: 1
|
||||
value: '7065'
|
||||
created_at: 2019-09-20 11:02:32.125400000 Z
|
||||
updated_at: 2019-09-20 11:02:32.125400000 Z
|
||||
footprint:
|
||||
|
||||
value_history_59:
|
||||
id: 59
|
||||
setting_id: 59
|
||||
invoicing_profile_id: 1
|
||||
value: 'Space reservation'
|
||||
created_at: 2019-09-20 11:02:32.125400000 Z
|
||||
updated_at: 2019-09-20 11:02:32.125400000 Z
|
||||
footprint:
|
||||
|
10
test/fixtures/invoices.yml
vendored
10
test/fixtures/invoices.yml
vendored
@ -10,7 +10,7 @@ invoice_1:
|
||||
invoicing_profile_id: 3
|
||||
statistic_profile_id: 3
|
||||
reference: 1604001/VL
|
||||
avoir_mode:
|
||||
payment_method:
|
||||
avoir_date:
|
||||
invoice_id:
|
||||
type:
|
||||
@ -31,7 +31,7 @@ invoice_2:
|
||||
invoicing_profile_id: 4
|
||||
statistic_profile_id: 4
|
||||
reference: '1604002'
|
||||
avoir_mode:
|
||||
payment_method:
|
||||
avoir_date:
|
||||
invoice_id:
|
||||
type:
|
||||
@ -52,7 +52,7 @@ invoice_3:
|
||||
invoicing_profile_id: 7
|
||||
statistic_profile_id: 7
|
||||
reference: '1203001'
|
||||
avoir_mode:
|
||||
payment_method:
|
||||
avoir_date:
|
||||
invoice_id:
|
||||
type:
|
||||
@ -74,7 +74,7 @@ invoice_4:
|
||||
invoicing_profile_id: 7
|
||||
statistic_profile_id: 7
|
||||
reference: '1203002'
|
||||
avoir_mode:
|
||||
payment_method:
|
||||
avoir_date:
|
||||
invoice_id:
|
||||
type:
|
||||
@ -95,7 +95,7 @@ invoice_5:
|
||||
invoicing_profile_id: 3
|
||||
statistic_profile_id: 3
|
||||
reference: '1506031'
|
||||
avoir_mode:
|
||||
payment_method:
|
||||
avoir_date:
|
||||
invoice_id:
|
||||
type:
|
||||
|
126
test/fixtures/settings.yml
vendored
126
test/fixtures/settings.yml
vendored
@ -220,3 +220,129 @@ setting_38:
|
||||
name: privacy_draft
|
||||
created_at: 2019-06-12 13:25:08.125640000 Z
|
||||
updated_at: 2019-06-12 13:25:08.125640000 Z
|
||||
|
||||
setting_39:
|
||||
id: 39
|
||||
name: accounting_journal_code
|
||||
created_at: 2019-09-20 11:02:32.125400000 Z
|
||||
updated_at: 2019-09-20 11:02:32.125400000 Z
|
||||
|
||||
setting_40:
|
||||
id: 40
|
||||
name: accounting_card_client_code
|
||||
created_at: 2019-09-20 11:02:32.125400000 Z
|
||||
updated_at: 2019-09-20 11:02:32.125400000 Z
|
||||
|
||||
setting_41:
|
||||
id: 41
|
||||
name: accounting_card_client_label
|
||||
created_at: 2019-09-20 11:02:32.125400000 Z
|
||||
updated_at: 2019-09-20 11:02:32.125400000 Z
|
||||
|
||||
setting_42:
|
||||
id: 42
|
||||
name: accounting_wallet_client_code
|
||||
created_at: 2019-09-20 11:02:32.125400000 Z
|
||||
updated_at: 2019-09-20 11:02:32.125400000 Z
|
||||
|
||||
setting_43:
|
||||
id: 43
|
||||
name: accounting_wallet_client_label
|
||||
created_at: 2019-09-20 11:02:32.125400000 Z
|
||||
updated_at: 2019-09-20 11:02:32.125400000 Z
|
||||
|
||||
setting_44:
|
||||
id: 44
|
||||
name: accounting_other_client_code
|
||||
created_at: 2019-09-20 11:02:32.125400000 Z
|
||||
updated_at: 2019-09-20 11:02:32.125400000 Z
|
||||
|
||||
setting_45:
|
||||
id: 45
|
||||
name: accounting_other_client_label
|
||||
created_at: 2019-09-20 11:02:32.125400000 Z
|
||||
updated_at: 2019-09-20 11:02:32.125400000 Z
|
||||
|
||||
setting_46:
|
||||
id: 46
|
||||
name: accounting_wallet_code
|
||||
created_at: 2019-09-20 11:02:32.125400000 Z
|
||||
updated_at: 2019-09-20 11:02:32.125400000 Z
|
||||
|
||||
setting_47:
|
||||
id: 47
|
||||
name: accounting_wallet_label
|
||||
created_at: 2019-09-20 11:02:32.125400000 Z
|
||||
updated_at: 2019-09-20 11:02:32.125400000 Z
|
||||
|
||||
setting_48:
|
||||
id: 48
|
||||
name: accounting_VAT_code
|
||||
created_at: 2019-09-20 11:02:32.125400000 Z
|
||||
updated_at: 2019-09-20 11:02:32.125400000 Z
|
||||
|
||||
setting_49:
|
||||
id: 49
|
||||
name: accounting_VAT_label
|
||||
created_at: 2019-09-20 11:02:32.125400000 Z
|
||||
updated_at: 2019-09-20 11:02:32.125400000 Z
|
||||
|
||||
setting_50:
|
||||
id: 50
|
||||
name: accounting_subscription_code
|
||||
created_at: 2019-09-20 11:02:32.125400000 Z
|
||||
updated_at: 2019-09-20 11:02:32.125400000 Z
|
||||
|
||||
setting_51:
|
||||
id: 51
|
||||
name: accounting_subscription_label
|
||||
created_at: 2019-09-20 11:02:32.125400000 Z
|
||||
updated_at: 2019-09-20 11:02:32.125400000 Z
|
||||
|
||||
setting_52:
|
||||
id: 52
|
||||
name: accounting_Machine_label
|
||||
created_at: 2019-09-20 11:02:32.125400000 Z
|
||||
updated_at: 2019-09-20 11:02:32.125400000 Z
|
||||
|
||||
setting_53:
|
||||
id: 53
|
||||
name: accounting_Training_code
|
||||
created_at: 2019-09-20 11:02:32.125400000 Z
|
||||
updated_at: 2019-09-20 11:02:32.125400000 Z
|
||||
|
||||
setting_54:
|
||||
id: 54
|
||||
name: accounting_Training_label
|
||||
created_at: 2019-09-20 11:02:32.125400000 Z
|
||||
updated_at: 2019-09-20 11:02:32.125400000 Z
|
||||
|
||||
setting_55:
|
||||
id: 55
|
||||
name: accounting_Event_code
|
||||
created_at: 2019-09-20 11:02:32.125400000 Z
|
||||
updated_at: 2019-09-20 11:02:32.125400000 Z
|
||||
|
||||
setting_56:
|
||||
id: 56
|
||||
name: accounting_Event_label
|
||||
created_at: 2019-09-20 11:02:32.125400000 Z
|
||||
updated_at: 2019-09-20 11:02:32.125400000 Z
|
||||
|
||||
setting_57:
|
||||
id: 57
|
||||
name: accounting_Space_code
|
||||
created_at: 2019-09-20 11:02:32.125400000 Z
|
||||
updated_at: 2019-09-20 11:02:32.125400000 Z
|
||||
|
||||
setting_58:
|
||||
id: 58
|
||||
name: accounting_Machine_code
|
||||
created_at: 2019-09-20 11:02:32.125400000 Z
|
||||
updated_at: 2019-09-20 11:02:32.125400000 Z
|
||||
|
||||
setting_59:
|
||||
id: 59
|
||||
name: accounting_Space_label
|
||||
created_at: 2019-09-20 11:02:32.125400000 Z
|
||||
updated_at: 2019-09-20 11:02:32.125400000 Z
|
@ -1,3 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Credits
|
||||
class TrainingTest < ActionDispatch::IntegrationTest
|
||||
|
||||
@ -12,15 +14,15 @@ module Credits
|
||||
|
||||
# First, we create a new credit
|
||||
post '/api/credits',
|
||||
{
|
||||
credit: {
|
||||
creditable_id: 5,
|
||||
creditable_type: 'Machine',
|
||||
hours: 1,
|
||||
plan_id: 1,
|
||||
}
|
||||
}.to_json,
|
||||
default_headers
|
||||
{
|
||||
credit: {
|
||||
creditable_id: 5,
|
||||
creditable_type: 'Machine',
|
||||
hours: 1,
|
||||
plan_id: 1
|
||||
}
|
||||
}.to_json,
|
||||
default_headers
|
||||
|
||||
# Check response format & status
|
||||
assert_equal 201, response.status, response.body
|
||||
@ -37,15 +39,15 @@ module Credits
|
||||
|
||||
test 'update a credit' do
|
||||
put '/api/credits/13',
|
||||
{
|
||||
credit: {
|
||||
creditable_id: 4,
|
||||
creditable_type: 'Machine',
|
||||
hours: 5,
|
||||
plan_id: 3,
|
||||
}
|
||||
}.to_json,
|
||||
default_headers
|
||||
{
|
||||
credit: {
|
||||
creditable_id: 4,
|
||||
creditable_type: 'Machine',
|
||||
hours: 5,
|
||||
plan_id: 3
|
||||
}
|
||||
}.to_json,
|
||||
default_headers
|
||||
|
||||
# Check response format & status
|
||||
assert_equal 200, response.status, response.body
|
||||
|
141
test/integration/exports/accounting_export_test.rb
Normal file
141
test/integration/exports/accounting_export_test.rb
Normal file
@ -0,0 +1,141 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Exports; end
|
||||
|
||||
class Exports::AccountingExportTest < ActionDispatch::IntegrationTest
|
||||
|
||||
setup do
|
||||
admin = User.with_role(:admin).first
|
||||
login_as(admin, scope: :user)
|
||||
end
|
||||
|
||||
test 'creation modification reservation and re-modification scenario' do
|
||||
|
||||
# First, we create a new export
|
||||
post '/api/accounting/export',
|
||||
{
|
||||
query: {
|
||||
columns: %w[journal_code date account_code account_label piece line_label debit_origin credit_origin debit_euro credit_euro lettering],
|
||||
encoding: 'ISO-8859-1',
|
||||
date_format: '%d/%m/%Y',
|
||||
start_date: '2012-03-12T00:00:00.000Z',
|
||||
end_date: DateTime.now.utc.iso8601,
|
||||
label_max_length: 50,
|
||||
decimal_separator: ',',
|
||||
export_invoices_at_zero: false
|
||||
}.to_json.to_s,
|
||||
extension: 'csv',
|
||||
type: 'acd',
|
||||
key: ';'
|
||||
}.to_json,
|
||||
default_headers
|
||||
|
||||
# Check response format & status
|
||||
assert_equal 200, response.status, response.body
|
||||
assert_equal Mime::JSON, response.content_type
|
||||
|
||||
# Check the export was created correctly
|
||||
res = json_response(response.body)
|
||||
e = Export.where(id: res[:export_id]).first
|
||||
assert_not_nil e, 'Export was not created in database'
|
||||
|
||||
# Run the worker
|
||||
worker = AccountingExportWorker.new
|
||||
worker.perform(e.id)
|
||||
|
||||
# notification
|
||||
assert_not_empty Notification.where(attached_object: e)
|
||||
|
||||
# resulting CSV file
|
||||
assert FileTest.exist?(e.file), 'CSV file was not generated'
|
||||
require 'csv'
|
||||
data = CSV.read(e.file, headers: true, col_sep: e.key)
|
||||
|
||||
# test values
|
||||
# first line = client line
|
||||
journal_code = Setting.find_by(name: 'accounting_journal_code').value
|
||||
assert_equal journal_code, data[0][I18n.t('accounting_export.journal_code')], 'Wrong journal code'
|
||||
|
||||
first_invoice = Invoice.first
|
||||
entry_date = first_invoice.created_at.to_date
|
||||
assert_equal entry_date, DateTime.parse(data[0][I18n.t('accounting_export.date')]), 'Wrong date'
|
||||
|
||||
if first_invoice.paid_with_stripe?
|
||||
card_client_code = Setting.find_by(name: 'accounting_card_client_code').value
|
||||
assert_equal card_client_code, data[0][I18n.t('accounting_export.account_code')], 'Account code for card client is wrong'
|
||||
|
||||
card_client_label = Setting.find_by(name: 'accounting_card_client_label').value
|
||||
assert_equal card_client_label, data[0][I18n.t('accounting_export.account_label')], 'Account label for card client is wrong'
|
||||
else
|
||||
STDERR.puts "WARNING: unable to test accurately accounting export: invoice #{first_invoice.id} was not paid by card"
|
||||
end
|
||||
|
||||
assert_equal first_invoice.reference, data[0][I18n.t('accounting_export.piece')], 'Piece (invoice reference) is wrong'
|
||||
|
||||
if first_invoice.subscription_invoice?
|
||||
assert_match I18n.t('accounting_export.subscription'),
|
||||
data[0][I18n.t('accounting_export.line_label')],
|
||||
'Line label does not contains the reference to the invoiced item'
|
||||
else
|
||||
STDERR.puts "WARNING: unable to test accurately accounting export: invoice #{first_invoice.id} does not have a subscription"
|
||||
end
|
||||
|
||||
if first_invoice.wallet_transaction_id.nil?
|
||||
assert_equal first_invoice.total / 100.00, data[0][I18n.t('accounting_export.debit_origin')].to_f, 'Origin debit amount does not match'
|
||||
assert_equal first_invoice.total / 100.00, data[0][I18n.t('accounting_export.debit_euro')].to_f, 'Euro debit amount does not match'
|
||||
else
|
||||
STDERR.puts "WARNING: unable to test accurately accounting export: invoice #{first_invoice.id} is using wallet"
|
||||
end
|
||||
|
||||
assert_equal 0, data[0][I18n.t('accounting_export.credit_origin')].to_f, 'Credit origin amount does not match'
|
||||
assert_equal 0, data[0][I18n.t('accounting_export.credit_euro')].to_f, 'Credit euro amount does not match'
|
||||
|
||||
# second line = sold item line
|
||||
assert_equal journal_code, data[1][I18n.t('accounting_export.journal_code')], 'Wrong journal code'
|
||||
assert_equal entry_date, DateTime.parse(data[1][I18n.t('accounting_export.date')]), 'Wrong date'
|
||||
|
||||
if first_invoice.subscription_invoice?
|
||||
subscription_code = Setting.find_by(name: 'accounting_subscription_code').value
|
||||
assert_equal subscription_code, data[1][I18n.t('accounting_export.account_code')], 'Account code for subscription is wrong'
|
||||
|
||||
subscription_label = Setting.find_by(name: 'accounting_subscription_label').value
|
||||
assert_equal subscription_label, data[1][I18n.t('accounting_export.account_label')], 'Account label for subscription is wrong'
|
||||
end
|
||||
|
||||
assert_equal first_invoice.reference, data[1][I18n.t('accounting_export.piece')], 'Piece (invoice reference) is wrong'
|
||||
assert_nil data[1][I18n.t('accounting_export.line_label')], 'Line label should be empty for non client lines'
|
||||
|
||||
item = first_invoice.invoice_items.first
|
||||
assert_equal item.amount / 100.00, data[1][I18n.t('accounting_export.credit_origin')].to_f, 'Origin credit amount does not match'
|
||||
assert_equal item.amount / 100.00, data[1][I18n.t('accounting_export.credit_euro')].to_f, 'Euro credit amount does not match'
|
||||
|
||||
assert_equal 0, data[1][I18n.t('accounting_export.debit_origin')].to_f, 'Debit origin amount does not match'
|
||||
assert_equal 0, data[1][I18n.t('accounting_export.debit_euro')].to_f, 'Debit euro amount does not match'
|
||||
|
||||
# test with another invoice
|
||||
last_invoice = Invoice.last
|
||||
client_row = data[data.length - 2]
|
||||
item_row = data[data.length - 1]
|
||||
|
||||
if last_invoice.invoiced_type == 'Reservation' && last_invoice.invoiced.reservable_type == 'Machine'
|
||||
assert_match I18n.t('accounting_export.Machine_reservation'),
|
||||
client_row[I18n.t('accounting_export.line_label')],
|
||||
'Line label does not contains the reference to the invoiced item'
|
||||
|
||||
machine_code = Setting.find_by(name: 'accounting_Machine_code').value
|
||||
assert_equal machine_code, item_row[I18n.t('accounting_export.account_code')], 'Account code for machine reservation is wrong'
|
||||
|
||||
machine_label = Setting.find_by(name: 'accounting_Machine_label').value
|
||||
assert_equal machine_label, item_row[I18n.t('accounting_export.account_label')], 'Account label for machine reservation is wrong'
|
||||
|
||||
else
|
||||
STDERR.puts "WARNING: unable to test accurately accounting export: invoice #{last_invoice.id} is not a Machine reservation"
|
||||
end
|
||||
|
||||
|
||||
# Clean CSV file
|
||||
require 'fileutils'
|
||||
FileUtils.rm(e.file)
|
||||
end
|
||||
end
|
||||
|
@ -37,7 +37,7 @@ class InvoicesTest < ActionDispatch::IntegrationTest
|
||||
|
||||
post '/api/invoices', { avoir: {
|
||||
avoir_date: date,
|
||||
avoir_mode: 'cash',
|
||||
payment_method: 'cash',
|
||||
description: 'Lorem ipsum',
|
||||
invoice_id: 4,
|
||||
invoice_items_ids: [4],
|
||||
@ -54,7 +54,7 @@ class InvoicesTest < ActionDispatch::IntegrationTest
|
||||
|
||||
assert_dates_equal date, refund[:avoir_date]
|
||||
assert_dates_equal date, refund[:date]
|
||||
assert_equal 'cash', refund[:avoir_mode]
|
||||
assert_equal 'cash', refund[:payment_method]
|
||||
assert_equal false, refund[:has_avoir]
|
||||
assert_equal 4, refund[:invoice_id]
|
||||
assert_equal 4, refund[:items][0][:invoice_item_id]
|
||||
@ -70,7 +70,7 @@ class InvoicesTest < ActionDispatch::IntegrationTest
|
||||
|
||||
post '/api/invoices', { avoir: {
|
||||
avoir_date: date,
|
||||
avoir_mode: 'cash',
|
||||
payment_method: 'cash',
|
||||
description: 'Unable to refund',
|
||||
invoice_id: 5,
|
||||
invoice_items_ids: [5],
|
||||
|
Loading…
Reference in New Issue
Block a user