webtranslateit-payday 1.2.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. checksums.yaml +7 -0
  2. data/.gitattributes +1 -0
  3. data/.github/dependabot.yml +11 -0
  4. data/.github/workflows/ci.yml +53 -0
  5. data/.gitignore +8 -0
  6. data/.rubocop.yml +11 -0
  7. data/.rubocop_todo.yml +22 -0
  8. data/CHANGELOG.md +76 -0
  9. data/Gemfile +3 -0
  10. data/Guardfile +10 -0
  11. data/README.md +175 -0
  12. data/Rakefile +12 -0
  13. data/fonts/NotoSans-Bold.ttf +0 -0
  14. data/fonts/NotoSans-Regular.ttf +0 -0
  15. data/lib/generators/payday/setup/USAGE +30 -0
  16. data/lib/generators/payday/setup/setup_generator.rb +44 -0
  17. data/lib/generators/payday/setup/templates/invoice.rb +5 -0
  18. data/lib/generators/payday/setup/templates/line_item.rb +5 -0
  19. data/lib/generators/payday/setup/templates/migration.rb +26 -0
  20. data/lib/payday/config.rb +35 -0
  21. data/lib/payday/i18n.rb +3 -0
  22. data/lib/payday/invoice.rb +52 -0
  23. data/lib/payday/invoiceable.rb +87 -0
  24. data/lib/payday/line_item.rb +46 -0
  25. data/lib/payday/line_itemable.rb +12 -0
  26. data/lib/payday/locale/de.yml +23 -0
  27. data/lib/payday/locale/en.yml +22 -0
  28. data/lib/payday/locale/es.yml +22 -0
  29. data/lib/payday/locale/fr.yml +22 -0
  30. data/lib/payday/locale/nl.yml +22 -0
  31. data/lib/payday/locale/zh-CN.yml +21 -0
  32. data/lib/payday/pdf_renderer.rb +322 -0
  33. data/lib/payday/version.rb +5 -0
  34. data/lib/payday.rb +18 -0
  35. data/payday.gemspec +35 -0
  36. data/spec/assets/default_logo.png +0 -0
  37. data/spec/assets/svg.pdf +2501 -0
  38. data/spec/assets/testing.pdf +14178 -0
  39. data/spec/assets/testing_predefined_amount.pdf +0 -0
  40. data/spec/assets/tiger.svg +52 -0
  41. data/spec/invoice_spec.rb +244 -0
  42. data/spec/line_item_spec.rb +30 -0
  43. data/spec/pdf_renderer_spec.rb +11 -0
  44. data/spec/spec_helper.rb +9 -0
  45. data/spec/support/asset_matchers.rb +26 -0
  46. metadata +297 -0
