mirror of
https://github.com/LaCasemate/fab-manager.git
synced 2025-02-06 01:08:21 +01:00
Merge branch 'dev' for release 2.4.7
This commit is contained in:
commit
96259ab257
@ -1 +1 @@
|
|||||||
2.4.6
|
2.4.7
|
4
.gitignore
vendored
4
.gitignore
vendored
@ -15,6 +15,7 @@
|
|||||||
# Ignore all logfiles and tempfiles.
|
# Ignore all logfiles and tempfiles.
|
||||||
/log/*.log
|
/log/*.log
|
||||||
/tmp
|
/tmp
|
||||||
|
/coverage
|
||||||
|
|
||||||
/public/uploads
|
/public/uploads
|
||||||
/public/assets
|
/public/assets
|
||||||
@ -39,5 +40,8 @@
|
|||||||
.vagrant
|
.vagrant
|
||||||
.docker
|
.docker
|
||||||
|
|
||||||
|
# Do not versionate coveralls token
|
||||||
|
.coveralls.yml
|
||||||
|
|
||||||
# Plugins are versioned is their own repository
|
# Plugins are versioned is their own repository
|
||||||
/plugins/*
|
/plugins/*
|
||||||
|
11
CHANGELOG.md
11
CHANGELOG.md
@ -1,6 +1,15 @@
|
|||||||
# Changelog Fab Manager
|
# Changelog Fab Manager
|
||||||
|
|
||||||
## v2.4.6 2016 Novembre 30
|
## v2.4.7 2016 December 14
|
||||||
|
|
||||||
|
- Improved automated testing
|
||||||
|
- Added an information notice about the processing time of deleting an administrator
|
||||||
|
- Ability to change the expiration date of a coupon after its creation
|
||||||
|
- Ability to generate a refund invoice when crediting user's wallet
|
||||||
|
- Fix a bug: unable to run rake db:migrate on first install
|
||||||
|
- Fix a bug: unable to create or edit a coupon of type 'percentage'
|
||||||
|
|
||||||
|
## v2.4.6 2016 November 30
|
||||||
|
|
||||||
- Change display of message about coupon application status
|
- Change display of message about coupon application status
|
||||||
- Fix a bug: compute price API return error 500 if reservable_id is not provided
|
- Fix a bug: compute price API return error 500 if reservable_id is not provided
|
||||||
|
3
Gemfile
3
Gemfile
@ -52,6 +52,8 @@ group :development do
|
|||||||
gem 'capistrano-maintenance', '0.0.5', require: false
|
gem 'capistrano-maintenance', '0.0.5', require: false
|
||||||
|
|
||||||
gem 'active_record_query_trace'
|
gem 'active_record_query_trace'
|
||||||
|
|
||||||
|
gem 'coveralls', require: false
|
||||||
end
|
end
|
||||||
|
|
||||||
group :test do
|
group :test do
|
||||||
@ -62,6 +64,7 @@ group :test do
|
|||||||
gem 'webmock'
|
gem 'webmock'
|
||||||
gem 'vcr'
|
gem 'vcr'
|
||||||
gem 'byebug'
|
gem 'byebug'
|
||||||
|
gem 'pdf-reader'
|
||||||
end
|
end
|
||||||
|
|
||||||
group :production do
|
group :production do
|
||||||
|
29
Gemfile.lock
29
Gemfile.lock
@ -1,6 +1,7 @@
|
|||||||
GEM
|
GEM
|
||||||
remote: https://rubygems.org/
|
remote: https://rubygems.org/
|
||||||
specs:
|
specs:
|
||||||
|
Ascii85 (1.0.2)
|
||||||
aasm (4.1.0)
|
aasm (4.1.0)
|
||||||
actionmailer (4.2.5)
|
actionmailer (4.2.5)
|
||||||
actionpack (= 4.2.5)
|
actionpack (= 4.2.5)
|
||||||
@ -41,6 +42,7 @@ GEM
|
|||||||
thread_safe (~> 0.3, >= 0.3.4)
|
thread_safe (~> 0.3, >= 0.3.4)
|
||||||
tzinfo (~> 1.1)
|
tzinfo (~> 1.1)
|
||||||
addressable (2.3.8)
|
addressable (2.3.8)
|
||||||
|
afm (0.2.2)
|
||||||
ansi (1.5.0)
|
ansi (1.5.0)
|
||||||
api-pagination (4.3.0)
|
api-pagination (4.3.0)
|
||||||
apipie-rails (0.3.6)
|
apipie-rails (0.3.6)
|
||||||
@ -118,6 +120,12 @@ GEM
|
|||||||
sass-rails (<= 5.0.1)
|
sass-rails (<= 5.0.1)
|
||||||
sprockets (< 2.13)
|
sprockets (< 2.13)
|
||||||
connection_pool (2.2.0)
|
connection_pool (2.2.0)
|
||||||
|
coveralls (0.8.16)
|
||||||
|
json (>= 1.8, < 3)
|
||||||
|
simplecov (~> 0.12.0)
|
||||||
|
term-ansicolor (~> 1.3.0)
|
||||||
|
thor (~> 0.19.1)
|
||||||
|
tins (>= 1.6.0, < 2)
|
||||||
crack (0.4.3)
|
crack (0.4.3)
|
||||||
safe_yaml (~> 1.0.0)
|
safe_yaml (~> 1.0.0)
|
||||||
database_cleaner (1.4.1)
|
database_cleaner (1.4.1)
|
||||||
@ -133,6 +141,7 @@ GEM
|
|||||||
warden (~> 1.2.3)
|
warden (~> 1.2.3)
|
||||||
devise-async (0.9.0)
|
devise-async (0.9.0)
|
||||||
devise (~> 3.2)
|
devise (~> 3.2)
|
||||||
|
docile (1.1.5)
|
||||||
domain_name (0.5.25)
|
domain_name (0.5.25)
|
||||||
unf (>= 0.0.5, < 1.0.0)
|
unf (>= 0.0.5, < 1.0.0)
|
||||||
elasticsearch (1.0.12)
|
elasticsearch (1.0.12)
|
||||||
@ -177,6 +186,7 @@ GEM
|
|||||||
has_secure_token (1.0.0)
|
has_secure_token (1.0.0)
|
||||||
activerecord (>= 3.0)
|
activerecord (>= 3.0)
|
||||||
hashdiff (0.3.0)
|
hashdiff (0.3.0)
|
||||||
|
hashery (2.1.2)
|
||||||
hashie (3.4.2)
|
hashie (3.4.2)
|
||||||
highline (1.7.1)
|
highline (1.7.1)
|
||||||
hike (1.2.3)
|
hike (1.2.3)
|
||||||
@ -265,6 +275,12 @@ GEM
|
|||||||
httparty (~> 0.13)
|
httparty (~> 0.13)
|
||||||
orm_adapter (0.5.0)
|
orm_adapter (0.5.0)
|
||||||
pdf-core (0.5.1)
|
pdf-core (0.5.1)
|
||||||
|
pdf-reader (1.4.0)
|
||||||
|
Ascii85 (~> 1.0.0)
|
||||||
|
afm (~> 0.2.1)
|
||||||
|
hashery (~> 2.0)
|
||||||
|
ruby-rc4
|
||||||
|
ttfunk
|
||||||
pg (0.18.1)
|
pg (0.18.1)
|
||||||
pkg-config (1.1.7)
|
pkg-config (1.1.7)
|
||||||
prawn (2.0.1)
|
prawn (2.0.1)
|
||||||
@ -335,6 +351,7 @@ GEM
|
|||||||
netrc (~> 0.7)
|
netrc (~> 0.7)
|
||||||
rolify (4.0.0)
|
rolify (4.0.0)
|
||||||
ruby-progressbar (1.7.5)
|
ruby-progressbar (1.7.5)
|
||||||
|
ruby-rc4 (0.1.5)
|
||||||
rubyzip (1.1.7)
|
rubyzip (1.1.7)
|
||||||
rufus-scheduler (3.0.9)
|
rufus-scheduler (3.0.9)
|
||||||
tzinfo
|
tzinfo
|
||||||
@ -365,6 +382,11 @@ GEM
|
|||||||
sidekiq (>= 2.17.3)
|
sidekiq (>= 2.17.3)
|
||||||
tilt (< 2.0.0)
|
tilt (< 2.0.0)
|
||||||
simple_oauth (0.3.1)
|
simple_oauth (0.3.1)
|
||||||
|
simplecov (0.12.0)
|
||||||
|
docile (~> 1.1.0)
|
||||||
|
json (>= 1.8, < 3)
|
||||||
|
simplecov-html (~> 0.10.0)
|
||||||
|
simplecov-html (0.10.0)
|
||||||
sinatra (1.4.6)
|
sinatra (1.4.6)
|
||||||
rack (~> 1.4)
|
rack (~> 1.4)
|
||||||
rack-protection (~> 1.4)
|
rack-protection (~> 1.4)
|
||||||
@ -382,6 +404,8 @@ GEM
|
|||||||
stripe (1.30.2)
|
stripe (1.30.2)
|
||||||
json (~> 1.8.1)
|
json (~> 1.8.1)
|
||||||
rest-client (~> 1.4)
|
rest-client (~> 1.4)
|
||||||
|
term-ansicolor (1.3.2)
|
||||||
|
tins (~> 1.0)
|
||||||
test_after_commit (1.0.0)
|
test_after_commit (1.0.0)
|
||||||
activerecord (>= 3.2)
|
activerecord (>= 3.2)
|
||||||
therubyracer (0.12.0)
|
therubyracer (0.12.0)
|
||||||
@ -392,6 +416,7 @@ GEM
|
|||||||
tilt (1.4.1)
|
tilt (1.4.1)
|
||||||
timers (4.0.1)
|
timers (4.0.1)
|
||||||
hitimes
|
hitimes
|
||||||
|
tins (1.13.0)
|
||||||
ttfunk (1.4.0)
|
ttfunk (1.4.0)
|
||||||
twitter (5.14.0)
|
twitter (5.14.0)
|
||||||
addressable (~> 2.3)
|
addressable (~> 2.3)
|
||||||
@ -462,6 +487,7 @@ DEPENDENCIES
|
|||||||
chroma
|
chroma
|
||||||
coffee-rails (~> 4.1.0)
|
coffee-rails (~> 4.1.0)
|
||||||
compass-rails (= 2.0.4)
|
compass-rails (= 2.0.4)
|
||||||
|
coveralls
|
||||||
database_cleaner
|
database_cleaner
|
||||||
devise
|
devise
|
||||||
devise-async
|
devise-async
|
||||||
@ -488,6 +514,7 @@ DEPENDENCIES
|
|||||||
omniauth
|
omniauth
|
||||||
omniauth-oauth2
|
omniauth-oauth2
|
||||||
openlab_ruby
|
openlab_ruby
|
||||||
|
pdf-reader
|
||||||
pg
|
pg
|
||||||
prawn
|
prawn
|
||||||
prawn-table
|
prawn-table
|
||||||
@ -522,4 +549,4 @@ DEPENDENCIES
|
|||||||
webmock
|
webmock
|
||||||
|
|
||||||
BUNDLED WITH
|
BUNDLED WITH
|
||||||
1.12.5
|
1.13.1
|
||||||
|
@ -58,8 +58,8 @@ Application.Controllers.controller "NewCouponController", ["$scope", "$state", '
|
|||||||
##
|
##
|
||||||
# Controller used in the coupon edition page
|
# Controller used in the coupon edition page
|
||||||
##
|
##
|
||||||
Application.Controllers.controller "EditCouponController", ["$scope", "$state", 'Coupon', 'couponPromise', '_t'
|
Application.Controllers.controller "EditCouponController", ["$scope", "$state", 'Coupon', 'couponPromise', '_t', 'growl'
|
||||||
, ($scope, $state, Coupon, couponPromise, _t) ->
|
, ($scope, $state, Coupon, couponPromise, _t, growl) ->
|
||||||
|
|
||||||
### PUBLIC SCOPE ###
|
### PUBLIC SCOPE ###
|
||||||
|
|
||||||
@ -73,6 +73,9 @@ Application.Controllers.controller "EditCouponController", ["$scope", "$state",
|
|||||||
## Options for the validity per user
|
## Options for the validity per user
|
||||||
$scope.validities = userValidities
|
$scope.validities = userValidities
|
||||||
|
|
||||||
|
## Mapping for validation errors
|
||||||
|
$scope.errors = {}
|
||||||
|
|
||||||
## Default parameters for AngularUI-Bootstrap datepicker (used for coupon validity limit selection)
|
## Default parameters for AngularUI-Bootstrap datepicker (used for coupon validity limit selection)
|
||||||
$scope.datePicker =
|
$scope.datePicker =
|
||||||
format: Fablab.uibDateFormat
|
format: Fablab.uibDateFormat
|
||||||
@ -98,11 +101,12 @@ Application.Controllers.controller "EditCouponController", ["$scope", "$state",
|
|||||||
# Callback to save the coupon's changes to the API
|
# Callback to save the coupon's changes to the API
|
||||||
##
|
##
|
||||||
$scope.updateCoupon = ->
|
$scope.updateCoupon = ->
|
||||||
|
$scope.errors = {}
|
||||||
Coupon.update {id: $scope.coupon.id}, coupon: $scope.coupon, (coupon) ->
|
Coupon.update {id: $scope.coupon.id}, coupon: $scope.coupon, (coupon) ->
|
||||||
$state.go('app.admin.pricing')
|
$state.go('app.admin.pricing')
|
||||||
, (err)->
|
, (err)->
|
||||||
growl.error(_t('unable_to_update_the_coupon_an_error_occurred'))
|
growl.error(_t('unable_to_update_the_coupon_an_error_occurred'))
|
||||||
console.error(err)
|
$scope.errors = err.data
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@ -105,8 +105,8 @@ class MembersController
|
|||||||
##
|
##
|
||||||
# Controller used in the members/groups management page
|
# Controller used in the members/groups management page
|
||||||
##
|
##
|
||||||
Application.Controllers.controller "AdminMembersController", ["$scope", 'membersPromise', 'adminsPromise', 'growl', 'Admin', 'dialogs', '_t', 'Member', 'Export'
|
Application.Controllers.controller "AdminMembersController", ["$scope","$sce", 'membersPromise', 'adminsPromise', 'growl', 'Admin', 'dialogs', '_t', 'Member', 'Export'
|
||||||
, ($scope, membersPromise, adminsPromise, growl, Admin, dialogs, _t, Member, Export) ->
|
, ($scope, $sce, membersPromise, adminsPromise, growl, Admin, dialogs, _t, Member, Export) ->
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@ -177,7 +177,7 @@ Application.Controllers.controller "AdminMembersController", ["$scope", 'members
|
|||||||
resolve:
|
resolve:
|
||||||
object: ->
|
object: ->
|
||||||
title: _t('confirmation_required')
|
title: _t('confirmation_required')
|
||||||
msg: _t('do_you_really_want_to_delete_this_administrator_this_cannot_be_undone')
|
msg: $sce.trustAsHtml(_t('do_you_really_want_to_delete_this_administrator_this_cannot_be_undone') + '<br/><br/>' +_t('this_may_take_a_while_please_wait'))
|
||||||
, -> # cancel confirmed
|
, -> # cancel confirmed
|
||||||
Admin.delete id: admin.id, ->
|
Admin.delete id: admin.id, ->
|
||||||
admins.splice(findAdminIdxById(admins, admin.id), 1)
|
admins.splice(findAdminIdxById(admins, admin.id), 1)
|
||||||
@ -423,11 +423,40 @@ Application.Controllers.controller "EditMemberController", ["$scope", "$state",
|
|||||||
templateUrl: '<%= asset_path "wallet/credit_modal.html" %>'
|
templateUrl: '<%= asset_path "wallet/credit_modal.html" %>'
|
||||||
controller: ['$scope', '$uibModalInstance', 'Wallet', ($scope, $uibModalInstance, Wallet) ->
|
controller: ['$scope', '$uibModalInstance', 'Wallet', ($scope, $uibModalInstance, Wallet) ->
|
||||||
|
|
||||||
|
# default: do not generate a refund invoice
|
||||||
|
$scope.generate_avoir = false
|
||||||
|
|
||||||
|
# date of the generated refund invoice
|
||||||
|
$scope.avoir_date = null
|
||||||
|
|
||||||
|
# optional description shown on the refund invoice
|
||||||
|
$scope.description = ''
|
||||||
|
|
||||||
|
# default configuration for the avoir date selector widget
|
||||||
|
$scope.datePicker =
|
||||||
|
format: Fablab.uibDateFormat
|
||||||
|
opened: false
|
||||||
|
options:
|
||||||
|
startingDay: Fablab.weekStartingDay
|
||||||
|
|
||||||
|
##
|
||||||
|
# Callback to open/close the date picker
|
||||||
|
##
|
||||||
|
$scope.toggleDatePicker = ($event) ->
|
||||||
|
$event.preventDefault()
|
||||||
|
$event.stopPropagation()
|
||||||
|
$scope.datePicker.opened = !$scope.datePicker.opened
|
||||||
|
|
||||||
##
|
##
|
||||||
# Modal dialog validation callback
|
# Modal dialog validation callback
|
||||||
##
|
##
|
||||||
$scope.ok = ->
|
$scope.ok = ->
|
||||||
Wallet.credit { id: wallet.id }, { amount: $scope.amount }, (_wallet)->
|
Wallet.credit { id: wallet.id },
|
||||||
|
amount: $scope.amount
|
||||||
|
avoir: $scope.generate_avoir
|
||||||
|
avoir_date: $scope.avoir_date
|
||||||
|
avoir_description: $scope.description
|
||||||
|
, (_wallet)->
|
||||||
|
|
||||||
growl.success(_t('wallet_credit_successfully'))
|
growl.success(_t('wallet_credit_successfully'))
|
||||||
$uibModalInstance.close(_wallet)
|
$uibModalInstance.close(_wallet)
|
||||||
|
@ -81,7 +81,7 @@
|
|||||||
<span class="help-block error" ng-show="couponForm['coupon[validity_per_user]'].$dirty && couponForm['coupon[validity_per_user]'].$error.required" translate>{{ 'validity_per_user_is_required' }}</span>
|
<span class="help-block error" ng-show="couponForm['coupon[validity_per_user]'].$dirty && couponForm['coupon[validity_per_user]'].$error.required" translate>{{ 'validity_per_user_is_required' }}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group" ng-class="{'has-error': errors['valid_until']}">
|
||||||
<label for="coupon[valid_until]" translate>{{ 'valid_until' }}</label>
|
<label for="coupon[valid_until]" translate>{{ 'valid_until' }}</label>
|
||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
<input type="text" id="coupon[valid_until]"
|
<input type="text" id="coupon[valid_until]"
|
||||||
@ -92,16 +92,16 @@
|
|||||||
datepicker-options="datePicker.options"
|
datepicker-options="datePicker.options"
|
||||||
is-open="datePicker.opened"
|
is-open="datePicker.opened"
|
||||||
min-date="datePicker.minDate"
|
min-date="datePicker.minDate"
|
||||||
ng-disabled="mode == 'EDIT'"
|
|
||||||
ng-click="toggleDatePicker($event)"/>
|
ng-click="toggleDatePicker($event)"/>
|
||||||
<span class="input-group-btn">
|
<span class="input-group-btn">
|
||||||
<button type="button" class="btn btn-default" ng-click="toggleDatePicker($event)" ng-disabled="mode == 'EDIT'"><i class="fa fa-calendar"></i></button>
|
<button type="button" class="btn btn-default" ng-click="toggleDatePicker($event)" ng-disabled="mode == 'EDIT'"><i class="fa fa-calendar"></i></button>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
<span class="help-block error" ng-show="errors['valid_until']">{{ errors['valid_until'].join(' ; ') }}</span>
|
||||||
|
|
||||||
<span class="help-block text-info text-xs">
|
<span class="text-info text-xs">
|
||||||
<i class="fa fa-lightbulb-o"></i> {{ 'leave_empty_for_no_limit' | translate }}
|
<i class="fa fa-lightbulb-o"></i> {{ 'leave_empty_for_no_limit' | translate }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group" ng-class="{'has-error': couponForm['coupon[max_usages]'].$dirty && couponForm['coupon[max_usages]'].$invalid}">
|
<div class="form-group" ng-class="{'has-error': couponForm['coupon[max_usages]'].$dirty && couponForm['coupon[max_usages]'].$invalid}">
|
||||||
@ -114,7 +114,7 @@
|
|||||||
min="0"/>
|
min="0"/>
|
||||||
<span class="help-block error" ng-show="couponForm['coupon[max_usages]'].$dirty && couponForm['coupon[max_usages]'].$error.min" translate>{{ 'max_usages_must_be_equal_or_greater_than_0' }}</span>
|
<span class="help-block error" ng-show="couponForm['coupon[max_usages]'].$dirty && couponForm['coupon[max_usages]'].$error.min" translate>{{ 'max_usages_must_be_equal_or_greater_than_0' }}</span>
|
||||||
|
|
||||||
<span class="help-block text-info text-xs">
|
<span class="text-info text-xs">
|
||||||
<i class="fa fa-lightbulb-o"></i> {{ 'leave_empty_for_no_limit' | translate }}
|
<i class="fa fa-lightbulb-o"></i> {{ 'leave_empty_for_no_limit' | translate }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
<h1>{{object.title}}</h1>
|
<h1>{{object.title}}</h1>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
<p>{{object.msg}}</p>
|
<p ng-bind-html="object.msg"></p>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
<button class="btn btn-info" ng-click="ok()" translate>{{ 'confirm' }}</button>
|
<button class="btn btn-info" ng-click="ok()" translate>{{ 'confirm' }}</button>
|
||||||
|
@ -3,11 +3,18 @@
|
|||||||
<h1 translate>{{ 'credit_title' }}</h1>
|
<h1 translate>{{ 'credit_title' }}</h1>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
|
|
||||||
|
<div class="alert alert-warning m-b-md m-b-sm">
|
||||||
|
<i class="fa fa-warning m-sm inline" aria-hidden="true"></i>
|
||||||
|
<div class="inline pull-right width-90 m-t-n-xs" translate>{{ 'warning_uneditable_credit' }}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<form name="walletForm" ng-class="{'has-error': walletForm.amount.$dirty && walletForm.amount.$invalid}">
|
<form name="walletForm" ng-class="{'has-error': walletForm.amount.$dirty && walletForm.amount.$invalid}">
|
||||||
<div class="text-right amountGroup m-r-md">
|
<div class="text-right amountGroup m-r-md">
|
||||||
<span class="beforeAmount" translate>{{ 'credit_label' }}</span>
|
<label for="amount" class="beforeAmount" translate>{{ 'credit_label' }}</label>
|
||||||
<input class="form-control m-l"
|
<input class="form-control m-l"
|
||||||
type="number"
|
type="number"
|
||||||
|
id="amount"
|
||||||
name="amount"
|
name="amount"
|
||||||
ng-model="amount"
|
ng-model="amount"
|
||||||
required min="1"
|
required min="1"
|
||||||
@ -16,10 +23,11 @@
|
|||||||
<span class="help-block" ng-show="walletForm.amount.$dirty && walletForm.amount.$error.required" translate>{{'amount_is_required'}}</span>
|
<span class="help-block" ng-show="walletForm.amount.$dirty && walletForm.amount.$error.required" translate>{{'amount_is_required'}}</span>
|
||||||
<span class="help-block" ng-show="walletForm.amount.$dirty && walletForm.amount.$error.min">{{ 'amount_minimum_1' | translate }} {{currencySymbol}}.</span>
|
<span class="help-block" ng-show="walletForm.amount.$dirty && walletForm.amount.$error.min">{{ 'amount_minimum_1' | translate }} {{currencySymbol}}.</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="text-right amountGroup m-t m-r-md">
|
<div class="text-right amountGroup m-t m-r-md" ng-class="{'has-error': walletForm.amount_confirm.$dirty && walletForm.amount_confirm.$invalid }">
|
||||||
<span class="beforeAmount" translate>{{ 'confirm_credit_label' }}</span>
|
<label for="amount_confirm" class="beforeAmount" translate>{{ 'confirm_credit_label' }}</label>
|
||||||
<input class="form-control m-l"
|
<input class="form-control m-l"
|
||||||
type="number"
|
type="number"
|
||||||
|
id="amount_confirm"
|
||||||
name="amount_confirm"
|
name="amount_confirm"
|
||||||
ng-model="amount_confirm"
|
ng-model="amount_confirm"
|
||||||
required
|
required
|
||||||
@ -30,12 +38,53 @@
|
|||||||
<span class="help-block" ng-show="walletForm.amount_confirm.$dirty && walletForm.amount_confirm.$error.required" translate>{{'amount_confirm_is_required'}}</span>
|
<span class="help-block" ng-show="walletForm.amount_confirm.$dirty && walletForm.amount_confirm.$error.required" translate>{{'amount_confirm_is_required'}}</span>
|
||||||
<span class="help-block" ng-show="walletForm.amount_confirm.$dirty && walletForm.amount_confirm.$error.pattern">{{ 'amount_confirm_does_not_match' | translate }}</span>
|
<span class="help-block" ng-show="walletForm.amount_confirm.$dirty && walletForm.amount_confirm.$error.pattern">{{ 'amount_confirm_does_not_match' | translate }}</span>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
|
||||||
|
|
||||||
<div class="alert alert-warning m-t-md m-b-sm">
|
<hr/>
|
||||||
<i class="fa fa-warning m-sm inline" aria-hidden="true"></i>
|
<div class="text-right m-t">
|
||||||
<div class="inline pull-right width-90 m-t-n-xs" translate>{{ 'warning_uneditable_credit' }}</div>
|
<label for="generate_avoir" translate>{{ 'generate_a_refund_invoice' }}</label>
|
||||||
</div>
|
<div class="inline m-l">
|
||||||
|
<input bs-switch
|
||||||
|
ng-model="generate_avoir"
|
||||||
|
id="generate_avoir"
|
||||||
|
name="generate_avoir"
|
||||||
|
type="checkbox"
|
||||||
|
switch-on-text="{{ 'yes' | translate }}"
|
||||||
|
switch-off-text="{{ 'no' | translate }}"
|
||||||
|
switch-animate="true"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div ng-show="generate_avoir">
|
||||||
|
<div class="m-t" ng-class="{'has-error': walletForm.avoir_date.$dirty && walletForm.avoir_date.$invalid }">
|
||||||
|
<label for="avoir_date" translate>{{ 'creation_date_for_the_refund' }}</label>
|
||||||
|
<div class="input-group">
|
||||||
|
<span class="input-group-addon"><i class="fa fa-calendar"></i></span>
|
||||||
|
<input type="text"
|
||||||
|
class="form-control"
|
||||||
|
id="avoir_date"
|
||||||
|
name="avoir_date"
|
||||||
|
ng-model="avoir_date"
|
||||||
|
uib-datepicker-popup="{{datePicker.format}}"
|
||||||
|
datepicker-options="datePicker.options"
|
||||||
|
is-open="datePicker.opened"
|
||||||
|
placeholder="{{datePicker.format}}"
|
||||||
|
ng-click="toggleDatePicker($event)"
|
||||||
|
ng-required="generate_avoir"/>
|
||||||
|
</div>
|
||||||
|
<span class="help-block" ng-show="walletForm.avoir_date.$dirty && walletForm.avoir_date.$error.required" translate>{{ 'creation_date_is_required' }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="m-t">
|
||||||
|
<label for="description" translate>{{ 'description_(optional)' }}</label>
|
||||||
|
<p translate>{{ 'will_appear_on_the_refund_invoice' }}</p>
|
||||||
|
<textarea class="form-control m-t-sm"
|
||||||
|
id="description"
|
||||||
|
name="description"
|
||||||
|
ng-model="description">
|
||||||
|
</textarea>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
|
@ -90,6 +90,6 @@ class API::CouponsController < API::ApiController
|
|||||||
end
|
end
|
||||||
|
|
||||||
def coupon_editable_params
|
def coupon_editable_params
|
||||||
params.require(:coupon).permit(:name, :active)
|
params.require(:coupon).permit(:name, :active, :valid_until)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -57,7 +57,7 @@ class API::InvoicesController < API::ApiController
|
|||||||
|
|
||||||
end
|
end
|
||||||
|
|
||||||
# only for create avoir
|
# only for create refund invoices (avoir)
|
||||||
def create
|
def create
|
||||||
authorize Invoice
|
authorize Invoice
|
||||||
invoice = Invoice.only_invoice.find(avoir_params[:invoice_id])
|
invoice = Invoice.only_invoice.find(avoir_params[:invoice_id])
|
||||||
|
@ -14,13 +14,22 @@ class API::WalletController < API::ApiController
|
|||||||
end
|
end
|
||||||
|
|
||||||
def credit
|
def credit
|
||||||
@wallet = Wallet.find(params[:id])
|
@wallet = Wallet.find(credit_params[:id])
|
||||||
authorize @wallet
|
authorize @wallet
|
||||||
service = WalletService.new(user: current_user, wallet: @wallet)
|
service = WalletService.new(user: current_user, wallet: @wallet)
|
||||||
if service.credit(params[:amount].to_f)
|
transaction = service.credit(credit_params[:amount].to_f)
|
||||||
|
if transaction
|
||||||
|
if credit_params[:avoir]
|
||||||
|
service.create_avoir(transaction, credit_params[:avoir_date], credit_params[:avoir_description])
|
||||||
|
end
|
||||||
render :show
|
render :show
|
||||||
else
|
else
|
||||||
head 422
|
head 422
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
def credit_params
|
||||||
|
params.permit(:id, :amount, :avoir, :avoir_date, :avoir_description)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
@ -11,6 +11,7 @@ class Coupon < ActiveRecord::Base
|
|||||||
validates :validity_per_user, presence: true
|
validates :validity_per_user, presence: true
|
||||||
validates :validity_per_user, inclusion: { in: %w(once forever) }
|
validates :validity_per_user, inclusion: { in: %w(once forever) }
|
||||||
validates_with CouponDiscountValidator
|
validates_with CouponDiscountValidator
|
||||||
|
validates_with CouponExpirationValidator
|
||||||
|
|
||||||
def safe_destroy
|
def safe_destroy
|
||||||
if self.invoices.size == 0
|
if self.invoices.size == 0
|
||||||
@ -76,6 +77,7 @@ class Coupon < ActiveRecord::Base
|
|||||||
attached_object: self
|
attached_object: self
|
||||||
end
|
end
|
||||||
|
|
||||||
|
private
|
||||||
def create_stripe_coupon
|
def create_stripe_coupon
|
||||||
StripeWorker.perform_async(:create_stripe_coupon, id)
|
StripeWorker.perform_async(:create_stripe_coupon, id)
|
||||||
end
|
end
|
||||||
|
@ -37,10 +37,12 @@ module PDF
|
|||||||
if Setting.find_by({name: 'invoice_code-active'}).value == 'true'
|
if Setting.find_by({name: 'invoice_code-active'}).value == 'true'
|
||||||
text I18n.t('invoices.code', CODE:Setting.find_by({name: 'invoice_code-value'}).value), :leading => 3
|
text I18n.t('invoices.code', CODE:Setting.find_by({name: 'invoice_code-value'}).value), :leading => 3
|
||||||
end
|
end
|
||||||
if invoice.is_a?(Avoir)
|
if invoice.invoiced_type != WalletTransaction.name
|
||||||
text I18n.t('invoices.order_number', NUMBER:invoice.invoice.order_number), :leading => 3
|
if invoice.is_a?(Avoir)
|
||||||
else
|
text I18n.t('invoices.order_number', NUMBER:invoice.invoice.order_number), :leading => 3
|
||||||
text I18n.t('invoices.order_number', NUMBER:invoice.order_number), :leading => 3
|
else
|
||||||
|
text I18n.t('invoices.order_number', NUMBER:invoice.order_number), :leading => 3
|
||||||
|
end
|
||||||
end
|
end
|
||||||
if invoice.is_a?(Avoir)
|
if invoice.is_a?(Avoir)
|
||||||
text I18n.t('invoices.refund_invoice_issued_on_DATE', DATE:I18n.l(invoice.avoir_date.to_date))
|
text I18n.t('invoices.refund_invoice_issued_on_DATE', DATE:I18n.l(invoice.avoir_date.to_date))
|
||||||
@ -68,7 +70,11 @@ module PDF
|
|||||||
# object
|
# object
|
||||||
move_down 25
|
move_down 25
|
||||||
if invoice.is_a?(Avoir)
|
if invoice.is_a?(Avoir)
|
||||||
object = I18n.t('invoices.cancellation_of_invoice_REF', REF: invoice.invoice.reference)
|
if invoice.invoiced_type == WalletTransaction.name
|
||||||
|
object = I18n.t('invoices.wallet_credit')
|
||||||
|
else
|
||||||
|
object = I18n.t('invoices.cancellation_of_invoice_REF', REF: invoice.invoice.reference)
|
||||||
|
end
|
||||||
else
|
else
|
||||||
case invoice.invoiced_type
|
case invoice.invoiced_type
|
||||||
when 'Reservation'
|
when 'Reservation'
|
||||||
@ -115,7 +121,7 @@ module PDF
|
|||||||
|
|
||||||
|
|
||||||
else ### Reservation
|
else ### Reservation
|
||||||
case invoice.invoiced.reservable_type
|
case invoice.invoiced.try(:reservable_type)
|
||||||
### Machine reservation
|
### Machine reservation
|
||||||
when 'Machine'
|
when 'Machine'
|
||||||
details += I18n.t('invoices.machine_reservation_DESCRIPTION', DESCRIPTION: item.description)
|
details += I18n.t('invoices.machine_reservation_DESCRIPTION', DESCRIPTION: item.description)
|
||||||
@ -130,6 +136,9 @@ module PDF
|
|||||||
invoice.invoiced.tickets.each do |t|
|
invoice.invoiced.tickets.each do |t|
|
||||||
details += "\n "+I18n.t('invoices.other_rate_ticket', count: t.booked, NAME: t.event_price_category.price_category.name)
|
details += "\n "+I18n.t('invoices.other_rate_ticket', count: t.booked, NAME: t.event_price_category.price_category.name)
|
||||||
end
|
end
|
||||||
|
### wallet credit
|
||||||
|
when nil
|
||||||
|
details = item.description
|
||||||
|
|
||||||
### Other cases (not expected)
|
### Other cases (not expected)
|
||||||
else
|
else
|
||||||
|
@ -21,7 +21,7 @@ class WalletService
|
|||||||
end
|
end
|
||||||
raise ActiveRecord::Rollback
|
raise ActiveRecord::Rollback
|
||||||
end
|
end
|
||||||
return false
|
false
|
||||||
end
|
end
|
||||||
|
|
||||||
## debit an amount to wallet, if debit success then return a wallet transaction
|
## debit an amount to wallet, if debit success then return a wallet transaction
|
||||||
@ -35,6 +35,27 @@ class WalletService
|
|||||||
end
|
end
|
||||||
raise ActiveRecord::Rollback
|
raise ActiveRecord::Rollback
|
||||||
end
|
end
|
||||||
return false
|
false
|
||||||
|
end
|
||||||
|
|
||||||
|
## create a refund invoice associated with the given wallet transaction
|
||||||
|
def create_avoir(wallet_transaction, avoir_date, description)
|
||||||
|
avoir = Avoir.new
|
||||||
|
avoir.type = 'Avoir'
|
||||||
|
avoir.invoiced = wallet_transaction
|
||||||
|
avoir.avoir_date = avoir_date
|
||||||
|
avoir.created_at = avoir_date
|
||||||
|
avoir.description = description
|
||||||
|
avoir.avoir_mode = 'wallet'
|
||||||
|
avoir.subscription_to_expire = false
|
||||||
|
avoir.user_id = wallet_transaction.wallet.user_id
|
||||||
|
avoir.total = wallet_transaction.amount * 100.0
|
||||||
|
avoir.save!
|
||||||
|
|
||||||
|
ii = InvoiceItem.new
|
||||||
|
ii.amount = wallet_transaction.amount * 100.0
|
||||||
|
ii.description = I18n.t('invoices.wallet_credit')
|
||||||
|
ii.invoice = avoir
|
||||||
|
ii.save!
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -1,15 +1,15 @@
|
|||||||
class CouponDiscountValidator < ActiveModel::Validator
|
class CouponDiscountValidator < ActiveModel::Validator
|
||||||
def validate(record)
|
def validate(record)
|
||||||
if !record.percent_off.nil?
|
if !record.percent_off.nil?
|
||||||
unless [0..100].include? record.percent_off
|
unless (0..100).include? record.percent_off
|
||||||
record.errors[:percent_off] << 'Percentage must be included between 0 and 100'
|
record.errors[:percent_off] << I18n.t('errors.messages.percentage_out_of_range')
|
||||||
end
|
end
|
||||||
elsif !record.amount_off.nil?
|
elsif !record.amount_off.nil?
|
||||||
unless record.amount_off > 0
|
unless record.amount_off > 0
|
||||||
record.errors[:amount_off] << I18n.t('errors.messages.greater_than_or_equal_to', count: 0)
|
record.errors[:amount_off] << I18n.t('errors.messages.greater_than_or_equal_to', count: 0)
|
||||||
end
|
end
|
||||||
else
|
else
|
||||||
record.errors[:percent_off] << 'cannot be blank when amount_off is blank too'
|
record.errors[:percent_off] << I18n.t('errors.messages.cannot_be_blank_at_same_time', field: 'amount_off')
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
19
app/validators/coupon_expiration_validator.rb
Normal file
19
app/validators/coupon_expiration_validator.rb
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
class CouponExpirationValidator < ActiveModel::Validator
|
||||||
|
##
|
||||||
|
# @param record {Coupon}
|
||||||
|
##
|
||||||
|
def validate(record)
|
||||||
|
previous = record.valid_until_was
|
||||||
|
current = record.valid_until
|
||||||
|
|
||||||
|
unless current.blank?
|
||||||
|
if current.end_of_day < Time.now
|
||||||
|
record.errors[:valid_until] << I18n.t('errors.messages.cannot_be_in_the_past')
|
||||||
|
end
|
||||||
|
|
||||||
|
if !previous.blank? and current.end_of_day < previous.end_of_day
|
||||||
|
record.errors[:valid_until] << I18n.t('errors.messages.cannot_be_before_previous_value')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
@ -1,3 +1,8 @@
|
|||||||
client = Elasticsearch::Client.new host: "http://#{Rails.application.secrets.elaticsearch_host}:9200", log: true
|
if Rails.env.test?
|
||||||
|
client = Elasticsearch::Client.new host: "http://#{Rails.application.secrets.elaticsearch_host}:9200", log: false
|
||||||
|
else
|
||||||
|
client = Elasticsearch::Client.new host: "http://#{Rails.application.secrets.elaticsearch_host}:9200", log: true
|
||||||
|
end
|
||||||
|
|
||||||
Elasticsearch::Model.client = client
|
Elasticsearch::Model.client = client
|
||||||
Elasticsearch::Persistence.client = client
|
Elasticsearch::Persistence.client = client
|
||||||
|
@ -336,6 +336,7 @@ en:
|
|||||||
groups: "Groups"
|
groups: "Groups"
|
||||||
authentication: "Authentication"
|
authentication: "Authentication"
|
||||||
do_you_really_want_to_delete_this_administrator_this_cannot_be_undone: "Do you really want to delete this administrator? This cannot be undone."
|
do_you_really_want_to_delete_this_administrator_this_cannot_be_undone: "Do you really want to delete this administrator? This cannot be undone."
|
||||||
|
this_may_take_a_while_please_wait: "Warning: this may take a while, please be patient."
|
||||||
administrator_successfully_deleted: "Administrator successfully deleted."
|
administrator_successfully_deleted: "Administrator successfully deleted."
|
||||||
unable_to_delete_the_administrator: "Unable to delete the administrator."
|
unable_to_delete_the_administrator: "Unable to delete the administrator."
|
||||||
add_a_group: "Add a group"
|
add_a_group: "Add a group"
|
||||||
|
@ -336,6 +336,7 @@ fr:
|
|||||||
groups: "Groupes"
|
groups: "Groupes"
|
||||||
authentication: "Authentification"
|
authentication: "Authentification"
|
||||||
do_you_really_want_to_delete_this_administrator_this_cannot_be_undone: "Êtes-vous sûr de vouloir supprimer cet administrateur ? Cette opération est irréversible."
|
do_you_really_want_to_delete_this_administrator_this_cannot_be_undone: "Êtes-vous sûr de vouloir supprimer cet administrateur ? Cette opération est irréversible."
|
||||||
|
this_may_take_a_while_please_wait: "Attention : ceci peut prendre un certain temps, merci de patienter."
|
||||||
administrator_successfully_deleted: "L'administrateur a bien été supprimé."
|
administrator_successfully_deleted: "L'administrateur a bien été supprimé."
|
||||||
unable_to_delete_the_administrator: "L'administrateur n'a pas pu être supprimé."
|
unable_to_delete_the_administrator: "L'administrateur n'a pas pu être supprimé."
|
||||||
add_a_group: "Ajouter un groupe"
|
add_a_group: "Ajouter un groupe"
|
||||||
|
@ -324,6 +324,11 @@ en:
|
|||||||
credit_title: 'Credit wallet'
|
credit_title: 'Credit wallet'
|
||||||
credit_label: 'Set the amount to be credited'
|
credit_label: 'Set the amount to be credited'
|
||||||
confirm_credit_label: 'Confirm the amount to be credited'
|
confirm_credit_label: 'Confirm the amount to be credited'
|
||||||
|
generate_a_refund_invoice: "Generate a refund invoice"
|
||||||
|
creation_date_for_the_refund: "Creation date for the refund"
|
||||||
|
creation_date_is_required: "Creation date is required."
|
||||||
|
description_(optional): "Description (optional):"
|
||||||
|
will_appear_on_the_refund_invoice: "Will appear on the refund invoice."
|
||||||
to_credit: 'Credit'
|
to_credit: 'Credit'
|
||||||
wallet_credit_successfully: "Wallet of user is credited successfully."
|
wallet_credit_successfully: "Wallet of user is credited successfully."
|
||||||
a_problem_occurred_for_wallet_credit: "A problem is occurred while taking the credit of wallet"
|
a_problem_occurred_for_wallet_credit: "A problem is occurred while taking the credit of wallet"
|
||||||
|
@ -324,6 +324,11 @@ fr:
|
|||||||
credit_title: 'Créditer le porte-monnaie'
|
credit_title: 'Créditer le porte-monnaie'
|
||||||
credit_label: 'Indiquez le montant à créditer'
|
credit_label: 'Indiquez le montant à créditer'
|
||||||
confirm_credit_label: 'Confirmez le montant à créditer'
|
confirm_credit_label: 'Confirmez le montant à créditer'
|
||||||
|
generate_a_refund_invoice: "Générer une facture d'avoir"
|
||||||
|
creation_date_for_the_refund: "Date d'émission de l'avoir"
|
||||||
|
creation_date_is_required: "La date d'émission est requise."
|
||||||
|
description_(optional): "Description (optionnelle) :"
|
||||||
|
will_appear_on_the_refund_invoice: "Apparaîtra sur la facture de remboursement."
|
||||||
to_credit: 'Créditer'
|
to_credit: 'Créditer'
|
||||||
wallet_credit_successfully: "Le porte-monnaie de l'utilisateur a été chargé avec succès."
|
wallet_credit_successfully: "Le porte-monnaie de l'utilisateur a été chargé avec succès."
|
||||||
a_problem_occurred_for_wallet_credit: "Un problème est survenu lors du chargement du porte-monnaie."
|
a_problem_occurred_for_wallet_credit: "Un problème est survenu lors du chargement du porte-monnaie."
|
||||||
|
@ -32,6 +32,10 @@ en:
|
|||||||
size_too_small: "is too small (should be at least %{file_size})"
|
size_too_small: "is too small (should be at least %{file_size})"
|
||||||
size_too_big: "is too big (should be at most %{file_size})"
|
size_too_big: "is too big (should be at most %{file_size})"
|
||||||
export_not_found: "Requested export was not found. It was probably deleted, please generate a new export."
|
export_not_found: "Requested export was not found. It was probably deleted, please generate a new export."
|
||||||
|
percentage_out_of_range: "Percentage must be included between 0 and 100"
|
||||||
|
cannot_be_blank_at_same_time: "cannot be blank when %{field} is blank too"
|
||||||
|
cannot_be_in_the_past: "cannot be in the past"
|
||||||
|
cannot_be_before_previous_value: "cannot be before the previous value"
|
||||||
|
|
||||||
activemodel:
|
activemodel:
|
||||||
errors:
|
errors:
|
||||||
@ -68,6 +72,7 @@ en:
|
|||||||
order_number: "Order #: %{NUMBER}"
|
order_number: "Order #: %{NUMBER}"
|
||||||
invoice_issued_on_DATE: "Invoice issued on %{DATE}"
|
invoice_issued_on_DATE: "Invoice issued on %{DATE}"
|
||||||
refund_invoice_issued_on_DATE: "Refund invoice issued on %{DATE}"
|
refund_invoice_issued_on_DATE: "Refund invoice issued on %{DATE}"
|
||||||
|
wallet_credit: "Wallet credit"
|
||||||
cancellation_of_invoice_REF: "Cancellation of invoice %{REF}"
|
cancellation_of_invoice_REF: "Cancellation of invoice %{REF}"
|
||||||
reservation_of_USER_on_DATE_at_TIME: "Reservation of %{USER} on %{DATE} at %{TIME}"
|
reservation_of_USER_on_DATE_at_TIME: "Reservation of %{USER} on %{DATE} at %{TIME}"
|
||||||
cancellation: "Cancellation"
|
cancellation: "Cancellation"
|
||||||
|
@ -32,6 +32,10 @@ fr:
|
|||||||
size_too_small: "est trop petite (au moins %{file_size})"
|
size_too_small: "est trop petite (au moins %{file_size})"
|
||||||
size_too_big: "est trop grande (pas plus de %{file_size})"
|
size_too_big: "est trop grande (pas plus de %{file_size})"
|
||||||
export_not_found: "L'export demandé n'a pas été trouvé. Il a probablement été supprimé, veuillez lancer la génération d'un nouvel export."
|
export_not_found: "L'export demandé n'a pas été trouvé. Il a probablement été supprimé, veuillez lancer la génération d'un nouvel export."
|
||||||
|
percentage_out_of_range: "Le pourcentage doit être inclus entre 0 et 100"
|
||||||
|
cannot_be_blank_at_same_time: "ou %{field} doit être rempli(e)"
|
||||||
|
cannot_be_in_the_past: "ne peut pas être dans le passé"
|
||||||
|
cannot_be_before_previous_value: "ne peut pas être antérieur(e) à la valeur précédente"
|
||||||
|
|
||||||
activemodel:
|
activemodel:
|
||||||
errors:
|
errors:
|
||||||
@ -68,6 +72,7 @@ fr:
|
|||||||
order_number: "N° Commande : %{NUMBER}"
|
order_number: "N° Commande : %{NUMBER}"
|
||||||
invoice_issued_on_DATE: "Facture éditée le %{DATE}"
|
invoice_issued_on_DATE: "Facture éditée le %{DATE}"
|
||||||
refund_invoice_issued_on_DATE: "Avoir édité le %{DATE}"
|
refund_invoice_issued_on_DATE: "Avoir édité le %{DATE}"
|
||||||
|
wallet_credit: "Crédit du porte-monnaie"
|
||||||
cancellation_of_invoice_REF: "Annulation de la facture %{REF}"
|
cancellation_of_invoice_REF: "Annulation de la facture %{REF}"
|
||||||
reservation_of_USER_on_DATE_at_TIME: "Réservation de %{USER} le %{DATE} à %{TIME}"
|
reservation_of_USER_on_DATE_at_TIME: "Réservation de %{USER} le %{DATE} à %{TIME}"
|
||||||
cancellation: "Annulation"
|
cancellation: "Annulation"
|
||||||
|
@ -1,42 +0,0 @@
|
|||||||
class InsertCustomAggregations < ActiveRecord::Migration
|
|
||||||
def up
|
|
||||||
# available reservations hours for machines
|
|
||||||
machine = StatisticIndex.find_by(es_type_key: 'machine')
|
|
||||||
machine_hours = StatisticType.find_by(key: 'hour', statistic_index_id: machine.id)
|
|
||||||
|
|
||||||
available_hours = StatisticCustomAggregation.new({
|
|
||||||
statistic_type_id: machine_hours.id,
|
|
||||||
es_index: 'fablab',
|
|
||||||
es_type: 'availabilities',
|
|
||||||
field: 'available_hours',
|
|
||||||
query: '{"size":0, "aggregations":{"%{aggs_name}":{"sum":{"field":"hours_duration"}}}, "query":{"bool":{"must":[{"range":{"start_at":{"gte":"%{start_date}", "lte":"%{end_date}"}}}, {"match":{"available_type":"machines"}}]}}}'
|
|
||||||
})
|
|
||||||
available_hours.save!
|
|
||||||
|
|
||||||
# available training tickets
|
|
||||||
training = StatisticIndex.find_by(es_type_key: 'training')
|
|
||||||
training_bookings = StatisticType.find_by(key: 'booking', statistic_index_id: training.id)
|
|
||||||
|
|
||||||
available_tickets = StatisticCustomAggregation.new({
|
|
||||||
statistic_type_id: training_bookings.id,
|
|
||||||
es_index: 'fablab',
|
|
||||||
es_type: 'availabilities',
|
|
||||||
field: 'available_tickets',
|
|
||||||
query: '{"size":0, "aggregations":{"%{aggs_name}":{"sum":{"field":"nb_total_places"}}}, "query":{"bool":{"must":[{"range":{"start_at":{"gte":"%{start_date}", "lte":"%{end_date}"}}}, {"match":{"available_type":"training"}}]}}}'
|
|
||||||
})
|
|
||||||
available_tickets.save!
|
|
||||||
end
|
|
||||||
|
|
||||||
def down
|
|
||||||
|
|
||||||
machine = StatisticIndex.find_by(es_type_key: 'machine')
|
|
||||||
machine_hours = StatisticType.find_by(key: 'hour', statistic_index_id: machine.id)
|
|
||||||
|
|
||||||
StatisticCustomAggregation.where(field: 'available_hours', statistic_type_id: machine_hours.id).first.destroy!
|
|
||||||
|
|
||||||
training = StatisticIndex.find_by(es_type_key: 'training')
|
|
||||||
training_bookings = StatisticType.find_by(key: 'booking', statistic_index_id: training.id)
|
|
||||||
|
|
||||||
StatisticCustomAggregation.where(field: 'available_tickets', statistic_type_id: training_bookings.id).first.destroy!
|
|
||||||
end
|
|
||||||
end
|
|
26
db/seeds.rb
26
db/seeds.rb
@ -397,3 +397,29 @@ unless Setting.find_by(name: 'reminder_delay').try(:value)
|
|||||||
setting.value = '24'
|
setting.value = '24'
|
||||||
setting.save
|
setting.save
|
||||||
end
|
end
|
||||||
|
|
||||||
|
if StatisticCustomAggregation.count == 0
|
||||||
|
# available reservations hours for machines
|
||||||
|
machine_hours = StatisticType.find_by(key: 'hour', statistic_index_id: 2)
|
||||||
|
|
||||||
|
available_hours = StatisticCustomAggregation.new({
|
||||||
|
statistic_type_id: machine_hours.id,
|
||||||
|
es_index: 'fablab',
|
||||||
|
es_type: 'availabilities',
|
||||||
|
field: 'available_hours',
|
||||||
|
query: '{"size":0, "aggregations":{"%{aggs_name}":{"sum":{"field":"hours_duration"}}}, "query":{"bool":{"must":[{"range":{"start_at":{"gte":"%{start_date}", "lte":"%{end_date}"}}}, {"match":{"available_type":"machines"}}]}}}'
|
||||||
|
})
|
||||||
|
available_hours.save!
|
||||||
|
|
||||||
|
# available training tickets
|
||||||
|
training_bookings = StatisticType.find_by(key: 'booking', statistic_index_id: 3)
|
||||||
|
|
||||||
|
available_tickets = StatisticCustomAggregation.new({
|
||||||
|
statistic_type_id: training_bookings.id,
|
||||||
|
es_index: 'fablab',
|
||||||
|
es_type: 'availabilities',
|
||||||
|
field: 'available_tickets',
|
||||||
|
query: '{"size":0, "aggregations":{"%{aggs_name}":{"sum":{"field":"nb_total_places"}}}, "query":{"bool":{"must":[{"range":{"start_at":{"gte":"%{start_date}", "lte":"%{end_date}"}}}, {"match":{"available_type":"training"}}]}}}'
|
||||||
|
})
|
||||||
|
available_tickets.save!
|
||||||
|
end
|
@ -125,7 +125,7 @@ Requires=docker.service
|
|||||||
[Service]
|
[Service]
|
||||||
Type=oneshot
|
Type=oneshot
|
||||||
ExecStart=/usr/bin/docker run --rm --name letsencrypt -v "/home/core/fabmanager/log:/var/log/letsencrypt" -v "/home/core/fabmanager/letsencrypt/etc:/etc/letsencrypt" -v "/home/core/fabmanager/letsencrypt/config:/letsencrypt-config" quay.io/letsencrypt/letsencrypt:latest -c "/letsencrypt-config/webroot.ini" certonly
|
ExecStart=/usr/bin/docker run --rm --name letsencrypt -v "/home/core/fabmanager/log:/var/log/letsencrypt" -v "/home/core/fabmanager/letsencrypt/etc:/etc/letsencrypt" -v "/home/core/fabmanager/letsencrypt/config:/letsencrypt-config" quay.io/letsencrypt/letsencrypt:latest -c "/letsencrypt-config/webroot.ini" certonly
|
||||||
ExecStartPost=-/usr/bin/docker restart fabmanager
|
ExecStartPost=-/usr/bin/docker restart fabmanager_nginx_1
|
||||||
```
|
```
|
||||||
|
|
||||||
Create file (with sudo) /etc/systemd/system/letsencrypt.timer with
|
Create file (with sudo) /etc/systemd/system/letsencrypt.timer with
|
||||||
@ -138,6 +138,9 @@ Requires=docker.service
|
|||||||
OnCalendar=*-*-1 06:00:00
|
OnCalendar=*-*-1 06:00:00
|
||||||
Persistent=true
|
Persistent=true
|
||||||
Unit=letsencrypt.service
|
Unit=letsencrypt.service
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=timers.target
|
||||||
```
|
```
|
||||||
|
|
||||||
Then deploy your app and read the "Generate SSL certificate by Letsencrypt" section to complete the installation of the letsencrypt certificate.
|
Then deploy your app and read the "Generate SSL certificate by Letsencrypt" section to complete the installation of the letsencrypt certificate.
|
||||||
@ -277,7 +280,9 @@ Remove your app and Run your app to apply changes
|
|||||||
Finally, if everything is ok, start letsencrypt timer to update the certificate every 1st of the month :
|
Finally, if everything is ok, start letsencrypt timer to update the certificate every 1st of the month :
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
sudo systemctl enable letsencrypt.timer
|
||||||
sudo systemctl start letsencrypt.timer
|
sudo systemctl start letsencrypt.timer
|
||||||
|
(check) sudo systemctl list-timers
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
module Events
|
module Events
|
||||||
class AsUserTest < ActionDispatch::IntegrationTest
|
class AsUserTest < ActionDispatch::IntegrationTest
|
||||||
|
|
||||||
test 'reserve event with many prices and payment means' do
|
test 'reserve event with many prices and payment means and VAT' do
|
||||||
|
|
||||||
vlonchamp = User.find_by(username: 'vlonchamp')
|
vlonchamp = User.find_by(username: 'vlonchamp')
|
||||||
login_as(vlonchamp, scope: :user)
|
login_as(vlonchamp, scope: :user)
|
||||||
@ -15,6 +15,15 @@ module Events
|
|||||||
users_credit_count = UsersCredit.count
|
users_credit_count = UsersCredit.count
|
||||||
wallet_transactions_count = WalletTransaction.count
|
wallet_transactions_count = WalletTransaction.count
|
||||||
|
|
||||||
|
# Enable the VAT at 19.6%
|
||||||
|
vat_active = Setting.find_by(name: 'invoice_VAT-active')
|
||||||
|
vat_active.value = 'true'
|
||||||
|
vat_active.save!
|
||||||
|
|
||||||
|
vat_rate = Setting.find_by(name: 'invoice_VAT-rate')
|
||||||
|
vat_rate.value = '19.6'
|
||||||
|
vat_rate.save!
|
||||||
|
|
||||||
# Reserve the 'radio' event
|
# Reserve the 'radio' event
|
||||||
VCR.use_cassette('reserve_event_with_many_prices_and_payment_means') do
|
VCR.use_cassette('reserve_event_with_many_prices_and_payment_means') do
|
||||||
post reservations_path, {
|
post reservations_path, {
|
||||||
|
82
test/integration/prices/compute_test.rb
Normal file
82
test/integration/prices/compute_test.rb
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
module Events
|
||||||
|
class AsAdminTest < ActionDispatch::IntegrationTest
|
||||||
|
|
||||||
|
setup do
|
||||||
|
admin = User.with_role(:admin).first
|
||||||
|
login_as(admin, scope: :user)
|
||||||
|
end
|
||||||
|
|
||||||
|
test 'compute price for a simple training' do
|
||||||
|
user = User.find_by(username: 'jdupond')
|
||||||
|
availability = Availability.find(2)
|
||||||
|
printer_training = availability.trainings.first
|
||||||
|
|
||||||
|
post '/api/prices/compute',
|
||||||
|
{
|
||||||
|
reservation: {
|
||||||
|
user_id: user.id,
|
||||||
|
reservable_id: printer_training.id,
|
||||||
|
reservable_type: printer_training.class.name,
|
||||||
|
slots_attributes: [
|
||||||
|
{
|
||||||
|
availability_id: availability.id,
|
||||||
|
end_at: availability.end_at,
|
||||||
|
offered: false,
|
||||||
|
start_at: availability.start_at
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}.to_json,
|
||||||
|
default_headers
|
||||||
|
|
||||||
|
# Check response format & status
|
||||||
|
assert_equal 200, response.status, response.body
|
||||||
|
assert_equal Mime::JSON, response.content_type
|
||||||
|
|
||||||
|
# Check the price was computed correctly
|
||||||
|
price = json_response(response.body)
|
||||||
|
assert_equal (printer_training.trainings_pricings.where(group_id: user.group_id).first.amount / 100.0), price[:price], 'Computed price did not match training price'
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
test 'compute price for a machine reservation with an offered slot and a subscription' do
|
||||||
|
user = User.find_by(username: 'jdupond')
|
||||||
|
availability = Availability.find(3)
|
||||||
|
laser = availability.machines.where(id: 1).first
|
||||||
|
plan = Plan.where(group_id: user.group_id, interval: 'month').first
|
||||||
|
|
||||||
|
post '/api/prices/compute',
|
||||||
|
{
|
||||||
|
reservation: {
|
||||||
|
user_id: user.id,
|
||||||
|
reservable_id: laser.id,
|
||||||
|
reservable_type: laser.class.name,
|
||||||
|
plan_id: plan.id,
|
||||||
|
slots_attributes: [
|
||||||
|
{
|
||||||
|
availability_id: availability.id,
|
||||||
|
end_at: (availability.start_at + 1.hour).strftime('%Y-%m-%d %H:%M:%S.%9N Z'),
|
||||||
|
offered: true,
|
||||||
|
start_at: availability.start_at.strftime('%Y-%m-%d %H:%M:%S.%9N Z')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
availability_id: availability.id,
|
||||||
|
end_at: (availability.start_at + 2.hour).strftime('%Y-%m-%d %H:%M:%S.%9N Z'),
|
||||||
|
offered: false,
|
||||||
|
start_at: (availability.start_at + 1.hour).strftime('%Y-%m-%d %H:%M:%S.%9N Z')
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}.to_json,
|
||||||
|
default_headers
|
||||||
|
|
||||||
|
# Check response format & status
|
||||||
|
assert_equal 200, response.status, response.body
|
||||||
|
assert_equal Mime::JSON, response.content_type
|
||||||
|
|
||||||
|
# Check the event was created correctly
|
||||||
|
price = json_response(response.body)
|
||||||
|
assert_equal ((laser.prices.where(group_id: user.group_id, plan_id: plan.id).first.amount + plan.amount) / 100.0), price[:price], 'Computed price did not match machine + subscription price'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
@ -74,5 +74,38 @@ class WalletsTest < ActionDispatch::IntegrationTest
|
|||||||
w.reload
|
w.reload
|
||||||
assert_equal w.amount, expected_amount
|
assert_equal w.amount, expected_amount
|
||||||
assert_equal w.amount, wallet[:amount]
|
assert_equal w.amount, wallet[:amount]
|
||||||
|
|
||||||
|
# no refund invoices should have been generated
|
||||||
|
assert_empty Invoice.where(invoiced: w.wallet_transactions.last)
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
test 'admin credit wallet with refund invoice generation' do
|
||||||
|
admin = users(:user_1)
|
||||||
|
login_as(admin, scope: :user)
|
||||||
|
w = @vlonchamp.wallet
|
||||||
|
amount = 10
|
||||||
|
avoir_date = Time.now.end_of_day
|
||||||
|
expected_amount = w.amount + amount
|
||||||
|
put "/api/wallet/#{w.id}/credit",
|
||||||
|
{
|
||||||
|
amount: amount,
|
||||||
|
avoir: true,
|
||||||
|
avoir_date: avoir_date,
|
||||||
|
avoir_description: 'Some text'
|
||||||
|
}
|
||||||
|
|
||||||
|
assert_equal 200, response.status
|
||||||
|
assert_equal Mime::JSON, response.content_type
|
||||||
|
wallet = json_response(response.body)
|
||||||
|
w.reload
|
||||||
|
assert_equal w.amount, expected_amount
|
||||||
|
assert_equal w.amount, wallet[:amount]
|
||||||
|
|
||||||
|
# refund invoice must be generated
|
||||||
|
invoice = Invoice.where(invoiced: w.wallet_transactions.last).first
|
||||||
|
assert_equal amount, (invoice.total / 100.0), 'Avoir total does not match the amount credited to the wallet'
|
||||||
|
assert_equal amount, (invoice.invoice_items.first.amount / 100.0), 'Invoice item amount does not match'
|
||||||
|
assert_invoice_pdf invoice
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -1,6 +1,14 @@
|
|||||||
require 'test_helper'
|
require 'test_helper'
|
||||||
|
|
||||||
class CouponTest < ActiveSupport::TestCase
|
class CouponTest < ActiveSupport::TestCase
|
||||||
|
|
||||||
|
test 'valid coupon with percentage' do
|
||||||
|
c = Coupon.new({name: 'Hot deals', code: 'HOT15', percent_off: 15, validity_per_user: 'once', valid_until: (Time.now + 2.weeks), max_usages: 100, active: true})
|
||||||
|
assert c.valid?
|
||||||
|
assert_equal 'active', c.status, 'Invalid coupon status'
|
||||||
|
assert_equal 'percent_off', c.type, 'Invalid coupon type'
|
||||||
|
end
|
||||||
|
|
||||||
test 'coupon must have a valid percentage' do
|
test 'coupon must have a valid percentage' do
|
||||||
c = Coupon.new({name: 'Amazing deal', code: 'DISCOUNT', percent_off: 200, validity_per_user: 'once'})
|
c = Coupon.new({name: 'Amazing deal', code: 'DISCOUNT', percent_off: 200, validity_per_user: 'once'})
|
||||||
assert c.invalid?
|
assert c.invalid?
|
||||||
@ -16,9 +24,11 @@ class CouponTest < ActiveSupport::TestCase
|
|||||||
assert c.invalid?
|
assert c.invalid?
|
||||||
end
|
end
|
||||||
|
|
||||||
test 'coupon with cash amount has amount_off type' do
|
test 'valid coupon with cash amount' do
|
||||||
c = Coupon.new({name: 'Essential Box', code: 'KWXX2M', amount_off: 2000, validity_per_user: 'once', max_usages: 1})
|
c = Coupon.new({name: 'Essential Box', code: 'KWXX2M', amount_off: 2000, validity_per_user: 'once', max_usages: 1, active: true})
|
||||||
assert_equal 'amount_off', c.type
|
assert c.valid?
|
||||||
|
assert_equal 'active', c.status, 'Invalid coupon status'
|
||||||
|
assert_equal 'amount_off', c.type, 'Invalid coupon type'
|
||||||
end
|
end
|
||||||
|
|
||||||
test 'coupon with cash amount cannot be used with cheaper cart' do
|
test 'coupon with cash amount cannot be used with cheaper cart' do
|
||||||
|
@ -1,3 +1,6 @@
|
|||||||
|
require 'coveralls'
|
||||||
|
Coveralls.wear!('rails')
|
||||||
|
|
||||||
ENV['RAILS_ENV'] ||= 'test'
|
ENV['RAILS_ENV'] ||= 'test'
|
||||||
require File.expand_path('../../config/environment', __FILE__)
|
require File.expand_path('../../config/environment', __FILE__)
|
||||||
require 'rails/test_help'
|
require 'rails/test_help'
|
||||||
@ -68,6 +71,33 @@ class ActiveSupport::TestCase
|
|||||||
|
|
||||||
assert File.exist?(invoice.file), 'Invoice PDF was not generated'
|
assert File.exist?(invoice.file), 'Invoice PDF was not generated'
|
||||||
|
|
||||||
|
# now we check the file content
|
||||||
|
reader = PDF::Reader.new(invoice.file)
|
||||||
|
assert_equal 1, reader.page_count # single page invoice
|
||||||
|
|
||||||
|
ht_amount = invoice.total
|
||||||
|
page = reader.pages.first
|
||||||
|
lines = page.text.scan(/^.+/)
|
||||||
|
lines.each do |line|
|
||||||
|
# check that the numbers printed into the PDF file match the total stored in DB
|
||||||
|
if line.include? I18n.t('invoices.total_amount')
|
||||||
|
assert_equal invoice.total / 100.0, parse_amount_from_invoice_line(line), 'Invoice total rendered in the PDF file does not match'
|
||||||
|
end
|
||||||
|
|
||||||
|
# check that the VAT was correctly applied if it was configured
|
||||||
|
if line.include? I18n.t('invoices.including_total_excluding_taxes')
|
||||||
|
ht_amount = parse_amount_from_invoice_line(line)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if Setting.find_by(name: 'invoice_VAT-active').value == 'true'
|
||||||
|
vat_rate = Setting.find_by({name: 'invoice_VAT-rate'}).value.to_f
|
||||||
|
computed_ht = sprintf('%.2f', (invoice.total / (vat_rate / 100 + 1)) / 100.0).to_f
|
||||||
|
|
||||||
|
assert_equal computed_ht, ht_amount, 'Total excluding taxes rendered in the PDF file is not computed correctly'
|
||||||
|
else
|
||||||
|
assert_equal invoice.total, ht_amount, 'VAT information was rendered in the PDF file despite that VAT was disabled'
|
||||||
|
end
|
||||||
File.delete(invoice.file)
|
File.delete(invoice.file)
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -89,6 +119,15 @@ class ActiveSupport::TestCase
|
|||||||
skip('Unable to test export which is not of the category "statistics"')
|
skip('Unable to test export which is not of the category "statistics"')
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
# Parse a line of text read from a PDF file and return the price included inside
|
||||||
|
# Line of text should be of form 'Label $10.00'
|
||||||
|
# @returns {float}
|
||||||
|
def parse_amount_from_invoice_line line
|
||||||
|
line[line.rindex(' ')+1..-1].tr(I18n.t('number.currency.format.unit'), '').to_f
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
class ActionDispatch::IntegrationTest
|
class ActionDispatch::IntegrationTest
|
||||||
|
Loading…
x
Reference in New Issue
Block a user