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