@@ -0,0 +1,87 @@
1
+ # Include {Payday::Invoiceable} in your Invoice class to make it Payday compatible. Payday
2
+ # expects that a +line_items+ method containing an Enumerable of {Payday::LineItem} compatible
3
+ # elements exists. Those LineItem objects primarily need to include quantity, price, and description methods.
4
+ #
5
+ # The +bill_to+ method should always be overwritten by your class. Otherwise, it'll say that your invoice should
6
+ # be billed to Goofy McGoofison. +ship_to+ is also available, but will not be used in rendered invoices if it
7
+ # doesn't exist.
8
+ #
9
+ # Although not required, if a +tax_rate+ method exists, {Payday::Invoiceable} will use it to calculate tax
10
+ # when generating an invoice. We include a simple tax method that calculates tax, but it's probably wiser
11
+ # to override this in your class (our calculated tax won't be stored to a database by default, for example).
12
+ #
13
+ # rubocop:todo Layout/LineLength
14
+ # If the +due_at+, +paid_at+, and +refunded_at+ methods are available, {Payday::Invoiceable} will use them to show due dates,
15
+ # rubocop:enable Layout/LineLength
16
+ # paid dates, and refunded dates, as well as stamps showing if the invoice is paid or due.
17
+ module Payday
18
+ module Invoiceable # rubocop:todo Style/Documentation
19
+ # Who the invoice is being sent to.
20
+ def bill_to
21
+ "Goofy McGoofison\nYour Invoice Doesn't\nHave It's Own BillTo Method"
22
+ end
23
+
24
+ # Calculates the subtotal of this invoice by adding up all of the line items
25
+ def subtotal
26
+ line_items.reduce(BigDecimal('0')) { |result, item| result + item.amount }
27
+ end
28
+
29
+ # The tax for this invoice, as a BigDecimal
30
+ def tax
31
+ if defined?(tax_rate)
32
+ subtotal * tax_rate / 100
33
+ else
34
+ 0
35
+ end
36
+ end
37
+
38
+ # TODO: Add a per weight unit shipping cost
39
+ # Calculates the shipping
40
+ def shipping
41
+ if defined?(shipping_rate)
42
+ shipping_rate
43
+ else
44
+ 0
45
+ end
46
+ end
47
+
48
+ # Calculates the total for this invoice.
49
+ def total
50
+ subtotal + tax + shipping
51
+ end
52
+
53
+ def overdue?
54
+ # rubocop:todo Layout/LineLength
55
+ defined?(due_at) && ((due_at.is_a?(Date) && due_at < Date.today) || (due_at.is_a?(Time) && due_at < Time.now)) && !paid_at
56
+ # rubocop:enable Layout/LineLength
57
+ end
58
+
59
+ def refunded?
60
+ defined?(refunded_at) && !!refunded_at
61
+ end
62
+
63
+ def paid?
64
+ defined?(paid_at) && !!paid_at
65
+ end
66
+
67
+ # Renders this invoice to pdf as a string
68
+ def render_pdf
69
+ Payday::PdfRenderer.render(self)
70
+ end
71
+
72
+ # Renders this invoice to pdf
73
+ def render_pdf_to_file(path)
74
+ Payday::PdfRenderer.render_to_file(self, path)
75
+ end
76
+
77
+ # Iterates through the details on this invoiceable. The block given should accept
78
+ # two parameters, the detail name and the actual detail value.
79
+ def each_detail(&block)
80
+ return if defined?(invoice_details).nil?
81
+
82
+ invoice_details.each do |detail|
83
+ block.call(detail[0], detail[1])
84
+ end
85
+ end
86
+ end
87
+ end
@@ -0,0 +1,46 @@
1
+ module Payday
2
+ # Represents a line item in an invoice.
3
+ #
4
+ # rubocop:todo Layout/LineLength
5
+ # +quantity+ and +price+ are written to be pretty picky, primarily because if we're not picky about what values are set to
6
+ # rubocop:enable Layout/LineLength
7
+ # them your invoice math could get pretty messed up. It's recommended that both values be set to +BigDecimal+ values.
8
+ # Otherwise, we'll do our best to convert the set values to a +BigDecimal+.
9
+ class LineItem
10
+ include LineItemable
11
+
12
+ attr_accessor :description, :display_quantity, :display_price
13
+ attr_reader :quantity, :price, :predefined_amount
14
+
15
+ # Initializes a new LineItem
16
+ def initialize(options = {})
17
+ if options[:predefined_amount]
18
+ self.predefined_amount = options[:predefined_amount]
19
+ else
20
+ self.quantity = options[:quantity] || '1'
21
+ self.display_quantity = options[:display_quantity]
22
+ self.display_price = options[:display_price]
23
+ self.price = options[:price] || '0.00'
24
+ end
25
+ self.description = options[:description] || ''
26
+ end
27
+
28
+ # Sets the quantity of this {LineItem}
29
+ def quantity=(value)
30
+ value = 0 if value.to_s.blank?
31
+ @quantity = BigDecimal(value.to_s)
32
+ end
33
+
34
+ # Sets the price for this {LineItem}
35
+ def price=(value)
36
+ value = 0 if value.to_s.blank?
37
+ @price = BigDecimal(value.to_s)
38
+ end
39
+
40
+ # Sets the predefined_amount for this {LineItem}
41
+ def predefined_amount=(value)
42
+ value = 0 if value.to_s.blank?
43
+ @predefined_amount = BigDecimal(value.to_s)
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,12 @@
1
+ # Include this module into your line item implementation to make sure that Payday stays happy
2
+ # with it, or just make sure that your line item implements the amount method.
3
+ module Payday
4
+ module LineItemable # rubocop:todo Style/Documentation
5
+ # Returns the total amount for this {LineItemable}, or +price * quantity+
6
+ def amount
7
+ return predefined_amount if predefined_amount
8
+
9
+ price * quantity
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,23 @@
1
+ de:
2
+ payday:
3
+ status:
4
+ overdue: ÜBERFÄLLIG
5
+ paid: BEZAHLT
6
+ refunded: ZURÜCKGEZAHLT
7
+ invoice:
8
+ bill_to: Rechnung an
9
+ ship_to: Versenden an
10
+ invoice_no: "Rechnungsnr.:"
11
+ invoice_date: "Rechnungsdatum:"
12
+ due_date: "Fälligkeit:"
13
+ paid_date: "Zahlungsdatum:"
14
+ subtotal: "Zwischensumme:"
15
+ shipping: "Versand:"
16
+ tax: "Steuern:"
17
+ total: "Gesamt:"
18
+ notes: "Anmerkungen"
19
+ line_item:
20
+ description: Beschreibung
21
+ unit_price: Stückpreis
22
+ quantity: Menge
23
+ amount: Betrag
@@ -0,0 +1,22 @@
1
+ en:
2
+ payday:
3
+ status:
4
+ paid: PAID
5
+ overdue: OVERDUE
6
+ refunded: REFUNDED
7
+ invoice:
8
+ bill_to: "Bill To"
9
+ ship_to: "Ship To"
10
+ invoice_no: "Invoice number:"
11
+ receipt_no: "Receipt number:"
12
+ due_date: "Due Date:"
13
+ paid_date: "Paid Date:"
14
+ subtotal: "Subtotal:"
15
+ shipping: "Shipping:"
16
+ tax: "Tax:"
17
+ total: "Total:"
18
+ line_item:
19
+ description: Description
20
+ unit_price: "Unit Price"
21
+ quantity: Quantity
22
+ amount: Amount
@@ -0,0 +1,22 @@
1
+ es:
2
+ payday:
3
+ status:
4
+ paid: PAGADO
5
+ overdue: ATRASADO
6
+ refunded: REINTEGRADO
7
+ invoice:
8
+ bill_to: "Facturar a"
9
+ ship_to: "Enviar a"
10
+ invoice_no: "Número de Factura Proforma:"
11
+ receipt_no: "Número de Factura:"
12
+ due_date: "Vencimiento:"
13
+ paid_date: "Pagado en:"
14
+ subtotal: "Subtotal:"
15
+ shipping: "Envío:"
16
+ tax: "Impuesto:"
17
+ total: "Total:"
18
+ line_item:
19
+ description: Descripción
20
+ unit_price: "Precio por Unidad"
21
+ quantity: Cantidad
22
+ amount: Precio
@@ -0,0 +1,22 @@
1
+ fr:
2
+ payday:
3
+ status:
4
+ paid: PAYÉ
5
+ overdue: "EN RETARD"
6
+ refunded: "REMBOURSÉ"
7
+ invoice:
8
+ bill_to: "Adresse de facturation"
9
+ ship_to: "Adresse de livraison"
10
+ invoice_no: "Numéro de facture proforma :"
11
+ receipt_no: "Numéro de facture :"
12
+ due_date: "Date limite de paiement :"
13
+ paid_date: "Date de paiement :"
14
+ subtotal: "Sous-total :"
15
+ shipping: "Livraison :"
16
+ tax: "Taxes :"
17
+ total: "Total :"
18
+ line_item:
19
+ description: Description
20
+ unit_price: "Prix Unitaire"
21
+ quantity: Quantité
22
+ amount: Prix
@@ -0,0 +1,22 @@
1
+ nl:
2
+ payday:
3
+ status:
4
+ paid: BETAALD
5
+ overdue: BETAALTERMIJN VERSTREKEN
6
+ refund: FAKTUUR
7
+ invoice:
8
+ bill_to: Te betalen door
9
+ ship_to: Te bezorgen aan
10
+ invoice_no: "Faktuur Nr:"
11
+ due_date: "Te betalen voor:"
12
+ paid_date: "Betaald op:"
13
+ subtotal: "Subtotaal:"
14
+ shipping: "Levering:"
15
+ tax: "Btw:"
16
+ total: "Totaal:"
17
+ notes: "Opmerkingen"
18
+ line_item:
19
+ description: Omschrijving
20
+ unit_price: Eenheidsprijs
21
+ quantity: Hoeveelheid
22
+ amount: Bedrag
@@ -0,0 +1,21 @@
1
+ zh-CN:
2
+ payday:
3
+ status:
4
+ paid: 付款成功
5
+ overdue: 已过期
6
+ invoice:
7
+ bill_to: 账单地址
8
+ ship_to: 送货地址
9
+ invoice_no: "单号 #:"
10
+ due_date: "过期日期:"
11
+ paid_date: "支付日期:"
12
+ subtotal: "小计:"
13
+ shipping: "邮费:"
14
+ tax: "税费:"
15
+ total: "总计:"
16
+ notes: "备注"
17
+ line_item:
18
+ description: 描述
19
+ unit_price: 单价
20
+ quantity: 数量
21
+ amount: 合计
@@ -0,0 +1,322 @@
1
+ module Payday
2
+ # The PDF renderer. We use this internally in Payday to render pdfs, but really you should just need to call
3
+ # {{Payday::Invoiceable#render_pdf}} to render pdfs yourself.
4
+ class PdfRenderer # rubocop:todo Metrics/ClassLength
5
+ # Renders the given invoice as a pdf on disk
6
+ def self.render_to_file(invoice, path)
7
+ pdf(invoice).render_file(path)
8
+ end
9
+
10
+ # Renders the given invoice as a pdf, returning a string
11
+ def self.render(invoice)
12
+ pdf(invoice).render
13
+ end
14
+
15
+ def self.pdf(invoice) # rubocop:todo Metrics/MethodLength, Metrics/AbcSize
16
+ pdf = Prawn::Document.new(page_size: invoice_or_default(invoice, :page_size))
17
+
18
+ font_dir = File.join(File.dirname(__dir__), '..', 'fonts')
19
+ pdf.font_families.update(
20
+ 'NotoSans' => { normal: File.join(font_dir, 'NotoSans-Regular.ttf'),
21
+ bold: File.join(font_dir, 'NotoSans-Bold.ttf') }
22
+ )
23
+
24
+ # set up some default styling
25
+ pdf.font_size(10)
26
+ pdf.font 'NotoSans'
27
+
28
+ stamp(invoice, pdf)
29
+ company_banner(invoice, pdf)
30
+ bill_to_ship_to(invoice, pdf)
31
+ invoice_details(invoice, pdf)
32
+ line_items_table(invoice, pdf)
33
+ totals_lines(invoice, pdf)
34
+ notes(invoice, pdf)
35
+
36
+ page_numbers(pdf)
37
+
38
+ pdf
39
+ end
40
+
41
+ def self.stamp(invoice, pdf) # rubocop:todo Metrics/MethodLength
42
+ stamp = nil
43
+ if invoice.refunded?
44
+ stamp = I18n.t 'payday.status.refunded', default: 'REFUNDED'
45
+ elsif invoice.paid?
46
+ stamp = I18n.t 'payday.status.paid', default: 'PAID'
47
+ elsif invoice.overdue?
48
+ stamp = I18n.t 'payday.status.overdue', default: 'OVERDUE'
49
+ end
50
+
51
+ if stamp
52
+ pdf.bounding_box([150, pdf.cursor - 50], width: pdf.bounds.width - 300) do
53
+ pdf.font('NotoSans') do
54
+ pdf.fill_color 'cc0000'
55
+ pdf.text stamp, align: :center, size: 25, rotate: 15, style: :bold
56
+ end
57
+ end
58
+ end
59
+
60
+ pdf.fill_color '000000'
61
+ end
62
+
63
+ # rubocop:todo Metrics/MethodLength
64
+ def self.company_banner(invoice, pdf) # rubocop:todo Metrics/AbcSize, Metrics/MethodLength
65
+ # render the logo
66
+ image = invoice_or_default(invoice, :invoice_logo)
67
+ height = nil
68
+ width = nil
69
+
70
+ # Handle images defined with a hash of options
71
+ if image.is_a?(Hash)
72
+ data = image
73
+ image = data[:filename]
74
+ width, height = data[:size].split('x').map(&:to_f)
75
+ end
76
+
77
+ if File.extname(image) == '.svg'
78
+ logo_info = pdf.svg(File.read(image), at: pdf.bounds.top_left, width: width, height: height)
79
+ logo_height = logo_info[:height]
80
+ else
81
+ logo_info = pdf.image(image, at: pdf.bounds.top_left, width: width, height: height)
82
+ logo_height = logo_info.scaled_height
83
+ end
84
+
85
+ # render the company details
86
+ table_data = []
87
+ table_data << [bold_cell(pdf, invoice_or_default(invoice, :company_name).strip, size: 12)]
88
+
89
+ invoice_or_default(invoice, :company_details).lines.each { |line| table_data << [line] }
90
+
91
+ table = pdf.make_table(table_data, cell_style: { borders: [], padding: 0 })
92
+ pdf.bounding_box([pdf.bounds.width - table.width, pdf.bounds.top], width: table.width,
93
+ height: table.height + 5) do
94
+ table.draw
95
+ end
96
+
97
+ pdf.move_cursor_to(pdf.bounds.top - logo_height - 20)
98
+ end
99
+ # rubocop:enable Metrics/MethodLength
100
+
101
+ # rubocop:todo Metrics/MethodLength
102
+ def self.bill_to_ship_to(invoice, pdf) # rubocop:todo Metrics/AbcSize, Metrics/MethodLength
103
+ bill_to_cell_style = { borders: [], padding: [2, 0] }
104
+ bill_to_ship_to_bottom = 0
105
+
106
+ # render bill to
107
+ pdf.float do
108
+ pdf.table([[bold_cell(pdf, I18n.t('payday.invoice.bill_to', default: 'Bill To'))],
109
+ [invoice.bill_to]], column_widths: [200], cell_style: bill_to_cell_style)
110
+ bill_to_ship_to_bottom = pdf.cursor
111
+ end
112
+
113
+ # render ship to
114
+ if defined?(invoice.ship_to) && !invoice.ship_to.nil?
115
+ table = pdf.make_table([[bold_cell(pdf, I18n.t('payday.invoice.ship_to', default: 'Ship To'))],
116
+ [invoice.ship_to]], column_widths: [200], cell_style: bill_to_cell_style)
117
+
118
+ pdf.bounding_box([pdf.bounds.width - table.width, pdf.cursor], width: table.width, height: table.height + 2) do
119
+ table.draw
120
+ end
121
+ end
122
+
123
+ # make sure we start at the lower of the bill_to or ship_to details
124
+ bill_to_ship_to_bottom = pdf.cursor if pdf.cursor < bill_to_ship_to_bottom
125
+ pdf.move_cursor_to(bill_to_ship_to_bottom - 20)
126
+ end
127
+ # rubocop:enable Metrics/MethodLength
128
+
129
+ # rubocop:todo Metrics/PerceivedComplexity
130
+ # rubocop:todo Metrics/MethodLength
131
+ # rubocop:todo Metrics/AbcSize
132
+ def self.invoice_details(invoice, pdf) # rubocop:todo Metrics/CyclomaticComplexity, Metrics/AbcSize, Metrics/MethodLength, Metrics/PerceivedComplexity
133
+ # invoice details
134
+ table_data = []
135
+
136
+ # invoice number
137
+ if defined?(invoice.invoice_number) && invoice.invoice_number
138
+ table_data << if invoice.paid?
139
+ [bold_cell(pdf, I18n.t('payday.invoice.receipt_no', default: 'Receipt #:')),
140
+ bold_cell(pdf, invoice.invoice_number.to_s, align: :right)]
141
+ else
142
+ [bold_cell(pdf, I18n.t('payday.invoice.invoice_no', default: 'Invoice #:')),
143
+ bold_cell(pdf, invoice.invoice_number.to_s, align: :right)]
144
+ end
145
+ end
146
+
147
+ # Due on
148
+ if defined?(invoice.due_at) && invoice.due_at
149
+ due_date = if invoice.due_at.is_a?(Date) || invoice.due_at.is_a?(Time)
150
+ invoice.due_at.strftime(Payday::Config.default.date_format)
151
+ else
152
+ invoice.due_at.to_s
153
+ end
154
+
155
+ table_data << [bold_cell(pdf, I18n.t('payday.invoice.due_date', default: 'Due Date:')),
156
+ bold_cell(pdf, due_date, align: :right)]
157
+ end
158
+
159
+ # Paid on
160
+ if defined?(invoice.paid_at) && invoice.paid_at
161
+ paid_date = if invoice.paid_at.is_a?(Date) || invoice.due_at.is_a?(Time)
162
+ invoice.paid_at.strftime(Payday::Config.default.date_format)
163
+ else
164
+ invoice.paid_at.to_s
165
+ end
166
+
167
+ table_data << [bold_cell(pdf, I18n.t('payday.invoice.paid_date', default: 'Paid Date:')),
168
+ bold_cell(pdf, paid_date, align: :right)]
169
+ end
170
+
171
+ # loop through invoice_details and include them
172
+ invoice.each_detail do |key, value|
173
+ table_data << [bold_cell(pdf, key),
174
+ bold_cell(pdf, value, align: :right)]
175
+ end
176
+
177
+ return unless table_data.length.positive?
178
+
179
+ pdf.table(table_data, cell_style: { borders: [], padding: [1, 10, 1, 1] })
180
+ end
181
+ # rubocop:enable Metrics/AbcSize
182
+ # rubocop:enable Metrics/MethodLength
183
+ # rubocop:enable Metrics/PerceivedComplexity
184
+
185
+ # rubocop:todo Metrics/MethodLength
186
+ def self.line_items_table(invoice, pdf) # rubocop:todo Metrics/AbcSize, Metrics/MethodLength
187
+ table_data = []
188
+ table_data << [bold_cell(pdf, I18n.t('payday.line_item.description', default: 'Description'), borders: []),
189
+ bold_cell(pdf, I18n.t('payday.line_item.unit_price', default: 'Unit Price'), align: :center,
190
+ borders: []),
191
+ bold_cell(pdf, I18n.t('payday.line_item.quantity', default: 'Quantity'), align: :center,
192
+ borders: []),
193
+ bold_cell(pdf, I18n.t('payday.line_item.amount', default: 'Amount'), align: :center, borders: [])]
194
+ invoice.line_items.each do |line|
195
+ table_data << if line.predefined_amount
196
+ [line.description, '', '', number_to_currency(line.predefined_amount, invoice)]
197
+ else
198
+ [line.description,
199
+ (line.display_price || number_to_currency(line.price, invoice)),
200
+ (line.display_quantity || BigDecimal(line.quantity.to_s).to_s('F')),
201
+ number_to_currency(line.amount, invoice)]
202
+ end
203
+ end
204
+
205
+ pdf.move_cursor_to(pdf.cursor - 20)
206
+ pdf.table(table_data, width: pdf.bounds.width, header: true,
207
+ cell_style: { border_width: 0.5, border_left_color: 'FFFFFF', border_right_color: 'FFFFFF',
208
+ border_top_color: 'F6F9FC', border_bottom_color: 'BCC6D0', padding: [5, 10],
209
+ inline_format: true },
210
+ row_colors: %w[F6F9FC ffffff]) do
211
+ # left align the number columns
212
+ columns(1..3).rows(1..row_length - 1).style(align: :right)
213
+
214
+ # set the column widths correctly
215
+ natural = natural_column_widths
216
+ natural[0] = width - natural[1] - natural[2] - natural[3]
217
+ end
218
+ end
219
+ # rubocop:enable Metrics/MethodLength
220
+
221
+ # rubocop:todo Metrics/MethodLength
222
+ def self.totals_lines(invoice, pdf) # rubocop:todo Metrics/AbcSize, Metrics/MethodLength
223
+ table_data = []
224
+ table_data << [
225
+ bold_cell(pdf, I18n.t('payday.invoice.subtotal', default: 'Subtotal:')),
226
+ cell(pdf, number_to_currency(invoice.subtotal, invoice), align: :right)
227
+ ]
228
+
229
+ tax_description = if invoice.tax_description.nil?
230
+ I18n.t('payday.invoice.tax', default: 'Tax:')
231
+ else
232
+ invoice.tax_description
233
+ end
234
+
235
+ table_data << [
236
+ bold_cell(pdf, tax_description),
237
+ cell(pdf, number_to_currency(invoice.tax, invoice), align: :right)
238
+ ]
239
+
240
+ if invoice.shipping_rate.positive?
241
+ shipping_description = if invoice.shipping_description.nil?
242
+ I18n.t('payday.invoice.shipping', default: 'Shipping:')
243
+ else
244
+ invoice.shipping_description
245
+ end
246
+
247
+ table_data << [
248
+ bold_cell(pdf, shipping_description),
249
+ cell(pdf, number_to_currency(invoice.shipping, invoice),
250
+ align: :right)
251
+ ]
252
+ end
253
+ table_data << [
254
+ bold_cell(pdf, I18n.t('payday.invoice.total', default: 'Total:'),
255
+ size: 12),
256
+ cell(pdf, number_to_currency(invoice.total, invoice),
257
+ size: 12, align: :right)
258
+ ]
259
+ table = pdf.make_table(table_data, cell_style: { borders: [] })
260
+ pdf.bounding_box([pdf.bounds.width - table.width, pdf.cursor],
261
+ width: table.width, height: table.height + 2) do
262
+ table.draw
263
+ end
264
+ end
265
+ # rubocop:enable Metrics/MethodLength
266
+
267
+ def self.notes(invoice, pdf) # rubocop:todo Metrics/AbcSize
268
+ return unless defined?(invoice.notes) && invoice.notes
269
+
270
+ pdf.move_cursor_to(pdf.cursor - 30)
271
+ pdf.font('NotoSans') do
272
+ pdf.text(I18n.t('payday.invoice.notes', default: 'Notes', style: :bold))
273
+ end
274
+ pdf.line_width = 0.5
275
+ pdf.stroke_color = 'cccccc'
276
+ pdf.stroke_line([0, pdf.cursor - 3, pdf.bounds.width, pdf.cursor - 3])
277
+ pdf.move_cursor_to(pdf.cursor - 10)
278
+ pdf.text(invoice.notes.to_s, inline_format: true)
279
+ end
280
+
281
+ def self.page_numbers(pdf)
282
+ return unless pdf.page_count > 1
283
+
284
+ pdf.number_pages('<page> / <total>', at: [pdf.bounds.right - 25, -10])
285
+ end
286
+
287
+ def self.invoice_or_default(invoice, property)
288
+ if invoice.respond_to?(property) && invoice.send(property)
289
+ invoice.send(property)
290
+ else
291
+ Payday::Config.default.send(property)
292
+ end
293
+ end
294
+
295
+ def self.cell(pdf, text, options = {})
296
+ Prawn::Table::Cell::Text.make(pdf, text, options)
297
+ end
298
+
299
+ def self.bold_cell(pdf, text, options = {})
300
+ cell(pdf, "<b>#{text}</b>", options.merge(inline_format: true))
301
+ end
302
+
303
+ # Converts this number to a formatted currency string
304
+ def self.number_to_currency(number, invoice)
305
+ Money.locale_backend = :currency
306
+ Money.rounding_mode = BigDecimal::ROUND_HALF_UP
307
+ currency = Money::Currency.wrap(invoice_or_default(invoice, :currency))
308
+ number *= currency.subunit_to_unit
309
+ number = number.round unless Money.default_infinite_precision
310
+ Money.new(number, currency).format
311
+ end
312
+
313
+ def self.max_cell_width(cell_proxy)
314
+ max = 0
315
+ cell_proxy.each do |cell|
316
+ max = cell.natural_content_width if cell.natural_content_width > max
317
+ end
318
+
319
+ max
320
+ end
321
+ end
322
+ end
@@ -0,0 +1,5 @@
1
+ # This is Payday!
2
+ module Payday
3
+ # Current Version
4
+ VERSION = '1.2.7'.freeze
5
+ end
data/lib/payday.rb ADDED
@@ -0,0 +1,18 @@
1
+ # Not much to see here
2
+ require 'date'
3
+ require 'time'
4
+ require 'bigdecimal'
5
+ require 'prawn'
6
+ require 'prawn/table'
7
+ require 'prawn-svg'
8
+ require 'money'
9
+ require 'active_support/all'
10
+
11
+ require_relative 'payday/version'
12
+ require_relative 'payday/config'
13
+ require_relative 'payday/i18n'
14
+ require_relative 'payday/line_itemable'
15
+ require_relative 'payday/line_item'
16
+ require_relative 'payday/pdf_renderer'
17
+ require_relative 'payday/invoiceable'
18
+ require_relative 'payday/invoice'
data/payday.gemspec ADDED
@@ -0,0 +1,35 @@
1
+ $LOAD_PATH.push File.expand_path('lib', __dir__)
2
+ require 'payday/version'
3
+
4
+ Gem::Specification.new do |s|
5
+ s.name = 'webtranslateit-payday'
6
+ s.version = Payday::VERSION
7
+ s.required_ruby_version = '>= 2.5'
8
+ s.platform = Gem::Platform::RUBY
9
+ s.authors = ['Alan Johnson', 'Edouard Briere']
10
+ s.email = ['edouard@webtranslateit.com']
11
+ s.homepage = 'https://github.com/webtranslateit/payday'
12
+ s.summary = 'A simple library for rendering invoices.'
13
+ s.description = 'Payday is a library for rendering invoices to pdf.'
14
+
15
+ s.add_dependency 'activesupport'
16
+ s.add_dependency('i18n', '>= 0.7', '< 2.0')
17
+ s.add_dependency('money', '~> 6.5')
18
+ s.add_dependency('prawn', '>= 1.0', '< 2.5')
19
+ s.add_dependency('prawn-svg', '>= 0.15.0', '< 0.32.1')
20
+ s.add_dependency('prawn-table', '>= 0.2.2')
21
+ s.add_dependency 'rexml'
22
+
23
+ s.add_development_dependency('guard')
24
+ s.add_development_dependency('guard-rspec')
25
+ s.add_development_dependency 'guard-rubocop'
26
+ s.add_development_dependency('rspec', '~> 3.10.0')
27
+ s.add_development_dependency('rubocop')
28
+ s.add_development_dependency('rubocop-rspec')
29
+
30
+ s.files = `git ls-files`.split("\n")
31
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
32
+ s.executables =
33
+ `git ls-files -- bin/*`.split("\n").map { |f| File.basename(f) }
34
+ s.require_paths = ['lib']
35
+ end
Binary file