1
0
mirror of https://github.com/LaCasemate/fab-manager.git synced 2025-01-30 19:52:20 +01:00

Merge branch 'invoicingprofile' into dev

This commit is contained in:
Sylvain 2019-06-03 14:25:10 +02:00
commit a803ef819a
42 changed files with 565 additions and 222 deletions

View File

@ -6,6 +6,7 @@
- Fix a bug: (spanish) some translations are not loaded correctly - Fix a bug: (spanish) some translations are not loaded correctly
- Fix a bug: some users may not appear in the admin's general listing - Fix a bug: some users may not appear in the admin's general listing
- Fix a bug: Availabilities export report an erroneous number of reservations for machine availabilities (#131) - Fix a bug: Availabilities export report an erroneous number of reservations for machine availabilities (#131)
- Fix a bug: close period reminder is sent before the first invoice's first anniversary
- Improved translations syntax according to YML specifications - Improved translations syntax according to YML specifications
- Refactored some Ruby code to match style guide - Refactored some Ruby code to match style guide
- [TODO DEPLOY] `rake fablab:fix:users_group_ids` - [TODO DEPLOY] `rake fablab:fix:users_group_ids`

View File

@ -14,9 +14,7 @@ FabManager is the Fab Lab management solution. It provides a comprehensive, web-
4.1. [General Guidelines](#general-guidelines)<br/> 4.1. [General Guidelines](#general-guidelines)<br/>
4.2. [Virtual Machine Instructions](#virtual-machine-instructions) 4.2. [Virtual Machine Instructions](#virtual-machine-instructions)
5. [PostgreSQL](#postgresql)<br/> 5. [PostgreSQL](#postgresql)<br/>
5.1. [Install PostgreSQL 9.4](#setup-postgresql)<br/> 5.1. [Install PostgreSQL 9.4](#setup-postgresql)
5.2. [Run the PostgreSQL command line interface](#run-postgresql-cli)<br/>
5.3. [PostgreSQL Limitations](#postgresql-limitations)
6. [ElasticSearch](#elasticsearch)<br/> 6. [ElasticSearch](#elasticsearch)<br/>
6.1. [Install ElasticSearch](#setup-elasticsearch)<br/> 6.1. [Install ElasticSearch](#setup-elasticsearch)<br/>
6.2. [Rebuild statistics](#rebuild-stats)<br/> 6.2. [Rebuild statistics](#rebuild-stats)<br/>
@ -293,55 +291,8 @@ We will use docker to easily install the required version of PostgreSQL.
On MacOS, you'll have to set the host to 127.0.0.1 (or localhost). On MacOS, you'll have to set the host to 127.0.0.1 (or localhost).
See [environment.md](doc/environment.md) for more details. See [environment.md](doc/environment.md) for more details.
4. Finally, have a look at the [PostgreSQL Limitations](#postgresql-limitations) section or some errors will occurs preventing you from finishing the installation procedure. 4 . Finally, you may want to have a look at detailed informations about PostgreSQL usage in fab-manager.
Some information about that is available in the [PostgreSQL Readme](doc/postgresql_readme.md).
<a name="run-postgresql-cli"></a>
### Run the PostgreSQL command line interface
You may want to access the psql command line tool to check the content of the database, or to run some maintenance routines.
This can be achieved doing the following:
1. Enter into the PostgreSQL container
```bash
docker exec -it fabmanager-postgres bash
```
2. Run the PostgreSQL administration command line interface, logged as the postgres user
```bash
su postgres
psql
```
<a name="postgresql-limitations"></a>
### PostgreSQL Limitations
- While setting up the database, we'll need to activate two PostgreSQL extensions: [unaccent](https://www.postgresql.org/docs/current/static/unaccent.html) and [trigram](https://www.postgresql.org/docs/current/static/pgtrgm.html).
This can only be achieved if the user, configured in `config/database.yml`, was granted the _SUPERUSER_ role **OR** if these extensions were white-listed.
So here's your choices, mainly depending on your security requirements:
- Use the default PostgreSQL super-user (postgres) as the database user. This is the default behavior in fab-manager.
- Set your user as _SUPERUSER_; run the following command in `psql` (after replacing `username` with you user name):
```sql
ALTER USER username WITH SUPERUSER;
```
- Install and configure the PostgreSQL extension [pgextwlist](https://github.com/dimitri/pgextwlist).
Please follow the instructions detailed on the extension website to whitelist `unaccent` and `trigram` for the user configured in `config/database.yml`.
- Some users may want to use another DBMS than PostgreSQL.
This is currently not supported, because of some PostgreSQL specific instructions that cannot be efficiently handled with the ActiveRecord ORM:
- `app/controllers/api/members_controllers.rb@list` is using `ILIKE`
- `app/controllers/api/invoices_controllers.rb@list` is using `ILIKE` and `date_trunc()`
- `db/migrate/20160613093842_create_unaccent_function.rb` is using [unaccent](https://www.postgresql.org/docs/current/static/unaccent.html) and [trigram](https://www.postgresql.org/docs/current/static/pgtrgm.html) modules and defines a PL/pgSQL function (`f_unaccent()`)
- `app/controllers/api/members_controllers.rb@search` is using `f_unaccent()` (see above) and `regexp_replace()`
- `db/migrate/20150604131525_add_meta_data_to_notifications.rb` is using [jsonb](https://www.postgresql.org/docs/9.4/static/datatype-json.html), a PostgreSQL 9.4+ datatype.
- `db/migrate/20160915105234_add_transformation_to_o_auth2_mapping.rb` is using [jsonb](https://www.postgresql.org/docs/9.4/static/datatype-json.html), a PostgreSQL 9.4+ datatype.
- `db/migrate/20181217103441_migrate_settings_value_to_history_values.rb` is using `SELECT DISTINCT ON`.
- `db/migrate/20190107111749_protect_accounting_periods.rb` is using `CREATE RULE` and `DROP RULE`.
- If you intend to contribute to the project code, you will need to run the test suite with `rake test`.
This also requires your user to have the _SUPERUSER_ role.
Please see the [known issues](#known-issues) section for more information about this.
<a name="elasticsearch"></a> <a name="elasticsearch"></a>
## ElasticSearch ## ElasticSearch

View File

@ -314,15 +314,14 @@ Application.Controllers.controller('EditMemberController', ['$scope', '$state',
// Form action on the above URL // Form action on the above URL
$scope.method = 'patch'; $scope.method = 'patch';
// List of tags associables with user // List of tags joinable with user
$scope.tags = tagsPromise; $scope.tags = tagsPromise;
// The user to edit // The user to edit
$scope.user = memberPromise; $scope.user = memberPromise;
// Should the passord be modified? // Should the password be modified?
$scope.password = $scope.password = { change: false };
{ change: false };
// the user subscription // the user subscription
if (($scope.user.subscribed_plan != null) && ($scope.user.subscription != null)) { if (($scope.user.subscribed_plan != null) && ($scope.user.subscription != null)) {
@ -576,22 +575,20 @@ Application.Controllers.controller('NewMemberController', ['$scope', '$state', '
// Form action on the above URL // Form action on the above URL
$scope.method = 'post'; $scope.method = 'post';
// Should the passord be set manually or generated? // Should the password be set manually or generated?
$scope.password = $scope.password = { change: false };
{ change: false };
// Default member's profile parameters // Default member's profile parameters
$scope.user = $scope.user = { plan_interval: '' };
{ plan_interval: '' };
// Callback when the admin check/unckeck the box telling that the new user is an organization. // Callback when the admin check/uncheck the box telling that the new user is an organization.
// Disable or enable the organization fields in the form, accordingly // Disable or enable the organization fields in the form, accordingly
$scope.toggleOrganization = function () { $scope.toggleOrganization = function () {
if ($scope.user.organization) { if ($scope.user.organization) {
if (!$scope.user.profile) { $scope.user.profile = {}; } if (!$scope.user.invoicing_profile) { $scope.user.invoicing_profile = {}; }
return $scope.user.profile.organization = {}; $scope.user.invoicing_profile.organization = {};
} else { } else {
return $scope.user.profile.organization = undefined; $scope.user.invoicing_profile.organization = undefined;
} }
}; };
@ -609,7 +606,8 @@ Application.Controllers.controller('NewAdminController', ['$state', '$scope', 'A
$scope.admin = { $scope.admin = {
profile_attributes: { profile_attributes: {
gender: true gender: true
} },
invoicing_profile_attributes: {}
}; };
// Default parameters for AngularUI-Bootstrap datepicker // Default parameters for AngularUI-Bootstrap datepicker

View File

@ -96,11 +96,11 @@
<div class="input-group"> <div class="input-group">
<span class="input-group-addon"><i class="fa fa-map-marker"></i> </span> <span class="input-group-addon"><i class="fa fa-map-marker"></i> </span>
<input type="hidden" <input type="hidden"
name="admin[profile_attributes][address_attributes][id]" name="admin[invoicing_profile_attributes][address_attributes][id]"
ng-value="admin.profile_attributes.address.id" /> ng-value="admin.invoicing_profile_attributes.address.id" />
<input ng-model="admin.profile_attributes.address_attributes.address" <input ng-model="admin.invoicing_profile_attributes.address_attributes.address"
type="text" type="text"
name="admin[profile_attributes][address_attributes][address]" name="admin[invoicing_profile_attributes][address_attributes][address]"
class="form-control" class="form-control"
id="user_address" id="user_address"
placeholder="{{ 'address' | translate }}"> placeholder="{{ 'address' | translate }}">

View File

@ -2,6 +2,7 @@
<input name="_method" type="hidden" ng-value="method"> <input name="_method" type="hidden" ng-value="method">
<input name="user[profile_attributes][id]" type="hidden" ng-value="user.profile.id"> <input name="user[profile_attributes][id]" type="hidden" ng-value="user.profile.id">
<input name="user[invoicing_profile_attributes][id]" type="hidden" ng-value="user.invoicing_profile.id">
<div class="row m-t"> <div class="row m-t">
<div class="col-sm-3 col-sm-offset-1"> <div class="col-sm-3 col-sm-offset-1">
@ -165,38 +166,38 @@
<span class="help-block" ng-show="userForm['user[password_confirmation]'].$error.match" translate>{{ 'confirmation_mismatch_with_password' }}</span> <span class="help-block" ng-show="userForm['user[password_confirmation]'].$error.match" translate>{{ 'confirmation_mismatch_with_password' }}</span>
</div> </div>
<div class="form-group" ng-if="user.profile.organization" ng-class="{'has-error': userForm['user[profile_attributes][organization_attributes][name]'].$dirty && userForm['user[profile_attributes][organization_attributes][name]'].$invalid}"> <div class="form-group" ng-if="user.invoicing_profile.organization" ng-class="{'has-error': userForm['user[invoicing_profile_attributes][organization_attributes][name]'].$dirty && userForm['user[invoicing_profile_attributes][organization_attributes][name]'].$invalid}">
<div class="input-group"> <div class="input-group">
<span class="input-group-addon"><i class="fa fa-building-o"></i> <span class="exponent"><i class="fa fa-asterisk" aria-hidden="true"></i></span></span> <span class="input-group-addon"><i class="fa fa-building-o"></i> <span class="exponent"><i class="fa fa-asterisk" aria-hidden="true"></i></span></span>
<input type="hidden" <input type="hidden"
name="user[profile_attributes][organization_attributes][id]" name="user[invoicing_profile_attributes][organization_attributes][id]"
ng-value="user.profile.organization.id" /> ng-value="user.invoicing_profile.organization.id" />
<input type="text" <input type="text"
name="user[profile_attributes][organization_attributes][name]" name="user[invoicing_profile_attributes][organization_attributes][name]"
ng-model="user.profile.organization.name" ng-model="user.invoicing_profile.organization.name"
class="form-control" class="form-control"
placeholder="{{ 'organization_name' | translate }}" placeholder="{{ 'organization_name' | translate }}"
ng-required="user.profile.organization" ng-required="user.invoicing_profile.organization"
ng-disabled="preventField['profile.organization_name'] && user.profile.organization.name && !userForm['user[profile_attributes][organization_attributes][name]'].$dirty"> ng-disabled="preventField['profile.organization_name'] && user.invoicing_profile.organization.name && !userForm['user[invoicing_profile_attributes][organization_attributes][name]'].$dirty">
</div> </div>
<span class="help-block" ng-show="userForm['user[profile_attributes][organization_attributes][name]'].$dirty && userForm['user[profile_attributes][organization_attributes][name]'].$error.required" translate>{{ 'organization_name_is_required' }}</span> <span class="help-block" ng-show="userForm['user[invoicing_][organization_attributes][name]'].$dirty && userForm['user[invoicing_profile_attributes][organization_attributes][name]'].$error.required" translate>{{ 'organization_name_is_required' }}</span>
</div> </div>
<div class="form-group" ng-if="user.profile.organization" ng-class="{'has-error': userForm['user[profile_attributes][organization_attributes][address_attributes][address]'].$dirty && userForm['user[profile_attributes][organization_attributes][address_attributes][address]'].$invalid}"> <div class="form-group" ng-if="user.invoicing_profile.organization" ng-class="{'has-error': userForm['user[invoicing_profile_attributes][organization_attributes][address_attributes][address]'].$dirty && userForm['user[invoicing_profile_attributes][organization_attributes][address_attributes][address]'].$invalid}">
<div class="input-group"> <div class="input-group">
<span class="input-group-addon"><i class="fa fa-map-marker"></i> <span class="exponent"><i class="fa fa-asterisk" aria-hidden="true"></i></span></span> <span class="input-group-addon"><i class="fa fa-map-marker"></i> <span class="exponent"><i class="fa fa-asterisk" aria-hidden="true"></i></span></span>
<input type="hidden" <input type="hidden"
name="user[profile_attributes][organization_attributes][address_attributes][id]" name="user[invoicing_profile_attributes][organization_attributes][address_attributes][id]"
ng-value="user.profile.organization.address.id" /> ng-value="user.invoicing_profile.organization.address.id" />
<input type="text" <input type="text"
name="user[profile_attributes][organization_attributes][address_attributes][address]" name="user[invoicing_profile_attributes][organization_attributes][address_attributes][address]"
ng-model="user.profile.organization.address.address" ng-model="user.invoicing_profile.organization.address.address"
class="form-control" class="form-control"
placeholder="{{ 'organization_address' | translate }}" placeholder="{{ 'organization_address' | translate }}"
ng-required="user.profile.organization" ng-required="user.invoicing_profile.organization"
ng-disabled="preventField['profile.organization_address'] && user.profile.organization.address.address && !userForm['user[profile_attributes][organization_attributes][address_attributes][address]'].$dirty"> ng-disabled="preventField['profile.organization_address'] && user.invoicing_profile.organization.address.address && !userForm['user[invoicing_profile_attributes][organization_attributes][address_attributes][address]'].$dirty">
</div> </div>
<span class="help-block" ng-show="userForm['user[profile_attributes][organization_attributes][address_attributes][address]'].$dirty && userForm['user[profile_attributes][organization_attributes][address_attributes][address]'].$error.required" translate>{{ 'organization_address_is_required' }}</span> <span class="help-block" ng-show="userForm['user[invoicing_profile_attributes][organization_attributes][address_attributes][address]'].$dirty && userForm['user[invoicing_profile_attributes][organization_attributes][address_attributes][address]'].$error.required" translate>{{ 'organization_address_is_required' }}</span>
</div> </div>
<div class="form-group" ng-class="{'has-error': userForm['user[profile_attributes][birthday]'].$dirty && userForm['user[profile_attributes][birthday]'].$invalid}"> <div class="form-group" ng-class="{'has-error': userForm['user[profile_attributes][birthday]'].$dirty && userForm['user[profile_attributes][birthday]'].$invalid}">
@ -224,14 +225,14 @@
<div class="input-group"> <div class="input-group">
<span class="input-group-addon"><i class="fa fa-map-marker"></i> </span> <span class="input-group-addon"><i class="fa fa-map-marker"></i> </span>
<input type="hidden" <input type="hidden"
name="user[profile_attributes][address_attributes][id]" name="user[invoicing_profile_attributes][address_attributes][id]"
ng-value="user.profile.address.id" /> ng-value="user.invoicing_profile.address.id" />
<input type="text" <input type="text"
name="user[profile_attributes][address_attributes][address]" name="user[invoicing_profile_attributes][address_attributes][address]"
ng-model="user.profile.address.address" ng-model="user.invoicing_profile.address.address"
class="form-control" class="form-control"
id="user_address" id="user_address"
ng-disabled="preventField['profile.address'] && user.profile.address.address && !userForm['user[profile_attributes][address_attributes][address]'].$dirty" ng-disabled="preventField['profile.address'] && user.invoicing_profile.address.address && !userForm['user[invoicing_profile_attributes][address_attributes][address]'].$dirty"
placeholder="{{ 'address' | translate }}"/> placeholder="{{ 'address' | translate }}"/>
</div> </div>
</div> </div>

View File

@ -45,7 +45,10 @@ class API::AdminsController < API::ApiController
private private
def admin_params def admin_params
params.require(:admin).permit(:username, :email, profile_attributes: [:first_name, :last_name, :gender, params.require(:admin).permit(
:birthday, :phone, address_attributes: [:address]]) :username, :email,
profile_attributes: %i[first_name last_name gender birthday phone],
invoicing_profile_attributes: [address_attributes: [:address]]
)
end end
end end

View File

@ -8,7 +8,7 @@ class API::InvoicesController < API::ApiController
def index def index
authorize Invoice authorize Invoice
@invoices = Invoice.includes( @invoices = Invoice.includes(
:avoir, :invoiced, invoice_items: %i[subscription invoice_item], user: %i[profile trainings] :avoir, :invoiced, :invoicing_profile, invoice_items: %i[subscription invoice_item]
).all.order('reference DESC') ).all.order('reference DESC')
end end

View File

@ -195,10 +195,11 @@ class API::MembersController < API::ApiController
:software_mastered, :website, :job, :facebook, :twitter, :software_mastered, :website, :job, :facebook, :twitter,
:google_plus, :viadeo, :linkedin, :instagram, :youtube, :vimeo, :google_plus, :viadeo, :linkedin, :instagram, :youtube, :vimeo,
:dailymotion, :github, :echosciences, :pinterest, :lastfm, :flickr, :dailymotion, :github, :echosciences, :pinterest, :lastfm, :flickr,
user_avatar_attributes: %i[id attachment destroy], user_avatar_attributes: %i[id attachment destroy]],
address_attributes: %i[id address], invoicing_profile_attributes: [
organization_attributes: [:id, :name, address_attributes: %i[id address],
address_attributes: %i[id address]]]) organization_attributes: [:id, :name, address_attributes: %i[id address]]
])
elsif current_user.admin? elsif current_user.admin?
params.require(:user).permit(:username, :email, :password, :password_confirmation, params.require(:user).permit(:username, :email, :password, :password_confirmation,
@ -208,10 +209,12 @@ class API::MembersController < API::ApiController
:software_mastered, :website, :job, :facebook, :twitter, :software_mastered, :website, :job, :facebook, :twitter,
:google_plus, :viadeo, :linkedin, :instagram, :youtube, :vimeo, :google_plus, :viadeo, :linkedin, :instagram, :youtube, :vimeo,
:dailymotion, :github, :echosciences, :pinterest, :lastfm, :flickr, :dailymotion, :github, :echosciences, :pinterest, :lastfm, :flickr,
user_avatar_attributes: %i[id attachment destroy], user_avatar_attributes: %i[id attachment destroy]],
address_attributes: %i[id address], invoicing_profile_attributes: [
organization_attributes: [:id, :name, :id,
address_attributes: %i[id address]]]) address_attributes: %i[id address],
organization_attributes: [:id, :name, address_attributes: %i[id address]]
])
end end
end end

View File

@ -32,12 +32,12 @@ class ApplicationController < ActionController::Base
def configure_permitted_parameters def configure_permitted_parameters
devise_parameter_sanitizer.permit(:sign_up, devise_parameter_sanitizer.permit(:sign_up,
keys: [ keys: [
{ profile_attributes: [ {
:phone, :last_name, :first_name, :gender, :birthday, profile_attributes: %i[phone last_name first_name gender birthday interest software_mastered],
:interest, :software_mastered, organization_attributes: [ invoicing_profile_attributes: [
:name, address_attributes: [:address] organization_attributes: [:name, address_attributes: [:address]]
] ]
] }, },
:username, :is_allow_contact, :is_allow_newsletter, :cgu, :group_id :username, :is_allow_contact, :is_allow_newsletter, :cgu, :group_id
]) ])
end end

View File

@ -12,10 +12,14 @@ class Invoice < ActiveRecord::Base
has_many :invoice_items, dependent: :destroy has_many :invoice_items, dependent: :destroy
accepts_nested_attributes_for :invoice_items accepts_nested_attributes_for :invoice_items
belongs_to :user belongs_to :invoicing_profile
belongs_to :wallet_transaction belongs_to :wallet_transaction
belongs_to :coupon belongs_to :coupon
belongs_to :subscription, foreign_type: 'Subscription', foreign_key: 'invoiced_id'
belongs_to :reservation, foreign_type: 'Reservation', foreign_key: 'invoiced_id'
belongs_to :offer_day, foreign_type: 'OfferDay', foreign_key: 'invoiced_id'
has_one :avoir, class_name: 'Invoice', foreign_key: :invoice_id, dependent: :destroy has_one :avoir, class_name: 'Invoice', foreign_key: :invoice_id, dependent: :destroy
belongs_to :operator, foreign_key: :operator_id, class_name: 'User' belongs_to :operator, foreign_key: :operator_id, class_name: 'User'
@ -37,6 +41,10 @@ class Invoice < ActiveRecord::Base
"#{ENV['INVOICE_PREFIX']}-#{id}_#{created_at.strftime('%d%m%Y')}.pdf" "#{ENV['INVOICE_PREFIX']}-#{id}_#{created_at.strftime('%d%m%Y')}.pdf"
end end
def user
invoicing_profile.user
end
def generate_reference def generate_reference
pattern = Setting.find_by(name: 'invoice_reference').value pattern = Setting.find_by(name: 'invoice_reference').value
@ -244,7 +252,7 @@ class Invoice < ActiveRecord::Base
def generate_and_send_invoice def generate_and_send_invoice
unless Rails.env.test? unless Rails.env.test?
puts "Creating an InvoiceWorker job to generate the following invoice: id(#{id}), invoiced_id(#{invoiced_id}), " \ puts "Creating an InvoiceWorker job to generate the following invoice: id(#{id}), invoiced_id(#{invoiced_id}), " \
"invoiced_type(#{invoiced_type}), user_id(#{user_id})" "invoiced_type(#{invoiced_type}), user_id(#{invoicing_profile.user_id})"
end end
InvoiceWorker.perform_async(id, user&.subscription&.expired_at) InvoiceWorker.perform_async(id, user&.subscription&.expired_at)
end end

View File

@ -0,0 +1,14 @@
class InvoicingProfile < ActiveRecord::Base
belongs_to :user
has_one :address, as: :placeable, dependent: :destroy
accepts_nested_attributes_for :address, allow_destroy: true
has_one :organization, dependent: :destroy
accepts_nested_attributes_for :organization, allow_destroy: false
has_many :invoices, dependent: :destroy
def full_name
# if first_name or last_name is nil, the empty string will be used as a temporary replacement
(first_name || '').humanize.titleize + ' ' + (last_name || '').humanize.titleize
end
end

View File

@ -1,5 +1,6 @@
class Organization < ActiveRecord::Base class Organization < ActiveRecord::Base
belongs_to :profile belongs_to :profile
belongs_to :invoicing_profile
has_one :address, as: :placeable, dependent: :destroy has_one :address, as: :placeable, dependent: :destroy
accepts_nested_attributes_for :address, allow_destroy: true accepts_nested_attributes_for :address, allow_destroy: true

View File

@ -1,21 +1,21 @@
# frozen_string_literal: true
# Personal data attached to an user (like first_name, date of birth, etc.)
class Profile < ActiveRecord::Base class Profile < ActiveRecord::Base
belongs_to :user belongs_to :user
has_one :user_avatar, as: :viewable, dependent: :destroy has_one :user_avatar, as: :viewable, dependent: :destroy
accepts_nested_attributes_for :user_avatar, accepts_nested_attributes_for :user_avatar,
allow_destroy: true, allow_destroy: true,
reject_if: proc { |attributes| attributes['attachment'].blank? } reject_if: proc { |attributes| attributes['attachment'].blank? }
has_one :address, as: :placeable, dependent: :destroy
accepts_nested_attributes_for :address, allow_destroy: true
has_one :organization, dependent: :destroy
accepts_nested_attributes_for :organization, allow_destroy: false
validates :first_name, presence: true, length: { maximum: 30 } validates :first_name, presence: true, length: { maximum: 30 }
validates :last_name, presence: true, length: { maximum: 30 } validates :last_name, presence: true, length: { maximum: 30 }
validates :gender, :inclusion => {:in => [true, false]} validates :gender, inclusion: { in: [true, false] }
validates :birthday, presence: true validates :birthday, presence: true
validates_numericality_of :phone, only_integer: true, allow_blank: false validates_numericality_of :phone, only_integer: true, allow_blank: false
after_save :update_invoicing_profile
def full_name def full_name
# if first_name or last_name is nil, the empty string will be used as a temporary replacement # if first_name or last_name is nil, the empty string will be used as a temporary replacement
(first_name || '').humanize.titleize + ' ' + (last_name || '').humanize.titleize (first_name || '').humanize.titleize + ' ' + (last_name || '').humanize.titleize
@ -40,13 +40,30 @@ class Profile < ActiveRecord::Base
def self.mapping def self.mapping
# we protect some fields as they are designed to be managed by the system and must not be updated externally # we protect some fields as they are designed to be managed by the system and must not be updated externally
blacklist = %w(id user_id created_at updated_at) blacklist = %w[id user_id created_at updated_at]
# model-relationships must be added manually # model-relationships must be added manually
additional = [%w(avatar string), %w(address string), %w(organization_name string), %w(organization_address string)] additional = [%w[avatar string], %w[address string], %w[organization_name string], %w[organization_address string]]
Profile.column_types Profile.column_types
.map{|k,v| [k, v.type.to_s]} .map { |k, v| [k, v.type.to_s] }
.delete_if { |col| blacklist.include?(col[0]) } .delete_if { |col| blacklist.include?(col[0]) }
.concat(additional) .concat(additional)
end
private
def update_invoicing_profile
if user.invoicing_profile.nil?
InvoicingProfile.create!(
user: user,
first_name: first_name,
last_name: last_name
)
else
user.invoicing_profile.update_attributes(
first_name: first_name,
last_name: last_name
)
end
end end
end end

View File

@ -227,7 +227,7 @@ class Reservation < ActiveRecord::Base
def save_with_payment(operator_id, coupon_code = nil) def save_with_payment(operator_id, coupon_code = nil)
begin begin
clean_pending_strip_invoice_items clean_pending_strip_invoice_items
build_invoice(user: user, operator_id: operator_id) build_invoice(invoicing_profile: user.invoicing_profile, operator_id: operator_id)
invoice_items = generate_invoice_items(false, coupon_code) invoice_items = generate_invoice_items(false, coupon_code)
rescue StandardError => e rescue StandardError => e
logger.error e logger.error e
@ -369,7 +369,7 @@ class Reservation < ActiveRecord::Base
end end
def save_with_local_payment(operator_id, coupon_code = nil) def save_with_local_payment(operator_id, coupon_code = nil)
build_invoice(user: user, operator_id: operator_id) build_invoice(invoicing_profile: user.invoicing_profile, operator_id: operator_id)
generate_invoice_items(true, coupon_code) generate_invoice_items(true, coupon_code)
return false unless valid? return false unless valid?

View File

@ -18,8 +18,8 @@ class Subscription < ActiveRecord::Base
after_save :notify_partner_subscribed_plan, if: :of_partner_plan? after_save :notify_partner_subscribed_plan, if: :of_partner_plan?
# Stripe subscription payment # Stripe subscription payment
# @params [invoice] if true then subscription pay itself, dont pay with reservation # @param invoice if true then subscription pay itself, dont pay with reservation
# if false then subscription pay with reservation # if false then subscription pay with reservation
def save_with_payment(operator_id, invoice = true, coupon_code = nil) def save_with_payment(operator_id, invoice = true, coupon_code = nil)
return unless valid? return unless valid?
@ -127,8 +127,8 @@ class Subscription < ActiveRecord::Base
end end
end end
# @params [invoice] if true then only the subscription is payed, without reservation # @param invoice if true then only the subscription is payed, without reservation
# if false then the subscription is payed with reservation # if false then the subscription is payed with reservation
def save_with_local_payment(operator_id, invoice = true, coupon_code = nil) def save_with_local_payment(operator_id, invoice = true, coupon_code = nil)
return false unless valid? return false unless valid?
@ -165,7 +165,7 @@ class Subscription < ActiveRecord::Base
end end
end end
invoice = Invoice.new(invoiced_id: id, invoiced_type: 'Subscription', user: user, total: total, stp_invoice_id: stp_invoice_id, coupon_id: coupon_id, operator_id: operator_id) invoice = Invoice.new(invoiced_id: id, invoiced_type: 'Subscription', invoicing_profile: user.invoicing_profile, total: total, stp_invoice_id: stp_invoice_id, coupon_id: coupon_id, operator_id: operator_id)
invoice.invoice_items.push InvoiceItem.new(amount: plan.amount, stp_invoice_item_id: stp_subscription_id, description: plan.name, subscription_id: self.id) invoice.invoice_items.push InvoiceItem.new(amount: plan.amount, stp_invoice_item_id: stp_subscription_id, description: plan.name, subscription_id: self.id)
invoice invoice
end end
@ -212,7 +212,7 @@ class Subscription < ActiveRecord::Base
return false if expiration <= expired_at return false if expiration <= expired_at
od = offer_days.create(start_at: expired_at, end_at: expiration) od = offer_days.create(start_at: expired_at, end_at: expiration)
invoice = Invoice.new(invoiced_id: od.id, invoiced_type: 'OfferDay', user: user, total: 0) invoice = Invoice.new(invoiced_id: od.id, invoiced_type: 'OfferDay', invoicing_profile: user.invoicing_profile, total: 0)
invoice.invoice_items.push InvoiceItem.new(amount: 0, description: plan.name, subscription_id: id) invoice.invoice_items.push InvoiceItem.new(amount: 0, description: plan.name, subscription_id: id)
invoice.save invoice.save

View File

@ -21,6 +21,9 @@ class User < ActiveRecord::Base
has_one :profile, dependent: :destroy has_one :profile, dependent: :destroy
accepts_nested_attributes_for :profile accepts_nested_attributes_for :profile
has_one :invoicing_profile, dependent: :nullify
accepts_nested_attributes_for :invoicing_profile
has_many :my_projects, foreign_key: :author_id, class_name: 'Project', dependent: :destroy has_many :my_projects, foreign_key: :author_id, class_name: 'Project', dependent: :destroy
has_many :project_users, dependent: :destroy has_many :project_users, dependent: :destroy
has_many :projects, through: :project_users has_many :projects, through: :project_users
@ -43,7 +46,6 @@ class User < ActiveRecord::Base
has_many :training_credits, through: :users_credits, source: :training_credit has_many :training_credits, through: :users_credits, source: :training_credit
has_many :machine_credits, through: :users_credits, source: :machine_credit has_many :machine_credits, through: :users_credits, source: :machine_credit
has_many :invoices, dependent: :destroy
has_many :operated_invoices, foreign_key: :operator_id, class_name: 'Invoice', dependent: :nullify has_many :operated_invoices, foreign_key: :operator_id, class_name: 'Invoice', dependent: :nullify
has_many :user_tags, dependent: :destroy has_many :user_tags, dependent: :destroy
@ -67,6 +69,7 @@ class User < ActiveRecord::Base
after_commit :create_stripe_customer, on: [:create] after_commit :create_stripe_customer, on: [:create]
after_commit :notify_admin_when_user_is_created, on: :create after_commit :notify_admin_when_user_is_created, on: :create
after_update :notify_group_changed, if: :group_id_changed? after_update :notify_group_changed, if: :group_id_changed?
after_save :update_invoicing_profile
attr_accessor :cgu attr_accessor :cgu
delegate :first_name, to: :profile delegate :first_name, to: :profile
@ -131,6 +134,10 @@ class User < ActiveRecord::Base
my_projects.to_a.concat projects my_projects.to_a.concat projects
end end
def invoices
invoicing_profile.invoices
end
def generate_subscription_invoice(operator_id) def generate_subscription_invoice(operator_id)
return unless subscription return unless subscription
@ -188,11 +195,11 @@ class User < ActiveRecord::Base
when 'profile.avatar' when 'profile.avatar'
profile.user_avatar.remote_attachment_url profile.user_avatar.remote_attachment_url
when 'profile.address' when 'profile.address'
profile.address.address invoicing_profile.address.address
when 'profile.organization_name' when 'profile.organization_name'
profile.organization.name invoicing_profile.organization.name
when 'profile.organization_address' when 'profile.organization_address'
profile.organization.address.address invoicing_profile.organization.address.address
else else
profile[parsed[2].to_sym] profile[parsed[2].to_sym]
end end
@ -211,15 +218,15 @@ class User < ActiveRecord::Base
profile.user_avatar ||= UserAvatar.new profile.user_avatar ||= UserAvatar.new
profile.user_avatar.remote_attachment_url = data profile.user_avatar.remote_attachment_url = data
when 'profile.address' when 'profile.address'
profile.address ||= Address.new invoicing_profile.address ||= Address.new
profile.address.address = data invoicing_profile.address.address = data
when 'profile.organization_name' when 'profile.organization_name'
profile.organization ||= Organization.new invoicing_profile.organization ||= Organization.new
profile.organization.name = data invoicing_profile.organization.name = data
when 'profile.organization_address' when 'profile.organization_address'
profile.organization ||= Organization.new invoicing_profile.organization ||= Organization.new
profile.organization.address ||= Address.new invoicing_profile.organization.address ||= Address.new
profile.organization.address.address = data invoicing_profile.organization.address.address = data
else else
profile[sso_mapping[8..-1].to_sym] = data unless data.nil? profile[sso_mapping[8..-1].to_sym] = data unless data.nil?
end end
@ -356,4 +363,17 @@ class User < ActiveRecord::Base
receiver: self, receiver: self,
attached_object: self attached_object: self
end end
def update_invoicing_profile
if invoicing_profile.nil?
InvoicingProfile.create!(
user: user,
email: email
)
else
invoicing_profile.update_attributes(
email: email
)
end
end
end end

View File

@ -52,23 +52,23 @@ class PDF::Invoice < Prawn::Document
end end
# user/organization's information # user/organization's information
if invoice&.user&.profile&.organization if invoice&.invoicing_profile&.organization
name = invoice.user.profile.organization.name name = invoice.invoicing_profile.organization.name
full_name = "#{name} (#{invoice.user.profile.full_name})" full_name = "#{name} (#{invoice.invoicing_profile.full_name})"
else else
name = invoice.user.profile.full_name name = invoice.invoicing_profile.full_name
full_name = name full_name = name
end end
address = if invoice&.user&.profile&.organization&.address address = if invoice&.invoicing_profile&.organization&.address
invoice.user.profile.organization.address.address invoice.invoicing_profile.organization.address.address
elsif invoice&.user&.profile&.address elsif invoice&.invoicing_profile&.address
invoice.user.profile.address.address invoice.invoicing_profile.address.address
else else
'' ''
end end
text_box "<b>#{name}</b>\n#{invoice.user.email}\n#{address}", text_box "<b>#{name}</b>\n#{invoice.invoicing_profile.email}\n#{address}",
at: [bounds.width - 130, bounds.top - 49], at: [bounds.width - 130, bounds.top - 49],
width: 130, width: 130,
align: :right, align: :right,

View File

@ -9,8 +9,8 @@ class InvoicesService
# @param size {number} number of items per page # @param size {number} number of items per page
# @param filters {Hash} allowed filters: number, customer, date. # @param filters {Hash} allowed filters: number, customer, date.
def self.list(order_key, direction, page, size, filters = {}) def self.list(order_key, direction, page, size, filters = {})
invoices = Invoice.includes(:avoir, :invoiced, invoice_items: %i[subscription invoice_item], user: %i[profile trainings]) invoices = Invoice.includes(:avoir, :invoicing_profile, invoice_items: %i[subscription invoice_item])
.joins(user: :profile) .joins(:invoicing_profile)
.order("#{order_key} #{direction}") .order("#{order_key} #{direction}")
.page(page) .page(page)
.per(size) .per(size)
@ -25,7 +25,7 @@ class InvoicesService
if filters[:customer].size.positive? if filters[:customer].size.positive?
# ILIKE => PostgreSQL case-insensitive LIKE # ILIKE => PostgreSQL case-insensitive LIKE
invoices = invoices.where( invoices = invoices.where(
'profiles.first_name ILIKE :search OR profiles.last_name ILIKE :search', 'invoicing_profiles.first_name ILIKE :search OR invoicing_profiles.last_name ILIKE :search',
search: "%#{filters[:customer]}%" search: "%#{filters[:customer]}%"
) )
end end

View File

@ -48,7 +48,7 @@ class WalletService
avoir.description = description avoir.description = description
avoir.avoir_mode = 'wallet' avoir.avoir_mode = 'wallet'
avoir.subscription_to_expire = false avoir.subscription_to_expire = false
avoir.user_id = wallet_transaction.wallet.user_id avoir.invoicing_profile_id = wallet_transaction.wallet.user.invoicing_profile.id
avoir.total = wallet_transaction.amount * 100.0 avoir.total = wallet_transaction.amount * 100.0
avoir.save! avoir.save!

View File

@ -6,8 +6,10 @@ json.profile_attributes do
json.gender admin.profile.gender json.gender admin.profile.gender
json.birthday admin.profile.birthday if admin.profile.birthday json.birthday admin.profile.birthday if admin.profile.birthday
json.phone admin.profile.phone json.phone admin.profile.phone
json.user_avatar do if admin.profile.user_avatar
json.id admin.profile.user_avatar.id json.user_avatar do
json.attachment_url admin.profile.user_avatar.attachment_url json.id admin.profile.user_avatar.id
end if admin.profile.user_avatar json.attachment_url admin.profile.user_avatar.attachment_url
end
end
end end

View File

@ -1,4 +1,5 @@
json.extract! @avoir, :id, :created_at, :reference, :invoiced_type, :user_id, :avoir_date, :avoir_mode, :invoice_id json.extract! @avoir, :id, :created_at, :reference, :invoiced_type, :avoir_date, :avoir_mode, :invoice_id
json.user_id @avoir.invoicing_profile.user_id
json.total (@avoir.total / 100.00) json.total (@avoir.total / 100.00)
json.name @avoir.user.profile.full_name json.name @avoir.user.profile.full_name
json.has_avoir false json.has_avoir false

View File

@ -1,11 +1,14 @@
# frozen_string_literal: true
max_invoices = @invoices.except(:offset, :limit, :order).count max_invoices = @invoices.except(:offset, :limit, :order).count
json.array!(@invoices) do |invoice| json.array!(@invoices) do |invoice|
json.maxInvoices max_invoices json.maxInvoices max_invoices
json.extract! invoice, :id, :created_at, :reference, :invoiced_type, :user_id, :avoir_date json.extract! invoice, :id, :created_at, :reference, :invoiced_type, :avoir_date
json.user_id invoice.invoicing_profile.user_id
json.total (invoice.total / 100.00) json.total (invoice.total / 100.00)
json.url invoice_url(invoice, format: :json) json.url invoice_url(invoice, format: :json)
json.name invoice.user.profile.full_name json.name invoice.invoicing_profile.full_name
json.has_avoir invoice.refunded? json.has_avoir invoice.refunded?
json.is_avoir invoice.is_a?(Avoir) json.is_avoir invoice.is_a?(Avoir)
json.is_subscription_invoice invoice.subscription_invoice? json.is_subscription_invoice invoice.subscription_invoice?

View File

@ -2,53 +2,72 @@ json.extract! member, :id, :username, :email, :group_id
json.role member.roles.first.name json.role member.roles.first.name
json.name member.profile.full_name json.name member.profile.full_name
json.need_completion member.need_completion? json.need_completion member.need_completion?
json.profile do json.profile do
json.id member.profile.id json.id member.profile.id
json.user_avatar do if member.profile.user_avatar
json.id member.profile.user_avatar.id json.user_avatar do
json.attachment_url member.profile.user_avatar.attachment_url json.id member.profile.user_avatar.id
end if member.profile.user_avatar json.attachment_url member.profile.user_avatar.attachment_url
end
end
json.first_name member.profile.first_name json.first_name member.profile.first_name
json.last_name member.profile.last_name json.last_name member.profile.last_name
json.gender member.profile.gender.to_s json.gender member.profile.gender.to_s
json.birthday member.profile.birthday.to_date.iso8601 if member.profile.birthday json.birthday member.profile.birthday.to_date.iso8601 if member.profile.birthday
json.interest member.profile.interest json.interest member.profile.interest
json.software_mastered member.profile.software_mastered json.software_mastered member.profile.software_mastered
json.address do
json.id member.profile.address.id
json.address member.profile.address.address
end if member.profile.address
json.phone member.profile.phone json.phone member.profile.phone
json.website member.profile.website json.website member.profile.website
json.job member.profile.job json.job member.profile.job
json.extract! member.profile, :facebook, :twitter, :google_plus, :viadeo, :linkedin, :instagram, :youtube, :vimeo, :dailymotion, :github, :echosciences, :pinterest, :lastfm, :flickr json.extract! member.profile, :facebook, :twitter, :google_plus, :viadeo, :linkedin, :instagram, :youtube, :vimeo, :dailymotion, :github, :echosciences, :pinterest, :lastfm, :flickr
json.organization do
json.id member.profile.organization.id
json.name member.profile.organization.name
json.address do
json.id member.profile.organization.address.id
json.address member.profile.organization.address.address
end if member.profile.organization.address
end if member.profile.organization
end end
json.subscribed_plan do
json.partial! 'api/shared/plan', plan: member.subscribed_plan json.invoicing_profile do
end if member.subscribed_plan json.id member.invoicing_profile.id
json.subscription do if member.invoicing_profile.address
json.id member.subscription.id json.address do
json.expired_at member.subscription.expired_at.iso8601 json.id member.invoicing_profile.address.id
json.canceled_at member.subscription.canceled_at.iso8601 if member.subscription.canceled_at json.address member.invoicing_profile.address.address
json.stripe member.subscription.stp_subscription_id.present? end
json.plan do
json.id member.subscription.plan.id
json.base_name member.subscription.plan.base_name
json.name member.subscription.plan.name
json.interval member.subscription.plan.interval
json.interval_count member.subscription.plan.interval_count
json.amount member.subscription.plan.amount ? (member.subscription.plan.amount / 100.0) : 0
end end
end if member.subscription
if member.invoicing_profile.organization
json.organization do
json.id member.invoicing_profile.organization.id
json.name member.invoicing_profile.organization.name
if member.invoicing_profile.organization.address
json.address do
json.id member.invoicing_profile.organization.address.id
json.address member.invoicing_profile.organization.address.address
end
end
end
end
end
if member.subscribed_plan
json.subscribed_plan do
json.partial! 'api/shared/plan', plan: member.subscribed_plan
end
end
if member.subscription
json.subscription do
json.id member.subscription.id
json.expired_at member.subscription.expired_at.iso8601
json.canceled_at member.subscription.canceled_at.iso8601 if member.subscription.canceled_at
json.stripe member.subscription.stp_subscription_id.present?
json.plan do
json.id member.subscription.plan.id
json.base_name member.subscription.plan.base_name
json.name member.subscription.plan.name
json.interval member.subscription.plan.interval
json.interval_count member.subscription.plan.interval_count
json.amount member.subscription.plan.amount ? (member.subscription.plan.amount / 100.0) : 0
end
end
end
json.training_credits member.training_credits do |tc| json.training_credits member.training_credits do |tc|
json.training_id tc.creditable_id json.training_id tc.creditable_id
end end

View File

@ -44,7 +44,7 @@ wb.add_worksheet(name: t('export_members.members')) do |sheet|
member.is_allow_newsletter, member.is_allow_newsletter,
member.profile.gender ? t('export_members.man') : t('export_members.woman'), member.profile.gender ? t('export_members.man') : t('export_members.woman'),
member.profile.age, member.profile.age,
member.profile.address ? member.profile.address.address : '', member.invoicing_profile.address ? member.invoicing_profile.address.address : '',
member.profile.phone, member.profile.phone,
member.profile.website, member.profile.website,
member.profile.job, member.profile.job,
@ -60,8 +60,8 @@ wb.add_worksheet(name: t('export_members.members')) do |sheet|
member.profile.facebook || '', member.profile.facebook || '',
member.profile.twitter || '', member.profile.twitter || '',
member.profile.echosciences || '', member.profile.echosciences || '',
member.profile.organization ? member.profile.organization.name : '', member.invoicing_profile.organization ? member.invoicing_profile.organization.name : '',
member.profile.organization ? member.profile.organization.address.address : '' member.invoicing_profile.organization ? member.invoicing_profile.organization.address.address : ''
] ]
styles = [nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, date, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil] styles = [nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, date, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil]

View File

@ -2,6 +2,6 @@
<p><%= t('.body.new_account_created') %> "<%= @attached_object.profile.full_name %> &lt;<%= @attached_object.email%>&gt;"</p> <p><%= t('.body.new_account_created') %> "<%= @attached_object.profile.full_name %> &lt;<%= @attached_object.email%>&gt;"</p>
<% if @attached_object.profile.organization %> <% if @attached_object.invoicing_profile.organization %>
<p><%= t('.body.account_for_organization') %> <%= @attached_object.profile.organization.name %></p> <p><%= t('.body.account_for_organization') %> <%= @attached_object.invoicing_profile.organization.name %></p>
<% end %> <% end %>

View File

@ -2,8 +2,12 @@ class ClosePeriodReminderWorker
include Sidekiq::Worker include Sidekiq::Worker
def perform def perform
return if Invoice.count.zero?
last_period = AccountingPeriod.order(closed_at: :desc).limit(1).last last_period = AccountingPeriod.order(closed_at: :desc).limit(1).last
return if Invoice.count == 0 || (last_period && last_period.end_at > (Time.current - 1.year)) first_invoice = Invoice.order(created_at: :asc).limit(1).last
return if !last_period && first_invoice.created_at > (Time.current - 1.year)
return if last_period && last_period.end_at > (Time.current - 1.year)
NotificationCenter.call type: 'notify_admin_close_period_reminder', NotificationCenter.call type: 'notify_admin_close_period_reminder',
receiver: User.admins, receiver: User.admins,

View File

@ -0,0 +1,15 @@
class CreateInvoicingProfiles < ActiveRecord::Migration
def change
create_table :invoicing_profiles do |t|
t.references :user, index: true, foreign_key: true
t.string :first_name
t.string :last_name
t.string :email
t.timestamps null: false
end
add_reference :organizations, :invoicing_profile, index: true, foreign_key: true
add_reference :invoices, :invoicing_profile, index: true, foreign_key: true
end
end

View File

@ -0,0 +1,37 @@
class MigrateProfileToInvoicingProfile < ActiveRecord::Migration
def up
User.all.each do |u|
p = u.profile
puts "WARNING: User #{u.id} has no profile" and next unless p
ip = InvoicingProfile.create!(
user: u,
first_name: p.first_name,
last_name: p.last_name,
email: u.email
)
Address.find_by(placeable_id: p.id, placeable_type: 'Profile')&.update_attributes(
placeable: ip
)
Organization.find_by(profile_id: p.id)&.update_attributes(
invoicing_profile_id: ip.id
)
end
end
def down
InvoicingProfile.all.each do |ip|
profile = ip.user.profile
profile.update_attributes(
first_name: ip.first_name,
last_name: ip.last_name
)
Address.find_by(placeable_id: ip.id, placeable_type: 'InvoicingProfile')&.update_attributes(
placeable: profile
)
Organization.find_by(invoicing_profile_id: ip.id)&.update_attributes(
profile_id: profile.id
)
end
end
end

View File

@ -0,0 +1,98 @@
# frozen_string_literal: true
# migrate the invoices from being attached to a user to invoicing_profiles which are GDPR compliant
class MigrateUserToInvoicingProfile < ActiveRecord::Migration
def up
# first, check the footprints
check_footprints
# if everything is ok, proceed with migration
# remove and save periods in memory
periods = backup_and_remove_periods
# migrate invoices
puts 'Migrating invoices. This may take a while...'
Invoice.order(:id).all.each do |i|
user = User.find(i.user_id)
i.update_column('invoicing_profile_id', user.invoicing_profile.id)
i.update_column('user_id', nil)
end
# chain all records
InvoiceItem.order(:id).all.each(&:chain_record)
Invoice.order(:id).all.each(&:chain_record)
# write memory dump into database
restore_periods(periods)
end
def down
# here we don't check footprints to save processing time and because this is pointless when reverting the migrations
# remove and save periods in memory
periods = backup_and_remove_periods
# reset invoices
Invoice.order(:created_at).all.each do |i|
i.update_column('user_id', i.invoicing_profile.user_id)
i.update_column('invoicing_profile_id', nil)
end
# chain all records
InvoiceItem.order(:id).all.each(&:chain_record)
Invoice.order(:id).all.each(&:chain_record)
# write memory dump into database
restore_periods(periods)
end
def check_footprints
if AccountingPeriod.count.positive?
last_period = AccountingPeriod.order(start_at: 'DESC').first
puts "Checking invoices footprints from #{last_period.end_at}. This may take a while..."
Invoice.where('created_at > ?', last_period.end_at).order(:id).each do |i|
raise "Invalid footprint for invoice #{i.id}" unless i.check_footprint
end
else
puts 'Checking all invoices footprints. This may take a while...'
Invoice.order(:id).all.each do |i|
raise "Invalid footprint for invoice #{i.id}" unless i.check_footprint
end
end
end
# will return an array of hash containing the removed periods data
def backup_and_remove_periods
return [] unless AccountingPeriod.count.positive?
puts 'Removing accounting archives...'
# 1. remove protection for AccountingPeriods
execute("DROP RULE IF EXISTS accounting_periods_del_protect ON #{AccountingPeriod.arel_table.name};")
# 2. backup AccountingPeriods in memory
periods = []
AccountingPeriod.all.each do |p|
periods.push(
start_at: p.start_at,
end_at: p.end_at,
closed_at: p.closed_at,
closed_by: p.closed_by
)
end
# 3. Delete periods from database
AccountingPeriod.all.each do |ap|
execute("DELETE FROM accounting_periods WHERE ID=#{ap.id};")
end
periods
end
def restore_periods(periods)
return unless periods.size.positive?
# 1. recreate AccountingPeriods
puts 'Recreating accounting archives. This may take a while...'
periods.each do |p|
AccountingPeriod.create!(
start_at: p[:start_at],
end_at: p[:end_at],
closed_at: p[:closed_at],
closed_by: p[:closed_by]
)
end
# 2. reset protection for AccountingPeriods
execute("CREATE RULE accounting_periods_del_protect AS ON DELETE TO #{AccountingPeriod.arel_table.name} DO INSTEAD NOTHING;")
end
end

View File

@ -0,0 +1,5 @@
class RemoveUserIdFromInvoice < ActiveRecord::Migration
def change
remove_column :invoices, :user_id, :integer
end
end

View File

@ -0,0 +1,5 @@
class RemoveProfileFromOrganization < ActiveRecord::Migration
def change
remove_reference :organizations, :profile, index: true, foreign_key: true
end
end

View File

@ -11,7 +11,7 @@
# #
# It's strongly recommended that you check this file into your version control system. # It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 20190320091148) do ActiveRecord::Schema.define(version: 20190529120814) do
# These are extensions that must be enabled in order to support this database # These are extensions that must be enabled in order to support this database
enable_extension "plpgsql" enable_extension "plpgsql"
@ -266,7 +266,6 @@ ActiveRecord::Schema.define(version: 20190320091148) do
t.integer "total" t.integer "total"
t.datetime "created_at" t.datetime "created_at"
t.datetime "updated_at" t.datetime "updated_at"
t.integer "user_id"
t.string "reference" t.string "reference"
t.string "avoir_mode" t.string "avoir_mode"
t.datetime "avoir_date" t.datetime "avoir_date"
@ -280,13 +279,25 @@ ActiveRecord::Schema.define(version: 20190320091148) do
t.string "footprint" t.string "footprint"
t.string "environment" t.string "environment"
t.integer "operator_id" t.integer "operator_id"
t.integer "invoicing_profile_id"
end end
add_index "invoices", ["coupon_id"], name: "index_invoices_on_coupon_id", using: :btree add_index "invoices", ["coupon_id"], name: "index_invoices_on_coupon_id", using: :btree
add_index "invoices", ["invoice_id"], name: "index_invoices_on_invoice_id", using: :btree add_index "invoices", ["invoice_id"], name: "index_invoices_on_invoice_id", using: :btree
add_index "invoices", ["user_id"], name: "index_invoices_on_user_id", using: :btree add_index "invoices", ["invoicing_profile_id"], name: "index_invoices_on_invoicing_profile_id", using: :btree
add_index "invoices", ["wallet_transaction_id"], name: "index_invoices_on_wallet_transaction_id", using: :btree add_index "invoices", ["wallet_transaction_id"], name: "index_invoices_on_wallet_transaction_id", using: :btree
create_table "invoicing_profiles", force: :cascade do |t|
t.integer "user_id"
t.string "first_name"
t.string "last_name"
t.string "email"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
end
add_index "invoicing_profiles", ["user_id"], name: "index_invoicing_profiles_on_user_id", using: :btree
create_table "licences", force: :cascade do |t| create_table "licences", force: :cascade do |t|
t.string "name", null: false t.string "name", null: false
t.text "description" t.text "description"
@ -383,12 +394,12 @@ ActiveRecord::Schema.define(version: 20190320091148) do
create_table "organizations", force: :cascade do |t| create_table "organizations", force: :cascade do |t|
t.string "name" t.string "name"
t.datetime "created_at", null: false t.datetime "created_at", null: false
t.datetime "updated_at", null: false t.datetime "updated_at", null: false
t.integer "profile_id" t.integer "invoicing_profile_id"
end end
add_index "organizations", ["profile_id"], name: "index_organizations_on_profile_id", using: :btree add_index "organizations", ["invoicing_profile_id"], name: "index_organizations_on_invoicing_profile_id", using: :btree
create_table "plans", force: :cascade do |t| create_table "plans", force: :cascade do |t|
t.string "name" t.string "name"
@ -884,11 +895,13 @@ ActiveRecord::Schema.define(version: 20190320091148) do
add_foreign_key "history_values", "settings" add_foreign_key "history_values", "settings"
add_foreign_key "history_values", "users" add_foreign_key "history_values", "users"
add_foreign_key "invoices", "coupons" add_foreign_key "invoices", "coupons"
add_foreign_key "invoices", "invoicing_profiles"
add_foreign_key "invoices", "users", column: "operator_id" add_foreign_key "invoices", "users", column: "operator_id"
add_foreign_key "invoices", "wallet_transactions" add_foreign_key "invoices", "wallet_transactions"
add_foreign_key "invoicing_profiles", "users"
add_foreign_key "o_auth2_mappings", "o_auth2_providers" add_foreign_key "o_auth2_mappings", "o_auth2_providers"
add_foreign_key "open_api_calls_count_tracings", "open_api_clients" add_foreign_key "open_api_calls_count_tracings", "open_api_clients"
add_foreign_key "organizations", "profiles" add_foreign_key "organizations", "invoicing_profiles"
add_foreign_key "prices", "groups" add_foreign_key "prices", "groups"
add_foreign_key "prices", "plans" add_foreign_key "prices", "plans"
add_foreign_key "projects_spaces", "projects" add_foreign_key "projects_spaces", "projects"

53
doc/postgresql_readme.md Normal file
View File

@ -0,0 +1,53 @@
# Detailed informations about PostgreSQL usage in fab-manager
<a name="run-postgresql-cli"></a>
## Run the PostgreSQL command line interface
You may want to access the psql command line tool to check the content of the database, or to run some maintenance routines.
This can be achieved doing the following:
1. Enter into the PostgreSQL container
```bash
docker exec -it fabmanager-postgres bash
```
2. Run the PostgreSQL administration command line interface, logged as the postgres user
```bash
su postgres
psql
```
<a name="postgresql-limitations"></a>
## PostgreSQL Limitations
- While setting up the database, we'll need to activate two PostgreSQL extensions: [unaccent](https://www.postgresql.org/docs/current/static/unaccent.html) and [trigram](https://www.postgresql.org/docs/current/static/pgtrgm.html).
This can only be achieved if the user, configured in `config/database.yml`, was granted the _SUPERUSER_ role **OR** if these extensions were white-listed.
So here's your choices, mainly depending on your security requirements:
- Use the default PostgreSQL super-user (postgres) as the database user. This is the default behavior in fab-manager.
- Set your user as _SUPERUSER_; run the following command in `psql` (after replacing `username` with you user name):
```sql
ALTER USER username WITH SUPERUSER;
```
- Install and configure the PostgreSQL extension [pgextwlist](https://github.com/dimitri/pgextwlist).
Please follow the instructions detailed on the extension website to whitelist `unaccent` and `trigram` for the user configured in `config/database.yml`.
- If you intend to contribute to the project code, you will need to run the test suite with `rake test`.
This also requires your user to have the _SUPERUSER_ role.
Please see the [known issues](../README.md#known-issues) section for more information about this.
<a name="using-another-dbms"></a>
## Using another DBMS
Some users may want to use another DBMS than PostgreSQL.
This is currently not supported, because of some PostgreSQL specific instructions that cannot be efficiently handled with the ActiveRecord ORM:
- `app/controllers/api/members_controllers.rb@list` is using `ILIKE`
- `app/controllers/api/invoices_controllers.rb@list` is using `ILIKE` and `date_trunc()`
- `db/migrate/20160613093842_create_unaccent_function.rb` is using [unaccent](https://www.postgresql.org/docs/current/static/unaccent.html) and [trigram](https://www.postgresql.org/docs/current/static/pgtrgm.html) modules and defines a PL/pgSQL function (`f_unaccent()`)
- `app/controllers/api/members_controllers.rb@search` is using `f_unaccent()` (see above) and `regexp_replace()`
- `db/migrate/20150604131525_add_meta_data_to_notifications.rb` is using [jsonb](https://www.postgresql.org/docs/9.4/static/datatype-json.html), a PostgreSQL 9.4+ datatype.
- `db/migrate/20160915105234_add_transformation_to_o_auth2_mapping.rb` is using [jsonb](https://www.postgresql.org/docs/9.4/static/datatype-json.html), a PostgreSQL 9.4+ datatype.
- `db/migrate/20181217103441_migrate_settings_value_to_history_values.rb` is using `SELECT DISTINCT ON`.
- `db/migrate/20190107111749_protect_accounting_periods.rb` is using `CREATE RULE` and `DROP RULE`.
- `db/migrate/20190522115230_migrate_user_to_invoicing_profile.rb` is using `CREATE RULE` and `DROP RULE`.

View File

@ -96,6 +96,8 @@ You can run the following script as root to easily perform all these operations:
```bash ```bash
\curl -sSL https://raw.githubusercontent.com/sleede/fab-manager/master/docker/setup.sh | bash \curl -sSL https://raw.githubusercontent.com/sleede/fab-manager/master/docker/setup.sh | bash
# OR, if you don't want to install fab-manager in /apps/fabmanager, use:
\curl -sSL https://raw.githubusercontent.com/sleede/fab-manager/master/docker/setup.sh | bash -s "/my/custom/path"
``` ```
### Setup folders and env file ### Setup folders and env file

View File

@ -30,8 +30,8 @@ namespace :fablab do
if AccountingPeriod.count.positive? if AccountingPeriod.count.positive?
last_period = AccountingPeriod.order(start_at: 'DESC').first last_period = AccountingPeriod.order(start_at: 'DESC').first
InvoiceItem.where('created_at > ?', last_period.end_at).order(:id).each(&:chain_record)
puts "Regenerating from #{last_period.end_at}..." puts "Regenerating from #{last_period.end_at}..."
InvoiceItem.where('created_at > ?', last_period.end_at).order(:id).each(&:chain_record)
else else
puts '(Re)generating all footprint...' puts '(Re)generating all footprint...'
InvoiceItem.order(:id).all.each(&:chain_record) InvoiceItem.order(:id).all.each(&:chain_record)

View File

@ -8,7 +8,7 @@ address_1:
country: country:
postal_code: postal_code:
placeable_id: 2 placeable_id: 2
placeable_type: Profile placeable_type: InvoicingProfile
created_at: 2016-04-04 15:06:22.166469000 Z created_at: 2016-04-04 15:06:22.166469000 Z
updated_at: 2016-04-04 15:06:22.166469000 Z updated_at: 2016-04-04 15:06:22.166469000 Z
@ -21,7 +21,7 @@ address_2:
country: country:
postal_code: postal_code:
placeable_id: 4 placeable_id: 4
placeable_type: Profile placeable_type: InvoicingProfile
created_at: 2016-04-04 15:10:42.353039000 Z created_at: 2016-04-04 15:10:42.353039000 Z
updated_at: 2016-04-04 15:10:42.353039000 Z updated_at: 2016-04-04 15:10:42.353039000 Z
@ -34,7 +34,7 @@ address_3:
country: country:
postal_code: postal_code:
placeable_id: 5 placeable_id: 5
placeable_type: Profile placeable_type: InvoicingProfile
created_at: 2016-04-04 15:14:08.579603000 Z created_at: 2016-04-04 15:14:08.579603000 Z
updated_at: 2016-04-04 15:14:08.579603000 Z updated_at: 2016-04-04 15:14:08.579603000 Z
@ -47,7 +47,7 @@ address_4:
country: country:
postal_code: postal_code:
placeable_id: 3 placeable_id: 3
placeable_type: Profile placeable_type: InvoicingProfile
created_at: 2016-04-05 08:35:18.597812000 Z created_at: 2016-04-05 08:35:18.597812000 Z
updated_at: 2016-04-05 08:35:18.597812000 Z updated_at: 2016-04-05 08:35:18.597812000 Z

View File

@ -7,7 +7,7 @@ invoice_1:
total: 10000 total: 10000
created_at: 2012-03-12 11:03:31.651441000 Z created_at: 2012-03-12 11:03:31.651441000 Z
updated_at: 2012-03-12 11:03:31.651441000 Z updated_at: 2012-03-12 11:03:31.651441000 Z
user_id: 3 invoicing_profile_id: 3
reference: 1604001/VL reference: 1604001/VL
avoir_mode: avoir_mode:
avoir_date: avoir_date:
@ -15,7 +15,7 @@ invoice_1:
type: type:
subscription_to_expire: subscription_to_expire:
description: description:
footprint: 9b1d216a49a65f5428c92af10e284d6dfe4070f6e65e5eacd735ef770540a16a footprint: d477d23a473c565e2c379263d4c86c9cc80cdd88adc9a3ff7246afccec0e2a18
environment: test environment: test
operator_id: operator_id:
@ -27,7 +27,7 @@ invoice_2:
total: 2000 total: 2000
created_at: 2012-03-12 13:40:22.342717000 Z created_at: 2012-03-12 13:40:22.342717000 Z
updated_at: 2012-03-12 13:40:22.342717000 Z updated_at: 2012-03-12 13:40:22.342717000 Z
user_id: 4 invoicing_profile_id: 4
reference: '1604002' reference: '1604002'
avoir_mode: avoir_mode:
avoir_date: avoir_date:
@ -35,7 +35,7 @@ invoice_2:
type: type:
subscription_to_expire: subscription_to_expire:
description: description:
footprint: 32c09fe7ba92501f9239c111abd6688cb7d4ea5fe16c201f56d8d28546031804 footprint: 4cef4ec78543075af4d782ef919ca95ccbdfbd3bad91f2dfe01fe9b5113eb4d4
environment: test environment: test
operator_id: operator_id:
@ -47,7 +47,7 @@ invoice_3:
total: 3000 total: 3000
created_at: 2015-06-10 11:20:01.341130000 Z created_at: 2015-06-10 11:20:01.341130000 Z
updated_at: 2015-06-10 11:20:01.341130000 Z updated_at: 2015-06-10 11:20:01.341130000 Z
user_id: 7 invoicing_profile_id: 7
reference: '1203001' reference: '1203001'
avoir_mode: avoir_mode:
avoir_date: avoir_date:
@ -55,7 +55,7 @@ invoice_3:
type: type:
subscription_to_expire: subscription_to_expire:
description: description:
footprint: bbb731b181eafd9a78b0b610afeddd3c92f55fcc11b9d58a2d4956cb30b28ee0 footprint: 295f687cfc1df1c9dfe6759f0c3a4d7e92bc8959ee909d944537dffa6b8a0a5e
environment: test environment: test
operator_id: operator_id:
@ -68,7 +68,7 @@ invoice_4:
total: 0 total: 0
created_at: 2016-04-05 08:35:52.931187000 Z created_at: 2016-04-05 08:35:52.931187000 Z
updated_at: 2016-04-05 08:35:52.931187000 Z updated_at: 2016-04-05 08:35:52.931187000 Z
user_id: 7 invoicing_profile_id: 7
reference: '1203002' reference: '1203002'
avoir_mode: avoir_mode:
avoir_date: avoir_date:
@ -76,7 +76,7 @@ invoice_4:
type: type:
subscription_to_expire: subscription_to_expire:
description: description:
footprint: 0b4afc997a22975102441c9dc3635a43bb098d31086f79189751d12e0fb0078c footprint: 18a80a204730011d5c5b753bf9ff86bda49acf7acbdcf31cf37d67df9ae6e53e
environment: test environment: test
operator_id: operator_id:
@ -88,7 +88,7 @@ invoice_5:
total: 1500 total: 1500
created_at: 2016-04-05 08:36:46.853368000 Z created_at: 2016-04-05 08:36:46.853368000 Z
updated_at: 2016-04-05 08:36:46.853368000 Z updated_at: 2016-04-05 08:36:46.853368000 Z
user_id: 3 invoicing_profile_id: 3
reference: '1506031' reference: '1506031'
avoir_mode: avoir_mode:
avoir_date: avoir_date:
@ -96,6 +96,6 @@ invoice_5:
type: type:
subscription_to_expire: subscription_to_expire:
description: description:
footprint: b580117a83436c91475f06ced6c043ce9677c86c2c04cd41ed10860fb214ec71 footprint: c94afc0e5054da75522d438e8f33e6fcadc94c960ce7bdcf4cb4d83e7ca2a8e9
environment: test environment: test
operator_id: operator_id:

48
test/fixtures/invoicing_profiles.yml vendored Normal file
View File

@ -0,0 +1,48 @@
admin:
id: 1
user_id: 1
first_name: admin
last_name: admin
email: admin@fab-manager.com
jdupont:
id: 2
user_id: 2
first_name: Jean
last_name: Dupont
email: jean.dupond@gmail.com
kdumas:
id: 4
user_id: 4
first_name: Kevin
last_name: Dumas
email: kevin.dumas@orange.fr
vlonchamp:
id: 5
user_id: 5
first_name: Vanessa
last_name: Lonchamp
email: vanessa.lonchamp@sfr.fr
gpartenaire:
id: 6
user_id: 6
first_name: Gilbert
last_name: Partenaire
email: gilbert.partenaire@nicolas.com
pdurand:
id: 3
user_id: 3
first_name: Paulette
last_name: Durand
email: paulette.durand@hotmail.fr
lseguin:
id: 7
user_id: 7
first_name: Lucile
last_name: Seguin
email: lucile.seguin@live.fr

View File

@ -1,4 +1,4 @@
casemate: casemate:
id: 1 id: 1
name: La Casemate name: La Casemate
profile_id: 7 invoicing_profile_id: 7

View File

@ -26,6 +26,8 @@ class AdminsTest < ActionDispatch::IntegrationTest
gender: true, gender: true,
birthday: '1999-09-19', birthday: '1999-09-19',
phone: '0547124852', phone: '0547124852',
},
invoicing_profile_attributes: {
address_attributes: { address_attributes: {
address: '6 Avenue Henri de Bournazel, 19000 Tulle' address: '6 Avenue Henri de Bournazel, 19000 Tulle'
} }

View File

@ -0,0 +1,7 @@
require 'test_helper'
class InvoicingProfileTest < ActiveSupport::TestCase
# test "the truth" do
# assert true
# end
end

View File

@ -99,6 +99,18 @@ class ActiveSupport::TestCase
else else
assert_equal invoice.total, ht_amount, 'VAT information was rendered in the PDF file despite that VAT was disabled' assert_equal invoice.total, ht_amount, 'VAT information was rendered in the PDF file despite that VAT was disabled'
end end
# check the recipient & the address
if invoice.invoicing_profile.organization
assert lines.first.include?(invoice.invoicing_profile.organization.name), 'On the PDF invoice, organization name is invalid'
assert invoice.invoicing_profile.organization.address.address.include?(lines[2].split(' ').last.strip), 'On the PDF invoice, organization address is invalid'
else
assert lines.first.include?(invoice.invoicing_profile.full_name), 'On the PDF invoice, customer name is invalid'
assert invoice.invoicing_profile.address.address.include?(lines[2].split(' ').last.strip), 'On the PDF invoice, customer address is invalid'
end
# check the email
assert lines[1].include?(invoice.invoicing_profile.email), 'On the PDF invoice, email is invalid'
File.delete(invoice.file) File.delete(invoice.file)
end end