1
0
mirror of https://github.com/LaCasemate/fab-manager.git synced 2025-01-30 19:52:20 +01:00

Merge branch 'hotfix' for release 4.0.4

This commit is contained in:
Cyril 2019-08-14 15:34:56 +02:00
commit 3d54198d35
12 changed files with 279 additions and 23 deletions

View File

@ -1,5 +1,10 @@
# Changelog Fab Manager
## v4.0.4 2019 August 14
- Fix a bug: #140 VAT rate is erronous in invoices.
Note: this bug was introduced in v4.0.3 and requires (if you are on v4.0.3) to regenerate the invoices since August 1st (if
- [TODO DEPLOY] `rake fablab:maintenance:regenerate_invoices[2019,8]`
## v4.0.3 2019 August 01
- Fix a bug: no user can be created after the last member was deleted

View File

@ -191,14 +191,14 @@ class PDF::Invoice < Prawn::Document
cp = invoice.coupon
discount = 0
if cp.type == 'percent_off'
discount = total_calc * cp.percent_off / 100.0
discount = total_calc * cp.percent_off / 100.00
elsif cp.type == 'amount_off'
# refunds of invoices with cash coupons: we need to ventilate coupons on paid items
if invoice.is_a?(Avoir)
paid_items = invoice.invoice.invoice_items.select{ |ii| ii.amount.positive? }.length
refund_items = invoice.invoice_items.select{ |ii| ii.amount.positive? }.length
discount = ((invoice.coupon.amount_off / paid_items) * refund_items) / 100.0
discount = ((invoice.coupon.amount_off / paid_items) * refund_items) / 100.00
else
discount = cp.amount_off / 100.00
end
@ -222,7 +222,7 @@ class PDF::Invoice < Prawn::Document
end
# total verification
total = invoice.total / 100.0
total = invoice.total / 100.00
puts "ERROR: totals are NOT equals => expected: #{total}, computed: #{total_calc}" if total_calc != total
# TVA
@ -231,7 +231,7 @@ class PDF::Invoice < Prawn::Document
vat_service = VatHistoryService.new
vat_rate = vat_service.invoice_vat(invoice)
vat = total / (vat_rate / 100 + 1)
vat = total / (vat_rate / 100.00 + 1)
data += [[I18n.t('invoices.including_VAT_RATE', RATE: vat_rate), number_to_currency(total - vat)]]
data += [[I18n.t('invoices.including_total_excluding_taxes'), number_to_currency(vat)]]
data += [[I18n.t('invoices.including_amount_payed_on_ordering'), number_to_currency(total)]]
@ -299,7 +299,7 @@ class PDF::Invoice < Prawn::Document
else
# subtract the wallet amount for this invoice from the total
if invoice.wallet_amount
wallet_amount = invoice.wallet_amount / 100.0
wallet_amount = invoice.wallet_amount / 100.00
total -= wallet_amount
else
wallet_amount = nil

View File

@ -0,0 +1,253 @@
# frozen_string_literal: false
# Provides the routine to export the accounting data to an external accounting software
class AccountingExportService
attr_reader :encoding, :format, :separator, :journal_code, :date_format, :columns, :vat_service
def initialize(columns, encoding = 'UTF-8', format = 'CSV', separator = ';', date_format = '%d/%m/%Y')
@encoding = encoding
@format = format
@separator = separator
@journal_code = Setting.find_by(name: 'accounting_journal_code')&.value || ''
@date_format = date_format
@columns = columns
@vat_service = VatHistoryService.new
end
def export(start_date, end_date, file)
# build CVS content
content = header_row
invoices = Invoice.where('created_at >= ? AND created_at <= ?', start_date, end_date).order('created_at ASC')
invoices.each do |i|
content << generate_rows(i)
end
# write content to file
File.open(file, "w:#{encoding}") { |f| f.puts content.encode(encoding, invalid: :replace, undef: :replace) }
end
private
def header_row
row = ''
columns.each do |column|
row << I18n.t("accounting_export.#{column}") << separator
end
"#{row}\n"
end
def generate_rows(invoice)
"#{client_row(invoice)}\n" \
"#{items_rows(invoice)}" \
"#{vat_row(invoice)}\n"
end
# Generate the "subscription" and "reservation" rows associated with the provided invoice
def items_rows(invoice)
rows = invoice.subscription_invoice? ? "#{subscription_row(invoice)}\n" : ''
if invoice.invoiced_type == 'Reservation'
invoice.invoice_items.each do |item|
rows << "#{reservation_row(invoice, item)}\n"
end
end
rows
end
# Generate the "client" row, which contains the debit to the client account, all taxes included
def client_row(invoice)
total = invoice.total / 100.00
row = ''
columns.each do |column|
case column
when 'journal_code'
row << journal_code
when 'date'
row << invoice.created_at&.strftime(date_format)
when 'account_code'
row << account(invoice, :client)
when 'account_label'
row << account(invoice, :client, :label)
when 'piece'
row << invoice.reference
when 'line_label'
row << label(invoice.invoicing_profile.full_name)
when 'debit_origin'
row << debit_client(invoice, total)
when 'credit_origin'
row << credit_client(invoice, total)
when 'debit_euro'
row << debit_client(invoice, total)
when 'credit_euro'
row << credit_client(invoice, total)
when 'lettering'
row << ''
else
puts "Unsupported column: #{column}"
end
row << separator
end
row
end
# Generate the "reservation" row, which contains the credit to the reservation account, all taxes excluded
def reservation_row(invoice, item)
wo_taxes = (item.amount / (vat_service.invoice_vat(invoice) / 100.00 + 1)) / 100.00
row = ''
columns.each do |column|
case column
when 'journal_code'
row << journal_code
when 'date'
row << invoice.created_at&.strftime(date_format)
when 'account_code'
row << account(invoice, :reservation)
when 'account_label'
row << account(invoice, :reservation, :label)
when 'piece'
row << invoice.reference
when 'line_label'
row << label(item.description)
when 'debit_origin'
row << debit(invoice, wo_taxes)
when 'credit_origin'
row << credit(invoice, wo_taxes)
when 'debit_euro'
row << debit(invoice, wo_taxes)
when 'credit_euro'
row << credit(invoice, wo_taxes)
when 'lettering'
row << ''
else
puts "Unsupported column: #{column}"
end
row << separator
end
row
end
# Generate the "subscription" row, which contains the credit to the subscription account, all taxes excluded
def subscription_row(invoice)
subscription_item = invoice.invoice_items.select(&:subscription).first
wo_taxes = (subscription_item.amount / (vat_service.invoice_vat(invoice) / 100.00 + 1)) / 100.00
row = ''
columns.each do |column|
case column
when 'journal_code'
row << journal_code
when 'date'
row << invoice.created_at&.strftime(date_format)
when 'account_code'
row << account(invoice, :subscription)
when 'account_label'
row << account(invoice, :subscription, :label)
when 'piece'
row << invoice.reference
when 'line_label'
row << label(subscription_item.description)
when 'debit_origin'
row << debit(invoice, wo_taxes)
when 'credit_origin'
row << credit(invoice, wo_taxes)
when 'debit_euro'
row << debit(invoice, wo_taxes)
when 'credit_euro'
row << credit(invoice, wo_taxes)
when 'lettering'
row << ''
else
puts "Unsupported column: #{column}"
end
row << separator
end
row
end
# Generate the "VAT" row, which contains the credit to the VAT account, with VAT amount only
def vat_row(invoice)
vat = (invoice.total - (invoice.total / (vat_service.invoice_vat(invoice) / 100.00 + 1))) / 100.00
row = ''
columns.each do |column|
case column
when 'journal_code'
row << journal_code
when 'date'
row << invoice.created_at&.strftime(date_format)
when 'account_code'
row << account(invoice, :vat)
when 'account_label'
row << account(invoice, :vat, :label)
when 'piece'
row << invoice.reference
when 'line_label'
row << I18n.t('accounting_export.VAT')
when 'debit_origin'
row << debit(invoice, vat)
when 'credit_origin'
row << credit(invoice, vat)
when 'debit_euro'
row << debit(invoice, vat)
when 'credit_euro'
row << credit(invoice, vat)
when 'lettering'
row << ''
else
puts "Unsupported column: #{column}"
end
row << separator
end
row
end
# Get the account code (or label) for the given invoice and the specified line type (client, vat, subscription or reservation)
def account(invoice, account, type = :code)
res = case account
when :client
Setting.find_by(name: "accounting_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
if invoice.invoiced_type == 'Reservation'
Setting.find_by(name: "accounting_#{invoice.invoiced.reservable_type}_#{type}")&.value
else
puts "WARN: Invoice #{invoice.id} has no reservation"
end
else
puts "Unsupported account #{account}"
end
res || ''
end
# 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
avoir ? amount.to_s : '0'
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
avoir ? '0' : amount.to_s
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
# Format the given text to match the accounting software rules for the labels
def label(text)
res = text.tr separator, ''
res.truncate(50)
end
end

View File

@ -22,7 +22,7 @@ class CouponService
unless _coupon.nil?
if _coupon.status(user_id, total) == 'active'
if _coupon.type == 'percent_off'
price = price - (price * _coupon.percent_off / 100.0)
price -= price * _coupon.percent_off / 100.00
elsif _coupon.type == 'amount_off'
# do not apply cash coupon unless it has a lower amount that the total price
if _coupon.amount_off <= price
@ -35,7 +35,6 @@ class CouponService
price
end
##
# Ventilate the discount of the provided coupon over the given amount proportionately to the invoice's total
# @param total {Number} total amount of the invoice expressed in monetary units
@ -44,11 +43,11 @@ class CouponService
##
def ventilate(total, amount, coupon)
price = amount
if !coupon.nil? and total != 0
if !coupon.nil? && total != 0
if coupon.type == 'percent_off'
price = amount - ( amount * coupon.percent_off / 100.0 )
price = amount - (amount * coupon.percent_off / 100.00)
elsif coupon.type == 'amount_off'
ratio = (coupon.amount_off / 100.0) / total
ratio = (coupon.amount_off / 100.00) / total
discount = amount * ratio.abs
price = amount - discount
else

View File

@ -28,7 +28,7 @@ class VatHistoryService
def vat_history
key_dates = []
Setting.find_by(name: 'invoice_VAT-rate').history_values.each do |rate|
key_dates.push(date: rate.created_at, rate: (rate.value.to_i / 100.0))
key_dates.push(date: rate.created_at, rate: rate.value.to_i)
end
Setting.find_by(name: 'invoice_VAT-active').history_values.each do |v|
key_dates.push(date: v.created_at, rate: 0) if v.value == 'false'

View File

@ -3,7 +3,7 @@ json.array!(@plans) do |plan|
json.ui_weight plan.ui_weight
json.group_id plan.group_id
json.base_name plan.base_name
json.amount plan.amount / 100.0
json.amount plan.amount / 100.00
json.interval plan.interval
json.interval_count plan.interval_count
json.type plan.type

View File

@ -22,7 +22,7 @@ wb.add_worksheet(name: t('export_subscriptions.subscriptions')) do |sheet|
t("duration.#{sub.plan.interval}", count: sub.plan.interval_count),
sub.created_at.to_date,
sub.expired_at.to_date,
number_to_currency(sub.plan.amount / 100),
number_to_currency(sub.plan.amount / 100.00),
(sub.stp_subscription_id.nil?)? t('export_subscriptions.local_payment') : t('export_subscriptions.online_payment')
]
styles = [nil, nil, nil, nil, nil, date, date, nil, nil]

View File

@ -5,14 +5,13 @@ namespace :fablab do
namespace :maintenance do
desc 'Regenerate the invoices PDF'
task :regenerate_invoices, %i[year month] => :environment do |_task, args|
year = args.year || Time.now.year
month = args.month || Time.now.month
start_date = Time.new(year.to_i, month.to_i, 1)
year = args.year || Time.current.year
month = args.month || Time.current.month
start_date = Time.zone.local(year.to_i, month.to_i, 1)
end_date = start_date.next_month
puts "-> Start regenerate the invoices PDF between #{I18n.l start_date, format: :long} and " \
"#{I18n.l end_date - 1.minute, format: :long}"
invoices = Invoice.only_invoice
.where('created_at >= :start_date AND created_at < :end_date', start_date: start_date, end_date: end_date)
invoices = Invoice.where('created_at >= :start_date AND created_at < :end_date', start_date: start_date, end_date: end_date)
.order(created_at: :asc)
invoices.each(&:regenerate_invoice_pdf)
puts '-> Done'

View File

@ -1,6 +1,6 @@
{
"name": "fab-manager",
"version": "4.0.3",
"version": "4.0.4",
"description": "FabManager is the FabLab management solution. It is web-based, open-source and totally free.",
"keywords": [
"fablab",

View File

@ -46,7 +46,7 @@ module Subscriptions
# Check that the user benefit from prices of his plan
printer = Machine.find_by(slug: 'imprimante-3d')
assert_equal 15, (printer.prices.find_by(group_id: user.group_id, plan_id: user.subscription.plan_id).amount / 100), 'machine hourly price does not match'
assert_equal 15, (printer.prices.find_by(group_id: user.group_id, plan_id: user.subscription.plan_id).amount / 100.00), 'machine hourly price does not match'
# Check notification was sent to the user
notification = Notification.find_by(notification_type_id: NotificationType.find_by_name('notify_member_subscribed_plan'), attached_object_type: 'Subscription', attached_object_id: subscription[:id])

View File

@ -43,7 +43,7 @@ class Subscriptions::CreateAsUserTest < ActionDispatch::IntegrationTest
# Check that the user benefit from prices of his plan
printer = Machine.find_by(slug: 'imprimante-3d')
assert_equal 15,
(printer.prices.find_by(group_id: @user.group_id, plan_id: @user.subscription.plan_id).amount / 100),
(printer.prices.find_by(group_id: @user.group_id, plan_id: @user.subscription.plan_id).amount / 100.00),
'machine hourly price does not match'
# Check notifications were sent for every admins
@ -133,7 +133,7 @@ class Subscriptions::CreateAsUserTest < ActionDispatch::IntegrationTest
assert_equal 10,
(printer.prices.find_by(
group_id: @vlonchamp.group_id,
plan_id: @vlonchamp.subscription.plan_id).amount / 100
plan_id: @vlonchamp.subscription.plan_id).amount / 100.00
),
'machine hourly price does not match'

View File

@ -94,7 +94,7 @@ class ActiveSupport::TestCase
if Setting.find_by(name: 'invoice_VAT-active').value == 'true'
vat_service = VatHistoryService.new
vat_rate = vat_service.invoice_vat(invoice)
computed_ht = sprintf('%.2f', (invoice.total / (vat_rate / 100 + 1)) / 100.0).to_f
computed_ht = sprintf('%.2f', (invoice.total / (vat_rate / 100.00 + 1)) / 100.00).to_f
assert_equal computed_ht, ht_amount, 'Total excluding taxes rendered in the PDF file is not computed correctly'
else