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 # 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 ## v4.0.3 2019 August 01
- Fix a bug: no user can be created after the last member was deleted - 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 cp = invoice.coupon
discount = 0 discount = 0
if cp.type == 'percent_off' 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' elsif cp.type == 'amount_off'
# refunds of invoices with cash coupons: we need to ventilate coupons on paid items # refunds of invoices with cash coupons: we need to ventilate coupons on paid items
if invoice.is_a?(Avoir) if invoice.is_a?(Avoir)
paid_items = invoice.invoice.invoice_items.select{ |ii| ii.amount.positive? }.length paid_items = invoice.invoice.invoice_items.select{ |ii| ii.amount.positive? }.length
refund_items = 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 else
discount = cp.amount_off / 100.00 discount = cp.amount_off / 100.00
end end
@ -222,7 +222,7 @@ class PDF::Invoice < Prawn::Document
end end
# total verification # 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 puts "ERROR: totals are NOT equals => expected: #{total}, computed: #{total_calc}" if total_calc != total
# TVA # TVA
@ -231,7 +231,7 @@ class PDF::Invoice < Prawn::Document
vat_service = VatHistoryService.new vat_service = VatHistoryService.new
vat_rate = vat_service.invoice_vat(invoice) 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_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_total_excluding_taxes'), number_to_currency(vat)]]
data += [[I18n.t('invoices.including_amount_payed_on_ordering'), number_to_currency(total)]] data += [[I18n.t('invoices.including_amount_payed_on_ordering'), number_to_currency(total)]]
@ -299,7 +299,7 @@ class PDF::Invoice < Prawn::Document
else else
# subtract the wallet amount for this invoice from the total # subtract the wallet amount for this invoice from the total
if invoice.wallet_amount if invoice.wallet_amount
wallet_amount = invoice.wallet_amount / 100.0 wallet_amount = invoice.wallet_amount / 100.00
total -= wallet_amount total -= wallet_amount
else else
wallet_amount = nil 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? unless _coupon.nil?
if _coupon.status(user_id, total) == 'active' if _coupon.status(user_id, total) == 'active'
if _coupon.type == 'percent_off' 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' elsif _coupon.type == 'amount_off'
# do not apply cash coupon unless it has a lower amount that the total price # do not apply cash coupon unless it has a lower amount that the total price
if _coupon.amount_off <= price if _coupon.amount_off <= price
@ -35,7 +35,6 @@ class CouponService
price price
end end
## ##
# Ventilate the discount of the provided coupon over the given amount proportionately to the invoice's total # 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 # @param total {Number} total amount of the invoice expressed in monetary units
@ -44,11 +43,11 @@ class CouponService
## ##
def ventilate(total, amount, coupon) def ventilate(total, amount, coupon)
price = amount price = amount
if !coupon.nil? and total != 0 if !coupon.nil? && total != 0
if coupon.type == 'percent_off' 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' elsif coupon.type == 'amount_off'
ratio = (coupon.amount_off / 100.0) / total ratio = (coupon.amount_off / 100.00) / total
discount = amount * ratio.abs discount = amount * ratio.abs
price = amount - discount price = amount - discount
else else

View File

@ -28,7 +28,7 @@ class VatHistoryService
def vat_history def vat_history
key_dates = [] key_dates = []
Setting.find_by(name: 'invoice_VAT-rate').history_values.each do |rate| 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 end
Setting.find_by(name: 'invoice_VAT-active').history_values.each do |v| 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' 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.ui_weight plan.ui_weight
json.group_id plan.group_id json.group_id plan.group_id
json.base_name plan.base_name json.base_name plan.base_name
json.amount plan.amount / 100.0 json.amount plan.amount / 100.00
json.interval plan.interval json.interval plan.interval
json.interval_count plan.interval_count json.interval_count plan.interval_count
json.type plan.type 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), t("duration.#{sub.plan.interval}", count: sub.plan.interval_count),
sub.created_at.to_date, sub.created_at.to_date,
sub.expired_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') (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] styles = [nil, nil, nil, nil, nil, date, date, nil, nil]

View File

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

View File

@ -1,6 +1,6 @@
{ {
"name": "fab-manager", "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.", "description": "FabManager is the FabLab management solution. It is web-based, open-source and totally free.",
"keywords": [ "keywords": [
"fablab", "fablab",

View File

@ -46,7 +46,7 @@ module Subscriptions
# Check that the user benefit from prices of his plan # Check that the user benefit from prices of his plan
printer = Machine.find_by(slug: 'imprimante-3d') 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 # 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]) 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 # Check that the user benefit from prices of his plan
printer = Machine.find_by(slug: 'imprimante-3d') printer = Machine.find_by(slug: 'imprimante-3d')
assert_equal 15, 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' 'machine hourly price does not match'
# Check notifications were sent for every admins # Check notifications were sent for every admins
@ -133,7 +133,7 @@ class Subscriptions::CreateAsUserTest < ActionDispatch::IntegrationTest
assert_equal 10, assert_equal 10,
(printer.prices.find_by( (printer.prices.find_by(
group_id: @vlonchamp.group_id, 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' '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' if Setting.find_by(name: 'invoice_VAT-active').value == 'true'
vat_service = VatHistoryService.new vat_service = VatHistoryService.new
vat_rate = vat_service.invoice_vat(invoice) 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' assert_equal computed_ht, ht_amount, 'Total excluding taxes rendered in the PDF file is not computed correctly'
else else