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:
commit
a803ef819a
@ -6,6 +6,7 @@
|
||||
- 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: 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
|
||||
- Refactored some Ruby code to match style guide
|
||||
- [TODO DEPLOY] `rake fablab:fix:users_group_ids`
|
||||
|
55
README.md
55
README.md
@ -14,9 +14,7 @@ FabManager is the Fab Lab management solution. It provides a comprehensive, web-
|
||||
4.1. [General Guidelines](#general-guidelines)<br/>
|
||||
4.2. [Virtual Machine Instructions](#virtual-machine-instructions)
|
||||
5. [PostgreSQL](#postgresql)<br/>
|
||||
5.1. [Install PostgreSQL 9.4](#setup-postgresql)<br/>
|
||||
5.2. [Run the PostgreSQL command line interface](#run-postgresql-cli)<br/>
|
||||
5.3. [PostgreSQL Limitations](#postgresql-limitations)
|
||||
5.1. [Install PostgreSQL 9.4](#setup-postgresql)
|
||||
6. [ElasticSearch](#elasticsearch)<br/>
|
||||
6.1. [Install ElasticSearch](#setup-elasticsearch)<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).
|
||||
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.
|
||||
|
||||
|
||||
<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.
|
||||
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="elasticsearch"></a>
|
||||
## ElasticSearch
|
||||
|
@ -314,15 +314,14 @@ Application.Controllers.controller('EditMemberController', ['$scope', '$state',
|
||||
// Form action on the above URL
|
||||
$scope.method = 'patch';
|
||||
|
||||
// List of tags associables with user
|
||||
// List of tags joinable with user
|
||||
$scope.tags = tagsPromise;
|
||||
|
||||
// The user to edit
|
||||
$scope.user = memberPromise;
|
||||
|
||||
// Should the passord be modified?
|
||||
$scope.password =
|
||||
{ change: false };
|
||||
// Should the password be modified?
|
||||
$scope.password = { change: false };
|
||||
|
||||
// the user subscription
|
||||
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
|
||||
$scope.method = 'post';
|
||||
|
||||
// Should the passord be set manually or generated?
|
||||
$scope.password =
|
||||
{ change: false };
|
||||
// Should the password be set manually or generated?
|
||||
$scope.password = { change: false };
|
||||
|
||||
// Default member's profile parameters
|
||||
$scope.user =
|
||||
{ plan_interval: '' };
|
||||
$scope.user = { 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
|
||||
$scope.toggleOrganization = function () {
|
||||
if ($scope.user.organization) {
|
||||
if (!$scope.user.profile) { $scope.user.profile = {}; }
|
||||
return $scope.user.profile.organization = {};
|
||||
if (!$scope.user.invoicing_profile) { $scope.user.invoicing_profile = {}; }
|
||||
$scope.user.invoicing_profile.organization = {};
|
||||
} 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 = {
|
||||
profile_attributes: {
|
||||
gender: true
|
||||
}
|
||||
},
|
||||
invoicing_profile_attributes: {}
|
||||
};
|
||||
|
||||
// Default parameters for AngularUI-Bootstrap datepicker
|
||||
|
@ -96,11 +96,11 @@
|
||||
<div class="input-group">
|
||||
<span class="input-group-addon"><i class="fa fa-map-marker"></i> </span>
|
||||
<input type="hidden"
|
||||
name="admin[profile_attributes][address_attributes][id]"
|
||||
ng-value="admin.profile_attributes.address.id" />
|
||||
<input ng-model="admin.profile_attributes.address_attributes.address"
|
||||
name="admin[invoicing_profile_attributes][address_attributes][id]"
|
||||
ng-value="admin.invoicing_profile_attributes.address.id" />
|
||||
<input ng-model="admin.invoicing_profile_attributes.address_attributes.address"
|
||||
type="text"
|
||||
name="admin[profile_attributes][address_attributes][address]"
|
||||
name="admin[invoicing_profile_attributes][address_attributes][address]"
|
||||
class="form-control"
|
||||
id="user_address"
|
||||
placeholder="{{ 'address' | translate }}">
|
||||
|
@ -2,6 +2,7 @@
|
||||
|
||||
<input name="_method" type="hidden" ng-value="method">
|
||||
<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="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>
|
||||
</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">
|
||||
<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"
|
||||
name="user[profile_attributes][organization_attributes][id]"
|
||||
ng-value="user.profile.organization.id" />
|
||||
name="user[invoicing_profile_attributes][organization_attributes][id]"
|
||||
ng-value="user.invoicing_profile.organization.id" />
|
||||
<input type="text"
|
||||
name="user[profile_attributes][organization_attributes][name]"
|
||||
ng-model="user.profile.organization.name"
|
||||
name="user[invoicing_profile_attributes][organization_attributes][name]"
|
||||
ng-model="user.invoicing_profile.organization.name"
|
||||
class="form-control"
|
||||
placeholder="{{ 'organization_name' | translate }}"
|
||||
ng-required="user.profile.organization"
|
||||
ng-disabled="preventField['profile.organization_name'] && user.profile.organization.name && !userForm['user[profile_attributes][organization_attributes][name]'].$dirty">
|
||||
ng-required="user.invoicing_profile.organization"
|
||||
ng-disabled="preventField['profile.organization_name'] && user.invoicing_profile.organization.name && !userForm['user[invoicing_profile_attributes][organization_attributes][name]'].$dirty">
|
||||
</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 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">
|
||||
<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"
|
||||
name="user[profile_attributes][organization_attributes][address_attributes][id]"
|
||||
ng-value="user.profile.organization.address.id" />
|
||||
name="user[invoicing_profile_attributes][organization_attributes][address_attributes][id]"
|
||||
ng-value="user.invoicing_profile.organization.address.id" />
|
||||
<input type="text"
|
||||
name="user[profile_attributes][organization_attributes][address_attributes][address]"
|
||||
ng-model="user.profile.organization.address.address"
|
||||
name="user[invoicing_profile_attributes][organization_attributes][address_attributes][address]"
|
||||
ng-model="user.invoicing_profile.organization.address.address"
|
||||
class="form-control"
|
||||
placeholder="{{ 'organization_address' | translate }}"
|
||||
ng-required="user.profile.organization"
|
||||
ng-disabled="preventField['profile.organization_address'] && user.profile.organization.address.address && !userForm['user[profile_attributes][organization_attributes][address_attributes][address]'].$dirty">
|
||||
ng-required="user.invoicing_profile.organization"
|
||||
ng-disabled="preventField['profile.organization_address'] && user.invoicing_profile.organization.address.address && !userForm['user[invoicing_profile_attributes][organization_attributes][address_attributes][address]'].$dirty">
|
||||
</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 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">
|
||||
<span class="input-group-addon"><i class="fa fa-map-marker"></i> </span>
|
||||
<input type="hidden"
|
||||
name="user[profile_attributes][address_attributes][id]"
|
||||
ng-value="user.profile.address.id" />
|
||||
name="user[invoicing_profile_attributes][address_attributes][id]"
|
||||
ng-value="user.invoicing_profile.address.id" />
|
||||
<input type="text"
|
||||
name="user[profile_attributes][address_attributes][address]"
|
||||
ng-model="user.profile.address.address"
|
||||
name="user[invoicing_profile_attributes][address_attributes][address]"
|
||||
ng-model="user.invoicing_profile.address.address"
|
||||
class="form-control"
|
||||
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 }}"/>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -45,7 +45,10 @@ class API::AdminsController < API::ApiController
|
||||
private
|
||||
|
||||
def admin_params
|
||||
params.require(:admin).permit(:username, :email, profile_attributes: [:first_name, :last_name, :gender,
|
||||
:birthday, :phone, address_attributes: [:address]])
|
||||
params.require(:admin).permit(
|
||||
:username, :email,
|
||||
profile_attributes: %i[first_name last_name gender birthday phone],
|
||||
invoicing_profile_attributes: [address_attributes: [:address]]
|
||||
)
|
||||
end
|
||||
end
|
||||
|
@ -8,7 +8,7 @@ class API::InvoicesController < API::ApiController
|
||||
def index
|
||||
authorize Invoice
|
||||
@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')
|
||||
end
|
||||
|
||||
|
@ -195,10 +195,11 @@ class API::MembersController < API::ApiController
|
||||
:software_mastered, :website, :job, :facebook, :twitter,
|
||||
:google_plus, :viadeo, :linkedin, :instagram, :youtube, :vimeo,
|
||||
:dailymotion, :github, :echosciences, :pinterest, :lastfm, :flickr,
|
||||
user_avatar_attributes: %i[id attachment destroy],
|
||||
address_attributes: %i[id address],
|
||||
organization_attributes: [:id, :name,
|
||||
address_attributes: %i[id address]]])
|
||||
user_avatar_attributes: %i[id attachment destroy]],
|
||||
invoicing_profile_attributes: [
|
||||
address_attributes: %i[id address],
|
||||
organization_attributes: [:id, :name, address_attributes: %i[id address]]
|
||||
])
|
||||
|
||||
elsif current_user.admin?
|
||||
params.require(:user).permit(:username, :email, :password, :password_confirmation,
|
||||
@ -208,10 +209,12 @@ class API::MembersController < API::ApiController
|
||||
:software_mastered, :website, :job, :facebook, :twitter,
|
||||
:google_plus, :viadeo, :linkedin, :instagram, :youtube, :vimeo,
|
||||
:dailymotion, :github, :echosciences, :pinterest, :lastfm, :flickr,
|
||||
user_avatar_attributes: %i[id attachment destroy],
|
||||
address_attributes: %i[id address],
|
||||
organization_attributes: [:id, :name,
|
||||
address_attributes: %i[id address]]])
|
||||
user_avatar_attributes: %i[id attachment destroy]],
|
||||
invoicing_profile_attributes: [
|
||||
:id,
|
||||
address_attributes: %i[id address],
|
||||
organization_attributes: [:id, :name, address_attributes: %i[id address]]
|
||||
])
|
||||
|
||||
end
|
||||
end
|
||||
|
@ -32,12 +32,12 @@ class ApplicationController < ActionController::Base
|
||||
def configure_permitted_parameters
|
||||
devise_parameter_sanitizer.permit(:sign_up,
|
||||
keys: [
|
||||
{ profile_attributes: [
|
||||
:phone, :last_name, :first_name, :gender, :birthday,
|
||||
:interest, :software_mastered, organization_attributes: [
|
||||
:name, address_attributes: [:address]
|
||||
{
|
||||
profile_attributes: %i[phone last_name first_name gender birthday interest software_mastered],
|
||||
invoicing_profile_attributes: [
|
||||
organization_attributes: [:name, address_attributes: [:address]]
|
||||
]
|
||||
] },
|
||||
},
|
||||
:username, :is_allow_contact, :is_allow_newsletter, :cgu, :group_id
|
||||
])
|
||||
end
|
||||
|
@ -12,10 +12,14 @@ class Invoice < ActiveRecord::Base
|
||||
|
||||
has_many :invoice_items, dependent: :destroy
|
||||
accepts_nested_attributes_for :invoice_items
|
||||
belongs_to :user
|
||||
belongs_to :invoicing_profile
|
||||
belongs_to :wallet_transaction
|
||||
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
|
||||
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"
|
||||
end
|
||||
|
||||
def user
|
||||
invoicing_profile.user
|
||||
end
|
||||
|
||||
def generate_reference
|
||||
pattern = Setting.find_by(name: 'invoice_reference').value
|
||||
|
||||
@ -244,7 +252,7 @@ class Invoice < ActiveRecord::Base
|
||||
def generate_and_send_invoice
|
||||
unless Rails.env.test?
|
||||
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
|
||||
InvoiceWorker.perform_async(id, user&.subscription&.expired_at)
|
||||
end
|
||||
|
14
app/models/invoicing_profile.rb
Normal file
14
app/models/invoicing_profile.rb
Normal 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
|
@ -1,5 +1,6 @@
|
||||
class Organization < ActiveRecord::Base
|
||||
belongs_to :profile
|
||||
belongs_to :invoicing_profile
|
||||
has_one :address, as: :placeable, dependent: :destroy
|
||||
accepts_nested_attributes_for :address, allow_destroy: true
|
||||
|
||||
|
@ -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
|
||||
belongs_to :user
|
||||
has_one :user_avatar, as: :viewable, dependent: :destroy
|
||||
accepts_nested_attributes_for :user_avatar,
|
||||
allow_destroy: true,
|
||||
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 :last_name, presence: true, length: { maximum: 30 }
|
||||
validates :gender, :inclusion => {:in => [true, false]}
|
||||
validates :gender, inclusion: { in: [true, false] }
|
||||
validates :birthday, presence: true
|
||||
validates_numericality_of :phone, only_integer: true, allow_blank: false
|
||||
|
||||
after_save :update_invoicing_profile
|
||||
|
||||
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
|
||||
@ -40,13 +40,30 @@ class Profile < ActiveRecord::Base
|
||||
|
||||
def self.mapping
|
||||
# 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
|
||||
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
|
||||
.map{|k,v| [k, v.type.to_s]}
|
||||
.delete_if { |col| blacklist.include?(col[0]) }
|
||||
.concat(additional)
|
||||
.map { |k, v| [k, v.type.to_s] }
|
||||
.delete_if { |col| blacklist.include?(col[0]) }
|
||||
.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
|
||||
|
@ -227,7 +227,7 @@ class Reservation < ActiveRecord::Base
|
||||
def save_with_payment(operator_id, coupon_code = nil)
|
||||
begin
|
||||
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)
|
||||
rescue StandardError => e
|
||||
logger.error e
|
||||
@ -369,7 +369,7 @@ class Reservation < ActiveRecord::Base
|
||||
end
|
||||
|
||||
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)
|
||||
|
||||
return false unless valid?
|
||||
|
@ -18,8 +18,8 @@ class Subscription < ActiveRecord::Base
|
||||
after_save :notify_partner_subscribed_plan, if: :of_partner_plan?
|
||||
|
||||
# Stripe subscription payment
|
||||
# @params [invoice] if true then subscription pay itself, dont pay with reservation
|
||||
# if false then subscription pay with reservation
|
||||
# @param invoice if true then subscription pay itself, dont pay with reservation
|
||||
# if false then subscription pay with reservation
|
||||
def save_with_payment(operator_id, invoice = true, coupon_code = nil)
|
||||
return unless valid?
|
||||
|
||||
@ -127,8 +127,8 @@ class Subscription < ActiveRecord::Base
|
||||
end
|
||||
end
|
||||
|
||||
# @params [invoice] if true then only the subscription is payed, without reservation
|
||||
# if false then the subscription is payed with reservation
|
||||
# @param invoice if true then only the subscription is payed, without reservation
|
||||
# if false then the subscription is payed with reservation
|
||||
def save_with_local_payment(operator_id, invoice = true, coupon_code = nil)
|
||||
return false unless valid?
|
||||
|
||||
@ -165,7 +165,7 @@ class Subscription < ActiveRecord::Base
|
||||
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
|
||||
end
|
||||
@ -212,7 +212,7 @@ class Subscription < ActiveRecord::Base
|
||||
return false if expiration <= expired_at
|
||||
|
||||
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.save
|
||||
|
||||
|
@ -21,6 +21,9 @@ class User < ActiveRecord::Base
|
||||
has_one :profile, dependent: :destroy
|
||||
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 :project_users, dependent: :destroy
|
||||
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 :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 :user_tags, dependent: :destroy
|
||||
@ -67,6 +69,7 @@ class User < ActiveRecord::Base
|
||||
after_commit :create_stripe_customer, on: [:create]
|
||||
after_commit :notify_admin_when_user_is_created, on: :create
|
||||
after_update :notify_group_changed, if: :group_id_changed?
|
||||
after_save :update_invoicing_profile
|
||||
|
||||
attr_accessor :cgu
|
||||
delegate :first_name, to: :profile
|
||||
@ -131,6 +134,10 @@ class User < ActiveRecord::Base
|
||||
my_projects.to_a.concat projects
|
||||
end
|
||||
|
||||
def invoices
|
||||
invoicing_profile.invoices
|
||||
end
|
||||
|
||||
def generate_subscription_invoice(operator_id)
|
||||
return unless subscription
|
||||
|
||||
@ -188,11 +195,11 @@ class User < ActiveRecord::Base
|
||||
when 'profile.avatar'
|
||||
profile.user_avatar.remote_attachment_url
|
||||
when 'profile.address'
|
||||
profile.address.address
|
||||
invoicing_profile.address.address
|
||||
when 'profile.organization_name'
|
||||
profile.organization.name
|
||||
invoicing_profile.organization.name
|
||||
when 'profile.organization_address'
|
||||
profile.organization.address.address
|
||||
invoicing_profile.organization.address.address
|
||||
else
|
||||
profile[parsed[2].to_sym]
|
||||
end
|
||||
@ -211,15 +218,15 @@ class User < ActiveRecord::Base
|
||||
profile.user_avatar ||= UserAvatar.new
|
||||
profile.user_avatar.remote_attachment_url = data
|
||||
when 'profile.address'
|
||||
profile.address ||= Address.new
|
||||
profile.address.address = data
|
||||
invoicing_profile.address ||= Address.new
|
||||
invoicing_profile.address.address = data
|
||||
when 'profile.organization_name'
|
||||
profile.organization ||= Organization.new
|
||||
profile.organization.name = data
|
||||
invoicing_profile.organization ||= Organization.new
|
||||
invoicing_profile.organization.name = data
|
||||
when 'profile.organization_address'
|
||||
profile.organization ||= Organization.new
|
||||
profile.organization.address ||= Address.new
|
||||
profile.organization.address.address = data
|
||||
invoicing_profile.organization ||= Organization.new
|
||||
invoicing_profile.organization.address ||= Address.new
|
||||
invoicing_profile.organization.address.address = data
|
||||
else
|
||||
profile[sso_mapping[8..-1].to_sym] = data unless data.nil?
|
||||
end
|
||||
@ -356,4 +363,17 @@ class User < ActiveRecord::Base
|
||||
receiver: self,
|
||||
attached_object: self
|
||||
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
|
||||
|
@ -52,23 +52,23 @@ class PDF::Invoice < Prawn::Document
|
||||
end
|
||||
|
||||
# user/organization's information
|
||||
if invoice&.user&.profile&.organization
|
||||
name = invoice.user.profile.organization.name
|
||||
full_name = "#{name} (#{invoice.user.profile.full_name})"
|
||||
if invoice&.invoicing_profile&.organization
|
||||
name = invoice.invoicing_profile.organization.name
|
||||
full_name = "#{name} (#{invoice.invoicing_profile.full_name})"
|
||||
else
|
||||
name = invoice.user.profile.full_name
|
||||
name = invoice.invoicing_profile.full_name
|
||||
full_name = name
|
||||
end
|
||||
|
||||
address = if invoice&.user&.profile&.organization&.address
|
||||
invoice.user.profile.organization.address.address
|
||||
elsif invoice&.user&.profile&.address
|
||||
invoice.user.profile.address.address
|
||||
address = if invoice&.invoicing_profile&.organization&.address
|
||||
invoice.invoicing_profile.organization.address.address
|
||||
elsif invoice&.invoicing_profile&.address
|
||||
invoice.invoicing_profile.address.address
|
||||
else
|
||||
''
|
||||
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],
|
||||
width: 130,
|
||||
align: :right,
|
||||
|
@ -9,8 +9,8 @@ class InvoicesService
|
||||
# @param size {number} number of items per page
|
||||
# @param filters {Hash} allowed filters: number, customer, date.
|
||||
def self.list(order_key, direction, page, size, filters = {})
|
||||
invoices = Invoice.includes(:avoir, :invoiced, invoice_items: %i[subscription invoice_item], user: %i[profile trainings])
|
||||
.joins(user: :profile)
|
||||
invoices = Invoice.includes(:avoir, :invoicing_profile, invoice_items: %i[subscription invoice_item])
|
||||
.joins(:invoicing_profile)
|
||||
.order("#{order_key} #{direction}")
|
||||
.page(page)
|
||||
.per(size)
|
||||
@ -25,7 +25,7 @@ class InvoicesService
|
||||
if filters[:customer].size.positive?
|
||||
# ILIKE => PostgreSQL case-insensitive LIKE
|
||||
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]}%"
|
||||
)
|
||||
end
|
||||
|
@ -48,7 +48,7 @@ class WalletService
|
||||
avoir.description = description
|
||||
avoir.avoir_mode = 'wallet'
|
||||
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.save!
|
||||
|
||||
|
@ -6,8 +6,10 @@ json.profile_attributes do
|
||||
json.gender admin.profile.gender
|
||||
json.birthday admin.profile.birthday if admin.profile.birthday
|
||||
json.phone admin.profile.phone
|
||||
json.user_avatar do
|
||||
json.id admin.profile.user_avatar.id
|
||||
json.attachment_url admin.profile.user_avatar.attachment_url
|
||||
end if admin.profile.user_avatar
|
||||
if admin.profile.user_avatar
|
||||
json.user_avatar do
|
||||
json.id admin.profile.user_avatar.id
|
||||
json.attachment_url admin.profile.user_avatar.attachment_url
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -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.name @avoir.user.profile.full_name
|
||||
json.has_avoir false
|
||||
|
@ -1,11 +1,14 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
max_invoices = @invoices.except(:offset, :limit, :order).count
|
||||
|
||||
json.array!(@invoices) do |invoice|
|
||||
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.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.is_avoir invoice.is_a?(Avoir)
|
||||
json.is_subscription_invoice invoice.subscription_invoice?
|
||||
|
@ -2,53 +2,72 @@ json.extract! member, :id, :username, :email, :group_id
|
||||
json.role member.roles.first.name
|
||||
json.name member.profile.full_name
|
||||
json.need_completion member.need_completion?
|
||||
|
||||
json.profile do
|
||||
json.id member.profile.id
|
||||
json.user_avatar do
|
||||
json.id member.profile.user_avatar.id
|
||||
json.attachment_url member.profile.user_avatar.attachment_url
|
||||
end if member.profile.user_avatar
|
||||
if member.profile.user_avatar
|
||||
json.user_avatar do
|
||||
json.id member.profile.user_avatar.id
|
||||
json.attachment_url member.profile.user_avatar.attachment_url
|
||||
end
|
||||
end
|
||||
json.first_name member.profile.first_name
|
||||
json.last_name member.profile.last_name
|
||||
json.gender member.profile.gender.to_s
|
||||
json.birthday member.profile.birthday.to_date.iso8601 if member.profile.birthday
|
||||
json.interest member.profile.interest
|
||||
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.website member.profile.website
|
||||
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.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
|
||||
json.subscribed_plan do
|
||||
json.partial! 'api/shared/plan', plan: member.subscribed_plan
|
||||
end if member.subscribed_plan
|
||||
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
|
||||
|
||||
json.invoicing_profile do
|
||||
json.id member.invoicing_profile.id
|
||||
if member.invoicing_profile.address
|
||||
json.address do
|
||||
json.id member.invoicing_profile.address.id
|
||||
json.address member.invoicing_profile.address.address
|
||||
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_id tc.creditable_id
|
||||
end
|
||||
|
@ -44,7 +44,7 @@ wb.add_worksheet(name: t('export_members.members')) do |sheet|
|
||||
member.is_allow_newsletter,
|
||||
member.profile.gender ? t('export_members.man') : t('export_members.woman'),
|
||||
member.profile.age,
|
||||
member.profile.address ? member.profile.address.address : '',
|
||||
member.invoicing_profile.address ? member.invoicing_profile.address.address : '',
|
||||
member.profile.phone,
|
||||
member.profile.website,
|
||||
member.profile.job,
|
||||
@ -60,8 +60,8 @@ wb.add_worksheet(name: t('export_members.members')) do |sheet|
|
||||
member.profile.facebook || '',
|
||||
member.profile.twitter || '',
|
||||
member.profile.echosciences || '',
|
||||
member.profile.organization ? member.profile.organization.name : '',
|
||||
member.profile.organization ? member.profile.organization.address.address : ''
|
||||
member.invoicing_profile.organization ? member.invoicing_profile.organization.name : '',
|
||||
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]
|
||||
|
||||
|
@ -2,6 +2,6 @@
|
||||
|
||||
<p><%= t('.body.new_account_created') %> "<%= @attached_object.profile.full_name %> <<%= @attached_object.email%>>"</p>
|
||||
|
||||
<% if @attached_object.profile.organization %>
|
||||
<p><%= t('.body.account_for_organization') %> <%= @attached_object.profile.organization.name %></p>
|
||||
<% if @attached_object.invoicing_profile.organization %>
|
||||
<p><%= t('.body.account_for_organization') %> <%= @attached_object.invoicing_profile.organization.name %></p>
|
||||
<% end %>
|
||||
|
@ -2,8 +2,12 @@ class ClosePeriodReminderWorker
|
||||
include Sidekiq::Worker
|
||||
|
||||
def perform
|
||||
return if Invoice.count.zero?
|
||||
|
||||
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',
|
||||
receiver: User.admins,
|
||||
|
15
db/migrate/20190521122429_create_invoicing_profiles.rb
Normal file
15
db/migrate/20190521122429_create_invoicing_profiles.rb
Normal 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
|
@ -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
|
@ -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
|
5
db/migrate/20190528140012_remove_user_id_from_invoice.rb
Normal file
5
db/migrate/20190528140012_remove_user_id_from_invoice.rb
Normal file
@ -0,0 +1,5 @@
|
||||
class RemoveUserIdFromInvoice < ActiveRecord::Migration
|
||||
def change
|
||||
remove_column :invoices, :user_id, :integer
|
||||
end
|
||||
end
|
@ -0,0 +1,5 @@
|
||||
class RemoveProfileFromOrganization < ActiveRecord::Migration
|
||||
def change
|
||||
remove_reference :organizations, :profile, index: true, foreign_key: true
|
||||
end
|
||||
end
|
29
db/schema.rb
29
db/schema.rb
@ -11,7 +11,7 @@
|
||||
#
|
||||
# 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
|
||||
enable_extension "plpgsql"
|
||||
@ -266,7 +266,6 @@ ActiveRecord::Schema.define(version: 20190320091148) do
|
||||
t.integer "total"
|
||||
t.datetime "created_at"
|
||||
t.datetime "updated_at"
|
||||
t.integer "user_id"
|
||||
t.string "reference"
|
||||
t.string "avoir_mode"
|
||||
t.datetime "avoir_date"
|
||||
@ -280,13 +279,25 @@ ActiveRecord::Schema.define(version: 20190320091148) do
|
||||
t.string "footprint"
|
||||
t.string "environment"
|
||||
t.integer "operator_id"
|
||||
t.integer "invoicing_profile_id"
|
||||
end
|
||||
|
||||
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", ["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
|
||||
|
||||
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|
|
||||
t.string "name", null: false
|
||||
t.text "description"
|
||||
@ -383,12 +394,12 @@ ActiveRecord::Schema.define(version: 20190320091148) do
|
||||
|
||||
create_table "organizations", force: :cascade do |t|
|
||||
t.string "name"
|
||||
t.datetime "created_at", null: false
|
||||
t.datetime "updated_at", null: false
|
||||
t.integer "profile_id"
|
||||
t.datetime "created_at", null: false
|
||||
t.datetime "updated_at", null: false
|
||||
t.integer "invoicing_profile_id"
|
||||
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|
|
||||
t.string "name"
|
||||
@ -884,11 +895,13 @@ ActiveRecord::Schema.define(version: 20190320091148) do
|
||||
add_foreign_key "history_values", "settings"
|
||||
add_foreign_key "history_values", "users"
|
||||
add_foreign_key "invoices", "coupons"
|
||||
add_foreign_key "invoices", "invoicing_profiles"
|
||||
add_foreign_key "invoices", "users", column: "operator_id"
|
||||
add_foreign_key "invoices", "wallet_transactions"
|
||||
add_foreign_key "invoicing_profiles", "users"
|
||||
add_foreign_key "o_auth2_mappings", "o_auth2_providers"
|
||||
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", "plans"
|
||||
add_foreign_key "projects_spaces", "projects"
|
||||
|
53
doc/postgresql_readme.md
Normal file
53
doc/postgresql_readme.md
Normal 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`.
|
@ -96,6 +96,8 @@ You can run the following script as root to easily perform all these operations:
|
||||
|
||||
```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
|
||||
|
@ -30,8 +30,8 @@ namespace :fablab do
|
||||
|
||||
if AccountingPeriod.count.positive?
|
||||
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}..."
|
||||
InvoiceItem.where('created_at > ?', last_period.end_at).order(:id).each(&:chain_record)
|
||||
else
|
||||
puts '(Re)generating all footprint...'
|
||||
InvoiceItem.order(:id).all.each(&:chain_record)
|
||||
|
8
test/fixtures/addresses.yml
vendored
8
test/fixtures/addresses.yml
vendored
@ -8,7 +8,7 @@ address_1:
|
||||
country:
|
||||
postal_code:
|
||||
placeable_id: 2
|
||||
placeable_type: Profile
|
||||
placeable_type: InvoicingProfile
|
||||
created_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:
|
||||
postal_code:
|
||||
placeable_id: 4
|
||||
placeable_type: Profile
|
||||
placeable_type: InvoicingProfile
|
||||
created_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:
|
||||
postal_code:
|
||||
placeable_id: 5
|
||||
placeable_type: Profile
|
||||
placeable_type: InvoicingProfile
|
||||
created_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:
|
||||
postal_code:
|
||||
placeable_id: 3
|
||||
placeable_type: Profile
|
||||
placeable_type: InvoicingProfile
|
||||
created_at: 2016-04-05 08:35:18.597812000 Z
|
||||
updated_at: 2016-04-05 08:35:18.597812000 Z
|
||||
|
||||
|
20
test/fixtures/invoices.yml
vendored
20
test/fixtures/invoices.yml
vendored
@ -7,7 +7,7 @@ invoice_1:
|
||||
total: 10000
|
||||
created_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
|
||||
avoir_mode:
|
||||
avoir_date:
|
||||
@ -15,7 +15,7 @@ invoice_1:
|
||||
type:
|
||||
subscription_to_expire:
|
||||
description:
|
||||
footprint: 9b1d216a49a65f5428c92af10e284d6dfe4070f6e65e5eacd735ef770540a16a
|
||||
footprint: d477d23a473c565e2c379263d4c86c9cc80cdd88adc9a3ff7246afccec0e2a18
|
||||
environment: test
|
||||
operator_id:
|
||||
|
||||
@ -27,7 +27,7 @@ invoice_2:
|
||||
total: 2000
|
||||
created_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'
|
||||
avoir_mode:
|
||||
avoir_date:
|
||||
@ -35,7 +35,7 @@ invoice_2:
|
||||
type:
|
||||
subscription_to_expire:
|
||||
description:
|
||||
footprint: 32c09fe7ba92501f9239c111abd6688cb7d4ea5fe16c201f56d8d28546031804
|
||||
footprint: 4cef4ec78543075af4d782ef919ca95ccbdfbd3bad91f2dfe01fe9b5113eb4d4
|
||||
environment: test
|
||||
operator_id:
|
||||
|
||||
@ -47,7 +47,7 @@ invoice_3:
|
||||
total: 3000
|
||||
created_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'
|
||||
avoir_mode:
|
||||
avoir_date:
|
||||
@ -55,7 +55,7 @@ invoice_3:
|
||||
type:
|
||||
subscription_to_expire:
|
||||
description:
|
||||
footprint: bbb731b181eafd9a78b0b610afeddd3c92f55fcc11b9d58a2d4956cb30b28ee0
|
||||
footprint: 295f687cfc1df1c9dfe6759f0c3a4d7e92bc8959ee909d944537dffa6b8a0a5e
|
||||
environment: test
|
||||
operator_id:
|
||||
|
||||
@ -68,7 +68,7 @@ invoice_4:
|
||||
total: 0
|
||||
created_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'
|
||||
avoir_mode:
|
||||
avoir_date:
|
||||
@ -76,7 +76,7 @@ invoice_4:
|
||||
type:
|
||||
subscription_to_expire:
|
||||
description:
|
||||
footprint: 0b4afc997a22975102441c9dc3635a43bb098d31086f79189751d12e0fb0078c
|
||||
footprint: 18a80a204730011d5c5b753bf9ff86bda49acf7acbdcf31cf37d67df9ae6e53e
|
||||
environment: test
|
||||
operator_id:
|
||||
|
||||
@ -88,7 +88,7 @@ invoice_5:
|
||||
total: 1500
|
||||
created_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'
|
||||
avoir_mode:
|
||||
avoir_date:
|
||||
@ -96,6 +96,6 @@ invoice_5:
|
||||
type:
|
||||
subscription_to_expire:
|
||||
description:
|
||||
footprint: b580117a83436c91475f06ced6c043ce9677c86c2c04cd41ed10860fb214ec71
|
||||
footprint: c94afc0e5054da75522d438e8f33e6fcadc94c960ce7bdcf4cb4d83e7ca2a8e9
|
||||
environment: test
|
||||
operator_id:
|
||||
|
48
test/fixtures/invoicing_profiles.yml
vendored
Normal file
48
test/fixtures/invoicing_profiles.yml
vendored
Normal 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
|
2
test/fixtures/organizations.yml
vendored
2
test/fixtures/organizations.yml
vendored
@ -1,4 +1,4 @@
|
||||
casemate:
|
||||
id: 1
|
||||
name: La Casemate
|
||||
profile_id: 7
|
||||
invoicing_profile_id: 7
|
||||
|
@ -26,6 +26,8 @@ class AdminsTest < ActionDispatch::IntegrationTest
|
||||
gender: true,
|
||||
birthday: '1999-09-19',
|
||||
phone: '0547124852',
|
||||
},
|
||||
invoicing_profile_attributes: {
|
||||
address_attributes: {
|
||||
address: '6 Avenue Henri de Bournazel, 19000 Tulle'
|
||||
}
|
||||
|
7
test/models/invoicing_profile_test.rb
Normal file
7
test/models/invoicing_profile_test.rb
Normal file
@ -0,0 +1,7 @@
|
||||
require 'test_helper'
|
||||
|
||||
class InvoicingProfileTest < ActiveSupport::TestCase
|
||||
# test "the truth" do
|
||||
# assert true
|
||||
# end
|
||||
end
|
@ -99,6 +99,18 @@ class ActiveSupport::TestCase
|
||||
else
|
||||
assert_equal invoice.total, ht_amount, 'VAT information was rendered in the PDF file despite that VAT was disabled'
|
||||
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)
|
||||
end
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user