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

Merge branch 'invoicingprofile' into dev

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

View File

@ -6,6 +6,7 @@
- Fix a bug: (spanish) some translations are not loaded correctly
- Fix a bug: 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`

View File

@ -14,9 +14,7 @@ FabManager is the Fab Lab management solution. It provides a comprehensive, web-
4.1. [General Guidelines](#general-guidelines)<br/>
4.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

View File

@ -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

View File

@ -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 }}">

View File

@ -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>

View File

@ -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

View File

@ -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

View File

@ -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],
user_avatar_attributes: %i[id attachment destroy]],
invoicing_profile_attributes: [
address_attributes: %i[id address],
organization_attributes: [:id, :name,
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],
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]]])
organization_attributes: [:id, :name, address_attributes: %i[id address]]
])
end
end

View File

@ -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

View File

@ -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

View File

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

View File

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

View File

@ -1,21 +1,21 @@
# frozen_string_literal: true
# Personal data attached to an user (like first_name, date of birth, etc.)
class Profile < ActiveRecord::Base
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)
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

View File

@ -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?

View File

@ -18,7 +18,7 @@ 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
# @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,7 +127,7 @@ class Subscription < ActiveRecord::Base
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
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

View File

@ -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

View File

@ -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,

View File

@ -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

View File

@ -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!

View File

@ -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
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 if admin.profile.user_avatar
end
end
end

View File

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

View File

@ -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?

View File

@ -2,39 +2,57 @@ 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
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 if member.profile.user_avatar
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.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
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 if member.subscribed_plan
end
end
if member.subscription
json.subscription do
json.id member.subscription.id
json.expired_at member.subscription.expired_at.iso8601
@ -48,7 +66,8 @@ json.subscription do
json.interval_count member.subscription.plan.interval_count
json.amount member.subscription.plan.amount ? (member.subscription.plan.amount / 100.0) : 0
end
end if member.subscription
end
end
json.training_credits member.training_credits do |tc|
json.training_id tc.creditable_id
end

View File

@ -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]

View File

@ -2,6 +2,6 @@
<p><%= t('.body.new_account_created') %> "<%= @attached_object.profile.full_name %> &lt;<%= @attached_object.email%>&gt;"</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 %>

View File

@ -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,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -11,7 +11,7 @@
#
# It's strongly recommended that you check this file into your version control system.
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"
@ -385,10 +396,10 @@ ActiveRecord::Schema.define(version: 20190320091148) do
t.string "name"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.integer "profile_id"
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
View File

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

View File

@ -96,6 +96,8 @@ You can run the following script as root to easily perform all these operations:
```bash
\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

View 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)

View File

@ -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

View File

@ -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
View File

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

View File

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

View File

@ -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'
}

View File

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

View File

@ -99,6 +99,18 @@ class ActiveSupport::TestCase
else
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