diff --git a/CHANGELOG.md b/CHANGELOG.md index 3bb6ad7ff..bf2fd242c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,8 @@ # Changelog Fab-manager +- Accounting data is now built each night and saved in database +- OpenAPI endpoint to fetch accounting data +- Fix a bug: providing an array of attributes to filter OpenApi data, results in error - [TODO DEPLOY] `rails fablab:maintenance:build_accounting_lines` - Add reservation deadline parameter (#414) diff --git a/app/controllers/open_api/v1/accounting_controller.rb b/app/controllers/open_api/v1/accounting_controller.rb index 0ca6163b2..11a95181c 100644 --- a/app/controllers/open_api/v1/accounting_controller.rb +++ b/app/controllers/open_api/v1/accounting_controller.rb @@ -1,26 +1,29 @@ # frozen_string_literal: true -# OpenAPI controller for the accounting lines +# authorized 3rd party softwares can fetch the accounting lines through the OpenAPI class OpenAPI::V1::AccountingController < OpenAPI::V1::BaseController extend OpenAPI::ApiDoc include Rails::Pagination expose_doc def index - @invoices = Invoice.order(created_at: :desc) - .includes(invoicing_profile: :user) - .references(:invoicing_profiles) + @lines = AccountingLine.order(date: :desc) + .includes(:invoice) - @invoices = @invoices.where(invoicing_profiles: { user_id: params[:user_id] }) if params[:user_id].present? + @lines = @lines.where('date >= ?', DateTime.parse(params[:after])) if params[:after].present? + @lines = @lines.where('date <= ?', DateTime.parse(params[:before])) if params[:before].present? + @lines = @lines.where(invoice_id: may_array(params[:invoice_id])) if params[:invoice_id].present? - return if params[:page].blank? - - @invoices = @invoices.page(params[:page]).per(per_page) - paginate @invoices, per_page: per_page + @lines = @lines.page(page).per(per_page) + paginate @lines, per_page: per_page end private + def page + params[:page] || 1 + end + def per_page params[:per_page] || 20 end diff --git a/app/controllers/open_api/v1/base_controller.rb b/app/controllers/open_api/v1/base_controller.rb index 3d96c8d8b..a71ccd264 100644 --- a/app/controllers/open_api/v1/base_controller.rb +++ b/app/controllers/open_api/v1/base_controller.rb @@ -4,7 +4,9 @@ module OpenAPI::V1; end # Parameters for OpenAPI endpoints -class OpenAPI::V1::BaseController < ActionController::Base +class OpenAPI::V1::BaseController < ActionController::Base # rubocop:disable Rails/ApplicationController + include ApplicationHelper + protect_from_forgery with: :null_session skip_before_action :verify_authenticity_token before_action :authenticate @@ -49,7 +51,7 @@ class OpenAPI::V1::BaseController < ActionController::Base end def render_unauthorized - render json: { errors: ['Bad credentials'] }, status: 401 + render json: { errors: ['Bad credentials'] }, status: :unauthorized end private diff --git a/app/controllers/open_api/v1/events_controller.rb b/app/controllers/open_api/v1/events_controller.rb index 920bf6ce8..247ef6685 100644 --- a/app/controllers/open_api/v1/events_controller.rb +++ b/app/controllers/open_api/v1/events_controller.rb @@ -16,10 +16,9 @@ class OpenAPI::V1::EventsController < OpenAPI::V1::BaseController @events.order(created_at: :desc) end + @events = @events.where(id: may_array(params[:id])) if params[:id].present? - @events = @events.where(id: params[:id]) if params[:id].present? - - return unless params[:page].present? + return if params[:page].blank? @events = @events.page(params[:page]).per(per_page) paginate @events, per_page: per_page diff --git a/app/controllers/open_api/v1/invoices_controller.rb b/app/controllers/open_api/v1/invoices_controller.rb index ec3f4176b..a4e9cf7d8 100644 --- a/app/controllers/open_api/v1/invoices_controller.rb +++ b/app/controllers/open_api/v1/invoices_controller.rb @@ -11,9 +11,9 @@ class OpenAPI::V1::InvoicesController < OpenAPI::V1::BaseController .includes(invoicing_profile: :user) .references(:invoicing_profiles) - @invoices = @invoices.where(invoicing_profiles: { user_id: params[:user_id] }) if params[:user_id].present? + @invoices = @invoices.where(invoicing_profiles: { user_id: may_array(params[:user_id]) }) if params[:user_id].present? - return unless params[:page].present? + return if params[:page].blank? @invoices = @invoices.page(params[:page]).per(per_page) paginate @invoices, per_page: per_page @@ -21,7 +21,7 @@ class OpenAPI::V1::InvoicesController < OpenAPI::V1::BaseController def download @invoice = Invoice.find(params[:id]) - send_file File.join(Rails.root, @invoice.file), type: 'application/pdf', disposition: 'inline', filename: @invoice.filename + send_file Rails.root.join(@invoice.file), type: 'application/pdf', disposition: 'inline', filename: @invoice.filename end private diff --git a/app/controllers/open_api/v1/reservations_controller.rb b/app/controllers/open_api/v1/reservations_controller.rb index a721b8b29..93322bc3c 100644 --- a/app/controllers/open_api/v1/reservations_controller.rb +++ b/app/controllers/open_api/v1/reservations_controller.rb @@ -11,11 +11,11 @@ class OpenAPI::V1::ReservationsController < OpenAPI::V1::BaseController .includes(statistic_profile: :user) .references(:statistic_profiles) - @reservations = @reservations.where(statistic_profiles: { user_id: params[:user_id] }) if params[:user_id].present? + @reservations = @reservations.where(statistic_profiles: { user_id: may_array(params[:user_id]) }) if params[:user_id].present? @reservations = @reservations.where(reservable_type: format_type(params[:reservable_type])) if params[:reservable_type].present? - @reservations = @reservations.where(reservable_id: params[:reservable_id]) if params[:reservable_id].present? + @reservations = @reservations.where(reservable_id: may_array(params[:reservable_id])) if params[:reservable_id].present? - return unless params[:page].present? + return if params[:page].blank? @reservations = @reservations.page(params[:page]).per(per_page) paginate @reservations, per_page: per_page diff --git a/app/controllers/open_api/v1/user_trainings_controller.rb b/app/controllers/open_api/v1/user_trainings_controller.rb index f367c1d6d..902eea7d0 100644 --- a/app/controllers/open_api/v1/user_trainings_controller.rb +++ b/app/controllers/open_api/v1/user_trainings_controller.rb @@ -12,11 +12,10 @@ class OpenAPI::V1::UserTrainingsController < OpenAPI::V1::BaseController .references(:statistic_profiles) .order(created_at: :desc) + @user_trainings = @user_trainings.where(statistic_profiles: { user_id: may_array(params[:user_id]) }) if params[:user_id].present? + @user_trainings = @user_trainings.where(training_id: may_array(params[:training_id])) if params[:training_id].present? - @user_trainings = @user_trainings.where(statistic_profiles: { user_id: params[:user_id] }) if params[:user_id].present? - @user_trainings = @user_trainings.where(training_id: params[:training_id]) if params[:training_id].present? - - return unless params[:page].present? + return if params[:page].blank? @user_trainings = @user_trainings.page(params[:page]).per(per_page) paginate @user_trainings, per_page: per_page diff --git a/app/controllers/open_api/v1/users_controller.rb b/app/controllers/open_api/v1/users_controller.rb index 00ac3b6ca..fd9e33590 100644 --- a/app/controllers/open_api/v1/users_controller.rb +++ b/app/controllers/open_api/v1/users_controller.rb @@ -13,9 +13,9 @@ class OpenAPI::V1::UsersController < OpenAPI::V1::BaseController email_param = params[:email].is_a?(String) ? params[:email].downcase : params[:email].map(&:downcase) @users = @users.where(email: email_param) end - @users = @users.where(id: params[:user_id]) if params[:user_id].present? + @users = @users.where(id: may_array(params[:user_id])) if params[:user_id].present? - return unless params[:page].present? + return if params[:page].blank? @users = @users.page(params[:page]).per(per_page) paginate @users, per_page: per_page diff --git a/app/doc/open_api/v1/accounting_doc.rb b/app/doc/open_api/v1/accounting_doc.rb index a04ee2cb1..0eb5d9dab 100644 --- a/app/doc/open_api/v1/accounting_doc.rb +++ b/app/doc/open_api/v1/accounting_doc.rb @@ -13,61 +13,76 @@ class OpenAPI::V1::AccountingDoc < OpenAPI::V1::BaseDoc doc_for :index do api :GET, "/#{API_VERSION}/accounting", 'Accounting lines' - description 'All accounting lines, with optional pagination and dates filtering. Ordered by *date* descendant.' + description 'All accounting lines, paginated (necessarily becauce there is a lot of data) with optional dates filtering. ' \ + 'Ordered by *date* descendant.' param_group :pagination param :after, DateTime, optional: true, desc: 'Filter accounting lines to lines after the given date.' param :before, DateTime, optional: true, desc: 'Filter accounting lines to lines before the given date.' + param :invoice_id, [Integer, Array], optional: true, desc: 'Scope the request to one or various invoices.' + example <<-LINES # /open_api/v1/accounting?after=2022-01-01T00:00:00+02:00&page=1&per_page=3 { "lines": [ { + "id": 1, + "line_type": "client", "journal_code": "VT01", "date": "2022-01-02T18:14:21+01:00", "account_code": "5802", - "account_label": "Wallet customers", - "analytical_code": "P3D71", - "invoice": { - "reference": "22010009/VL", - "id": 274, - "label": "Dupont Marcel, 22010009/VL, subscr.", - }, - "user_id": 6512, - "amount": 200, - "currency": "EUR", - "invoice_url": "/open_api/v1/invoices/247/download" - }, - { - "journal_code": "VT01", - "date": "2022-01-02T18:14:21+01:00", - "account_code": "5801", "account_label": "Card customers", - "analytical_code": "P3D71", + "analytical_code": "", "invoice": { "reference": "22010009/VL", "id": 274, - "label": "Dupont Marcel, 22010009/VL, subscr.", + "label": "Subscription of Dupont Marcel for 1 month starting from 2022, january 2nd", + "url": "/open_api/v1/invoices/247/download" }, - "user_id": 6512, - "amount": 100, + "user_invoicing_profile_id": 6512, + "debit": 1400, + "credit": 0 "currency": "EUR", - "invoice_url": "/open_api/v1/invoices/247/download" + "summary": "Dupont Marcel, 22010009/VL, subscr." }, { + "id": 2, + "line_type": "item", "journal_code": "VT01", "date": "2022-01-02T18:14:21+01:00", - "account_code": "5802", - "account_label": "Wallet customers", + "account_code": "7071", + "account_label": "Subscriptions", "analytical_code": "P3D71", "invoice": { "reference": "22010009/VL", "id": 274, - "label": "Dupont Marcel, 22010009/VL, subscr.", + "label": "Subscription of Dupont Marcel for 1 month starting from 2022, january 2nd", + "url": "/open_api/v1/invoices/247/download" }, - "user_id": 6512, - "amount": 200, + "user_invoicing_profile_id": 6512, + "debit": 0, + "credit": 1167 "currency": "EUR", - "invoice_url": "/open_api/v1/invoices/247/download" + "summary": "Dupont Marcel, 22010009/VL, subscr." + }, + { + "id": 3, + "line_type": "vat", + "journal_code": "VT01", + "date": "2022-01-02T18:14:21+01:00", + "account_code": "4457", + "account_label": "Collected VAT", + "analytical_code": "P3D71", + "invoice": { + "reference": "22010009/VL", + "id": 274, + "label": "Subscription of Dupont Marcel for 1 month starting from 2022, january 2nd", + "url": "/open_api/v1/invoices/247/download" + }, + "user_invoicing_profile_id": 6512, + "debit": 0, + "credit": 233 + "currency": "EUR", + "summary": "Dupont Marcel, 22010009/VL, subscr." } ] } diff --git a/app/frontend/src/javascript/components/form/form-multi-file-upload.tsx b/app/frontend/src/javascript/components/form/form-multi-file-upload.tsx index d3b012204..474a21732 100644 --- a/app/frontend/src/javascript/components/form/form-multi-file-upload.tsx +++ b/app/frontend/src/javascript/components/form/form-multi-file-upload.tsx @@ -39,7 +39,7 @@ export const FormMultiFileUpload =
- {output.map((field: FileType, index) => ( + {output?.map((field: FileType, index) => ( true) - &.order('profile_custom_fields.id ASC') - &.select { |f| f.value.present? } - &.map do |f| - "#{f.profile_custom_field.label}: #{f.value}" - end - else - name = invoice.invoicing_profile.full_name - full_name = name - end - - 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 + name = Invoices::RecipientService.name(invoice) + others = Invoices::RecipientService.organization_data(invoice) + address = Invoices::RecipientService.address(invoice) text_box "#{name}\n#{invoice.invoicing_profile.email}\n#{address}\n#{others&.join("\n")}", at: [bounds.width - 180, bounds.top - 49], width: 180, align: :right, inline_format: true - name = full_name # object move_down 28 - if invoice.is_a?(Avoir) - object = if invoice.main_item.object_type == WalletTransaction.name - I18n.t('invoices.wallet_credit') - else - I18n.t('invoices.cancellation_of_invoice_REF', REF: invoice.invoice.reference) - end - else - case invoice.main_item.object_type - when 'Reservation' - object = I18n.t('invoices.reservation_of_USER_on_DATE_at_TIME', - USER: name, - DATE: I18n.l(invoice.main_item.object.slots[0].start_at.to_date), - TIME: I18n.l(invoice.main_item.object.slots[0].start_at, format: :hour_minute)) - invoice.invoice_items.each do |item| - next unless item.object_type == Subscription.name - - subscription = item.object - cancellation = invoice.is_a?(Avoir) ? "#{I18n.t('invoices.cancellation')} - " : '' - object = "\n- #{object}\n- #{cancellation + subscription_verbose(subscription, name)}" - break - end - when 'Subscription' - object = subscription_verbose(invoice.main_item.object, name) - when 'OfferDay' - object = offer_day_verbose(invoice.main_item.object, name) - when 'Error' - object = I18n.t('invoices.error_invoice') - when 'StatisticProfilePrepaidPack' - object = I18n.t('invoices.prepaid_pack') - when 'OrderItem' - object = I18n.t('invoices.order') - else - Rails.logger.error "specified main_item.object_type type (#{invoice.main_item.object_type}) is unknown" - end - end - text "#{I18n.t('invoices.object')} #{object}" + text "#{I18n.t('invoices.object')} #{Invoices::LabelService.build(invoice)}" # details table of the invoice's elements move_down 20 @@ -370,22 +315,6 @@ class PDF::Invoice < Prawn::Document end end - def subscription_verbose(subscription, username) - subscription_start_at = subscription.expired_at - subscription.plan.duration - duration_verbose = I18n.t("duration.#{subscription.plan.interval}", count: subscription.plan.interval_count) - I18n.t('invoices.subscription_of_NAME_for_DURATION_starting_from_DATE', - NAME: username, - DURATION: duration_verbose, - DATE: I18n.l(subscription_start_at.to_date)) - end - - def offer_day_verbose(offer_day, username) - I18n.t('invoices.subscription_of_NAME_extended_starting_from_STARTDATE_until_ENDDATE', - NAME: username, - STARTDATE: I18n.l(offer_day.start_at.to_date), - ENDDATE: I18n.l(offer_day.end_at.to_date)) - end - ## # Remove every unsupported html tag from the given html text (like

, , ...). # The supported tags are , , and
. diff --git a/app/services/invoices/label_service.rb b/app/services/invoices/label_service.rb new file mode 100644 index 000000000..fbd1dd604 --- /dev/null +++ b/app/services/invoices/label_service.rb @@ -0,0 +1,74 @@ +# frozen_string_literal: true + +# module definition +module Invoices; end + +# Build a label for the given invoice +class Invoices::LabelService + class << self + def build(invoice) + username = Invoices::RecipientService.name(invoice) + if invoice.is_a?(Avoir) + avoir_label(invoice) + else + case invoice.main_item.object_type + when 'Reservation' + reservation_invoice_label(invoice, username) + when 'Subscription' + subscription_label(invoice.main_item.object, username) + when 'OfferDay' + offer_day_label(invoice.main_item.object, username) + when 'Error' + I18n.t('invoices.error_invoice') + when 'StatisticProfilePrepaidPack' + I18n.t('invoices.prepaid_pack') + when 'OrderItem' + I18n.t('invoices.order') + else + Rails.logger.error "specified main_item.object_type type (#{invoice.main_item.object_type}) is unknown" + nil + end + end + end + + private + + def avoir_label(invoice) + return I18n.t('invoices.wallet_credit') if invoice.main_item.object_type == WalletTransaction.name + + I18n.t('invoices.cancellation_of_invoice_REF', REF: invoice.invoice.reference) + end + + def reservation_invoice_label(invoice, username) + label = I18n.t('invoices.reservation_of_USER_on_DATE_at_TIME', + USER: username, + DATE: I18n.l(invoice.main_item.object.slots[0].start_at.to_date), + TIME: I18n.l(invoice.main_item.object.slots[0].start_at, format: :hour_minute)) + invoice.invoice_items.each do |item| + next unless item.object_type == Subscription.name + + subscription = item.object + cancellation = invoice.is_a?(Avoir) ? "#{I18n.t('invoices.cancellation')} - " : '' + label = "\n- #{label}\n- #{cancellation + subscription_label(subscription, username)}" + break + end + label + end + + def subscription_label(subscription, username) + subscription_start_at = subscription.expired_at - subscription.plan.duration + duration_verbose = I18n.t("duration.#{subscription.plan.interval}", count: subscription.plan.interval_count) + I18n.t('invoices.subscription_of_NAME_for_DURATION_starting_from_DATE', + NAME: username, + DURATION: duration_verbose, + DATE: I18n.l(subscription_start_at.to_date)) + end + + def offer_day_label(offer_day, username) + I18n.t('invoices.subscription_of_NAME_extended_starting_from_STARTDATE_until_ENDDATE', + NAME: username, + STARTDATE: I18n.l(offer_day.start_at.to_date), + ENDDATE: I18n.l(offer_day.end_at.to_date)) + end + end +end diff --git a/app/services/invoices/recipient_service.rb b/app/services/invoices/recipient_service.rb new file mode 100644 index 000000000..4a8abe281 --- /dev/null +++ b/app/services/invoices/recipient_service.rb @@ -0,0 +1,41 @@ +# frozen_string_literal: true + +# module definition +module Invoices; end + +# The recipient may be be an individual or an organization +class Invoices::RecipientService + class << self + # Get the full name of the recipient for the given invoice. + def name(invoice) + if invoice.invoicing_profile.organization + name = invoice.invoicing_profile.organization.name + "#{name} (#{invoice.invoicing_profile.full_name})" + else + invoice.invoicing_profile.full_name + end + end + + # Get the street address of the recipient for the given invoice. + def address(invoice) + if invoice.invoicing_profile&.organization&.address + invoice.invoicing_profile.organization.address.address + elsif invoice.invoicing_profile&.address + invoice.invoicing_profile.address.address + else + '' + end + end + + # Get the optional data in profile_custom_fields, if the recipient is an organization + def organization_data(invoice) + return unless invoice.invoicing_profile.organization + + invoice.invoicing_profile.user_profile_custom_fields&.joins(:profile_custom_field) + &.where('profile_custom_fields.actived' => true) + &.order('profile_custom_fields.id ASC') + &.select { |f| f.value.present? } + &.map { |f| "#{f.profile_custom_field.label}: #{f.value}" } + end + end +end diff --git a/app/services/invoices_service.rb b/app/services/invoices_service.rb index 78dd1bcae..97c23eae2 100644 --- a/app/services/invoices_service.rb +++ b/app/services/invoices_service.rb @@ -62,7 +62,7 @@ class InvoicesService ## # Create an Invoice with an associated array of InvoiceItem matching the given parameters # @param payment_details {Hash} as generated by ShoppingCart.total - # @param operator_profile_id {Number} ID of the user that operates the invoice generation (may be an admin, a manager or the customer himself) + # @param operator_profile_id {Number} ID of the user that operates the invoice generation (an admin, a manager or the customer himself) # @param objects {Array} the booked reservation and/or subscription or pack # @param user {User} the customer # @param payment_id {String} ID of the payment, a returned by the gateway, if the current invoice is paid by card @@ -100,17 +100,17 @@ class InvoicesService def self.generate_invoice_items(invoice, payment_details, objects) objects.each_with_index do |object, index| if object.is_a?(Reservation) && object.reservable.is_a?(Event) - InvoicesService.generate_event_item(invoice, object, payment_details, index.zero?) + InvoicesService.generate_event_item(invoice, object, payment_details, main: index.zero?) elsif object.is_a?(Subscription) - InvoicesService.generate_subscription_item(invoice, object, payment_details, index.zero?) + InvoicesService.generate_subscription_item(invoice, object, payment_details, main: index.zero?) elsif object.is_a?(Reservation) - InvoicesService.generate_reservation_item(invoice, object, payment_details, index.zero?) + InvoicesService.generate_reservation_item(invoice, object, payment_details, main: index.zero?) elsif object.is_a?(StatisticProfilePrepaidPack) - InvoicesService.generate_prepaid_pack_item(invoice, object, payment_details, index.zero?) + InvoicesService.generate_prepaid_pack_item(invoice, object, payment_details, main: index.zero?) elsif object.is_a?(OrderItem) - InvoicesService.generate_order_item(invoice, object, payment_details, index.zero?) + InvoicesService.generate_order_item(invoice, object, payment_details, main: index.zero?) else - InvoicesService.generate_generic_item(invoice, object, payment_details, index.zero?) + InvoicesService.generate_generic_item(invoice, object, payment_details, main: index.zero?) end end end @@ -119,21 +119,21 @@ class InvoicesService # Generate an InvoiceItem for each slot in the given reservation and save them in invoice.invoice_items. # This method must be called if reservation.reservable is an Event ## - def self.generate_event_item(invoice, reservation, payment_details, main = false) + def self.generate_event_item(invoice, reservation, payment_details, main: false) raise TypeError unless reservation.reservable.is_a? Event reservation.slots_reservations.map(&:slot).each do |slot| description = "#{reservation.reservable.name}\n" description += if slot.start_at.to_date == slot.end_at.to_date - "#{I18n.l slot.start_at.to_date, format: :long} #{I18n.l slot.start_at, format: :hour_minute}" \ - " - #{I18n.l slot.end_at, format: :hour_minute}" + "#{I18n.l slot.start_at.to_date, format: :long} #{I18n.l slot.start_at, format: :hour_minute} " \ + "- #{I18n.l slot.end_at, format: :hour_minute}" else - I18n.t('events.from_STARTDATE_to_ENDDATE', - STARTDATE: I18n.l(slot.start_at.to_date, format: :long), - ENDDATE: I18n.l(slot.end_at.to_date, format: :long)) + ' ' + - I18n.t('events.from_STARTTIME_to_ENDTIME', - STARTTIME: I18n.l(slot.start_at, format: :hour_minute), - ENDTIME: I18n.l(slot.end_at, format: :hour_minute)) + "#{I18n.t('events.from_STARTDATE_to_ENDDATE', + STARTDATE: I18n.l(slot.start_at.to_date, format: :long), + ENDDATE: I18n.l(slot.end_at.to_date, format: :long))} " \ + "#{I18n.t('events.from_STARTTIME_to_ENDTIME', + STARTTIME: I18n.l(slot.start_at, format: :hour_minute), + ENDTIME: I18n.l(slot.end_at, format: :hour_minute))}" end price_slot = payment_details[:elements][:slots].detect { |p_slot| p_slot[:start_at].to_time.in_time_zone == slot[:start_at] } @@ -150,7 +150,7 @@ class InvoicesService # Generate an InvoiceItem for each slot in the given reservation and save them in invoice.invoice_items. # This method must be called if reservation.reservable is a Space, a Machine or a Training ## - def self.generate_reservation_item(invoice, reservation, payment_details, main = false) + def self.generate_reservation_item(invoice, reservation, payment_details, main: false) raise TypeError unless [Space, Machine, Training].include? reservation.reservable.class reservation.slots_reservations.map(&:slot).each do |slot| @@ -171,7 +171,7 @@ class InvoicesService # Generate an InvoiceItem for the given subscription and save it in invoice.invoice_items. # This method must be called only with a valid subscription ## - def self.generate_subscription_item(invoice, subscription, payment_details, main = false) + def self.generate_subscription_item(invoice, subscription, payment_details, main: false) raise TypeError unless subscription invoice.invoice_items.push InvoiceItem.new( @@ -186,7 +186,7 @@ class InvoicesService # Generate an InvoiceItem for the given StatisticProfilePrepaidPack and save it in invoice.invoice_items. # This method must be called only with a valid pack-statistic_profile relation ## - def self.generate_prepaid_pack_item(invoice, pack, payment_details, main = false) + def self.generate_prepaid_pack_item(invoice, pack, payment_details, main: false) raise TypeError unless pack invoice.invoice_items.push InvoiceItem.new( @@ -201,7 +201,7 @@ class InvoicesService # Generate an InvoiceItem for given OrderItem and sva it in invoice.invoice_items # This method must be called whith an order ## - def self.generate_order_item(invoice, item, _payment_details, main = false) + def self.generate_order_item(invoice, item, _payment_details, main: false) raise TypeError unless item invoice.invoice_items.push InvoiceItem.new( @@ -212,7 +212,7 @@ class InvoicesService ) end - def self.generate_generic_item(invoice, item, payment_details, main = false) + def self.generate_generic_item(invoice, item, payment_details, main: false) invoice.invoice_items.push InvoiceItem.new( amount: payment_details[:elements][item.class.name.to_sym], description: item.class.name, diff --git a/app/services/price_service.rb b/app/services/price_service.rb index 9fbd37ace..bc74baca6 100644 --- a/app/services/price_service.rb +++ b/app/services/price_service.rb @@ -2,12 +2,14 @@ # Provides methods for Prices class PriceService + extend ApplicationHelper + def self.list(filters) prices = Price.where(nil) prices = prices.where(priceable_type: filters[:priceable_type]) if filters[:priceable_type].present? - prices = prices.where(priceable_id: filters[:priceable_id]) if filters[:priceable_id].present? - prices = prices.where(group_id: filters[:group_id]) if filters[:group_id].present? + prices = prices.where(priceable_id: may_array(filters[:priceable_id])) if filters[:priceable_id].present? + prices = prices.where(group_id: may_array(filters[:group_id])) if filters[:group_id].present? if filters[:plan_id].present? plan_id = /no|nil|null|undefined/i.match?(filters[:plan_id]) ? nil : filters[:plan_id] prices = prices.where(plan_id: plan_id) diff --git a/app/views/open_api/v1/accounting/index.json.jbuilder b/app/views/open_api/v1/accounting/index.json.jbuilder new file mode 100644 index 000000000..3948df41a --- /dev/null +++ b/app/views/open_api/v1/accounting/index.json.jbuilder @@ -0,0 +1,12 @@ +# frozen_string_literal: true + + +json.lines @lines do |line| + json.extract! line, :id, :line_type, :journal_code, :date, :account_code, :account_label, :analytical_code, :debit, :credit, :currency, :summary + json.invoice do + json.extract! line.invoice, :reference, :id + json.label Invoices::LabelService.build(line.invoice) + json.url download_open_api_v1_invoice_path(line.invoice) + end + json.user_invoicing_profile_id line.invoicing_profile_id +end diff --git a/config/routes.rb b/config/routes.rb index 4759349aa..672fc886a 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -279,6 +279,7 @@ Rails.application.routes.draw do end resources :events resources :availabilities + resources :accounting end end end diff --git a/test/integration/open_api/accounting_test.rb b/test/integration/open_api/accounting_test.rb index 0b53f4bd3..181a63a0b 100644 --- a/test/integration/open_api/accounting_test.rb +++ b/test/integration/open_api/accounting_test.rb @@ -12,5 +12,58 @@ class OpenApi::AccountingTest < ActionDispatch::IntegrationTest test 'list all accounting lines' do get '/open_api/v1/accounting', headers: open_api_headers(@token) assert_response :success + assert_equal Mime[:json], response.content_type + + lines = json_response(response.body) + assert_not_empty lines[:lines] + assert_not_nil lines[:lines][0][:id] + assert_not_empty lines[:lines][0][:line_type] + assert_not_empty lines[:lines][0][:journal_code] + assert_not_empty lines[:lines][0][:date] + assert_not_empty lines[:lines][0][:account_code] + assert_not_empty lines[:lines][0][:account_label] + assert_nil lines[:lines][0][:analytical_code] + assert_not_nil lines[:lines][0][:invoice] + assert_not_empty lines[:lines][0][:invoice][:reference] + assert_not_nil lines[:lines][0][:invoice][:id] + assert_not_empty lines[:lines][0][:invoice][:label] + assert_not_empty lines[:lines][0][:invoice][:url] + assert_not_nil lines[:lines][0][:user_invoicing_profile_id] + assert_not_nil lines[:lines][0][:debit] + assert_not_nil lines[:lines][0][:credit] + assert_not_empty lines[:lines][0][:currency] + assert_not_empty lines[:lines][0][:summary] + end + + test 'list all accounting lines with pagination' do + get '/open_api/v1/accounting?page=1&per_page=5', headers: open_api_headers(@token) + assert_response :success + assert_equal Mime[:json], response.content_type + + lines = json_response(response.body) + assert_equal 5, lines[:lines].count + end + + test 'list all accounting lines with dates filtering' do + get '/open_api/v1/accounting?after=2022-09-01T00:00:00+02:00&before=2022-09-30T23:59:59+02:00', headers: open_api_headers(@token) + assert_response :success + assert_equal Mime[:json], response.content_type + + lines = json_response(response.body) + assert lines[:lines].count.positive? + assert(lines[:lines].all? do |line| + date = DateTime.parse(line[:date]) + date >= '2022-09-01'.to_date && date <= '2022-09-30'.to_date + end) + end + + test 'list all accounting lines with invoices filtering' do + get '/open_api/v1/accounting?invoice_id=[1,2,3]', headers: open_api_headers(@token) + assert_response :success + assert_equal Mime[:json], response.content_type + + lines = json_response(response.body) + assert lines[:lines].count.positive? + assert(lines[:lines].all? { |line| [1, 2, 3].include?(line[:invoice][:id]) }) end end diff --git a/test/integration/open_api/prices_test.rb b/test/integration/open_api/prices_test.rb index 19086af3b..efe43a3d5 100644 --- a/test/integration/open_api/prices_test.rb +++ b/test/integration/open_api/prices_test.rb @@ -29,4 +29,13 @@ class OpenApi::PricesTest < ActionDispatch::IntegrationTest assert_equal [1], json_response(response.body)[:prices].pluck(:priceable_id).uniq end + + test 'list all prices for some groups' do + get '/open_api/v1/prices?group_id=[1,2]', headers: open_api_headers(@token) + assert_response :success + assert_equal Mime[:json], response.content_type + + prices = json_response(response.body) + assert_equal [1, 2], prices[:prices].pluck(:group_id).uniq.sort + end end diff --git a/test/integration/open_api/reservations_test.rb b/test/integration/open_api/reservations_test.rb index 975351afc..478934836 100644 --- a/test/integration/open_api/reservations_test.rb +++ b/test/integration/open_api/reservations_test.rb @@ -12,6 +12,7 @@ class OpenApi::ReservationsTest < ActionDispatch::IntegrationTest test 'list all reservations' do get '/open_api/v1/reservations', headers: open_api_headers(@token) assert_response :success + assert_equal Mime[:json], response.content_type assert_equal Reservation.count, json_response(response.body)[:reservations].length end @@ -19,27 +20,50 @@ class OpenApi::ReservationsTest < ActionDispatch::IntegrationTest test 'list all reservations with pagination' do get '/open_api/v1/reservations?page=1&per_page=5', headers: open_api_headers(@token) assert_response :success + assert_equal Mime[:json], response.content_type - assert json_response(response.body)[:reservations].length <= 5 + reservations = json_response(response.body) + assert reservations[:reservations].count <= 5 end test 'list all reservations for a user' do get '/open_api/v1/reservations?user_id=3', headers: open_api_headers(@token) assert_response :success + assert_equal Mime[:json], response.content_type + + reservations = json_response(response.body) + assert_not_empty reservations[:reservations] + assert_equal [3], reservations[:reservations].pluck(:user_id).uniq end test 'list all reservations for a user with pagination' do get '/open_api/v1/reservations?user_id=3&page=1&per_page=5', headers: open_api_headers(@token) assert_response :success + assert_equal Mime[:json], response.content_type + + reservations = json_response(response.body) + assert reservations[:reservations].count <= 5 + assert_equal [3], reservations[:reservations].pluck(:user_id).uniq end test 'list all machine reservations for a user' do get '/open_api/v1/reservations?reservable_type=Machine&user_id=3', headers: open_api_headers(@token) assert_response :success + assert_equal Mime[:json], response.content_type + + reservations = json_response(response.body) + assert_not_empty reservations[:reservations] + assert_equal [3], reservations[:reservations].pluck(:user_id).uniq + assert_equal ['Machine'], reservations[:reservations].pluck(:reservable_type).uniq end test 'list all machine 4 reservations' do get '/open_api/v1/reservations?reservable_type=Machine&reservable_id=4', headers: open_api_headers(@token) assert_response :success + assert_equal Mime[:json], response.content_type + + reservations = json_response(response.body) + assert_not_empty reservations[:reservations] + assert_equal [4], reservations[:reservations].pluck(:reservable_id).uniq end end diff --git a/test/integration/open_api/users_test.rb b/test/integration/open_api/users_test.rb index 3c3c238cf..5dfdaf268 100644 --- a/test/integration/open_api/users_test.rb +++ b/test/integration/open_api/users_test.rb @@ -22,6 +22,21 @@ class OpenApi::UsersTest < ActionDispatch::IntegrationTest test 'list all users filtering by IDs' do get '/open_api/v1/users?user_id=[3,4,5]', headers: open_api_headers(@token) assert_response :success + assert_equal Mime[:json], response.content_type + + users = json_response(response.body) + assert users[:users].count.positive? + assert(users[:users].all? { |user| [3, 4, 5].include?(user[:id]) }) + end + + test 'list a user filtering by ID' do + get '/open_api/v1/users?user_id=2', headers: open_api_headers(@token) + assert_response :success + assert_equal Mime[:json], response.content_type + + users = json_response(response.body) + assert_equal 1, users[:users].count + assert_equal 2, users[:users].first[:id] end test 'list all users filtering by email' do