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,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