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: (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`
|
||||||
|
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.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
|
||||||
|
@ -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
|
||||||
|
@ -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 }}">
|
||||||
|
@ -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>
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
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
|
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
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
@ -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?
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
@ -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,
|
||||||
|
@ -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
|
||||||
|
@ -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!
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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?
|
||||||
|
@ -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
|
||||||
|
@ -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]
|
||||||
|
|
||||||
|
@ -2,6 +2,6 @@
|
|||||||
|
|
||||||
<p><%= t('.body.new_account_created') %> "<%= @attached_object.profile.full_name %> <<%= @attached_object.email%>>"</p>
|
<p><%= t('.body.new_account_created') %> "<%= @attached_object.profile.full_name %> <<%= @attached_object.email%>>"</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 %>
|
||||||
|
@ -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,
|
||||||
|
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.
|
# 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
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
|
```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
|
||||||
|
@ -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)
|
||||||
|
8
test/fixtures/addresses.yml
vendored
8
test/fixtures/addresses.yml
vendored
@ -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
|
||||||
|
|
||||||
|
20
test/fixtures/invoices.yml
vendored
20
test/fixtures/invoices.yml
vendored
@ -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
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:
|
casemate:
|
||||||
id: 1
|
id: 1
|
||||||
name: La Casemate
|
name: La Casemate
|
||||||
profile_id: 7
|
invoicing_profile_id: 7
|
||||||
|
@ -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'
|
||||||
}
|
}
|
||||||
|
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
|
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
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user