invoicing 0.1.0

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 (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,268 @@
1
+ require 'builder'
2
+
3
+ module Invoicing
4
+ module LedgerItem
5
+ # Included into ActiveRecord model object when +acts_as_ledger_item+ is invoked.
6
+ module RenderUBL
7
+
8
+ # Renders this invoice or credit note into a complete XML document conforming to the
9
+ # {OASIS Universal Business Language}[http://ubl.xml.org/] (UBL) open standard for interchange
10
+ # of business documents ({specification}[http://www.oasis-open.org/committees/ubl/]). This
11
+ # format, albeit a bit verbose, is increasingly being adopted as an international standard. It
12
+ # can represent some very complicated multi-currency, multi-party business relationships, but
13
+ # is still quite usable for simple cases.
14
+ #
15
+ # It is recommended that you present machine-readable UBL data in your application in the
16
+ # same way as you present human-readable invoices in HTML. For example, in a Rails controller,
17
+ # you could use:
18
+ #
19
+ # class AccountsController < ApplicationController
20
+ # def show
21
+ # @ledger_item = LedgerItem.find(params[:id])
22
+ # # ... check whether current user has access to this document ...
23
+ # respond_to do |format|
24
+ # format.html # show.html.erb
25
+ # format.xml { render :xml => @ledger_item.render_ubl }
26
+ # end
27
+ # end
28
+ # end
29
+ def render_ubl(options={})
30
+ UBLOutputBuilder.new(self, options).build
31
+ end
32
+
33
+
34
+ class UBLOutputBuilder #:nodoc:
35
+ # XML Namespaces required by UBL
36
+ UBL_NAMESPACES = {
37
+ "xmlns:cac" => "urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2",
38
+ "xmlns:cbc" => "urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2"
39
+ }
40
+
41
+ UBL_DOC_NAMESPACES = {
42
+ :Invoice => "urn:oasis:names:specification:ubl:schema:xsd:Invoice-2",
43
+ :SelfBilledInvoice => "urn:oasis:names:specification:ubl:schema:xsd:SelfBilledInvoice-2",
44
+ :CreditNote => "urn:oasis:names:specification:ubl:schema:xsd:CreditNote-2",
45
+ :SelfBilledCreditNote => "urn:oasis:names:specification:ubl:schema:xsd:SelfBilledCreditNote-2"
46
+ }
47
+
48
+ attr_reader :ledger_item, :options, :cached_values, :doc_type, :factor
49
+
50
+ def initialize(ledger_item, options)
51
+ @ledger_item = ledger_item
52
+ @options = options
53
+ @cached_values = {}
54
+ subtype = ledger_item.send(:ledger_item_class_info).subtype
55
+ @doc_type =
56
+ if [:invoice, :credit_note].include? subtype
57
+ if total_amount >= BigDecimal('0')
58
+ @factor = BigDecimal('1')
59
+ sender_details[:is_self] ? :Invoice : :SelfBilledInvoice
60
+ else
61
+ @factor = BigDecimal('-1')
62
+ sender_details[:is_self] ? :CreditNote : :SelfBilledCreditNote
63
+ end
64
+ else
65
+ raise RuntimeError, "render_ubl not implemented for ledger item subtype #{subtype.inspect}"
66
+ end
67
+ end
68
+
69
+ # For convenience while building the XML structure, method_missing redirects method calls
70
+ # to the ledger item (taking account of method renaming via acts_as_ledger_item options);
71
+ # calls to foo_of(line_item) are redirected to line_item.foo (taking account of method
72
+ # renaming via acts_as_line_item options).
73
+ def method_missing(method_id, *args, &block)
74
+ method_id = method_id.to_sym
75
+ if method_id.to_s =~ /^(.*)_of$/
76
+ method_id = $1.to_sym
77
+ line_item = args[0]
78
+ line_item.send(:line_item_class_info).get(line_item, method_id)
79
+ else
80
+ cached_values[method_id] ||= ledger_item.send(:ledger_item_class_info).get(ledger_item, method_id)
81
+ end
82
+ end
83
+
84
+ # Returns a UBL XML rendering of the ledger item previously passed to the constructor.
85
+ def build
86
+ ubl = Builder::XmlMarkup.new :indent => 4
87
+ ubl.instruct! :xml
88
+
89
+ ubl.ubl doc_type, UBL_NAMESPACES.clone.update({'xmlns:ubl' => UBL_DOC_NAMESPACES[doc_type]}) do |invoice|
90
+ invoice.cbc :ID, identifier
91
+ invoice.cbc :UUID, uuid if uuid
92
+
93
+ issue_date_formatted, issue_time_formatted = (issue_date || Time.now).in_time_zone.xmlschema.split('T')
94
+ invoice.cbc :IssueDate, issue_date_formatted
95
+ invoice.cbc :IssueTime, issue_time_formatted
96
+
97
+ # Different document types have the child elements InvoiceTypeCode, Note and
98
+ # TaxPointDate in a different order. WTF?!
99
+ if doc_type == :Invoice
100
+ invoice.cbc :InvoiceTypeCode, method_missing(:type)
101
+ invoice.cbc :Note, description
102
+ invoice.cbc :TaxPointDate, issue_date_formatted
103
+ else
104
+ invoice.cbc :TaxPointDate, issue_date_formatted
105
+ invoice.cbc :InvoiceTypeCode, method_missing(:type) if doc_type == :SelfBilledInvoice
106
+ invoice.cbc :Note, description
107
+ end
108
+
109
+ invoice.cac :InvoicePeriod do |invoice_period|
110
+ build_period(invoice_period, period_start, period_end)
111
+ end if period_start && period_end
112
+
113
+ if [:Invoice, :CreditNote].include?(doc_type)
114
+
115
+ invoice.cac :AccountingSupplierParty do |supplier|
116
+ build_party supplier, sender_details
117
+ end
118
+ invoice.cac :AccountingCustomerParty do |customer|
119
+ customer.cbc :SupplierAssignedAccountID, recipient_id
120
+ build_party customer, recipient_details
121
+ end
122
+
123
+ elsif [:SelfBilledInvoice, :SelfBilledCreditNote].include?(doc_type)
124
+
125
+ invoice.cac :AccountingCustomerParty do |customer|
126
+ build_party customer, recipient_details
127
+ end
128
+ invoice.cac :AccountingSupplierParty do |supplier|
129
+ supplier.cbc :CustomerAssignedAccountID, sender_id
130
+ build_party supplier, sender_details
131
+ end
132
+
133
+ end
134
+
135
+ invoice.cac :PaymentTerms do |payment_terms|
136
+ payment_terms.cac :SettlementPeriod do |settlement_period|
137
+ build_period(settlement_period, issue_date || Time.now, due_date)
138
+ end
139
+ end if due_date && [:Invoice, :SelfBilledInvoice].include?(doc_type)
140
+
141
+ invoice.cac :TaxTotal do |tax_total|
142
+ tax_total.cbc :TaxAmount, (factor*tax_amount).to_s, :currencyID => currency
143
+ end if tax_amount
144
+
145
+ invoice.cac :LegalMonetaryTotal do |monetary_total|
146
+ monetary_total.cbc :TaxExclusiveAmount, (factor*(total_amount - tax_amount)).to_s,
147
+ :currencyID => currency if tax_amount
148
+ monetary_total.cbc :PayableAmount, (factor*total_amount).to_s, :currencyID => currency
149
+ end
150
+
151
+ line_items.sorted(:tax_point).each do |line_item|
152
+ line_tag = if [:CreditNote, :SelfBilledCreditNote].include? doc_type
153
+ :CreditNoteLine
154
+ else
155
+ :InvoiceLine
156
+ end
157
+
158
+ invoice.cac line_tag do |invoice_line|
159
+ build_line_item(invoice_line, line_item)
160
+ end
161
+ end
162
+ end
163
+ ubl.target!
164
+ end
165
+
166
+
167
+ # Given a <tt>Builder::XmlMarkup</tt> instance and two datetime objects, builds a UBL
168
+ # representation of the period between the two dates and times, something like the
169
+ # following:
170
+ #
171
+ # <cbc:StartDate>2008-05-06</cbc:StartTime>
172
+ # <cbc:StartTime>12:34:56+02:00</cbc:StartTime>
173
+ # <cbc:EndDate>2008-07-02</cbc:EndDate>
174
+ # <cbc:EndTime>01:02:03+02:00</cbc:EndTime>
175
+ def build_period(xml, start_datetime, end_datetime)
176
+ start_date, start_time = start_datetime.in_time_zone.xmlschema.split('T')
177
+ end_date, end_time = end_datetime.in_time_zone.xmlschema.split('T')
178
+ xml.cbc :StartDate, start_date
179
+ xml.cbc :StartTime, start_time
180
+ xml.cbc :EndDate, end_date
181
+ xml.cbc :EndTime, end_time
182
+ end
183
+
184
+
185
+ # Given a <tt>Builder::XmlMarkup</tt> instance and a supplier/customer details hash (as
186
+ # returned by <tt>LedgerItem#sender_details</tt> and <tt>LedgerItem#recipient_details</tt>,
187
+ # builds a UBL representation of that party, something like the following:
188
+ #
189
+ # <cac:Party>
190
+ # <cac:PartyName>
191
+ # <cbc:Name>The Big Bank</cbc:Name>
192
+ # </cac:PartyName>
193
+ # <cac:PostalAddress>
194
+ # <cbc:StreetName>Paved With Gold Street</cbc:StreetName>
195
+ # <cbc:CityName>London</cbc:CityName>
196
+ # <cbc:PostalZone>E14 5HQ</cbc:PostalZone>
197
+ # <cac:Country><cbc:IdentificationCode>GB</cbc:IdentificationCode></cac:Country>
198
+ # </cac:PostalAddress>
199
+ # </cac:Party>
200
+ def build_party(xml, details)
201
+ xml.cac :Party do |party|
202
+ party.cac :PartyName do |party_name|
203
+ party_name.cbc :Name, details[:name]
204
+ end if details[:name]
205
+
206
+ party.cac :PostalAddress do |postal_address|
207
+ street1, street2 = details[:address].strip.split("\n", 2)
208
+ postal_address.cbc :StreetName, street1 if street1
209
+ postal_address.cbc :AdditionalStreetName, street2 if street2
210
+ postal_address.cbc :CityName, details[:city] if details[:city]
211
+ postal_address.cbc :PostalZone, details[:postal_code] if details[:postal_code]
212
+ postal_address.cbc :CountrySubentity, details[:state] if details[:state]
213
+ postal_address.cac :Country do |country|
214
+ country.cbc :IdentificationCode, details[:country_code] if details[:country_code]
215
+ country.cbc :Name, details[:country] if details[:country]
216
+ end if details[:country_code] || details[:country]
217
+ end
218
+
219
+ party.cac :PartyTaxScheme do |party_tax_scheme|
220
+ party_tax_scheme.cbc :CompanyID, details[:tax_number]
221
+ party_tax_scheme.cac :TaxScheme do |tax_scheme|
222
+ tax_scheme.cbc :ID, "VAT" # TODO: make country-dependent (e.g. GST in Australia)
223
+ end
224
+ end if details[:tax_number]
225
+
226
+ party.cac :Contact do |contact|
227
+ contact.cbc :Name, details[:contact_name]
228
+ end if details[:contact_name]
229
+ end
230
+ end
231
+
232
+
233
+ # Given a <tt>Builder::XmlMarkup</tt> instance and a +LineItem+ instance, builds a UBL
234
+ # representation of that line item, something like the following:
235
+ #
236
+ # <cbc:ID>123</cbc:ID>
237
+ # <cbc:UUID>0cc659f0-cfac-012b-481d-0017f22d32c0</cbc:UUID>
238
+ # <cbc:InvoicedQuantity>1</cbc:InvoicedQuantity>
239
+ # <cbc:LineExtensionAmount currencyID="GBP">123.45</cbc:LineExtensionAmount>
240
+ # <cbc:TaxPointDate>2009-01-01</cbc:TaxPointDate>
241
+ # <cac:TaxTotal><cbc:TaxAmount currencyID="GBP">12.34</cbc:TaxAmount></cac:TaxTotal>
242
+ # <cac:Item><cbc:Description>Foo bar baz</cbc:Description></cac:Item>
243
+ def build_line_item(invoice_line, line_item)
244
+ invoice_line.cbc :ID, id_of(line_item)
245
+ invoice_line.cbc :UUID, uuid_of(line_item) if uuid_of(line_item)
246
+ quantity_tag = [:Invoice, :SelfBilledInvoice].include?(doc_type) ? :InvoicedQuantity : :CreditedQuantity
247
+ invoice_line.cbc quantity_tag, quantity_of(line_item) if quantity_of(line_item)
248
+ invoice_line.cbc :LineExtensionAmount, (factor*net_amount_of(line_item)).to_s, :currencyID => currency
249
+ invoice_line.cbc :TaxPointDate, tax_point_of(line_item).in_time_zone.strftime('%Y-%m-%d') if tax_point_of(line_item)
250
+
251
+ invoice_line.cac :TaxTotal do |tax_total|
252
+ tax_total.cbc :TaxAmount, (factor*tax_amount_of(line_item)).to_s, :currencyID => currency
253
+ end if tax_amount_of(line_item)
254
+
255
+ invoice_line.cac :Item do |item|
256
+ item.cbc :Description, description_of(line_item)
257
+ #cac:BuyersItemIdentification
258
+ #cac:SellersItemIdentification
259
+ #cac:ClassifiedTaxCategory
260
+ #cac:ItemInstance
261
+ end
262
+
263
+ #cac:Price
264
+ end
265
+ end
266
+ end
267
+ end
268
+ end
@@ -0,0 +1,246 @@
1
+ module Invoicing
2
+ # = Line item objects
3
+ #
4
+ # A line item is a single charge on an invoice or credit note, for example representing the sale
5
+ # of one particular product. An invoice or credit note with a non-zero +total_amount+ must have at
6
+ # least one +LineItem+ object associated with it, and its +total_amount+ must equal the sum of the
7
+ # +net_amount+ and +tax_amount+ values of all +LineItem+ objects associated with it. For details
8
+ # on invoices and credit notes, see the +LedgerItem+ module.
9
+ #
10
+ # Many of the important principles set down in the +LedgerItem+ module also apply for line items;
11
+ # for example, once you have created a line item you generally shouldn't change it again. If you
12
+ # need to correct a mistake, create an additional line item of the same type but a negative value.
13
+ #
14
+ # == Using +LineItem+
15
+ #
16
+ # In all likelihood you will have different types of charges which you need to make to your customers.
17
+ # We store all those different types of line item in the same database table and use ActiveRecord's
18
+ # single table inheritance to build a class hierarchy. You must create at least one line item
19
+ # model class in your application, like this:
20
+ #
21
+ # class LineItem < ActiveRecord::Base
22
+ # acts_as_line_item
23
+ # belongs_to :ledger_item
24
+ # end
25
+ #
26
+ # You may then create a class hierarchy to suit your needs, for example:
27
+ #
28
+ # class ProductSale < LineItem
29
+ # belongs_to :product
30
+ #
31
+ # def description
32
+ # product.title
33
+ # end
34
+ # end
35
+ #
36
+ # class ShippingCharges < LineItem
37
+ # def description
38
+ # "Shipping charges"
39
+ # end
40
+ # end
41
+ #
42
+ # You may associate line items of any type with credit notes and invoices interchangeably. This
43
+ # means, for example, that if you overcharge a customer for shipping, you can send them a credit
44
+ # note with a +ShippingCharges+ line item, thus making it explicit what it is you are refunding.
45
+ # On a credit note/refund the line item's +net_amount+ and +tax_amount+ should be negative.
46
+ # +Payment+ records usually do not have any associated line items.
47
+ #
48
+ # == Required methods/database columns
49
+ #
50
+ # The following methods/database columns are <b>required</b> for +LineItem+ objects (you may give them
51
+ # different names, but then you need to tell +acts_as_line_item+ about your custom names):
52
+ #
53
+ # +type+::
54
+ # String to store the class name, for ActiveRecord single table inheritance.
55
+ #
56
+ # +ledger_item+::
57
+ # You should define an association <tt>belongs_to :ledger_item, ...</tt> which returns the
58
+ # +LedgerItem+ object (invoice/credit note) to which this line item belongs.
59
+ #
60
+ # +ledger_item_id+::
61
+ # A foreign key of integer type, which stores the ID of the model object returned by the
62
+ # +ledger_item+ association.
63
+ #
64
+ # +net_amount+::
65
+ # A decimal column containing the monetary amount charged by this line item, not including tax.
66
+ # The value is typically positive on an invoice and negative on a credit note. The currency is
67
+ # not explicitly specified on the line item, but is taken to be the currency of the invoice or
68
+ # credit note to which it belongs. (This is deliberate, because you mustn't mix different
69
+ # currencies within one invoice.) See the documentation of the +CurrencyValue+ module for notes
70
+ # on suitable datatypes for monetary values. +acts_as_currency_value+ is automatically applied
71
+ # to this attribute.
72
+ #
73
+ # +tax_amount+::
74
+ # A decimal column containing the monetary amount of tax which is added to +net_amount+ to
75
+ # obtain the total price. This may of course be zero if no tax applies; otherwise it should have
76
+ # the same sign as +net_amount+. +CurrencyValue+ applies as with +net_amount+. If you have
77
+ # several different taxes being applied, please check with your accountant. We suggest that you
78
+ # put VAT or sales tax in this +tax_amount+ column, and any other taxes (e.g. duty on alcohol or
79
+ # tobacco, or separate state/city taxes) in separate line items. If you are not obliged to pay
80
+ # tax, lucky you -- put zeroes in this column and await the day when you have enough business
81
+ # that you *do* have to pay tax.
82
+ #
83
+ # +description+::
84
+ # A method which returns a short string explaining to your user what this line item is for.
85
+ # Can be a database column but doesn't have to be.
86
+ #
87
+ # == Optional methods/database columns
88
+ #
89
+ # The following methods/database columns are <b>optional, but recommended</b> for +LineItem+ objects:
90
+ #
91
+ # +uuid+::
92
+ # A Universally Unique Identifier (UUID)[http://en.wikipedia.org/wiki/UUID] string for this line item.
93
+ # It may seem unnecessary now, but may help you to keep track of your data later on as your system
94
+ # grows. If you have the +uuid+ gem installed and this column is present, a UUID is automatically
95
+ # generated when you create a new line item.
96
+ #
97
+ # +tax_point+::
98
+ # A datetime column which indicates the date on which the sale is made and/or the service is provided.
99
+ # It is related to the +issue_date+ on the associated invoice/credit note, but does not necessarily
100
+ # have the same value. The exact technicalities will vary by jurisdiction, but generally this is the
101
+ # point in time which determines into which month or which tax period you count a sale. The value may
102
+ # be the same as +created_at+ or +updated_at+, but not necessarily.
103
+ #
104
+ # +tax_rate_id+, +tax_rate+::
105
+ # +tax_rate_id+ is a foreign key of integer type, and +tax_rate+ is a +belongs_to+ association
106
+ # based on it. It refers to another model in your application which represents the tax rate
107
+ # applied to this line item. The tax rate model object should use +acts_as_tax_rate+. This
108
+ # attribute is necessary if you want tax calculations to be performed automatically.
109
+ #
110
+ # +price_id+, +price+::
111
+ # +price_id+ is a foreign key of integer type, and +price+ is a +belongs_to+ association based
112
+ # on it. It refers to another model in your application which represents the unit price (e.g. a
113
+ # reference to a the product, or to a particular price band of a service). The model object thus
114
+ # referred to should use +acts_as_price+. This attribute allows you to get better reports of how
115
+ # much you sold of what.
116
+ #
117
+ # +quantity+::
118
+ # A numeric (integer or decimal) type, saying how many units of a particular product or service
119
+ # this line item represents. Default is 1. Note that if you specify a +quantity+, the values for
120
+ # +net_amount+ and +tax_amount+ must be the cost of the given quantity as a whole; if you need
121
+ # to display the unit price, you can get it by dividing +net_amount+ by +quantity+, or by
122
+ # referring to the +price+ association.
123
+ #
124
+ # +creator_id+::
125
+ # The ID of the user whose action caused this line item to be created or updated. This can be useful
126
+ # for audit trail purposes, particularly if you allow multiple users of your application to act on
127
+ # behalf of the same customer organisation.
128
+ #
129
+ # +created_at+, +updated_at+::
130
+ # These standard datetime columns are also recommended.
131
+ #
132
+ module LineItem
133
+ module ActMethods
134
+ # Declares that the current class is a model for line items (i.e. individual items on invoices
135
+ # and credit notes).
136
+ #
137
+ # The name of any attribute or method required by +LineItem+ (as documented on the
138
+ # +LineItem+ module) may be used as an option, with the value being the name under which
139
+ # that particular method or attribute can be found. This allows you to use names other than
140
+ # the defaults. For example, if your database column storing the line item value is called
141
+ # +net_price+ instead of +net_amount+:
142
+ #
143
+ # acts_as_line_item :net_amount => :net_price
144
+ def acts_as_line_item(*args)
145
+ Invoicing::ClassInfo.acts_as(Invoicing::LineItem, self, args)
146
+
147
+ info = line_item_class_info
148
+ if info.previous_info.nil? # Called for the first time?
149
+ # Set the 'amount' columns to act as currency values
150
+ acts_as_currency_value(info.method(:net_amount), info.method(:tax_amount))
151
+
152
+ extend Invoicing::FindSubclasses
153
+
154
+ # Dynamically created named scopes
155
+ named_scope :in_effect, lambda{
156
+ ledger_assoc_id = line_item_class_info.method(:ledger_item).to_sym
157
+ ledger_refl = reflections[ledger_assoc_id]
158
+ ledger_table = ledger_refl.table_name # not quoted_table_name because it'll be quoted again
159
+ status_column = ledger_refl.klass.send(:ledger_item_class_info).method(:status)
160
+ { :joins => ledger_assoc_id,
161
+ :conditions => {"#{ledger_table}.#{status_column}" => ['closed', 'cleared'] } }
162
+ }
163
+
164
+ named_scope :sorted, lambda{|column|
165
+ column = line_item_class_info.method(column).to_s
166
+ if column_names.include?(column)
167
+ {:order => "#{connection.quote_column_name(column)}, #{connection.quote_column_name(primary_key)}"}
168
+ else
169
+ {:order => connection.quote_column_name(primary_key)}
170
+ end
171
+ }
172
+ end
173
+ end
174
+ end
175
+
176
+ # Overrides the default constructor of <tt>ActiveRecord::Base</tt> when +acts_as_line_item+
177
+ # is called. If the +uuid+ gem is installed, this constructor creates a new UUID and assigns
178
+ # it to the +uuid+ property when a new line item model object is created.
179
+ def initialize(*args)
180
+ super
181
+ # Initialise uuid attribute if possible
182
+ info = line_item_class_info
183
+ if self.has_attribute?(info.method(:uuid)) && info.uuid_generator
184
+ write_attribute(info.method(:uuid), info.uuid_generator.generate)
185
+ end
186
+ end
187
+
188
+ # Returns the currency code of the ledger item to which this line item belongs.
189
+ def currency
190
+ ledger_item = line_item_class_info.get(self, :ledger_item)
191
+ raise RuntimeError, 'Cannot determine currency for line item without a ledger item' if ledger_item.nil?
192
+ ledger_item.send(:ledger_item_class_info).get(ledger_item, :currency)
193
+ end
194
+
195
+ # The sum of +net_amount+ and +tax_amount+.
196
+ def gross_amount
197
+ net_amount = line_item_class_info.get(self, :net_amount)
198
+ tax_amount = line_item_class_info.get(self, :tax_amount)
199
+ (net_amount && tax_amount) ? (net_amount + tax_amount) : nil
200
+ end
201
+
202
+ # +gross_amount+ formatted in human-readable form using the line item's currency.
203
+ def gross_amount_formatted
204
+ format_currency_value(gross_amount)
205
+ end
206
+
207
+ # We don't actually implement anything using +method_missing+ at the moment, but use it to
208
+ # generate slightly more useful error messages in certain cases.
209
+ def method_missing(method_id, *args)
210
+ method_name = method_id.to_s
211
+ if ['ledger_item', line_item_class_info.method(:ledger_item)].include? method_name
212
+ raise RuntimeError, "You need to define an association like 'belongs_to :ledger_item' on #{self.class.name}. If you " +
213
+ "have defined the association with a different name, pass the option :ledger_item => :your_association_name to " +
214
+ "acts_as_line_item."
215
+ else
216
+ super
217
+ end
218
+ end
219
+
220
+
221
+ # Stores state in the ActiveRecord class object
222
+ class ClassInfo < Invoicing::ClassInfo::Base #:nodoc:
223
+ attr_reader :uuid_generator
224
+
225
+ def initialize(model_class, previous_info, args)
226
+ super
227
+
228
+ begin # try to load the UUID gem
229
+ require 'uuid'
230
+ @uuid_generator = UUID.new
231
+ rescue LoadError, NameError # silently ignore if gem not found
232
+ @uuid_generator = nil
233
+ end
234
+ end
235
+
236
+ # Allow methods generated by +CurrencyValue+ to be renamed as well
237
+ def method(name)
238
+ if name.to_s =~ /^(.*)_formatted$/
239
+ "#{super($1)}_formatted"
240
+ else
241
+ super
242
+ end
243
+ end
244
+ end
245
+ end
246
+ end