diff --git a/CHANGELOG.md b/CHANGELOG.md index 657d46872..4e6f1c5e8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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` diff --git a/README.md b/README.md index 1f8955085..a52dda3f7 100644 --- a/README.md +++ b/README.md @@ -14,9 +14,7 @@ FabManager is the Fab Lab management solution. It provides a comprehensive, web- 4.1. [General Guidelines](#general-guidelines)
4.2. [Virtual Machine Instructions](#virtual-machine-instructions) 5. [PostgreSQL](#postgresql)
-5.1. [Install PostgreSQL 9.4](#setup-postgresql)
-5.2. [Run the PostgreSQL command line interface](#run-postgresql-cli)
-5.3. [PostgreSQL Limitations](#postgresql-limitations) +5.1. [Install PostgreSQL 9.4](#setup-postgresql) 6. [ElasticSearch](#elasticsearch)
6.1. [Install ElasticSearch](#setup-elasticsearch)
6.2. [Rebuild statistics](#rebuild-stats)
@@ -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. - - - -### 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 - ``` - - -### 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). ## ElasticSearch diff --git a/app/assets/javascripts/controllers/admin/members.js.erb b/app/assets/javascripts/controllers/admin/members.js.erb index c8c49ed49..72dde78c8 100644 --- a/app/assets/javascripts/controllers/admin/members.js.erb +++ b/app/assets/javascripts/controllers/admin/members.js.erb @@ -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 diff --git a/app/assets/templates/shared/_admin_form.html b/app/assets/templates/shared/_admin_form.html index 469f3d41c..df886e807 100644 --- a/app/assets/templates/shared/_admin_form.html +++ b/app/assets/templates/shared/_admin_form.html @@ -96,11 +96,11 @@
- + diff --git a/app/assets/templates/shared/_member_form.html.erb b/app/assets/templates/shared/_member_form.html.erb index f3d00c46d..faf26868b 100644 --- a/app/assets/templates/shared/_member_form.html.erb +++ b/app/assets/templates/shared/_member_form.html.erb @@ -2,6 +2,7 @@ +
@@ -165,38 +166,38 @@ {{ 'confirmation_mismatch_with_password' }}
-
+
+ name="user[invoicing_profile_attributes][organization_attributes][id]" + ng-value="user.invoicing_profile.organization.id" /> + 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">
- {{ 'organization_name_is_required' }} + {{ 'organization_name_is_required' }}
-
+
+ name="user[invoicing_profile_attributes][organization_attributes][address_attributes][id]" + ng-value="user.invoicing_profile.organization.address.id" /> + 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">
- {{ 'organization_address_is_required' }} + {{ 'organization_address_is_required' }}
@@ -224,14 +225,14 @@
+ name="user[invoicing_profile_attributes][address_attributes][id]" + ng-value="user.invoicing_profile.address.id" />
diff --git a/app/controllers/api/admins_controller.rb b/app/controllers/api/admins_controller.rb index ec2700b3d..7ec272de5 100644 --- a/app/controllers/api/admins_controller.rb +++ b/app/controllers/api/admins_controller.rb @@ -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 diff --git a/app/controllers/api/invoices_controller.rb b/app/controllers/api/invoices_controller.rb index c04a39ef4..8fba607db 100644 --- a/app/controllers/api/invoices_controller.rb +++ b/app/controllers/api/invoices_controller.rb @@ -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 diff --git a/app/controllers/api/members_controller.rb b/app/controllers/api/members_controller.rb index 044855153..bcf835d56 100644 --- a/app/controllers/api/members_controller.rb +++ b/app/controllers/api/members_controller.rb @@ -195,10 +195,11 @@ class API::MembersController < API::ApiController :software_mastered, :website, :job, :facebook, :twitter, :google_plus, :viadeo, :linkedin, :instagram, :youtube, :vimeo, :dailymotion, :github, :echosciences, :pinterest, :lastfm, :flickr, - user_avatar_attributes: %i[id attachment destroy], - address_attributes: %i[id address], - organization_attributes: [:id, :name, - address_attributes: %i[id address]]]) + user_avatar_attributes: %i[id attachment destroy]], + invoicing_profile_attributes: [ + address_attributes: %i[id address], + organization_attributes: [:id, :name, address_attributes: %i[id address]] + ]) elsif current_user.admin? params.require(:user).permit(:username, :email, :password, :password_confirmation, @@ -208,10 +209,12 @@ class API::MembersController < API::ApiController :software_mastered, :website, :job, :facebook, :twitter, :google_plus, :viadeo, :linkedin, :instagram, :youtube, :vimeo, :dailymotion, :github, :echosciences, :pinterest, :lastfm, :flickr, - user_avatar_attributes: %i[id attachment destroy], - address_attributes: %i[id address], - organization_attributes: [:id, :name, - address_attributes: %i[id address]]]) + user_avatar_attributes: %i[id attachment destroy]], + invoicing_profile_attributes: [ + :id, + address_attributes: %i[id address], + organization_attributes: [:id, :name, address_attributes: %i[id address]] + ]) end end diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index f59db1763..69be77e19 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -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 diff --git a/app/models/invoice.rb b/app/models/invoice.rb index 898a4f06d..44fbf9ddf 100644 --- a/app/models/invoice.rb +++ b/app/models/invoice.rb @@ -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 diff --git a/app/models/invoicing_profile.rb b/app/models/invoicing_profile.rb new file mode 100644 index 000000000..9fa546f11 --- /dev/null +++ b/app/models/invoicing_profile.rb @@ -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 diff --git a/app/models/organization.rb b/app/models/organization.rb index e227ace37..1c21f4fed 100644 --- a/app/models/organization.rb +++ b/app/models/organization.rb @@ -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 diff --git a/app/models/profile.rb b/app/models/profile.rb index 073ed582f..57e2cfdb3 100644 --- a/app/models/profile.rb +++ b/app/models/profile.rb @@ -1,21 +1,21 @@ +# frozen_string_literal: true + +# Personal data attached to an user (like first_name, date of birth, etc.) class Profile < ActiveRecord::Base belongs_to :user has_one :user_avatar, as: :viewable, dependent: :destroy accepts_nested_attributes_for :user_avatar, allow_destroy: true, reject_if: proc { |attributes| attributes['attachment'].blank? } - has_one :address, as: :placeable, dependent: :destroy - accepts_nested_attributes_for :address, allow_destroy: true - - has_one :organization, dependent: :destroy - accepts_nested_attributes_for :organization, allow_destroy: false validates :first_name, presence: true, length: { maximum: 30 } validates :last_name, presence: true, length: { maximum: 30 } - validates :gender, :inclusion => {:in => [true, false]} + validates :gender, inclusion: { in: [true, false] } validates :birthday, presence: true validates_numericality_of :phone, only_integer: true, allow_blank: false + after_save :update_invoicing_profile + def full_name # if first_name or last_name is nil, the empty string will be used as a temporary replacement (first_name || '').humanize.titleize + ' ' + (last_name || '').humanize.titleize @@ -40,13 +40,30 @@ class Profile < ActiveRecord::Base def self.mapping # we protect some fields as they are designed to be managed by the system and must not be updated externally - blacklist = %w(id user_id created_at updated_at) + blacklist = %w[id user_id created_at updated_at] # model-relationships must be added manually - additional = [%w(avatar string), %w(address string), %w(organization_name string), %w(organization_address string)] + additional = [%w[avatar string], %w[address string], %w[organization_name string], %w[organization_address string]] Profile.column_types - .map{|k,v| [k, v.type.to_s]} - .delete_if { |col| blacklist.include?(col[0]) } - .concat(additional) + .map { |k, v| [k, v.type.to_s] } + .delete_if { |col| blacklist.include?(col[0]) } + .concat(additional) + end + + private + + def update_invoicing_profile + if user.invoicing_profile.nil? + InvoicingProfile.create!( + user: user, + first_name: first_name, + last_name: last_name + ) + else + user.invoicing_profile.update_attributes( + first_name: first_name, + last_name: last_name + ) + end end end diff --git a/app/models/reservation.rb b/app/models/reservation.rb index fdc72f27a..24744f6b1 100644 --- a/app/models/reservation.rb +++ b/app/models/reservation.rb @@ -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? diff --git a/app/models/subscription.rb b/app/models/subscription.rb index 7d17219e7..a6ca65f50 100644 --- a/app/models/subscription.rb +++ b/app/models/subscription.rb @@ -18,8 +18,8 @@ class Subscription < ActiveRecord::Base after_save :notify_partner_subscribed_plan, if: :of_partner_plan? # Stripe subscription payment - # @params [invoice] if true then subscription pay itself, dont pay with reservation - # if false then subscription pay with reservation + # @param invoice if true then subscription pay itself, dont pay with reservation + # if false then subscription pay with reservation def save_with_payment(operator_id, invoice = true, coupon_code = nil) return unless valid? @@ -127,8 +127,8 @@ class Subscription < ActiveRecord::Base end end - # @params [invoice] if true then only the subscription is payed, without reservation - # if false then the subscription is payed with reservation + # @param invoice if true then only the subscription is payed, without reservation + # if false then the subscription is payed with reservation def save_with_local_payment(operator_id, invoice = true, coupon_code = nil) return false unless valid? @@ -165,7 +165,7 @@ class Subscription < ActiveRecord::Base end end - invoice = Invoice.new(invoiced_id: id, invoiced_type: 'Subscription', user: user, total: total, stp_invoice_id: stp_invoice_id, coupon_id: coupon_id, operator_id: operator_id) + invoice = Invoice.new(invoiced_id: id, invoiced_type: 'Subscription', invoicing_profile: user.invoicing_profile, total: total, stp_invoice_id: stp_invoice_id, coupon_id: coupon_id, operator_id: operator_id) invoice.invoice_items.push InvoiceItem.new(amount: plan.amount, stp_invoice_item_id: stp_subscription_id, description: plan.name, subscription_id: self.id) invoice end @@ -212,7 +212,7 @@ class Subscription < ActiveRecord::Base return false if expiration <= expired_at od = offer_days.create(start_at: expired_at, end_at: expiration) - invoice = Invoice.new(invoiced_id: od.id, invoiced_type: 'OfferDay', user: user, total: 0) + invoice = Invoice.new(invoiced_id: od.id, invoiced_type: 'OfferDay', invoicing_profile: user.invoicing_profile, total: 0) invoice.invoice_items.push InvoiceItem.new(amount: 0, description: plan.name, subscription_id: id) invoice.save diff --git a/app/models/user.rb b/app/models/user.rb index 2c6f618e6..3038c7be4 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -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 diff --git a/app/pdfs/pdf/invoice.rb b/app/pdfs/pdf/invoice.rb index 3cd030ea2..dcb89866e 100644 --- a/app/pdfs/pdf/invoice.rb +++ b/app/pdfs/pdf/invoice.rb @@ -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 "#{name}\n#{invoice.user.email}\n#{address}", + text_box "#{name}\n#{invoice.invoicing_profile.email}\n#{address}", at: [bounds.width - 130, bounds.top - 49], width: 130, align: :right, diff --git a/app/services/invoices_service.rb b/app/services/invoices_service.rb index c0ed5044b..3a0f45e0c 100644 --- a/app/services/invoices_service.rb +++ b/app/services/invoices_service.rb @@ -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 diff --git a/app/services/wallet_service.rb b/app/services/wallet_service.rb index c8ebea8be..a60b98d19 100644 --- a/app/services/wallet_service.rb +++ b/app/services/wallet_service.rb @@ -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! diff --git a/app/views/api/admins/_admin.json.jbuilder b/app/views/api/admins/_admin.json.jbuilder index 203f97f62..5f3f8bda9 100644 --- a/app/views/api/admins/_admin.json.jbuilder +++ b/app/views/api/admins/_admin.json.jbuilder @@ -6,8 +6,10 @@ json.profile_attributes do json.gender admin.profile.gender json.birthday admin.profile.birthday if admin.profile.birthday json.phone admin.profile.phone - json.user_avatar do - json.id admin.profile.user_avatar.id - json.attachment_url admin.profile.user_avatar.attachment_url - end if admin.profile.user_avatar + if admin.profile.user_avatar + json.user_avatar do + json.id admin.profile.user_avatar.id + json.attachment_url admin.profile.user_avatar.attachment_url + end + end end diff --git a/app/views/api/invoices/avoir.json.jbuilder b/app/views/api/invoices/avoir.json.jbuilder index 2f43bb928..3deb7d270 100644 --- a/app/views/api/invoices/avoir.json.jbuilder +++ b/app/views/api/invoices/avoir.json.jbuilder @@ -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 diff --git a/app/views/api/invoices/list.json.jbuilder b/app/views/api/invoices/list.json.jbuilder index 629139c1d..76e11a5b1 100644 --- a/app/views/api/invoices/list.json.jbuilder +++ b/app/views/api/invoices/list.json.jbuilder @@ -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? diff --git a/app/views/api/members/_member.json.jbuilder b/app/views/api/members/_member.json.jbuilder index 6926e6a13..61f25cc0d 100644 --- a/app/views/api/members/_member.json.jbuilder +++ b/app/views/api/members/_member.json.jbuilder @@ -2,53 +2,72 @@ json.extract! member, :id, :username, :email, :group_id json.role member.roles.first.name json.name member.profile.full_name json.need_completion member.need_completion? + json.profile do json.id member.profile.id - json.user_avatar do - json.id member.profile.user_avatar.id - json.attachment_url member.profile.user_avatar.attachment_url - end if member.profile.user_avatar + if member.profile.user_avatar + json.user_avatar do + json.id member.profile.user_avatar.id + json.attachment_url member.profile.user_avatar.attachment_url + end + end json.first_name member.profile.first_name json.last_name member.profile.last_name json.gender member.profile.gender.to_s json.birthday member.profile.birthday.to_date.iso8601 if member.profile.birthday json.interest member.profile.interest json.software_mastered member.profile.software_mastered - json.address do - json.id member.profile.address.id - json.address member.profile.address.address - end if member.profile.address json.phone member.profile.phone json.website member.profile.website json.job member.profile.job json.extract! member.profile, :facebook, :twitter, :google_plus, :viadeo, :linkedin, :instagram, :youtube, :vimeo, :dailymotion, :github, :echosciences, :pinterest, :lastfm, :flickr - json.organization do - json.id member.profile.organization.id - json.name member.profile.organization.name - json.address do - json.id member.profile.organization.address.id - json.address member.profile.organization.address.address - end if member.profile.organization.address - end if member.profile.organization - end -json.subscribed_plan do - json.partial! 'api/shared/plan', plan: member.subscribed_plan -end if member.subscribed_plan -json.subscription do - json.id member.subscription.id - json.expired_at member.subscription.expired_at.iso8601 - json.canceled_at member.subscription.canceled_at.iso8601 if member.subscription.canceled_at - json.stripe member.subscription.stp_subscription_id.present? - json.plan do - json.id member.subscription.plan.id - json.base_name member.subscription.plan.base_name - json.name member.subscription.plan.name - json.interval member.subscription.plan.interval - json.interval_count member.subscription.plan.interval_count - json.amount member.subscription.plan.amount ? (member.subscription.plan.amount / 100.0) : 0 + +json.invoicing_profile do + json.id member.invoicing_profile.id + if member.invoicing_profile.address + json.address do + json.id member.invoicing_profile.address.id + json.address member.invoicing_profile.address.address + end end -end if member.subscription + + if member.invoicing_profile.organization + json.organization do + json.id member.invoicing_profile.organization.id + json.name member.invoicing_profile.organization.name + if member.invoicing_profile.organization.address + json.address do + json.id member.invoicing_profile.organization.address.id + json.address member.invoicing_profile.organization.address.address + end + end + end + end +end + +if member.subscribed_plan + json.subscribed_plan do + json.partial! 'api/shared/plan', plan: member.subscribed_plan + end +end + +if member.subscription + json.subscription do + json.id member.subscription.id + json.expired_at member.subscription.expired_at.iso8601 + json.canceled_at member.subscription.canceled_at.iso8601 if member.subscription.canceled_at + json.stripe member.subscription.stp_subscription_id.present? + json.plan do + json.id member.subscription.plan.id + json.base_name member.subscription.plan.base_name + json.name member.subscription.plan.name + json.interval member.subscription.plan.interval + json.interval_count member.subscription.plan.interval_count + json.amount member.subscription.plan.amount ? (member.subscription.plan.amount / 100.0) : 0 + end + end +end json.training_credits member.training_credits do |tc| json.training_id tc.creditable_id end diff --git a/app/views/exports/users_members.xlsx.axlsx b/app/views/exports/users_members.xlsx.axlsx index 98b3ef990..2686e18b3 100644 --- a/app/views/exports/users_members.xlsx.axlsx +++ b/app/views/exports/users_members.xlsx.axlsx @@ -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] diff --git a/app/views/notifications_mailer/notify_admin_when_user_is_created.html.erb b/app/views/notifications_mailer/notify_admin_when_user_is_created.html.erb index a2bc58d9a..21fdc31ef 100644 --- a/app/views/notifications_mailer/notify_admin_when_user_is_created.html.erb +++ b/app/views/notifications_mailer/notify_admin_when_user_is_created.html.erb @@ -2,6 +2,6 @@

