2019-07-31 16:52:11 +02:00
|
|
|
# frozen_string_literal: false
|
2019-07-29 17:51:53 +02:00
|
|
|
|
|
|
|
# Provides the routine to export the accounting data to an external accounting software
|
|
|
|
class AccountingExportService
|
2019-09-17 12:35:34 +02:00
|
|
|
include ActionView::Helpers::NumberHelper
|
2019-07-29 17:51:53 +02:00
|
|
|
|
2019-09-19 13:57:33 +02:00
|
|
|
attr_reader :encoding, :format, :separator, :journal_code, :date_format, :columns, :decimal_separator, :label_max_length,
|
2019-09-17 12:35:34 +02:00
|
|
|
:export_zeros
|
|
|
|
|
|
|
|
def initialize(columns, encoding: 'UTF-8', format: 'CSV', separator: ';')
|
2019-07-29 17:51:53 +02:00
|
|
|
@encoding = encoding
|
|
|
|
@format = format
|
|
|
|
@separator = separator
|
2019-09-17 12:35:34 +02:00
|
|
|
@decimal_separator = ','
|
|
|
|
@date_format = '%d/%m/%Y'
|
|
|
|
@label_max_length = 50
|
|
|
|
@export_zeros = false
|
2020-05-13 15:02:03 +02:00
|
|
|
@journal_code = Setting.get('accounting_journal_code') || ''
|
2019-07-30 16:06:35 +02:00
|
|
|
@date_format = date_format
|
2019-07-29 17:51:53 +02:00
|
|
|
@columns = columns
|
|
|
|
end
|
|
|
|
|
2019-09-17 12:35:34 +02:00
|
|
|
def set_options(decimal_separator: ',', date_format: '%d/%m/%Y', label_max_length: 50, export_zeros: false)
|
|
|
|
@decimal_separator = decimal_separator
|
|
|
|
@date_format = date_format
|
|
|
|
@label_max_length = label_max_length
|
|
|
|
@export_zeros = export_zeros
|
|
|
|
end
|
|
|
|
|
2019-07-30 16:06:35 +02:00
|
|
|
def export(start_date, end_date, file)
|
2019-10-07 17:12:49 +02:00
|
|
|
# build CSV content
|
2019-08-01 09:49:09 +02:00
|
|
|
content = header_row
|
2019-07-29 17:51:53 +02:00
|
|
|
invoices = Invoice.where('created_at >= ? AND created_at <= ?', start_date, end_date).order('created_at ASC')
|
2019-09-17 12:35:34 +02:00
|
|
|
invoices = invoices.where('total > 0') unless export_zeros
|
2019-07-29 17:51:53 +02:00
|
|
|
invoices.each do |i|
|
2021-03-23 12:32:59 +01:00
|
|
|
puts "processing invoice #{i.id}..." unless Rails.env.test?
|
2019-08-01 09:49:09 +02:00
|
|
|
content << generate_rows(i)
|
2019-07-29 17:51:53 +02:00
|
|
|
end
|
|
|
|
|
|
|
|
# write content to file
|
2019-08-01 09:49:09 +02:00
|
|
|
File.open(file, "w:#{encoding}") { |f| f.puts content.encode(encoding, invalid: :replace, undef: :replace) }
|
2019-07-29 17:51:53 +02:00
|
|
|
end
|
|
|
|
|
|
|
|
private
|
|
|
|
|
2019-08-01 09:49:09 +02:00
|
|
|
def header_row
|
|
|
|
row = ''
|
|
|
|
columns.each do |column|
|
|
|
|
row << I18n.t("accounting_export.#{column}") << separator
|
|
|
|
end
|
2019-08-01 10:24:34 +02:00
|
|
|
"#{row}\n"
|
2019-08-01 09:49:09 +02:00
|
|
|
end
|
|
|
|
|
2019-07-29 17:51:53 +02:00
|
|
|
def generate_rows(invoice)
|
2019-09-19 13:57:33 +02:00
|
|
|
rows = client_rows(invoice) + items_rows(invoice)
|
|
|
|
|
2019-09-18 13:28:53 +02:00
|
|
|
vat = vat_row(invoice)
|
2019-09-19 13:57:33 +02:00
|
|
|
rows += "#{vat}\n" unless vat.nil?
|
|
|
|
|
|
|
|
rows
|
2019-07-29 17:51:53 +02:00
|
|
|
end
|
|
|
|
|
|
|
|
# Generate the "subscription" and "reservation" rows associated with the provided invoice
|
|
|
|
def items_rows(invoice)
|
2019-08-01 09:49:09 +02:00
|
|
|
rows = invoice.subscription_invoice? ? "#{subscription_row(invoice)}\n" : ''
|
2021-05-27 15:58:55 +02:00
|
|
|
if invoice.main_item.object_type == 'Reservation'
|
|
|
|
items = invoice.invoice_items.reject { |ii| ii.object_type == 'Subscription' }
|
2019-10-07 12:08:08 +02:00
|
|
|
items.each do |item|
|
2019-08-01 09:49:09 +02:00
|
|
|
rows << "#{reservation_row(invoice, item)}\n"
|
|
|
|
end
|
2021-05-27 15:58:55 +02:00
|
|
|
elsif invoice.main_item.object_type == 'WalletTransaction'
|
2019-09-19 11:10:26 +02:00
|
|
|
rows << "#{wallet_row(invoice)}\n"
|
2021-05-27 15:58:55 +02:00
|
|
|
elsif invoice.main_item.object_type == 'Error'
|
|
|
|
items = invoice.invoice_items.reject { |ii| ii.object_type == 'Subscription' }
|
2021-05-24 16:34:27 +02:00
|
|
|
items.each do |item|
|
|
|
|
rows << "#{error_row(invoice, item)}\n"
|
|
|
|
end
|
2019-07-29 17:51:53 +02:00
|
|
|
end
|
2019-08-01 09:49:09 +02:00
|
|
|
rows
|
2019-07-29 17:51:53 +02:00
|
|
|
end
|
|
|
|
|
2019-09-19 13:57:33 +02:00
|
|
|
# Generate the "client" rows, which contains the debit to the client account, all taxes included
|
|
|
|
def client_rows(invoice)
|
|
|
|
rows = ''
|
2019-10-02 10:25:06 +02:00
|
|
|
invoice.payment_means.each do |details|
|
2019-09-19 13:57:33 +02:00
|
|
|
rows << row(
|
|
|
|
invoice,
|
2020-06-08 15:17:56 +02:00
|
|
|
account(invoice, :projets, means: details[:means]),
|
|
|
|
account(invoice, :projets, means: details[:means], type: :label),
|
2019-09-19 13:57:33 +02:00
|
|
|
details[:amount] / 100.00,
|
2019-10-02 10:25:06 +02:00
|
|
|
line_label: label(invoice),
|
2019-09-19 13:57:33 +02:00
|
|
|
debit_method: :debit_client,
|
|
|
|
credit_method: :credit_client
|
|
|
|
)
|
|
|
|
rows << "\n"
|
2019-07-29 17:51:53 +02:00
|
|
|
end
|
2019-09-19 13:57:33 +02:00
|
|
|
rows
|
2019-07-29 17:51:53 +02:00
|
|
|
end
|
|
|
|
|
|
|
|
# Generate the "reservation" row, which contains the credit to the reservation account, all taxes excluded
|
|
|
|
def reservation_row(invoice, item)
|
2019-09-19 13:57:33 +02:00
|
|
|
row(
|
|
|
|
invoice,
|
|
|
|
account(invoice, :reservation),
|
|
|
|
account(invoice, :reservation, type: :label),
|
2019-10-07 14:02:40 +02:00
|
|
|
item.net_amount / 100.00,
|
|
|
|
line_label: label(invoice)
|
2019-09-19 13:57:33 +02:00
|
|
|
)
|
2019-07-29 17:51:53 +02:00
|
|
|
end
|
|
|
|
|
|
|
|
# Generate the "subscription" row, which contains the credit to the subscription account, all taxes excluded
|
|
|
|
def subscription_row(invoice)
|
2021-05-27 15:58:55 +02:00
|
|
|
subscription_item = invoice.invoice_items.select { |ii| ii.object_type == 'Subscription' }.first
|
2019-09-19 13:57:33 +02:00
|
|
|
row(
|
|
|
|
invoice,
|
|
|
|
account(invoice, :subscription),
|
|
|
|
account(invoice, :subscription, type: :label),
|
2019-10-07 14:02:40 +02:00
|
|
|
subscription_item.net_amount / 100.00,
|
|
|
|
line_label: label(invoice)
|
2019-09-19 13:57:33 +02:00
|
|
|
)
|
2019-07-29 17:51:53 +02:00
|
|
|
end
|
2019-07-30 10:27:47 +02:00
|
|
|
|
2019-09-19 11:10:26 +02:00
|
|
|
# Generate the "wallet" row, which contains the credit to the wallet account, all taxes excluded
|
|
|
|
# This applies to wallet crediting, when an Avoir is generated at this time
|
|
|
|
def wallet_row(invoice)
|
2019-09-19 13:57:33 +02:00
|
|
|
row(
|
|
|
|
invoice,
|
|
|
|
account(invoice, :wallet),
|
|
|
|
account(invoice, :wallet, type: :label),
|
2019-10-07 14:02:40 +02:00
|
|
|
invoice.invoice_items.first.net_amount / 100.00,
|
|
|
|
line_label: label(invoice)
|
2019-09-19 13:57:33 +02:00
|
|
|
)
|
2019-09-19 11:10:26 +02:00
|
|
|
end
|
|
|
|
|
2019-07-29 17:51:53 +02:00
|
|
|
# Generate the "VAT" row, which contains the credit to the VAT account, with VAT amount only
|
|
|
|
def vat_row(invoice)
|
2019-09-19 13:57:33 +02:00
|
|
|
rate = VatHistoryService.new.invoice_vat(invoice)
|
2019-09-18 13:28:53 +02:00
|
|
|
# we do not render the VAT row if it was disabled for this invoice
|
|
|
|
return nil if rate.zero?
|
|
|
|
|
2019-09-19 13:57:33 +02:00
|
|
|
row(
|
|
|
|
invoice,
|
|
|
|
account(invoice, :vat),
|
|
|
|
account(invoice, :vat, type: :label),
|
2019-10-07 14:02:40 +02:00
|
|
|
invoice.invoice_items.map(&:vat).map(&:to_i).reduce(:+) / 100.00,
|
|
|
|
line_label: label(invoice)
|
2019-09-19 13:57:33 +02:00
|
|
|
)
|
|
|
|
end
|
|
|
|
|
2021-05-24 16:34:27 +02:00
|
|
|
def error_row(invoice, item)
|
|
|
|
row(
|
|
|
|
invoice,
|
|
|
|
account(invoice, :error),
|
|
|
|
account(invoice, :error, type: :label),
|
|
|
|
item.net_amount / 100.00,
|
|
|
|
line_label: label(invoice)
|
|
|
|
)
|
|
|
|
end
|
|
|
|
|
2019-09-19 13:57:33 +02:00
|
|
|
# Generate a row of the export, filling the configured columns with the provided values
|
|
|
|
def row(invoice, account_code, account_label, amount, line_label: '', debit_method: :debit, credit_method: :credit)
|
2019-07-29 17:51:53 +02:00
|
|
|
row = ''
|
|
|
|
columns.each do |column|
|
|
|
|
case column
|
2019-07-31 16:52:11 +02:00
|
|
|
when 'journal_code'
|
2019-08-01 09:49:09 +02:00
|
|
|
row << journal_code
|
2019-07-31 16:52:11 +02:00
|
|
|
when 'date'
|
2019-08-01 09:49:09 +02:00
|
|
|
row << invoice.created_at&.strftime(date_format)
|
2019-07-31 16:52:11 +02:00
|
|
|
when 'account_code'
|
2019-09-19 13:57:33 +02:00
|
|
|
row << account_code
|
2019-07-31 16:52:11 +02:00
|
|
|
when 'account_label'
|
2019-09-19 13:57:33 +02:00
|
|
|
row << account_label
|
2019-07-31 16:52:11 +02:00
|
|
|
when 'piece'
|
2019-08-01 09:49:09 +02:00
|
|
|
row << invoice.reference
|
2019-07-31 16:52:11 +02:00
|
|
|
when 'line_label'
|
2019-09-19 13:57:33 +02:00
|
|
|
row << line_label
|
2019-07-31 16:52:11 +02:00
|
|
|
when 'debit_origin'
|
2019-09-19 13:57:33 +02:00
|
|
|
row << method(debit_method).call(invoice, amount)
|
2019-07-31 16:52:11 +02:00
|
|
|
when 'credit_origin'
|
2019-09-19 13:57:33 +02:00
|
|
|
row << method(credit_method).call(invoice, amount)
|
2019-07-31 16:52:11 +02:00
|
|
|
when 'debit_euro'
|
2019-09-19 13:57:33 +02:00
|
|
|
row << method(debit_method).call(invoice, amount)
|
2019-07-31 16:52:11 +02:00
|
|
|
when 'credit_euro'
|
2019-09-19 13:57:33 +02:00
|
|
|
row << method(credit_method).call(invoice, amount)
|
2019-08-01 09:49:09 +02:00
|
|
|
when 'lettering'
|
|
|
|
row << ''
|
2019-07-29 17:51:53 +02:00
|
|
|
else
|
|
|
|
puts "Unsupported column: #{column}"
|
|
|
|
end
|
2019-08-01 09:49:09 +02:00
|
|
|
row << separator
|
2019-07-29 17:51:53 +02:00
|
|
|
end
|
2019-08-01 09:49:09 +02:00
|
|
|
row
|
2019-07-29 17:51:53 +02:00
|
|
|
end
|
|
|
|
|
|
|
|
# Get the account code (or label) for the given invoice and the specified line type (client, vat, subscription or reservation)
|
2019-09-19 13:57:33 +02:00
|
|
|
def account(invoice, account, type: :code, means: :other)
|
|
|
|
case account
|
2020-06-08 15:17:56 +02:00
|
|
|
when :projets
|
2019-09-19 13:57:33 +02:00
|
|
|
Setting.find_by(name: "accounting_#{means}_client_#{type}")&.value
|
|
|
|
when :vat
|
|
|
|
Setting.find_by(name: "accounting_VAT_#{type}")&.value
|
|
|
|
when :subscription
|
|
|
|
if invoice.subscription_invoice?
|
|
|
|
Setting.find_by(name: "accounting_subscription_#{type}")&.value
|
|
|
|
else
|
|
|
|
puts "WARN: Invoice #{invoice.id} has no subscription"
|
|
|
|
end
|
|
|
|
when :reservation
|
2021-05-27 15:58:55 +02:00
|
|
|
if invoice.main_item.object_type == 'Reservation'
|
|
|
|
Setting.find_by(name: "accounting_#{invoice.main_item.object.reservable_type}_#{type}")&.value
|
2019-09-19 13:57:33 +02:00
|
|
|
else
|
|
|
|
puts "WARN: Invoice #{invoice.id} has no reservation"
|
|
|
|
end
|
|
|
|
when :wallet
|
2021-05-27 15:58:55 +02:00
|
|
|
if invoice.main_item.object_type == 'WalletTransaction'
|
2019-09-19 13:57:33 +02:00
|
|
|
Setting.find_by(name: "accounting_wallet_#{type}")&.value
|
|
|
|
else
|
|
|
|
puts "WARN: Invoice #{invoice.id} is not a wallet credit"
|
|
|
|
end
|
2021-05-24 16:34:27 +02:00
|
|
|
when :error
|
|
|
|
Setting.find_by(name: "accounting_Error_#{type}")&.value
|
2019-09-19 13:57:33 +02:00
|
|
|
else
|
|
|
|
puts "Unsupported account #{account}"
|
|
|
|
end || ''
|
2019-07-29 17:51:53 +02:00
|
|
|
end
|
2019-07-30 10:27:47 +02:00
|
|
|
|
|
|
|
# Fill the value of the "debit" column: if the invoice is a refund, returns the given amount, returns 0 otherwise
|
|
|
|
def debit(invoice, amount)
|
|
|
|
avoir = invoice.is_a? Avoir
|
2019-09-17 12:35:34 +02:00
|
|
|
avoir ? format_number(amount) : '0'
|
2019-07-30 10:27:47 +02:00
|
|
|
end
|
|
|
|
|
|
|
|
# Fill the value of the "credit" column: if the invoice is a refund, returns 0, otherwise, returns the given amount
|
|
|
|
def credit(invoice, amount)
|
|
|
|
avoir = invoice.is_a? Avoir
|
2019-09-17 12:35:34 +02:00
|
|
|
avoir ? '0' : format_number(amount)
|
2019-07-30 10:27:47 +02:00
|
|
|
end
|
|
|
|
|
|
|
|
# Fill the value of the "debit" column for the client row: if the invoice is a refund, returns 0, otherwise, returns the given amount
|
|
|
|
def debit_client(invoice, amount)
|
|
|
|
credit(invoice, amount)
|
|
|
|
end
|
|
|
|
|
|
|
|
# Fill the value of the "credit" column, for the client row: if the invoice is a refund, returns the given amount, returns 0 otherwise
|
|
|
|
def credit_client(invoice, amount)
|
|
|
|
debit(invoice, amount)
|
|
|
|
end
|
2019-08-01 14:58:10 +02:00
|
|
|
|
2019-09-17 12:35:34 +02:00
|
|
|
# Format the given number as a string, using the configured separator
|
|
|
|
def format_number(num)
|
|
|
|
number_to_currency(num, unit: '', separator: decimal_separator, delimiter: '', precision: 2)
|
|
|
|
end
|
|
|
|
|
|
|
|
# Create a text from the given invoice, matching the accounting software rules for the labels
|
|
|
|
def label(invoice)
|
|
|
|
name = "#{invoice.invoicing_profile.last_name} #{invoice.invoicing_profile.first_name}".tr separator, ''
|
|
|
|
reference = invoice.reference
|
2019-09-19 11:25:12 +02:00
|
|
|
|
2019-09-17 12:35:34 +02:00
|
|
|
items = invoice.subscription_invoice? ? [I18n.t('accounting_export.subscription')] : []
|
2021-05-27 15:58:55 +02:00
|
|
|
items.push I18n.t("accounting_export.#{invoice.main_item.object.reservable_type}_reservation") if invoice.main_item.object_type == 'Reservation'
|
|
|
|
items.push I18n.t('accounting_export.wallet') if invoice.main_item.object_type == 'WalletTransaction'
|
2019-09-19 11:25:12 +02:00
|
|
|
|
2019-09-17 12:35:34 +02:00
|
|
|
summary = items.join(' + ')
|
|
|
|
res = "#{reference}, #{summary}"
|
|
|
|
"#{name.truncate(label_max_length - res.length)}, #{res}"
|
2019-08-01 14:58:10 +02:00
|
|
|
end
|
2019-07-29 17:51:53 +02:00
|
|
|
end
|