invoicing 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (62) hide show
  1. data/CHANGELOG +3 -0
  2. data/LICENSE +20 -0
  3. data/Manifest +60 -0
  4. data/README +48 -0
  5. data/Rakefile +75 -0
  6. data/invoicing.gemspec +41 -0
  7. data/lib/invoicing.rb +9 -0
  8. data/lib/invoicing/cached_record.rb +107 -0
  9. data/lib/invoicing/class_info.rb +187 -0
  10. data/lib/invoicing/connection_adapter_ext.rb +44 -0
  11. data/lib/invoicing/countries/uk.rb +24 -0
  12. data/lib/invoicing/currency_value.rb +212 -0
  13. data/lib/invoicing/find_subclasses.rb +193 -0
  14. data/lib/invoicing/ledger_item.rb +718 -0
  15. data/lib/invoicing/ledger_item/render_html.rb +515 -0
  16. data/lib/invoicing/ledger_item/render_ubl.rb +268 -0
  17. data/lib/invoicing/line_item.rb +246 -0
  18. data/lib/invoicing/price.rb +9 -0
  19. data/lib/invoicing/tax_rate.rb +9 -0
  20. data/lib/invoicing/taxable.rb +355 -0
  21. data/lib/invoicing/time_dependent.rb +388 -0
  22. data/lib/invoicing/version.rb +21 -0
  23. data/test/cached_record_test.rb +100 -0
  24. data/test/class_info_test.rb +253 -0
  25. data/test/connection_adapter_ext_test.rb +71 -0
  26. data/test/currency_value_test.rb +184 -0
  27. data/test/find_subclasses_test.rb +120 -0
  28. data/test/fixtures/README +7 -0
  29. data/test/fixtures/cached_record.sql +22 -0
  30. data/test/fixtures/class_info.sql +28 -0
  31. data/test/fixtures/currency_value.sql +29 -0
  32. data/test/fixtures/find_subclasses.sql +43 -0
  33. data/test/fixtures/ledger_item.sql +39 -0
  34. data/test/fixtures/line_item.sql +33 -0
  35. data/test/fixtures/price.sql +4 -0
  36. data/test/fixtures/tax_rate.sql +4 -0
  37. data/test/fixtures/taxable.sql +14 -0
  38. data/test/fixtures/time_dependent.sql +35 -0
  39. data/test/ledger_item_test.rb +352 -0
  40. data/test/line_item_test.rb +139 -0
  41. data/test/models/README +4 -0
  42. data/test/models/test_subclass_in_another_file.rb +3 -0
  43. data/test/models/test_subclass_not_in_database.rb +6 -0
  44. data/test/price_test.rb +9 -0
  45. data/test/ref-output/creditnote3.html +82 -0
  46. data/test/ref-output/creditnote3.xml +89 -0
  47. data/test/ref-output/invoice1.html +93 -0
  48. data/test/ref-output/invoice1.xml +111 -0
  49. data/test/ref-output/invoice2.html +86 -0
  50. data/test/ref-output/invoice2.xml +98 -0
  51. data/test/ref-output/invoice_null.html +36 -0
  52. data/test/render_html_test.rb +69 -0
  53. data/test/render_ubl_test.rb +32 -0
  54. data/test/setup.rb +37 -0
  55. data/test/tax_rate_test.rb +9 -0
  56. data/test/taxable_test.rb +180 -0
  57. data/test/test_helper.rb +48 -0
  58. data/test/time_dependent_test.rb +180 -0
  59. data/website/curvycorners.js +1 -0
  60. data/website/screen.css +149 -0
  61. data/website/template.html.erb +43 -0
  62. metadata +180 -0
