mirror of
https://github.com/LaCasemate/fab-manager.git
synced 2025-01-18 07:52:23 +01:00
(feat) OpenAPI endpoint for accounting
Also: (bug) filter by array in openAPI = error
This commit is contained in:
parent
523529228c
commit
a55880a0ad
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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."
|
||||
}
|
||||
]
|
||||
}
|
||||
|
@ -39,7 +39,7 @@ export const FormMultiFileUpload = <TFieldValues extends FieldValues, TContext e
|
||||
return (
|
||||
<div className={`form-multi-file-upload ${className || ''}`}>
|
||||
<div className="list">
|
||||
{output.map((field: FileType, index) => (
|
||||
{output?.map((field: FileType, index) => (
|
||||
<FormFileUpload key={index}
|
||||
defaultFile={field}
|
||||
id={`${id}.${index}`}
|
||||
|
@ -83,15 +83,22 @@ module ApplicationHelper
|
||||
(BigDecimal(amount.to_s) * 100.0).to_f
|
||||
end
|
||||
|
||||
# Return the given parameter as it, or as an array if it can be parsed as an array
|
||||
def may_array(param)
|
||||
return param unless param&.chars&.first == '[' && param&.chars&.last == ']'
|
||||
|
||||
param.gsub(/[\[\]]/i, '').split(',')
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
## inspired by gems/actionview-4.2.5/lib/action_view/helpers/translation_helper.rb
|
||||
# rubocop:disable Rails/HelperInstanceVariable
|
||||
def scope_key_by_partial(key)
|
||||
if key.to_s.first == '.'
|
||||
raise "Cannot use t(#{key.inspect}) shortcut because path is not available" unless @virtual_path
|
||||
raise "Cannot use t(#{key.inspect}) shortcut because path is not available" unless @virtual_path # rubocop:disable Rails/HelperInstanceVariable
|
||||
|
||||
@virtual_path.gsub(%r{/_?}, '.') + key.to_s
|
||||
@virtual_path.gsub(%r{/_?}, '.') + key.to_s # rubocop:disable Rails/HelperInstanceVariable
|
||||
else
|
||||
key
|
||||
end
|
||||
|
@ -53,74 +53,19 @@ class PDF::Invoice < Prawn::Document
|
||||
end
|
||||
|
||||
# user/organization's information
|
||||
if invoice.invoicing_profile.organization
|
||||
name = invoice.invoicing_profile.organization.name
|
||||
full_name = "#{name} (#{invoice.invoicing_profile.full_name})"
|
||||
others = 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 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 "<b>#{name}</b>\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 <p>, <span>, ...).
|
||||
# The supported tags are <b>, <u>, <i> and <br>.
|
||||
|
74
app/services/invoices/label_service.rb
Normal file
74
app/services/invoices/label_service.rb
Normal file
@ -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
|
41
app/services/invoices/recipient_service.rb
Normal file
41
app/services/invoices/recipient_service.rb
Normal file
@ -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
|
@ -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<Reservation|Subscription|StatisticProfilePrepaidPack>} 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,7 +119,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 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|
|
||||
@ -128,12 +128,12 @@ class InvoicesService
|
||||
"#{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',
|
||||
"#{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',
|
||||
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))
|
||||
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,
|
||||
|
@ -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)
|
||||
|
12
app/views/open_api/v1/accounting/index.json.jbuilder
Normal file
12
app/views/open_api/v1/accounting/index.json.jbuilder
Normal file
@ -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
|
@ -279,6 +279,7 @@ Rails.application.routes.draw do
|
||||
end
|
||||
resources :events
|
||||
resources :availabilities
|
||||
resources :accounting
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
Loading…
x
Reference in New Issue
Block a user