2019-02-26 16:11:37 +01:00
|
|
|
# frozen_string_literal: true
|
|
|
|
|
|
|
|
# Generate a downloadable PDF file for the recorded invoice
|
2023-02-24 17:26:55 +01:00
|
|
|
class Pdf::Invoice < Prawn::Document
|
2019-02-26 16:11:37 +01:00
|
|
|
require 'stringio'
|
|
|
|
include ActionView::Helpers::NumberHelper
|
|
|
|
include ApplicationHelper
|
|
|
|
|
2023-01-05 12:09:16 +01:00
|
|
|
# @param invoice [Invoice]
|
|
|
|
def initialize(invoice)
|
2019-02-26 16:11:37 +01:00
|
|
|
super(margin: 70)
|
|
|
|
|
|
|
|
# fonts
|
|
|
|
opensans = Rails.root.join('vendor/assets/fonts/OpenSans-Regular.ttf').to_s
|
|
|
|
opensans_bold = Rails.root.join('vendor/assets/fonts/OpenSans-Bold.ttf').to_s
|
|
|
|
opensans_bolditalic = Rails.root.join('vendor/assets/fonts/OpenSans-BoldItalic.ttf').to_s
|
|
|
|
opensans_italic = Rails.root.join('vendor/assets/fonts/OpenSans-Italic.ttf').to_s
|
|
|
|
|
|
|
|
font_families.update(
|
|
|
|
'Open-Sans' => {
|
|
|
|
normal: { file: opensans, font: 'Open-Sans' },
|
|
|
|
bold: { file: opensans_bold, font: 'Open-Sans-Bold' },
|
|
|
|
italic: { file: opensans_italic, font: 'Open-Sans-Oblique' },
|
|
|
|
bold_italic: { file: opensans_bolditalic, font: 'Open-Sans-BoldOblique' }
|
|
|
|
}
|
|
|
|
)
|
|
|
|
|
|
|
|
# logo
|
2022-03-23 13:30:55 +01:00
|
|
|
img_b64 = Setting.get('invoice_logo')
|
2019-07-10 10:54:35 +02:00
|
|
|
begin
|
2022-03-23 16:51:36 +01:00
|
|
|
image StringIO.new(Base64.decode64(img_b64)), fit: [415, 40]
|
2019-07-10 10:54:35 +02:00
|
|
|
rescue StandardError => e
|
2022-07-26 17:27:33 +02:00
|
|
|
Rails.logger.error "Unable to decode invoice logo from base64: #{e}"
|
2019-07-10 10:54:35 +02:00
|
|
|
end
|
2019-02-26 16:11:37 +01:00
|
|
|
move_down 20
|
|
|
|
font('Open-Sans', size: 10) do
|
|
|
|
# general information
|
2023-03-27 17:18:44 +02:00
|
|
|
text I18n.t(invoice.is_a?(Avoir) ? 'invoices.refund_invoice_reference' : 'invoices.invoice_reference',
|
|
|
|
**{ REF: invoice.reference }), leading: 3
|
2023-02-24 17:26:55 +01:00
|
|
|
text I18n.t('invoices.code', **{ CODE: Setting.get('invoice_code-value') }), leading: 3 if Setting.get('invoice_code-active')
|
2023-01-05 12:09:16 +01:00
|
|
|
if invoice.main_item&.object_type != WalletTransaction.name
|
2023-03-24 17:21:44 +01:00
|
|
|
text I18n.t('invoices.order_number', **{ NUMBER: invoice.order_number }), leading: 3
|
2019-02-26 16:11:37 +01:00
|
|
|
end
|
|
|
|
if invoice.is_a?(Avoir)
|
2023-02-24 17:26:55 +01:00
|
|
|
text I18n.t('invoices.refund_invoice_issued_on_DATE', **{ DATE: I18n.l(invoice.avoir_date.to_date) })
|
2019-02-26 16:11:37 +01:00
|
|
|
else
|
2023-02-24 17:26:55 +01:00
|
|
|
text I18n.t('invoices.invoice_issued_on_DATE', **{ DATE: I18n.l(invoice.created_at.to_date) })
|
2019-02-26 16:11:37 +01:00
|
|
|
end
|
2016-03-23 18:39:41 +01:00
|
|
|
|
2019-02-26 16:11:37 +01:00
|
|
|
# user/organization's information
|
2022-11-23 17:35:39 +01:00
|
|
|
name = Invoices::RecipientService.name(invoice)
|
|
|
|
others = Invoices::RecipientService.organization_data(invoice)
|
|
|
|
address = Invoices::RecipientService.address(invoice)
|
2016-08-02 15:55:49 +02:00
|
|
|
|
2022-03-18 19:44:30 +01:00
|
|
|
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,
|
2019-02-26 16:11:37 +01:00
|
|
|
align: :right,
|
|
|
|
inline_format: true
|
|
|
|
|
|
|
|
# object
|
2022-03-18 19:44:30 +01:00
|
|
|
move_down 28
|
2022-11-23 17:35:39 +01:00
|
|
|
text "#{I18n.t('invoices.object')} #{Invoices::LabelService.build(invoice)}"
|
2016-03-23 18:39:41 +01:00
|
|
|
|
2019-02-26 16:11:37 +01:00
|
|
|
# details table of the invoice's elements
|
|
|
|
move_down 20
|
|
|
|
text I18n.t('invoices.order_summary'), leading: 4
|
|
|
|
move_down 2
|
|
|
|
data = [[I18n.t('invoices.details'), I18n.t('invoices.amount')]]
|
2016-03-23 18:39:41 +01:00
|
|
|
|
2019-02-26 16:11:37 +01:00
|
|
|
total_calc = 0
|
2019-09-18 17:14:59 +02:00
|
|
|
total_ht = 0
|
|
|
|
total_vat = 0
|
2019-02-26 16:11:37 +01:00
|
|
|
# going through invoice_items
|
|
|
|
invoice.invoice_items.each do |item|
|
|
|
|
price = item.amount.to_i / 100.00
|
2016-03-23 18:39:41 +01:00
|
|
|
|
2023-01-05 12:09:16 +01:00
|
|
|
data += [[Invoices::ItemLabelService.build(invoice, item), number_to_currency(price)]]
|
2019-02-26 16:11:37 +01:00
|
|
|
total_calc += price
|
2019-09-18 17:14:59 +02:00
|
|
|
total_ht += item.net_amount
|
|
|
|
total_vat += item.vat
|
2019-02-26 16:11:37 +01:00
|
|
|
end
|
2016-08-10 16:33:26 +02:00
|
|
|
|
2019-02-26 16:11:37 +01:00
|
|
|
## subtract the coupon, if any
|
|
|
|
unless invoice.coupon_id.nil?
|
|
|
|
cp = invoice.coupon
|
2022-10-27 16:45:38 +02:00
|
|
|
coupon_service = CouponService.new
|
|
|
|
total_without_coupon = coupon_service.invoice_total_no_coupon(invoice)
|
|
|
|
discount = (total_without_coupon - invoice.total) / 100.00
|
2016-08-10 16:33:26 +02:00
|
|
|
|
2019-02-26 16:11:37 +01:00
|
|
|
total_calc -= discount
|
|
|
|
|
|
|
|
# discount textual description
|
|
|
|
literal_discount = cp.percent_off
|
2019-09-18 17:14:59 +02:00
|
|
|
literal_discount = number_to_currency(cp.amount_off / 100.00) if cp.type == 'amount_off'
|
2016-03-23 18:39:41 +01:00
|
|
|
|
2019-02-26 16:11:37 +01:00
|
|
|
# add a row for the coupon
|
|
|
|
data += [[_t('invoices.coupon_CODE_discount_of_DISCOUNT',
|
|
|
|
CODE: cp.code,
|
|
|
|
DISCOUNT: literal_discount,
|
2019-09-18 17:14:59 +02:00
|
|
|
TYPE: cp.type), number_to_currency(-discount)]]
|
2019-02-26 16:11:37 +01:00
|
|
|
end
|
2016-03-23 18:39:41 +01:00
|
|
|
|
2019-02-26 16:11:37 +01:00
|
|
|
# total verification
|
2019-08-14 10:54:19 +02:00
|
|
|
total = invoice.total / 100.00
|
2022-07-26 17:27:33 +02:00
|
|
|
Rails.logger.error "totals are NOT equals => expected: #{total}, computed: #{total_calc}" if total_calc != total
|
2016-03-23 18:39:41 +01:00
|
|
|
|
2019-02-26 16:11:37 +01:00
|
|
|
# TVA
|
2019-09-18 17:14:59 +02:00
|
|
|
vat_service = VatHistoryService.new
|
2021-12-29 10:57:59 +01:00
|
|
|
vat_rate_group = vat_service.invoice_vat(invoice)
|
2022-07-26 17:27:33 +02:00
|
|
|
if total_vat.zero?
|
|
|
|
data += [[I18n.t('invoices.total_amount'), number_to_currency(total)]]
|
|
|
|
else
|
2019-02-26 16:11:37 +01:00
|
|
|
data += [[I18n.t('invoices.total_including_all_taxes'), number_to_currency(total)]]
|
2021-12-23 19:36:23 +01:00
|
|
|
vat_rate_group.each do |_type, rate|
|
2022-12-23 15:52:10 +01:00
|
|
|
data += [[I18n.t('invoices.including_VAT_RATE',
|
2023-02-24 17:26:55 +01:00
|
|
|
**{ RATE: rate[:vat_rate],
|
|
|
|
AMOUNT: number_to_currency(rate[:amount] / 100.00),
|
|
|
|
NAME: Setting.get('invoice_VAT-name') }),
|
2022-09-09 16:35:49 +02:00
|
|
|
number_to_currency(rate[:total_vat] / 100.00)]]
|
2021-12-23 19:36:23 +01:00
|
|
|
end
|
2019-09-18 17:14:59 +02:00
|
|
|
data += [[I18n.t('invoices.including_total_excluding_taxes'), number_to_currency(total_ht / 100.00)]]
|
2019-02-26 16:11:37 +01:00
|
|
|
data += [[I18n.t('invoices.including_amount_payed_on_ordering'), number_to_currency(total)]]
|
|
|
|
|
|
|
|
# checking the round number
|
2022-07-27 12:04:17 +02:00
|
|
|
rounded = (sprintf('%.2f', total_vat / 100.00).to_f + sprintf('%.2f', total_ht / 100.00).to_f).to_s
|
2022-07-26 17:27:33 +02:00
|
|
|
if rounded != sprintf('%.2f', total_calc)
|
|
|
|
Rails.logger.error 'rounding the numbers cause an invoice inconsistency. ' \
|
|
|
|
"Total expected: #{sprintf('%.2f', total_calc)}, total computed: #{rounded}"
|
2016-03-23 18:39:41 +01:00
|
|
|
end
|
2019-02-26 16:11:37 +01:00
|
|
|
end
|
2016-03-23 18:39:41 +01:00
|
|
|
|
2019-02-26 16:11:37 +01:00
|
|
|
# display table
|
|
|
|
table(data, header: true, column_widths: [400, 72], cell_style: { inline_format: true }) do
|
|
|
|
row(0).font_style = :bold
|
|
|
|
column(1).style align: :right
|
|
|
|
|
2021-12-23 19:36:23 +01:00
|
|
|
if total_vat != 0
|
2019-02-26 16:11:37 +01:00
|
|
|
# Total incl. taxes
|
|
|
|
row(-1).style align: :right
|
|
|
|
row(-1).background_color = 'E4E4E4'
|
|
|
|
row(-1).font_style = :bold
|
2021-12-23 19:36:23 +01:00
|
|
|
vat_rate_group.size.times do |i|
|
|
|
|
# including VAT xx%
|
|
|
|
row(-2 - i).style align: :right
|
|
|
|
row(-2 - i).background_color = 'E4E4E4'
|
|
|
|
row(-2 - i).font_style = :italic
|
|
|
|
end
|
2019-02-26 16:11:37 +01:00
|
|
|
# including total excl. taxes
|
2021-12-23 19:36:23 +01:00
|
|
|
row(-3 - vat_rate_group.size + 1).style align: :right
|
|
|
|
row(-3 - vat_rate_group.size + 1).background_color = 'E4E4E4'
|
|
|
|
row(-3 - vat_rate_group.size + 1).font_style = :italic
|
2019-02-26 16:11:37 +01:00
|
|
|
# including amount payed on ordering
|
2021-12-23 19:36:23 +01:00
|
|
|
row(-4 - vat_rate_group.size + 1).style align: :right
|
|
|
|
row(-4 - vat_rate_group.size + 1).background_color = 'E4E4E4'
|
|
|
|
row(-4 - vat_rate_group.size + 1).font_style = :bold
|
2016-03-23 18:39:41 +01:00
|
|
|
end
|
2019-02-26 16:11:37 +01:00
|
|
|
end
|
2016-03-23 18:39:41 +01:00
|
|
|
|
2019-02-26 16:11:37 +01:00
|
|
|
# optional description for refunds
|
|
|
|
move_down 20
|
|
|
|
text invoice.description if invoice.is_a?(Avoir) && invoice.description
|
2016-07-19 13:16:49 +02:00
|
|
|
|
2019-02-26 16:11:37 +01:00
|
|
|
# payment details
|
|
|
|
move_down 20
|
2023-01-05 12:09:16 +01:00
|
|
|
text Invoices::PaymentDetailsService.build(invoice, total)
|
2016-03-23 18:39:41 +01:00
|
|
|
|
2019-02-26 16:11:37 +01:00
|
|
|
# important information
|
|
|
|
move_down 40
|
2023-01-05 12:09:16 +01:00
|
|
|
txt = parse_html(Setting.get('invoice_text').to_s)
|
2019-02-26 16:11:37 +01:00
|
|
|
txt.each_line do |line|
|
|
|
|
text line, style: :bold, inline_format: true
|
2016-03-23 18:39:41 +01:00
|
|
|
end
|
|
|
|
|
2019-02-26 16:11:37 +01:00
|
|
|
# address and legals information
|
|
|
|
move_down 40
|
2023-01-05 12:09:16 +01:00
|
|
|
txt = parse_html(Setting.get('invoice_legals').to_s)
|
2019-02-26 16:11:37 +01:00
|
|
|
txt.each_line do |line|
|
|
|
|
text line, align: :right, leading: 4, inline_format: true
|
|
|
|
end
|
2016-03-23 18:39:41 +01:00
|
|
|
end
|
2019-02-27 17:44:52 +01:00
|
|
|
|
|
|
|
# factice watermark
|
|
|
|
return unless %w[staging test development].include?(invoice.environment)
|
|
|
|
|
2019-03-11 12:41:58 +01:00
|
|
|
transparent(0.1) do
|
2019-02-27 17:44:52 +01:00
|
|
|
rotate(45, origin: [0, 0]) do
|
2022-10-27 16:45:38 +02:00
|
|
|
image Rails.root.join("app/pdfs/data/watermark-#{I18n.default_locale}.png"), at: [90, 150]
|
2019-02-27 17:44:52 +01:00
|
|
|
end
|
|
|
|
end
|
2019-02-26 16:11:37 +01:00
|
|
|
end
|
2016-03-23 18:39:41 +01:00
|
|
|
|
2019-02-26 16:11:37 +01:00
|
|
|
private
|
|
|
|
|
|
|
|
##
|
|
|
|
# Remove every unsupported html tag from the given html text (like <p>, <span>, ...).
|
|
|
|
# The supported tags are <b>, <u>, <i> and <br>.
|
|
|
|
# @param html [String] single line html text
|
|
|
|
# @return [String] multi line simplified html text
|
|
|
|
##
|
|
|
|
def parse_html(html)
|
|
|
|
ActionController::Base.helpers.sanitize(html, tags: %w[b u i br])
|
2016-03-23 18:39:41 +01:00
|
|
|
end
|
|
|
|
end
|