invoicing 0.2.1 → 1.0.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.
- 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
|