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.
Files changed (78) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +1 -0
  3. data/README.md +57 -0
  4. data/Rakefile +16 -37
  5. data/lib/invoicing.rb +20 -10
  6. data/lib/invoicing/cached_record.rb +9 -6
  7. data/lib/invoicing/class_info.rb +34 -34
  8. data/lib/invoicing/connection_adapter_ext.rb +4 -4
  9. data/lib/invoicing/countries/uk.rb +6 -6
  10. data/lib/invoicing/currency_value.rb +39 -32
  11. data/lib/invoicing/find_subclasses.rb +40 -15
  12. data/lib/invoicing/ledger_item.rb +166 -145
  13. data/lib/invoicing/ledger_item/pdf_generator.rb +108 -0
  14. data/lib/invoicing/ledger_item/render_html.rb +76 -73
  15. data/lib/invoicing/ledger_item/render_ubl.rb +37 -35
  16. data/lib/invoicing/line_item.rb +43 -38
  17. data/lib/invoicing/price.rb +1 -1
  18. data/lib/invoicing/tax_rate.rb +3 -6
  19. data/lib/invoicing/taxable.rb +37 -32
  20. data/lib/invoicing/time_dependent.rb +40 -40
  21. data/lib/invoicing/version.rb +4 -4
  22. data/lib/rails/generators/invoicing/invoicing_generator.rb +14 -0
  23. data/lib/rails/generators/invoicing/ledger_item/ledger_item_generator.rb +17 -0
  24. data/lib/rails/generators/invoicing/ledger_item/templates/migration.rb +25 -0
  25. data/lib/rails/generators/invoicing/ledger_item/templates/model.rb +5 -0
  26. data/lib/rails/generators/invoicing/line_item/line_item_generator.rb +17 -0
  27. data/lib/rails/generators/invoicing/line_item/templates/migration.rb +20 -0
  28. data/lib/rails/generators/invoicing/line_item/templates/model.rb +5 -0
  29. data/lib/rails/generators/invoicing/tax_rate/tax_rate_generator.rb +17 -0
  30. data/lib/rails/generators/invoicing/tax_rate/templates/migration.rb +14 -0
  31. data/lib/rails/generators/invoicing/tax_rate/templates/model.rb +3 -0
  32. metadata +110 -153
  33. data.tar.gz.sig +0 -1
  34. data/History.txt +0 -31
  35. data/Manifest.txt +0 -62
  36. data/PostInstall.txt +0 -10
  37. data/README.rdoc +0 -58
  38. data/script/console +0 -10
  39. data/script/destroy +0 -14
  40. data/script/generate +0 -14
  41. data/tasks/rcov.rake +0 -4
  42. data/test/cached_record_test.rb +0 -100
  43. data/test/class_info_test.rb +0 -253
  44. data/test/connection_adapter_ext_test.rb +0 -79
  45. data/test/currency_value_test.rb +0 -209
  46. data/test/find_subclasses_test.rb +0 -120
  47. data/test/fixtures/README +0 -7
  48. data/test/fixtures/cached_record.sql +0 -22
  49. data/test/fixtures/class_info.sql +0 -28
  50. data/test/fixtures/currency_value.sql +0 -29
  51. data/test/fixtures/find_subclasses.sql +0 -43
  52. data/test/fixtures/ledger_item.sql +0 -39
  53. data/test/fixtures/line_item.sql +0 -33
  54. data/test/fixtures/price.sql +0 -4
  55. data/test/fixtures/tax_rate.sql +0 -4
  56. data/test/fixtures/taxable.sql +0 -14
  57. data/test/fixtures/time_dependent.sql +0 -35
  58. data/test/ledger_item_test.rb +0 -444
  59. data/test/line_item_test.rb +0 -139
  60. data/test/models/README +0 -4
  61. data/test/models/test_subclass_in_another_file.rb +0 -3
  62. data/test/models/test_subclass_not_in_database.rb +0 -6
  63. data/test/price_test.rb +0 -9
  64. data/test/ref-output/creditnote3.html +0 -82
  65. data/test/ref-output/creditnote3.xml +0 -89
  66. data/test/ref-output/invoice1.html +0 -93
  67. data/test/ref-output/invoice1.xml +0 -111
  68. data/test/ref-output/invoice2.html +0 -86
  69. data/test/ref-output/invoice2.xml +0 -98
  70. data/test/ref-output/invoice_null.html +0 -36
  71. data/test/render_html_test.rb +0 -70
  72. data/test/render_ubl_test.rb +0 -44
  73. data/test/setup.rb +0 -37
  74. data/test/tax_rate_test.rb +0 -9
  75. data/test/taxable_test.rb +0 -180
  76. data/test/test_helper.rb +0 -72
  77. data/test/time_dependent_test.rb +0 -180
  78. 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
 
@@ -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
@@ -2,7 +2,7 @@ module Invoicing
2
2
  module Price
3
3
  module ActMethods
4
4
  def acts_as_price(*args)
5
-
5
+
6
6
  end
7
7
  end
8
8
  end
@@ -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
- def initialize(model_class, previous_info, args)
18
- super
19
- end
20
- end
17
+ end
21
18
  end
22
19
  end
@@ -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