invoicing 0.2.1 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
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