@@ -0,0 +1,515 @@
1
+ # encoding: utf-8
2
+
3
+ require 'builder'
4
+
5
+ module Invoicing
6
+ module LedgerItem
7
+ # Included into ActiveRecord model object when +acts_as_ledger_item+ is invoked.
8
+ module RenderHTML
9
+ # Shortcut for rendering an invoice or a credit note into a human-readable HTML format.
10
+ # Can be called without any arguments, in which case a general-purpose representation is
11
+ # produced. Can also be given options and a block for customising the output:
12
+ #
13
+ # @invoice = Invoice.find(params[:id])
14
+ # @invoice.render_html :quantity_column => false do |i|
15
+ # i.date_format "%d %B %Y" # overwrites default "%Y-%m-%d"
16
+ # i.recipient_label "Customer" # overwrites default "Recipient"
17
+ # i.sender_label "Supplier" # overwrites default "Sender"
18
+ # i.description_tag do |params|
19
+ # "<p>Thank you for your order. Here is our invoice for your records.</p>\n" +
20
+ # "<p>Description: #{params[:description]}</p>\n"
21
+ # end
22
+ # end
23
+ def render_html(options={}, &block)
24
+ output_builder = HTMLOutputBuilder.new(self, options)
25
+ yield output_builder if block_given?
26
+ output_builder.build
27
+ end
28
+
29
+
30
+ class HTMLOutputBuilder #:nodoc:
31
+
32
+ HTML_ESCAPE = { '&' => '&amp;', '>' => '&gt;', '<' => '&lt;', '"' => '&quot;' }
33
+
34
+ attr_reader :ledger_item, :current_line_item, :options, :custom_fragments, :factor
35
+
36
+ def initialize(ledger_item, options)
37
+ @ledger_item = ledger_item
38
+ @options = default_options
39
+ @options.update(options)
40
+ @custom_fragments = {}
41
+ total_amount = get(:total_amount)
42
+ @factor = (total_amount && total_amount < BigDecimal('0')) ? BigDecimal('-1') : BigDecimal('1')
43
+ end
44
+
45
+ def default_options
46
+ line_items = get(:line_items)
47
+ {
48
+ :tax_point_column => line_items.map{|i| line_get(:tax_point, i) }.compact != [],
49
+ :quantity_column => line_items.map{|i| line_get(:quantity, i) }.compact != [],
50
+ :description_column => true,
51
+ :net_amount_column => true,
52
+ :tax_amount_column => line_items.map{|i| line_get(:tax_amount, i) }.compact != [],
53
+ :gross_amount_column => false,
54
+ :subtotal => true
55
+ }
56
+ end
57
+
58
+ def h(s)
59
+ s.to_s.gsub(/[#{HTML_ESCAPE.keys.join}]/) { |char| HTML_ESCAPE[char] }
60
+ end
61
+
62
+ # foo { block } => call block when invoking fragment foo
63
+ # foo "string" => return string when invoking fragment foo
64
+ # invoke_foo => invoke fragment foo; if none set, delegate to default_foo
65
+ def method_missing(method_id, *args, &block)
66
+ method_id = method_id.to_sym
67
+ if method_id.to_s =~ /^invoke_(.*)$/
68
+ method_id = $1.to_sym
69
+ if custom_fragments[method_id]
70
+ return custom_fragments[method_id].call(*args, &block)
71
+ else
72
+ return send("default_#{method_id}", *args, &block)
73
+ end
74
+ end
75
+
76
+ return super unless respond_to? "default_#{method_id}"
77
+
78
+ if block_given? && args.empty?
79
+ custom_fragments[method_id] = proc &block
80
+ elsif args.length == 1
81
+ custom_fragments[method_id] = proc{ args[0].to_s }
82
+ else
83
+ raise ArgumentError, "#{method_id} expects exactly one value or block argument"
84
+ end
85
+ end
86
+
87
+ # Returns the value of a (potentially renamed) method on the ledger item
88
+ def get(method_id)
89
+ ledger_item.send(:ledger_item_class_info).get(ledger_item, method_id)
90
+ end
91
+
92
+ # Returns the value of a (potentially renamed) method on a line item
93
+ def line_get(method_id, line_item = current_line_item)
94
+ line_item.send(:line_item_class_info).get(line_item, method_id)
95
+ end
96
+
97
+ # String for one level of indentation
98
+ def indent
99
+ ' '
100
+ end
101
+
102
+ # This is quite meta. :)
103
+ #
104
+ # params_hash(:sender_label, :sender_tax_number => :tax_number_label)
105
+ # # => {:sender_label => invoke_sender_label,
106
+ # # :sender_tax_number => invoke_sender_tax_number(params_hash(:tax_number_label))}
107
+ def params_hash(*param_names)
108
+ result = {}
109
+ param_names.flatten!
110
+ options = param_names.extract_options!
111
+
112
+ param_names.each{|param| result[param.to_sym] = send("invoke_#{param}") }
113
+
114
+ options.each_pair do |key, value|
115
+ result[key.to_sym] = send("invoke_#{key}", params_hash(value))
116
+ end
117
+
118
+ result
119
+ end
120
+
121
+ # Renders an invoice or credit note to HTML
122
+ def build
123
+ addresses_table_deps = [:sender_label, :recipient_label, :sender_address, :recipient_address, {
124
+ :sender_tax_number => :tax_number_label,
125
+ :recipient_tax_number => :tax_number_label
126
+ }]
127
+
128
+ metadata_table_deps = [{
129
+ :identifier => :identifier_label,
130
+ :issue_date => [:date_format, :issue_date_label],
131
+ :period_start => [:date_format, :period_start_label],
132
+ :period_end => [:date_format, :period_end_label],
133
+ :due_date => [:date_format, :due_date_label]
134
+ }]
135
+
136
+ line_items_header_deps = [:line_tax_point_label, :line_quantity_label, :line_description_label,
137
+ :line_net_amount_label, :line_tax_amount_label, :line_gross_amount_label]
138
+
139
+ line_items_subtotal_deps = [:subtotal_label, :net_amount_label, :tax_amount_label,
140
+ :gross_amount_label, {
141
+ :net_amount => :net_amount_label,
142
+ :tax_amount => :tax_amount_label,
143
+ :total_amount => :gross_amount_label
144
+ }]
145
+
146
+ line_items_total_deps = [:total_label, :net_amount_label, :tax_amount_label,
147
+ :gross_amount_label, {
148
+ :net_amount => :net_amount_label,
149
+ :tax_amount => :tax_amount_label,
150
+ :total_amount => :gross_amount_label
151
+ }]
152
+
153
+ page_layout_deps = {
154
+ :title_tag => :title,
155
+ :addresses_table => addresses_table_deps,
156
+ :metadata_table => metadata_table_deps,
157
+ :description_tag => :description,
158
+ :line_items_table => [:line_items_list, {
159
+ :line_items_header => line_items_header_deps,
160
+ :line_items_subtotal => line_items_subtotal_deps,
161
+ :line_items_total => line_items_total_deps
162
+ }]
163
+ }
164
+
165
+ invoke_page_layout(params_hash(page_layout_deps))
166
+ end
167
+
168
+ def default_date_format
169
+ "%Y-%m-%d"
170
+ end
171
+
172
+ def default_invoice_label
173
+ "Invoice"
174
+ end
175
+
176
+ def default_credit_note_label
177
+ "Credit Note"
178
+ end
179
+
180
+ def default_recipient_label
181
+ "Recipient"
182
+ end
183
+
184
+ def default_sender_label
185
+ "Sender"
186
+ end
187
+
188
+ def default_tax_number_label
189
+ "VAT number:<br />"
190
+ end
191
+
192
+ def default_identifier_label
193
+ "Invoice no.:"
194
+ end
195
+
196
+ def default_issue_date_label
197
+ "Issue date:"
198
+ end
199
+
200
+ def default_period_start_label
201
+ "Period from:"
202
+ end
203
+
204
+ def default_period_end_label
205
+ "Period until:"
206
+ end
207
+
208
+ def default_due_date_label
209
+ "Payment due:"
210
+ end
211
+
212
+ def default_line_tax_point_label
213
+ "Tax point"
214
+ end
215
+
216
+ def default_line_quantity_label
217
+ "Quantity"
218
+ end
219
+
220
+ def default_line_description_label
221
+ "Description"
222
+ end
223
+
224
+ def default_line_net_amount_label
225
+ "Net price"
226
+ end
227
+
228
+ def default_line_tax_amount_label
229
+ "VAT"
230
+ end
231
+
232
+ def default_line_gross_amount_label
233
+ "Gross price"
234
+ end
235
+
236
+ def default_subtotal_label
237
+ "Subtotal"
238
+ end
239
+
240
+ def default_total_label
241
+ "Total"
242
+ end
243
+
244
+ def default_net_amount_label
245
+ "Net: "
246
+ end
247
+
248
+ def default_tax_amount_label
249
+ "VAT: "
250
+ end
251
+
252
+ def default_gross_amount_label
253
+ ""
254
+ end
255
+
256
+ def default_title
257
+ (factor == BigDecimal('-1')) ? invoke_credit_note_label : invoke_invoice_label
258
+ end
259
+
260
+ def default_title_tag(params)
261
+ "<h1 class=\"invoice\">#{params[:title]}</h1>\n"
262
+ end
263
+
264
+ def default_address(details)
265
+ html = "#{indent*3}<div class=\"fn org\">#{ h(details[:name]) }</div>\n"
266
+ html << "#{indent*3}<div class=\"contact\">#{ h(details[:contact_name])}</div>\n" unless details[:contact_name].blank?
267
+ html << "#{indent*3}<div class=\"adr\">\n"
268
+ html << "#{indent*4}<span class=\"street-address\">#{h(details[:address]).strip.gsub(/\n/, '<br />')}</span><br />\n"
269
+ html << "#{indent*4}<span class=\"locality\">#{ h(details[:city]) }</span><br />\n" unless details[:city].blank?
270
+ html << "#{indent*4}<span class=\"region\">#{ h(details[:state]) }</span><br />\n" unless details[:state].blank?
271
+ html << "#{indent*4}<span class=\"postal-code\">#{ h(details[:postal_code]) }</span><br />\n" unless details[:postal_code].blank?
272
+ html << "#{indent*4}<span class=\"country-name\">#{h(details[:country]) }</span>\n" unless details[:country].blank?
273
+ html << "#{indent*3}</div>\n"
274
+ end
275
+
276
+ def default_sender_address
277
+ invoke_address(get(:sender_details))
278
+ end
279
+
280
+ def default_recipient_address
281
+ invoke_address(get(:recipient_details))
282
+ end
283
+
284
+ def default_sender_tax_number(params)
285
+ sender_tax_number = get(:sender_details)[:tax_number]
286
+ "#{params[:tax_number_label]}<span class=\"tax-number\">#{h(sender_tax_number)}</span>"
287
+ end
288
+
289
+ def default_recipient_tax_number(params)
290
+ recipient_tax_number = get(:recipient_details)[:tax_number]
291
+ "#{params[:tax_number_label]}<span class=\"tax-number\">#{h(recipient_tax_number)}</span>"
292
+ end
293
+
294
+ def default_addresses_table(params)
295
+ html = "#{indent*0}<table class=\"invoice addresses\">\n"
296
+ html << "#{indent*1}<tr>\n"
297
+ html << "#{indent*2}<th class=\"recipient\">#{params[:recipient_label]}</th>\n"
298
+ html << "#{indent*2}<th class=\"sender\">#{params[:sender_label]}</th>\n"
299
+ html << "#{indent*1}</tr>\n"
300
+ html << "#{indent*1}<tr>\n"
301
+ html << "#{indent*2}<td class=\"recipient vcard\">\n#{params[:recipient_address]}"
302
+ html << "#{indent*2}</td>\n"
303
+ html << "#{indent*2}<td class=\"sender vcard\">\n#{params[:sender_address]}"
304
+ html << "#{indent*2}</td>\n"
305
+ html << "#{indent*1}</tr>\n"
306
+ html << "#{indent*1}<tr>\n"
307
+ html << "#{indent*2}<td class=\"recipient\">\n"
308
+ html << "#{indent*3}#{params[:recipient_tax_number]}\n"
309
+ html << "#{indent*2}</td>\n"
310
+ html << "#{indent*2}<td class=\"sender\">\n"
311
+ html << "#{indent*3}#{params[:sender_tax_number]}\n"
312
+ html << "#{indent*2}</td>\n"
313
+ html << "#{indent*1}</tr>\n"
314
+ html << "#{indent*0}</table>\n"
315
+ end
316
+
317
+ def default_metadata_item(params, key, value)
318
+ label = params["#{key}_label".to_sym]
319
+ html = "#{indent*1}<tr class=\"#{key.to_s.gsub(/_/, '-')}\">\n"
320
+ html << "#{indent*2}<th>#{label}</th>\n"
321
+ html << "#{indent*2}<td>#{h(value)}</td>\n"
322
+ html << "#{indent*1}</tr>\n"
323
+ end
324
+
325
+ def default_identifier(params)
326
+ invoke_metadata_item(params, :identifier, get(:identifier))
327
+ end
328
+
329
+ def default_issue_date(params)
330
+ if issue_date = get(:issue_date)
331
+ invoke_metadata_item(params, :issue_date, issue_date.strftime(params[:date_format]))
332
+ else
333
+ ""
334
+ end
335
+ end
336
+
337
+ def default_period_start(params)
338
+ if period_start = get(:period_start)
339
+ invoke_metadata_item(params, :period_start, period_start.strftime(params[:date_format]))
340
+ else
341
+ ""
342
+ end
343
+ end
344
+
345
+ def default_period_end(params)
346
+ if period_end = get(:period_end)
347
+ invoke_metadata_item(params, :period_end, period_end.strftime(params[:date_format]))
348
+ else
349
+ ""
350
+ end
351
+ end
352
+
353
+ def default_due_date(params)
354
+ if due_date = get(:due_date)
355
+ invoke_metadata_item(params, :due_date, due_date.strftime(params[:date_format]))
356
+ else
357
+ ""
358
+ end
359
+ end
360
+
361
+ def default_metadata_table(params)
362
+ "<table class=\"invoice metadata\">\n" + params[:identifier] + params[:issue_date] +
363
+ params[:period_start] + params[:period_end] + params[:due_date] + "#{indent*0}</table>\n"
364
+ end
365
+
366
+ def default_description
367
+ h(get(:description))
368
+ end
369
+
370
+ def default_description_tag(params)
371
+ "<p class=\"invoice description\">#{params[:description]}</p>\n"
372
+ end
373
+
374
+ def default_line_items_header(params)
375
+ html = "#{indent*1}<tr>\n"
376
+ html << "#{indent*2}<th class=\"tax-point\">#{ params[:line_tax_point_label] }</th>\n" if options[:tax_point_column]
377
+ html << "#{indent*2}<th class=\"quantity\">#{ params[:line_quantity_label] }</th>\n" if options[:quantity_column]
378
+ html << "#{indent*2}<th class=\"description\">#{ params[:line_description_label] }</th>\n" if options[:description_column]
379
+ html << "#{indent*2}<th class=\"net-amount\">#{ params[:line_net_amount_label] }</th>\n" if options[:net_amount_column]
380
+ html << "#{indent*2}<th class=\"tax-amount\">#{ params[:line_tax_amount_label] }</th>\n" if options[:tax_amount_column]
381
+ html << "#{indent*2}<th class=\"gross-amount\">#{params[:line_gross_amount_label]}</th>\n" if options[:gross_amount_column]
382
+ html << "#{indent*1}</tr>\n"
383
+ end
384
+
385
+ def default_line_tax_point(params)
386
+ if tax_point = line_get(:tax_point)
387
+ h(tax_point.strftime(params[:date_format]))
388
+ else
389
+ ""
390
+ end
391
+ end
392
+
393
+ def default_line_quantity(params)
394
+ h(line_get(:quantity).to_s)
395
+ end
396
+
397
+ def default_line_description(params)
398
+ h(line_get(:description))
399
+ end
400
+
401
+ def default_line_net_amount(params)
402
+ if net_amount = line_get(:net_amount)
403
+ h(current_line_item.format_currency_value(net_amount*factor))
404
+ else
405
+ "—"
406
+ end
407
+ end
408
+
409
+ def default_line_tax_amount(params)
410
+ if tax_amount = line_get(:tax_amount)
411
+ h(current_line_item.format_currency_value(tax_amount*factor))
412
+ else
413
+ "—"
414
+ end
415
+ end
416
+
417
+ def default_line_gross_amount(params)
418
+ if gross_amount = line_get(:gross_amount)
419
+ h(current_line_item.format_currency_value(gross_amount*factor))
420
+ else
421
+ "—"
422
+ end
423
+ end
424
+
425
+ def default_net_amount(params)
426
+ if net_amount = get(:net_amount)
427
+ h(ledger_item.format_currency_value(net_amount*factor))
428
+ else
429
+ "—"
430
+ end
431
+ end
432
+
433
+ def default_tax_amount(params)
434
+ if tax_amount = get(:tax_amount)
435
+ h(ledger_item.format_currency_value(tax_amount*factor))
436
+ else
437
+ "—"
438
+ end
439
+ end
440
+
441
+ def default_total_amount(params)
442
+ if total_amount = get(:total_amount)
443
+ h(ledger_item.format_currency_value(total_amount*factor))
444
+ else
445
+ "—"
446
+ end
447
+ end
448
+
449
+ def default_line_item(params)
450
+ html = "#{indent*1}<tr>\n"
451
+ html << "#{indent*2}<td class=\"tax-point\">#{ params[:line_tax_point] }</td>\n" if options[:tax_point_column]
452
+ html << "#{indent*2}<td class=\"quantity\">#{ params[:line_quantity] }</td>\n" if options[:quantity_column]
453
+ html << "#{indent*2}<td class=\"description\">#{ params[:line_description] }</td>\n" if options[:description_column]
454
+ html << "#{indent*2}<td class=\"net-amount\">#{ params[:line_net_amount] }</td>\n" if options[:net_amount_column]
455
+ html << "#{indent*2}<td class=\"tax-amount\">#{ params[:line_tax_amount] }</td>\n" if options[:tax_amount_column]
456
+ html << "#{indent*2}<td class=\"gross-amount\">#{params[:line_gross_amount]}</td>\n" if options[:gross_amount_column]
457
+ html << "#{indent*1}</tr>\n"
458
+ end
459
+
460
+ def default_line_items_list
461
+ get(:line_items).sorted(:tax_point).map do |line_item|
462
+ @current_line_item = line_item
463
+ invoke_line_item(params_hash(
464
+ :line_tax_point => [:line_tax_point_label, :date_format],
465
+ :line_quantity => [:line_quantity_label],
466
+ :line_description => [:line_description_label],
467
+ :line_net_amount => [:line_net_amount_label],
468
+ :line_tax_amount => [:line_tax_amount_label],
469
+ :line_gross_amount => [:line_gross_amount_label]
470
+ ))
471
+ end.join
472
+ end
473
+
474
+ def default_line_items_subtotal(params)
475
+ colspan = 0
476
+ colspan += 1 if options[:tax_point_column]
477
+ colspan += 1 if options[:quantity_column]
478
+ colspan += 1 if options[:description_column]
479
+ html = "#{indent*1}<tr class=\"subtotal\">\n"
480
+ html << "#{indent*2}<th colspan=\"#{colspan}\">#{params[:subtotal_label]}</th>\n"
481
+ html << "#{indent*2}<td class=\"net-amount\">#{params[:net_amount_label]}#{params[:net_amount]}</td>\n" if options[:net_amount_column]
482
+ html << "#{indent*2}<td class=\"tax-amount\">#{params[:tax_amount_label]}#{params[:tax_amount]}</td>\n" if options[:tax_amount_column]
483
+ html << "#{indent*2}<td class=\"gross-amount\"></td>\n" if options[:gross_amount_column]
484
+ html << "#{indent*1}</tr>\n"
485
+ end
486
+
487
+ def default_line_items_total(params)
488
+ colspan = -1
489
+ colspan += 1 if options[:tax_point_column]
490
+ colspan += 1 if options[:quantity_column]
491
+ colspan += 1 if options[:description_column]
492
+ colspan += 1 if options[:net_amount_column]
493
+ colspan += 1 if options[:tax_amount_column]
494
+ colspan += 1 if options[:gross_amount_column]
495
+ html = "#{indent*1}<tr class=\"total\">\n"
496
+ html << "#{indent*2}<th colspan=\"#{colspan}\">#{params[:total_label]}</th>\n"
497
+ html << "#{indent*2}<td class=\"total-amount\">#{params[:gross_amount_label]}#{params[:total_amount]}</td>\n"
498
+ html << "#{indent*1}</tr>\n"
499
+ end
500
+
501
+ def default_line_items_table(params)
502
+ html = "<table class=\"invoice line-items\">\n"
503
+ html << params[:line_items_header] + params[:line_items_list]
504
+ html << params[:line_items_subtotal] if options[:subtotal]
505
+ html << params[:line_items_total] + "</table>\n"
506
+ end
507
+
508
+ def default_page_layout(params)
509
+ params[:title_tag] + params[:addresses_table] + params[:metadata_table] +
510
+ params[:description_tag] + params[:line_items_table]
511
+ end
512
+ end
513
+ end
514
+ end
515
+ end