<%= t('.body.new_account_created') %> "<%= @attached_object.profile.full_name %> <<%= @attached_object.email%>>"

-<% if @attached_object.profile.organization %> -

<%= t('.body.account_for_organization') %> <%= @attached_object.profile.organization.name %>

+<% if @attached_object.invoicing_profile.organization %> +

<%= t('.body.account_for_organization') %> <%= @attached_object.invoicing_profile.organization.name %>

<% end %> diff --git a/app/workers/close_period_reminder_worker.rb b/app/workers/close_period_reminder_worker.rb index d1131e329..e13835a32 100644 --- a/app/workers/close_period_reminder_worker.rb +++ b/app/workers/close_period_reminder_worker.rb @@ -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, diff --git a/db/migrate/20190521122429_create_invoicing_profiles.rb b/db/migrate/20190521122429_create_invoicing_profiles.rb new file mode 100644 index 000000000..fd072e9b9 --- /dev/null +++ b/db/migrate/20190521122429_create_invoicing_profiles.rb @@ -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 diff --git a/db/migrate/20190521124609_migrate_profile_to_invoicing_profile.rb b/db/migrate/20190521124609_migrate_profile_to_invoicing_profile.rb new file mode 100644 index 000000000..25357d16b --- /dev/null +++ b/db/migrate/20190521124609_migrate_profile_to_invoicing_profile.rb @@ -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 diff --git a/db/migrate/20190522115230_migrate_user_to_invoicing_profile.rb b/db/migrate/20190522115230_migrate_user_to_invoicing_profile.rb new file mode 100644 index 000000000..b1d863e44 --- /dev/null +++ b/db/migrate/20190522115230_migrate_user_to_invoicing_profile.rb @@ -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 diff --git a/db/migrate/20190528140012_remove_user_id_from_invoice.rb b/db/migrate/20190528140012_remove_user_id_from_invoice.rb new file mode 100644 index 000000000..961ac3402 --- /dev/null +++ b/db/migrate/20190528140012_remove_user_id_from_invoice.rb @@ -0,0 +1,5 @@ +class RemoveUserIdFromInvoice < ActiveRecord::Migration + def change + remove_column :invoices, :user_id, :integer + end +end diff --git a/db/migrate/20190529120814_remove_profile_from_organization.rb b/db/migrate/20190529120814_remove_profile_from_organization.rb new file mode 100644 index 000000000..32c908c70 --- /dev/null +++ b/db/migrate/20190529120814_remove_profile_from_organization.rb @@ -0,0 +1,5 @@ +class RemoveProfileFromOrganization < ActiveRecord::Migration + def change + remove_reference :organizations, :profile, index: true, foreign_key: true + end +end diff --git a/db/schema.rb b/db/schema.rb index 87bda757b..08f2bbda9 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -11,7 +11,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20190320091148) do +ActiveRecord::Schema.define(version: 20190529120814) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -266,7 +266,6 @@ ActiveRecord::Schema.define(version: 20190320091148) do t.integer "total" t.datetime "created_at" t.datetime "updated_at" - t.integer "user_id" t.string "reference" t.string "avoir_mode" t.datetime "avoir_date" @@ -280,13 +279,25 @@ ActiveRecord::Schema.define(version: 20190320091148) do t.string "footprint" t.string "environment" t.integer "operator_id" + t.integer "invoicing_profile_id" end add_index "invoices", ["coupon_id"], name: "index_invoices_on_coupon_id", using: :btree add_index "invoices", ["invoice_id"], name: "index_invoices_on_invoice_id", using: :btree - add_index "invoices", ["user_id"], name: "index_invoices_on_user_id", using: :btree + add_index "invoices", ["invoicing_profile_id"], name: "index_invoices_on_invoicing_profile_id", using: :btree add_index "invoices", ["wallet_transaction_id"], name: "index_invoices_on_wallet_transaction_id", using: :btree + create_table "invoicing_profiles", force: :cascade do |t| + t.integer "user_id" + t.string "first_name" + t.string "last_name" + t.string "email" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + end + + add_index "invoicing_profiles", ["user_id"], name: "index_invoicing_profiles_on_user_id", using: :btree + create_table "licences", force: :cascade do |t| t.string "name", null: false t.text "description" @@ -383,12 +394,12 @@ ActiveRecord::Schema.define(version: 20190320091148) do create_table "organizations", force: :cascade do |t| t.string "name" - t.datetime "created_at", null: false - t.datetime "updated_at", null: false - t.integer "profile_id" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.integer "invoicing_profile_id" end - add_index "organizations", ["profile_id"], name: "index_organizations_on_profile_id", using: :btree + add_index "organizations", ["invoicing_profile_id"], name: "index_organizations_on_invoicing_profile_id", using: :btree create_table "plans", force: :cascade do |t| t.string "name" @@ -884,11 +895,13 @@ ActiveRecord::Schema.define(version: 20190320091148) do add_foreign_key "history_values", "settings" add_foreign_key "history_values", "users" add_foreign_key "invoices", "coupons" + add_foreign_key "invoices", "invoicing_profiles" add_foreign_key "invoices", "users", column: "operator_id" add_foreign_key "invoices", "wallet_transactions" + add_foreign_key "invoicing_profiles", "users" add_foreign_key "o_auth2_mappings", "o_auth2_providers" add_foreign_key "open_api_calls_count_tracings", "open_api_clients" - add_foreign_key "organizations", "profiles" + add_foreign_key "organizations", "invoicing_profiles" add_foreign_key "prices", "groups" add_foreign_key "prices", "plans" add_foreign_key "projects_spaces", "projects" diff --git a/doc/postgresql_readme.md b/doc/postgresql_readme.md new file mode 100644 index 000000000..aa593bfaf --- /dev/null +++ b/doc/postgresql_readme.md @@ -0,0 +1,53 @@ +# Detailed informations about PostgreSQL usage in fab-manager + + +## 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 + ``` + + +## 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. + + + +## 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`. \ No newline at end of file diff --git a/docker/README.md b/docker/README.md index 08a91d583..b7b08b334 100644 --- a/docker/README.md +++ b/docker/README.md @@ -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 diff --git a/lib/tasks/fablab/setup.rake b/lib/tasks/fablab/setup.rake index 6f0c75d4f..5920a4d65 100644 --- a/lib/tasks/fablab/setup.rake +++ b/lib/tasks/fablab/setup.rake @@ -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) diff --git a/test/fixtures/addresses.yml b/test/fixtures/addresses.yml index 54549dd21..e6cbab1a7 100644 --- a/test/fixtures/addresses.yml +++ b/test/fixtures/addresses.yml @@ -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 diff --git a/test/fixtures/invoices.yml b/test/fixtures/invoices.yml index 6f90b2a03..71c588753 100644 --- a/test/fixtures/invoices.yml +++ b/test/fixtures/invoices.yml @@ -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: diff --git a/test/fixtures/invoicing_profiles.yml b/test/fixtures/invoicing_profiles.yml new file mode 100644 index 000000000..aede0635b --- /dev/null +++ b/test/fixtures/invoicing_profiles.yml @@ -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 diff --git a/test/fixtures/organizations.yml b/test/fixtures/organizations.yml index beca74d5b..0ea72a682 100644 --- a/test/fixtures/organizations.yml +++ b/test/fixtures/organizations.yml @@ -1,4 +1,4 @@ casemate: id: 1 name: La Casemate - profile_id: 7 + invoicing_profile_id: 7 diff --git a/test/integration/admins_test.rb b/test/integration/admins_test.rb index 2d874790e..458f56f3a 100644 --- a/test/integration/admins_test.rb +++ b/test/integration/admins_test.rb @@ -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' } diff --git a/test/models/invoicing_profile_test.rb b/test/models/invoicing_profile_test.rb new file mode 100644 index 000000000..2cf8236d0 --- /dev/null +++ b/test/models/invoicing_profile_test.rb @@ -0,0 +1,7 @@ +require 'test_helper' + +class InvoicingProfileTest < ActiveSupport::TestCase + # test "the truth" do + # assert true + # end +end diff --git a/test/test_helper.rb b/test/test_helper.rb index 7387deaed..3c7af7e67 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -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