mirror of
https://github.com/LaCasemate/fab-manager.git
synced 2025-01-29 18:52:22 +01:00
Merge branch 'hotfix' for release 4.0.4
This commit is contained in:
commit
3d54198d35
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
@ -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
|
||||
|
@ -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'
|
||||
|
@ -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
|
||||
|
@ -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]
|
||||
|
@ -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'
|
||||
|
@ -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",
|
||||
|
@ -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])
|
||||
|
@ -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'
|
||||
|
||||
|
@ -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
|
||||
|
Loading…
x
Reference in New Issue
Block a user