invoicing 0.2.1 → 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/LICENSE +1 -0
- data/README.md +57 -0
- data/Rakefile +16 -37
- data/lib/invoicing.rb +20 -10
- data/lib/invoicing/cached_record.rb +9 -6
- data/lib/invoicing/class_info.rb +34 -34
- data/lib/invoicing/connection_adapter_ext.rb +4 -4
- data/lib/invoicing/countries/uk.rb +6 -6
- data/lib/invoicing/currency_value.rb +39 -32
- data/lib/invoicing/find_subclasses.rb +40 -15
- data/lib/invoicing/ledger_item.rb +166 -145
- data/lib/invoicing/ledger_item/pdf_generator.rb +108 -0
- data/lib/invoicing/ledger_item/render_html.rb +76 -73
- data/lib/invoicing/ledger_item/render_ubl.rb +37 -35
- data/lib/invoicing/line_item.rb +43 -38
- data/lib/invoicing/price.rb +1 -1
- data/lib/invoicing/tax_rate.rb +3 -6
- data/lib/invoicing/taxable.rb +37 -32
- data/lib/invoicing/time_dependent.rb +40 -40
- data/lib/invoicing/version.rb +4 -4
- data/lib/rails/generators/invoicing/invoicing_generator.rb +14 -0
- data/lib/rails/generators/invoicing/ledger_item/ledger_item_generator.rb +17 -0
- data/lib/rails/generators/invoicing/ledger_item/templates/migration.rb +25 -0
- data/lib/rails/generators/invoicing/ledger_item/templates/model.rb +5 -0
- data/lib/rails/generators/invoicing/line_item/line_item_generator.rb +17 -0
- data/lib/rails/generators/invoicing/line_item/templates/migration.rb +20 -0
- data/lib/rails/generators/invoicing/line_item/templates/model.rb +5 -0
- data/lib/rails/generators/invoicing/tax_rate/tax_rate_generator.rb +17 -0
- data/lib/rails/generators/invoicing/tax_rate/templates/migration.rb +14 -0
- data/lib/rails/generators/invoicing/tax_rate/templates/model.rb +3 -0
- metadata +110 -153
- data.tar.gz.sig +0 -1
- data/History.txt +0 -31
- data/Manifest.txt +0 -62
- data/PostInstall.txt +0 -10
- data/README.rdoc +0 -58
- data/script/console +0 -10
- data/script/destroy +0 -14
- data/script/generate +0 -14
- data/tasks/rcov.rake +0 -4
- data/test/cached_record_test.rb +0 -100
- data/test/class_info_test.rb +0 -253
- data/test/connection_adapter_ext_test.rb +0 -79
- data/test/currency_value_test.rb +0 -209
- data/test/find_subclasses_test.rb +0 -120
- data/test/fixtures/README +0 -7
- data/test/fixtures/cached_record.sql +0 -22
- data/test/fixtures/class_info.sql +0 -28
- data/test/fixtures/currency_value.sql +0 -29
- data/test/fixtures/find_subclasses.sql +0 -43
- data/test/fixtures/ledger_item.sql +0 -39
- data/test/fixtures/line_item.sql +0 -33
- data/test/fixtures/price.sql +0 -4
- data/test/fixtures/tax_rate.sql +0 -4
- data/test/fixtures/taxable.sql +0 -14
- data/test/fixtures/time_dependent.sql +0 -35
- data/test/ledger_item_test.rb +0 -444
- data/test/line_item_test.rb +0 -139
- data/test/models/README +0 -4
- data/test/models/test_subclass_in_another_file.rb +0 -3
- data/test/models/test_subclass_not_in_database.rb +0 -6
- data/test/price_test.rb +0 -9
- data/test/ref-output/creditnote3.html +0 -82
- data/test/ref-output/creditnote3.xml +0 -89
- data/test/ref-output/invoice1.html +0 -93
- data/test/ref-output/invoice1.xml +0 -111
- data/test/ref-output/invoice2.html +0 -86
- data/test/ref-output/invoice2.xml +0 -98
- data/test/ref-output/invoice_null.html +0 -36
- data/test/render_html_test.rb +0 -70
- data/test/render_ubl_test.rb +0 -44
- data/test/setup.rb +0 -37
- data/test/tax_rate_test.rb +0 -9
- data/test/taxable_test.rb +0 -180
- data/test/test_helper.rb +0 -72
- data/test/time_dependent_test.rb +0 -180
- metadata.gz.sig +0 -4
@@ -1,10 +1,12 @@
|
|
1
|
+
require "active_support/concern"
|
1
2
|
require 'builder'
|
2
3
|
|
3
4
|
module Invoicing
|
4
5
|
module LedgerItem
|
5
6
|
# Included into ActiveRecord model object when +acts_as_ledger_item+ is invoked.
|
6
7
|
module RenderUBL
|
7
|
-
|
8
|
+
extend ActiveSupport::Concern
|
9
|
+
|
8
10
|
# Renders this invoice or credit note into a complete XML document conforming to the
|
9
11
|
# {OASIS Universal Business Language}[http://ubl.xml.org/] (UBL) open standard for interchange
|
10
12
|
# of business documents ({specification}[http://www.oasis-open.org/committees/ubl/]). This
|
@@ -29,30 +31,30 @@ module Invoicing
|
|
29
31
|
def render_ubl(options={})
|
30
32
|
UBLOutputBuilder.new(self, options).build
|
31
33
|
end
|
32
|
-
|
33
|
-
|
34
|
+
|
35
|
+
|
34
36
|
class UBLOutputBuilder #:nodoc:
|
35
37
|
# XML Namespaces required by UBL
|
36
38
|
UBL_NAMESPACES = {
|
37
39
|
"xmlns:cac" => "urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2",
|
38
40
|
"xmlns:cbc" => "urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2"
|
39
41
|
}
|
40
|
-
|
42
|
+
|
41
43
|
UBL_DOC_NAMESPACES = {
|
42
44
|
:Invoice => "urn:oasis:names:specification:ubl:schema:xsd:Invoice-2",
|
43
45
|
:SelfBilledInvoice => "urn:oasis:names:specification:ubl:schema:xsd:SelfBilledInvoice-2",
|
44
46
|
:CreditNote => "urn:oasis:names:specification:ubl:schema:xsd:CreditNote-2",
|
45
47
|
:SelfBilledCreditNote => "urn:oasis:names:specification:ubl:schema:xsd:SelfBilledCreditNote-2"
|
46
48
|
}
|
47
|
-
|
49
|
+
|
48
50
|
attr_reader :ledger_item, :options, :cached_values, :doc_type, :factor
|
49
|
-
|
51
|
+
|
50
52
|
def initialize(ledger_item, options)
|
51
53
|
@ledger_item = ledger_item
|
52
54
|
@options = options
|
53
55
|
@cached_values = {}
|
54
56
|
subtype = ledger_item.send(:ledger_item_class_info).subtype
|
55
|
-
@doc_type =
|
57
|
+
@doc_type =
|
56
58
|
if [:invoice, :credit_note].include? subtype
|
57
59
|
if total_amount >= BigDecimal('0')
|
58
60
|
@factor = BigDecimal('1')
|
@@ -65,7 +67,7 @@ module Invoicing
|
|
65
67
|
raise RuntimeError, "render_ubl not implemented for ledger item subtype #{subtype.inspect}"
|
66
68
|
end
|
67
69
|
end
|
68
|
-
|
70
|
+
|
69
71
|
# For convenience while building the XML structure, method_missing redirects method calls
|
70
72
|
# to the ledger item (taking account of method renaming via acts_as_ledger_item options);
|
71
73
|
# calls to foo_of(line_item) are redirected to line_item.foo (taking account of method
|
@@ -80,20 +82,20 @@ module Invoicing
|
|
80
82
|
cached_values[method_id] ||= ledger_item.send(:ledger_item_class_info).get(ledger_item, method_id)
|
81
83
|
end
|
82
84
|
end
|
83
|
-
|
85
|
+
|
84
86
|
# Returns a UBL XML rendering of the ledger item previously passed to the constructor.
|
85
87
|
def build
|
86
88
|
ubl = Builder::XmlMarkup.new :indent => 4
|
87
89
|
ubl.instruct! :xml
|
88
|
-
|
90
|
+
|
89
91
|
ubl.ubl doc_type, UBL_NAMESPACES.clone.update({'xmlns:ubl' => UBL_DOC_NAMESPACES[doc_type]}) do |invoice|
|
90
92
|
invoice.cbc :ID, identifier
|
91
93
|
invoice.cbc :UUID, uuid if uuid
|
92
|
-
|
94
|
+
|
93
95
|
issue_date_formatted, issue_time_formatted = date_and_time(issue_date || Time.now)
|
94
96
|
invoice.cbc :IssueDate, issue_date_formatted
|
95
97
|
invoice.cbc :IssueTime, issue_time_formatted
|
96
|
-
|
98
|
+
|
97
99
|
# Different document types have the child elements InvoiceTypeCode, Note and
|
98
100
|
# TaxPointDate in a different order. WTF?!
|
99
101
|
if doc_type == :Invoice
|
@@ -105,13 +107,13 @@ module Invoicing
|
|
105
107
|
invoice.cbc :InvoiceTypeCode, method_missing(:type) if doc_type == :SelfBilledInvoice
|
106
108
|
invoice.cbc :Note, description
|
107
109
|
end
|
108
|
-
|
110
|
+
|
109
111
|
invoice.cac :InvoicePeriod do |invoice_period|
|
110
112
|
build_period(invoice_period, period_start, period_end)
|
111
113
|
end if period_start && period_end
|
112
|
-
|
114
|
+
|
113
115
|
if [:Invoice, :CreditNote].include?(doc_type)
|
114
|
-
|
116
|
+
|
115
117
|
invoice.cac :AccountingSupplierParty do |supplier|
|
116
118
|
build_party supplier, sender_details
|
117
119
|
end
|
@@ -119,9 +121,9 @@ module Invoicing
|
|
119
121
|
customer.cbc :SupplierAssignedAccountID, recipient_id
|
120
122
|
build_party customer, recipient_details
|
121
123
|
end
|
122
|
-
|
124
|
+
|
123
125
|
elsif [:SelfBilledInvoice, :SelfBilledCreditNote].include?(doc_type)
|
124
|
-
|
126
|
+
|
125
127
|
invoice.cac :AccountingCustomerParty do |customer|
|
126
128
|
build_party customer, recipient_details
|
127
129
|
end
|
@@ -129,32 +131,32 @@ module Invoicing
|
|
129
131
|
supplier.cbc :CustomerAssignedAccountID, sender_id
|
130
132
|
build_party supplier, sender_details
|
131
133
|
end
|
132
|
-
|
134
|
+
|
133
135
|
end
|
134
|
-
|
136
|
+
|
135
137
|
invoice.cac :PaymentTerms do |payment_terms|
|
136
138
|
payment_terms.cac :SettlementPeriod do |settlement_period|
|
137
139
|
build_period(settlement_period, issue_date || Time.now, due_date)
|
138
140
|
end
|
139
141
|
end if due_date && [:Invoice, :SelfBilledInvoice].include?(doc_type)
|
140
|
-
|
142
|
+
|
141
143
|
invoice.cac :TaxTotal do |tax_total|
|
142
144
|
tax_total.cbc :TaxAmount, (factor*tax_amount).to_s, :currencyID => currency
|
143
145
|
end if tax_amount
|
144
|
-
|
146
|
+
|
145
147
|
invoice.cac :LegalMonetaryTotal do |monetary_total|
|
146
148
|
monetary_total.cbc :TaxExclusiveAmount, (factor*(total_amount - tax_amount)).to_s,
|
147
149
|
:currencyID => currency if tax_amount
|
148
150
|
monetary_total.cbc :PayableAmount, (factor*total_amount).to_s, :currencyID => currency
|
149
151
|
end
|
150
|
-
|
152
|
+
|
151
153
|
line_items.sorted(:tax_point).each do |line_item|
|
152
154
|
line_tag = if [:CreditNote, :SelfBilledCreditNote].include? doc_type
|
153
155
|
:CreditNoteLine
|
154
156
|
else
|
155
157
|
:InvoiceLine
|
156
158
|
end
|
157
|
-
|
159
|
+
|
158
160
|
invoice.cac line_tag do |invoice_line|
|
159
161
|
build_line_item(invoice_line, line_item)
|
160
162
|
end
|
@@ -162,8 +164,8 @@ module Invoicing
|
|
162
164
|
end
|
163
165
|
ubl.target!
|
164
166
|
end
|
165
|
-
|
166
|
-
|
167
|
+
|
168
|
+
|
167
169
|
# Given a <tt>Builder::XmlMarkup</tt> instance and two datetime objects, builds a UBL
|
168
170
|
# representation of the period between the two dates and times, something like the
|
169
171
|
# following:
|
@@ -180,8 +182,8 @@ module Invoicing
|
|
180
182
|
xml.cbc :EndDate, end_date
|
181
183
|
xml.cbc :EndTime, end_time
|
182
184
|
end
|
183
|
-
|
184
|
-
|
185
|
+
|
186
|
+
|
185
187
|
# Given a <tt>Builder::XmlMarkup</tt> instance and a supplier/customer details hash (as
|
186
188
|
# returned by <tt>LedgerItem#sender_details</tt> and <tt>LedgerItem#recipient_details</tt>,
|
187
189
|
# builds a UBL representation of that party, something like the following:
|
@@ -203,7 +205,7 @@ module Invoicing
|
|
203
205
|
party.cac :PartyName do |party_name|
|
204
206
|
party_name.cbc :Name, details[:name]
|
205
207
|
end if details[:name]
|
206
|
-
|
208
|
+
|
207
209
|
party.cac :PostalAddress do |postal_address|
|
208
210
|
street1, street2 = details[:address].strip.split("\n", 2)
|
209
211
|
postal_address.cbc :StreetName, street1 if street1
|
@@ -216,21 +218,21 @@ module Invoicing
|
|
216
218
|
country.cbc :Name, details[:country] if details[:country]
|
217
219
|
end if details[:country_code] || details[:country]
|
218
220
|
end
|
219
|
-
|
221
|
+
|
220
222
|
party.cac :PartyTaxScheme do |party_tax_scheme|
|
221
223
|
party_tax_scheme.cbc :CompanyID, details[:tax_number]
|
222
224
|
party_tax_scheme.cac :TaxScheme do |tax_scheme|
|
223
225
|
tax_scheme.cbc :ID, "VAT" # TODO: make country-dependent (e.g. GST in Australia)
|
224
226
|
end
|
225
227
|
end if details[:tax_number]
|
226
|
-
|
228
|
+
|
227
229
|
party.cac :Contact do |contact|
|
228
230
|
contact.cbc :Name, details[:contact_name]
|
229
231
|
end if details[:contact_name]
|
230
232
|
end
|
231
233
|
end
|
232
|
-
|
233
|
-
|
234
|
+
|
235
|
+
|
234
236
|
# Given a <tt>Builder::XmlMarkup</tt> instance and a +LineItem+ instance, builds a UBL
|
235
237
|
# representation of that line item, something like the following:
|
236
238
|
#
|
@@ -251,11 +253,11 @@ module Invoicing
|
|
251
253
|
tax_point_date, tax_point_time = date_and_time(tax_point_of(line_item))
|
252
254
|
invoice_line.cbc :TaxPointDate, tax_point_date
|
253
255
|
end
|
254
|
-
|
256
|
+
|
255
257
|
invoice_line.cac :TaxTotal do |tax_total|
|
256
258
|
tax_total.cbc :TaxAmount, (factor*tax_amount_of(line_item)).to_s, :currencyID => currency
|
257
259
|
end if tax_amount_of(line_item)
|
258
|
-
|
260
|
+
|
259
261
|
invoice_line.cac :Item do |item|
|
260
262
|
item.cbc :Description, description_of(line_item)
|
261
263
|
#cac:BuyersItemIdentification
|
@@ -263,7 +265,7 @@ module Invoicing
|
|
263
265
|
#cac:ClassifiedTaxCategory
|
264
266
|
#cac:ItemInstance
|
265
267
|
end
|
266
|
-
|
268
|
+
|
267
269
|
#cac:Price
|
268
270
|
end
|
269
271
|
|
data/lib/invoicing/line_item.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
require "active_support/concern"
|
2
|
+
|
1
3
|
module Invoicing
|
2
4
|
# = Line item objects
|
3
5
|
#
|
@@ -27,12 +29,12 @@ module Invoicing
|
|
27
29
|
#
|
28
30
|
# class ProductSale < LineItem
|
29
31
|
# belongs_to :product
|
30
|
-
#
|
32
|
+
#
|
31
33
|
# def description
|
32
34
|
# product.title
|
33
35
|
# end
|
34
36
|
# end
|
35
|
-
#
|
37
|
+
#
|
36
38
|
# class ShippingCharges < LineItem
|
37
39
|
# def description
|
38
40
|
# "Shipping charges"
|
@@ -52,11 +54,11 @@ module Invoicing
|
|
52
54
|
#
|
53
55
|
# +type+::
|
54
56
|
# String to store the class name, for ActiveRecord single table inheritance.
|
55
|
-
#
|
57
|
+
#
|
56
58
|
# +ledger_item+::
|
57
59
|
# You should define an association <tt>belongs_to :ledger_item, ...</tt> which returns the
|
58
60
|
# +LedgerItem+ object (invoice/credit note) to which this line item belongs.
|
59
|
-
#
|
61
|
+
#
|
60
62
|
# +ledger_item_id+::
|
61
63
|
# A foreign key of integer type, which stores the ID of the model object returned by the
|
62
64
|
# +ledger_item+ association.
|
@@ -69,7 +71,7 @@ module Invoicing
|
|
69
71
|
# currencies within one invoice.) See the documentation of the +CurrencyValue+ module for notes
|
70
72
|
# on suitable datatypes for monetary values. +acts_as_currency_value+ is automatically applied
|
71
73
|
# to this attribute.
|
72
|
-
#
|
74
|
+
#
|
73
75
|
# +tax_amount+::
|
74
76
|
# A decimal column containing the monetary amount of tax which is added to +net_amount+ to
|
75
77
|
# obtain the total price. This may of course be zero if no tax applies; otherwise it should have
|
@@ -130,6 +132,8 @@ module Invoicing
|
|
130
132
|
# These standard datetime columns are also recommended.
|
131
133
|
#
|
132
134
|
module LineItem
|
135
|
+
extend ActiveSupport::Concern
|
136
|
+
|
133
137
|
module ActMethods
|
134
138
|
# Declares that the current class is a model for line items (i.e. individual items on invoices
|
135
139
|
# and credit notes).
|
@@ -143,38 +147,39 @@ module Invoicing
|
|
143
147
|
# acts_as_line_item :net_amount => :net_price
|
144
148
|
def acts_as_line_item(*args)
|
145
149
|
Invoicing::ClassInfo.acts_as(Invoicing::LineItem, self, args)
|
146
|
-
|
150
|
+
|
147
151
|
info = line_item_class_info
|
148
152
|
if info.previous_info.nil? # Called for the first time?
|
149
153
|
# Set the 'amount' columns to act as currency values
|
150
154
|
acts_as_currency_value(info.method(:net_amount), info.method(:tax_amount))
|
151
|
-
|
152
|
-
before_validation :calculate_tax_amount
|
153
|
-
|
155
|
+
|
154
156
|
extend Invoicing::FindSubclasses
|
155
|
-
|
156
|
-
# Dynamically created named scopes
|
157
|
-
named_scope :in_effect, lambda{
|
158
|
-
ledger_assoc_id = line_item_class_info.method(:ledger_item).to_sym
|
159
|
-
ledger_refl = reflections[ledger_assoc_id]
|
160
|
-
ledger_table = ledger_refl.table_name # not quoted_table_name because it'll be quoted again
|
161
|
-
status_column = ledger_refl.klass.send(:ledger_item_class_info).method(:status)
|
162
|
-
{ :joins => ledger_assoc_id,
|
163
|
-
:conditions => {"#{ledger_table}.#{status_column}" => ['closed', 'cleared'] } }
|
164
|
-
}
|
165
|
-
|
166
|
-
named_scope :sorted, lambda{|column|
|
167
|
-
column = line_item_class_info.method(column).to_s
|
168
|
-
if column_names.include?(column)
|
169
|
-
{:order => "#{connection.quote_column_name(column)}, #{connection.quote_column_name(primary_key)}"}
|
170
|
-
else
|
171
|
-
{:order => connection.quote_column_name(primary_key)}
|
172
|
-
end
|
173
|
-
}
|
174
157
|
end
|
175
158
|
end
|
176
159
|
end
|
177
|
-
|
160
|
+
|
161
|
+
included do
|
162
|
+
before_validation :calculate_tax_amount
|
163
|
+
|
164
|
+
# Dynamically created named scopes
|
165
|
+
scope :in_effect, lambda {
|
166
|
+
ledger_assoc = line_item_class_info.method(:ledger_item).to_sym
|
167
|
+
ledger_refl = reflections[ledger_assoc]
|
168
|
+
ledger_table = ledger_refl.table_name # not quoted_table_name because it'll be quoted again
|
169
|
+
status_column = ledger_refl.klass.send(:ledger_item_class_info).method(:status)
|
170
|
+
joins(ledger_assoc).where("#{ledger_table}.#{status_column}" => ['closed', 'cleared'])
|
171
|
+
}
|
172
|
+
|
173
|
+
scope :sorted, lambda { |column|
|
174
|
+
column = line_item_class_info.method(column).to_s
|
175
|
+
if column_names.include?(column)
|
176
|
+
order("#{connection.quote_column_name(column)}, #{connection.quote_column_name(primary_key)}")
|
177
|
+
else
|
178
|
+
order(connection.quote_column_name(primary_key))
|
179
|
+
end
|
180
|
+
}
|
181
|
+
end
|
182
|
+
|
178
183
|
# Overrides the default constructor of <tt>ActiveRecord::Base</tt> when +acts_as_line_item+
|
179
184
|
# is called. If the +uuid+ gem is installed, this constructor creates a new UUID and assigns
|
180
185
|
# it to the +uuid+ property when a new line item model object is created.
|
@@ -193,12 +198,12 @@ module Invoicing
|
|
193
198
|
raise RuntimeError, 'Cannot determine currency for line item without a ledger item' if ledger_item.nil?
|
194
199
|
ledger_item.send(:ledger_item_class_info).get(ledger_item, :currency)
|
195
200
|
end
|
196
|
-
|
201
|
+
|
197
202
|
def calculate_tax_amount
|
198
203
|
return unless respond_to? :net_amount_taxed
|
199
204
|
self.tax_amount = net_amount_taxed - net_amount
|
200
205
|
end
|
201
|
-
|
206
|
+
|
202
207
|
# The sum of +net_amount+ and +tax_amount+.
|
203
208
|
def gross_amount
|
204
209
|
net_amount = line_item_class_info.get(self, :net_amount)
|
@@ -210,7 +215,7 @@ module Invoicing
|
|
210
215
|
def gross_amount_formatted
|
211
216
|
format_currency_value(gross_amount)
|
212
217
|
end
|
213
|
-
|
218
|
+
|
214
219
|
# We don't actually implement anything using +method_missing+ at the moment, but use it to
|
215
220
|
# generate slightly more useful error messages in certain cases.
|
216
221
|
def method_missing(method_id, *args)
|
@@ -225,15 +230,15 @@ module Invoicing
|
|
225
230
|
super
|
226
231
|
end
|
227
232
|
end
|
228
|
-
|
229
|
-
|
233
|
+
|
234
|
+
|
230
235
|
# Stores state in the ActiveRecord class object
|
231
236
|
class ClassInfo < Invoicing::ClassInfo::Base #:nodoc:
|
232
237
|
attr_reader :uuid_generator
|
233
|
-
|
238
|
+
|
234
239
|
def initialize(model_class, previous_info, args)
|
235
240
|
super
|
236
|
-
|
241
|
+
|
237
242
|
begin # try to load the UUID gem
|
238
243
|
require 'uuid'
|
239
244
|
@uuid_generator = UUID.new
|
@@ -241,7 +246,7 @@ module Invoicing
|
|
241
246
|
@uuid_generator = nil
|
242
247
|
end
|
243
248
|
end
|
244
|
-
|
249
|
+
|
245
250
|
# Allow methods generated by +CurrencyValue+ to be renamed as well
|
246
251
|
def method(name)
|
247
252
|
if name.to_s =~ /^(.*)_formatted$/
|
@@ -252,4 +257,4 @@ module Invoicing
|
|
252
257
|
end
|
253
258
|
end
|
254
259
|
end
|
255
|
-
end
|
260
|
+
end
|
data/lib/invoicing/price.rb
CHANGED
data/lib/invoicing/tax_rate.rb
CHANGED
@@ -3,20 +3,17 @@ module Invoicing
|
|
3
3
|
module ActMethods
|
4
4
|
def acts_as_tax_rate(*args)
|
5
5
|
Invoicing::ClassInfo.acts_as(Invoicing::TaxRate, self, args)
|
6
|
+
|
6
7
|
info = tax_rate_class_info
|
7
|
-
|
8
8
|
if info.previous_info.nil? # Called for the first time?
|
9
9
|
# Import TimeDependent functionality
|
10
10
|
acts_as_time_dependent :value => :rate
|
11
11
|
end
|
12
12
|
end
|
13
13
|
end
|
14
|
-
|
14
|
+
|
15
15
|
# Stores state in the ActiveRecord class object
|
16
16
|
class ClassInfo < Invoicing::ClassInfo::Base #:nodoc:
|
17
|
-
|
18
|
-
super
|
19
|
-
end
|
20
|
-
end
|
17
|
+
end
|
21
18
|
end
|
22
19
|
end
|
data/lib/invoicing/taxable.rb
CHANGED
@@ -1,5 +1,7 @@
|
|
1
1
|
# encoding: utf-8
|
2
2
|
|
3
|
+
require "active_support/concern"
|
4
|
+
|
3
5
|
module Invoicing
|
4
6
|
# = Computation of tax on prices
|
5
7
|
#
|
@@ -108,7 +110,7 @@ module Invoicing
|
|
108
110
|
# </tr>
|
109
111
|
# <% end %>
|
110
112
|
# </table>
|
111
|
-
#
|
113
|
+
#
|
112
114
|
# <h1>New product</h1>
|
113
115
|
# <% form_for(@product) do |f| %>
|
114
116
|
# <%= f.error_messages %>
|
@@ -143,33 +145,33 @@ module Invoicing
|
|
143
145
|
# # Should return a number greater than or equal to the input value. Don't worry about rounding --
|
144
146
|
# # CurrencyValue deals with that.
|
145
147
|
# end
|
146
|
-
#
|
148
|
+
#
|
147
149
|
# def remove_tax(params)
|
148
150
|
# # Does the reverse of apply_tax -- converts a value with tax into a value without tax. The params
|
149
151
|
# # hash has the same format. First applying tax and then removing it again should always result in the
|
150
152
|
# # starting value (for the the same object and the same environment -- it may depend on time,
|
151
153
|
# # global variables, etc).
|
152
154
|
# end
|
153
|
-
#
|
155
|
+
#
|
154
156
|
# def tax_info(params, *args)
|
155
157
|
# # Should return a short string to explain to users which operation has been performed by apply_tax
|
156
158
|
# # (e.g. if apply_tax has added VAT, the string could be "inc. VAT"). The params hash is the same as
|
157
159
|
# # given to apply_tax. Additional parameters are optional; if any arguments are passed to a call to
|
158
160
|
# # model_object.<attr>_tax_info then they are passed on here.
|
159
161
|
# end
|
160
|
-
#
|
162
|
+
#
|
161
163
|
# def tax_details(params, *args)
|
162
164
|
# # Like tax_info, but should return a longer string for use in user interface elements which are less
|
163
165
|
# # limited in size.
|
164
166
|
# end
|
165
|
-
#
|
167
|
+
#
|
166
168
|
# def mixin_methods
|
167
169
|
# # Optionally you can define a method mixin_methods which returns a list of method names which should
|
168
170
|
# # be included in classes which use this tax logic. Methods defined here become instance methods of
|
169
171
|
# # model objects with acts_as_taxable attributes. For example:
|
170
172
|
# [:some_other_method]
|
171
173
|
# end
|
172
|
-
#
|
174
|
+
#
|
173
175
|
# def some_other_method(params, *args)
|
174
176
|
# # some_other_method was named by mixin_methods to be included in model objects. For example, if the
|
175
177
|
# # class MyProduct uses MyTaxLogic, then MyProduct.find(1).some_other_method(:foo, 'bar') will
|
@@ -181,7 +183,7 @@ module Invoicing
|
|
181
183
|
#
|
182
184
|
#
|
183
185
|
# == Currency rounding errors
|
184
|
-
#
|
186
|
+
#
|
185
187
|
# Both the taxed and the untaxed value of an attribute are currency values, and so they must both be rounded
|
186
188
|
# to the accuracy which is conventional for the currency in use (see the discussion of precision and rounding
|
187
189
|
# in the +CurrencyValue+ module). If we are always storing untaxed values and outputting taxed values to the
|
@@ -191,20 +193,22 @@ module Invoicing
|
|
191
193
|
# again, it is again rounded to the currency's conventional precision, and displayed to the user. If the
|
192
194
|
# rounding steps have rounded the number upwards twice, or downwards twice, it may happen that the value
|
193
195
|
# displayed to the user differs slightly from the one they originally entered.
|
194
|
-
#
|
196
|
+
#
|
195
197
|
# We believe that storing untaxed values and performing currency rounding are the right things to do, and this
|
196
198
|
# apparent rounding error is a natural consequence. This module therefore tries to deal with the error
|
197
199
|
# elegantly: If you assign a value to a taxed attribute and immediately read it again, it will return the
|
198
200
|
# same value as if it had been stored and loaded again (i.e. the number you read has been rounded twice --
|
199
201
|
# make sure the currency code has been assigned to the object beforehand, so that the +CurrencyValue+ module
|
200
202
|
# knows which precision to apply).
|
201
|
-
#
|
203
|
+
#
|
202
204
|
# Moreover, after assigning a value to a <tt><attr>_taxed=</tt> attribute, the <tt><attr>_tax_rounding_error</tt>
|
203
205
|
# method can tell you whether and by how much the value has changed as a result of removing and re-applying
|
204
206
|
# tax. A negative number indicates that the converted amount is less than the input; a positive number indicates
|
205
207
|
# that it is more than entered by the user; and zero means that there was no difference.
|
206
|
-
#
|
208
|
+
#
|
207
209
|
module Taxable
|
210
|
+
extend ActiveSupport::Concern
|
211
|
+
|
208
212
|
module ActMethods
|
209
213
|
# Declares that one or more attributes on this model object store monetary values to which tax may be
|
210
214
|
# applied. Takes one or more attribute names, followed by an options hash:
|
@@ -216,14 +220,14 @@ module Invoicing
|
|
216
220
|
# +acts_as_taxable+ implies +acts_as_currency_value+ with the same options. See the +Taxable+ for details.
|
217
221
|
def acts_as_taxable(*args)
|
218
222
|
Invoicing::ClassInfo.acts_as(Invoicing::Taxable, self, args)
|
219
|
-
|
223
|
+
|
220
224
|
attrs = taxable_class_info.new_args.map{|a| a.to_s }
|
221
225
|
currency_attrs = attrs + attrs.map{|attr| "#{attr}_taxed"}
|
222
226
|
currency_opts = taxable_class_info.all_options.update({:conversion_input => :convert_taxable_value})
|
223
227
|
acts_as_currency_value(currency_attrs, currency_opts)
|
224
|
-
|
228
|
+
|
225
229
|
attrs.each {|attr| generate_attr_taxable_methods(attr) }
|
226
|
-
|
230
|
+
|
227
231
|
if tax_logic = taxable_class_info.all_options[:tax_logic]
|
228
232
|
other_methods = (tax_logic.respond_to?(:mixin_methods) ? tax_logic.mixin_methods : []) || []
|
229
233
|
other_methods.each {|method_name| generate_attr_taxable_other_method(method_name.to_s) }
|
@@ -232,7 +236,7 @@ module Invoicing
|
|
232
236
|
end
|
233
237
|
end
|
234
238
|
end
|
235
|
-
|
239
|
+
|
236
240
|
# If +write_attribute+ is called on a taxable attribute, we note whether the taxed or the untaxed
|
237
241
|
# version contains the latest correct value. We don't do the conversion immediately in case the tax
|
238
242
|
# logic requires the value of another attribute (which may be assigned later) to do its calculation.
|
@@ -241,18 +245,19 @@ module Invoicing
|
|
241
245
|
attr_regex = taxable_class_info.all_args.map{|a| a.to_s }.join('|')
|
242
246
|
@taxed_or_untaxed ||= {}
|
243
247
|
@taxed_attributes ||= {}
|
244
|
-
|
248
|
+
|
245
249
|
if attribute =~ /^(#{attr_regex})$/
|
246
250
|
@taxed_or_untaxed[attribute] = :untaxed
|
247
251
|
@taxed_attributes[attribute] = nil
|
248
252
|
elsif attribute =~ /^(#{attr_regex})_taxed$/
|
253
|
+
@attributes[attribute] = value
|
249
254
|
@taxed_or_untaxed[$1] = :taxed
|
250
255
|
@taxed_attributes[$1] = value
|
251
256
|
end
|
252
|
-
|
257
|
+
|
253
258
|
super
|
254
259
|
end
|
255
|
-
|
260
|
+
|
256
261
|
# Called internally to convert between taxed and untaxed values. You shouldn't usually need to
|
257
262
|
# call this method from elsewhere.
|
258
263
|
def convert_taxable_value(attr) #:nodoc:
|
@@ -262,10 +267,10 @@ module Invoicing
|
|
262
267
|
|
263
268
|
@taxed_or_untaxed ||= {}
|
264
269
|
from_status = @taxed_or_untaxed[attr_without_suffix] || :untaxed # taxed or untaxed most recently assigned?
|
265
|
-
|
270
|
+
|
266
271
|
attr_to_read = attr_without_suffix
|
267
272
|
attr_to_read += '_taxed' if from_status == :taxed
|
268
|
-
|
273
|
+
|
269
274
|
if from_status == :taxed && to_status == :taxed
|
270
275
|
# Special case: remove tax, apply rounding errors, apply tax again, apply rounding errors again.
|
271
276
|
write_attribute(attr_without_suffix, send(attr_without_suffix))
|
@@ -274,21 +279,21 @@ module Invoicing
|
|
274
279
|
taxable_class_info.convert(self, attr_without_suffix, read_attribute(attr_to_read), from_status, to_status)
|
275
280
|
end
|
276
281
|
end
|
277
|
-
|
282
|
+
|
278
283
|
protected :write_attribute, :convert_taxable_value
|
279
284
|
|
280
285
|
|
281
286
|
module ClassMethods #:nodoc:
|
282
287
|
# Generate additional accessor method for attribute with getter +method_name+.
|
283
288
|
def generate_attr_taxable_methods(method_name) #:nodoc:
|
284
|
-
|
289
|
+
|
285
290
|
define_method("#{method_name}_tax_rounding_error") do
|
286
291
|
original_value = read_attribute("#{method_name}_taxed")
|
287
292
|
return nil if original_value.nil? # Can only have a rounding error if the taxed attr was assigned
|
288
|
-
|
293
|
+
|
289
294
|
original_value = BigDecimal.new(original_value.to_s)
|
290
295
|
converted_value = send("#{method_name}_taxed")
|
291
|
-
|
296
|
+
|
292
297
|
return nil if converted_value.nil?
|
293
298
|
converted_value - original_value
|
294
299
|
end
|
@@ -302,19 +307,19 @@ module Invoicing
|
|
302
307
|
tax_logic = taxable_class_info.all_options[:tax_logic]
|
303
308
|
tax_logic.tax_details({:model_object => self, :attribute => method_name, :value => send(method_name)}, *args)
|
304
309
|
end
|
305
|
-
|
310
|
+
|
306
311
|
define_method("#{method_name}_with_tax_info") do |*args|
|
307
312
|
amount = send("#{method_name}_taxed_formatted")
|
308
313
|
tax_info = send("#{method_name}_tax_info").to_s
|
309
314
|
tax_info.blank? ? amount : "#{amount} #{tax_info}"
|
310
315
|
end
|
311
|
-
|
316
|
+
|
312
317
|
define_method("#{method_name}_with_tax_details") do |*args|
|
313
318
|
amount = send("#{method_name}_taxed_formatted")
|
314
319
|
tax_details = send("#{method_name}_tax_details").to_s
|
315
320
|
tax_details.blank? ? amount : "#{amount} #{tax_details}"
|
316
321
|
end
|
317
|
-
|
322
|
+
|
318
323
|
define_method("#{method_name}_taxed_before_type_cast") do
|
319
324
|
@taxed_attributes ||= {}
|
320
325
|
@taxed_attributes[method_name] ||
|
@@ -322,7 +327,7 @@ module Invoicing
|
|
322
327
|
send("#{method_name}_taxed")
|
323
328
|
end
|
324
329
|
end
|
325
|
-
|
330
|
+
|
326
331
|
# Generate a proxy method called +method_name+ which is forwarded to the +tax_logic+ object.
|
327
332
|
def generate_attr_taxable_other_method(method_name) #:nodoc:
|
328
333
|
define_method(method_name) do |*args|
|
@@ -333,8 +338,8 @@ module Invoicing
|
|
333
338
|
|
334
339
|
private :generate_attr_taxable_methods, :generate_attr_taxable_other_method
|
335
340
|
end # module ClassMethods
|
336
|
-
|
337
|
-
|
341
|
+
|
342
|
+
|
338
343
|
class ClassInfo < Invoicing::ClassInfo::Base #:nodoc:
|
339
344
|
# Performs the conversion between taxed and untaxed values. Arguments +from_status+ and
|
340
345
|
# +to_status+ must each be either <tt>:taxed</tt> or <tt>:untaxed</tt>.
|
@@ -342,7 +347,7 @@ module Invoicing
|
|
342
347
|
return nil if value.nil?
|
343
348
|
value = BigDecimal.new(value.to_s)
|
344
349
|
return value if from_status == to_status
|
345
|
-
|
350
|
+
|
346
351
|
if to_status == :taxed
|
347
352
|
all_options[:tax_logic].apply_tax({:model_object => object, :attribute => attr_without_suffix, :value => value})
|
348
353
|
else
|
@@ -350,6 +355,6 @@ module Invoicing
|
|
350
355
|
end
|
351
356
|
end
|
352
357
|
end
|
353
|
-
|
358
|
+
|
354
359
|
end
|
355
|
-
end
|
360
|
+
end
|