1
0
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:
Sylvain 2022-11-23 17:35:39 +01:00
parent 523529228c
commit a55880a0ad
22 changed files with 344 additions and 156 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View 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

View 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

View File

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

View File

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

View 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

View File

@ -279,6 +279,7 @@ Rails.application.routes.draw do
end
resources :events
resources :availabilities
resources :accounting
end
end
end

View File

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

View File

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

View File

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

View File

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