invoicing 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG +3 -0
- data/LICENSE +20 -0
- data/Manifest +60 -0
- data/README +48 -0
- data/Rakefile +75 -0
- data/invoicing.gemspec +41 -0
- data/lib/invoicing.rb +9 -0
- data/lib/invoicing/cached_record.rb +107 -0
- data/lib/invoicing/class_info.rb +187 -0
- data/lib/invoicing/connection_adapter_ext.rb +44 -0
- data/lib/invoicing/countries/uk.rb +24 -0
- data/lib/invoicing/currency_value.rb +212 -0
- data/lib/invoicing/find_subclasses.rb +193 -0
- data/lib/invoicing/ledger_item.rb +718 -0
- data/lib/invoicing/ledger_item/render_html.rb +515 -0
- data/lib/invoicing/ledger_item/render_ubl.rb +268 -0
- data/lib/invoicing/line_item.rb +246 -0
- data/lib/invoicing/price.rb +9 -0
- data/lib/invoicing/tax_rate.rb +9 -0
- data/lib/invoicing/taxable.rb +355 -0
- data/lib/invoicing/time_dependent.rb +388 -0
- data/lib/invoicing/version.rb +21 -0
- data/test/cached_record_test.rb +100 -0
- data/test/class_info_test.rb +253 -0
- data/test/connection_adapter_ext_test.rb +71 -0
- data/test/currency_value_test.rb +184 -0
- data/test/find_subclasses_test.rb +120 -0
- data/test/fixtures/README +7 -0
- data/test/fixtures/cached_record.sql +22 -0
- data/test/fixtures/class_info.sql +28 -0
- data/test/fixtures/currency_value.sql +29 -0
- data/test/fixtures/find_subclasses.sql +43 -0
- data/test/fixtures/ledger_item.sql +39 -0
- data/test/fixtures/line_item.sql +33 -0
- data/test/fixtures/price.sql +4 -0
- data/test/fixtures/tax_rate.sql +4 -0
- data/test/fixtures/taxable.sql +14 -0
- data/test/fixtures/time_dependent.sql +35 -0
- data/test/ledger_item_test.rb +352 -0
- data/test/line_item_test.rb +139 -0
- data/test/models/README +4 -0
- data/test/models/test_subclass_in_another_file.rb +3 -0
- data/test/models/test_subclass_not_in_database.rb +6 -0
- data/test/price_test.rb +9 -0
- data/test/ref-output/creditnote3.html +82 -0
- data/test/ref-output/creditnote3.xml +89 -0
- data/test/ref-output/invoice1.html +93 -0
- data/test/ref-output/invoice1.xml +111 -0
- data/test/ref-output/invoice2.html +86 -0
- data/test/ref-output/invoice2.xml +98 -0
- data/test/ref-output/invoice_null.html +36 -0
- data/test/render_html_test.rb +69 -0
- data/test/render_ubl_test.rb +32 -0
- data/test/setup.rb +37 -0
- data/test/tax_rate_test.rb +9 -0
- data/test/taxable_test.rb +180 -0
- data/test/test_helper.rb +48 -0
- data/test/time_dependent_test.rb +180 -0
- data/website/curvycorners.js +1 -0
- data/website/screen.css +149 -0
- data/website/template.html.erb +43 -0
- 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
|