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,3 +1,5 @@
1
+ require "active_support/concern"
2
+
1
3
  module Invoicing
2
4
  # = Input and output of monetary values
3
5
  #
@@ -56,9 +58,10 @@ module Invoicing
56
58
  # The string returned by a +_formatted+ method is UTF-8 encoded -- remember most currency symbols (except $)
57
59
  # are outside basic 7-bit ASCII.
58
60
  module CurrencyValue
59
-
61
+ extend ActiveSupport::Concern
62
+
60
63
  # Data about currencies, indexed by ISO 4217 code. (Currently a very short list, in need of extending.)
61
- # The values are hashes, in which the following keys are recognised:
64
+ # The values are hashes, in which the following keys are recognised:
62
65
  # <tt>:round</tt>:: Smallest unit of the currency in normal use, to which values are rounded. Default is 0.01.
63
66
  # <tt>:symbol</tt>:: Symbol or string usually used to denote the currency. Encoded as UTF-8. Default is ISO 4217 code.
64
67
  # <tt>:suffix</tt>:: +true+ if the currency symbol appears after the number, +false+ if it appears before. Default +false+.
@@ -72,7 +75,7 @@ module Invoicing
72
75
  'INR' => {:symbol => "\xE2\x82\xA8"}, # Indian Rupee
73
76
  'JPY' => {:symbol => "\xC2\xA5", :round => 1} # Japanese Yen
74
77
  }
75
-
78
+
76
79
  module ActMethods
77
80
  # Declares that the current model object has columns storing monetary amounts. Pass those attribute
78
81
  # names to +acts_as_currency_value+. By default, we try to find an attribute or method called +currency+,
@@ -111,26 +114,30 @@ module Invoicing
111
114
  # (The example above is actually a real part of +LedgerItem+.)
112
115
  def acts_as_currency_value(*args)
113
116
  Invoicing::ClassInfo.acts_as(Invoicing::CurrencyValue, self, args)
114
-
115
- # Register callback if this is the first time acts_as_currency_value has been called
116
- before_save :write_back_currency_values if currency_value_class_info.previous_info.nil?
117
117
  end
118
118
  end
119
119
 
120
+ included do
121
+ # Register callback if this is the first time acts_as_currency_value has been called
122
+ before_save :write_back_currency_values, :if => "currency_value_class_info.previous_info.nil?"
123
+ end
124
+
120
125
  # Format a numeric monetary value into a human-readable string, in the currency of the
121
126
  # current model object.
122
127
  def format_currency_value(value, options={})
123
128
  currency_value_class_info.format_value(self, value, options)
124
129
  end
125
-
126
-
130
+
131
+
127
132
  # Called automatically via +before_save+. Writes the result of converting +CurrencyValue+ attributes
128
133
  # back to the actual attributes, so that they are saved in the database. (This doesn't happen in
129
134
  # +convert_currency_values+ to avoid losing the +_before_type_cast+ attribute values.)
130
135
  def write_back_currency_values
131
- currency_value_class_info.all_args.each {|attr| write_attribute(attr, send(attr)) }
136
+ currency_value_class_info.all_args.each do |attr|
137
+ write_attribute(attr, send(attr))
138
+ end
132
139
  end
133
-
140
+
134
141
  protected :write_back_currency_values
135
142
 
136
143
 
@@ -138,7 +145,7 @@ module Invoicing
138
145
  # These methods do not depend on ActiveRecord and can thus also be called externally.
139
146
  module Formatter
140
147
  class << self
141
-
148
+
142
149
  # Given the three-letter ISO 4217 code of a currency, returns a hash with useful bits of information:
143
150
  # <tt>:code</tt>:: The ISO 4217 code of the currency.
144
151
  # <tt>:round</tt>:: Smallest unit of the currency in normal use, to which values are rounded. Default is 0.01.
@@ -154,15 +161,15 @@ module Invoicing
154
161
  info.update(::Invoicing::CurrencyValue::CURRENCIES[code])
155
162
  end
156
163
  options.each_pair {|key, value| info[key] = value if valid_options.include? key }
157
-
164
+
158
165
  info[:suffix] ||= (info[:code] == info[:symbol]) && !info[:code].nil?
159
166
  info[:space] ||= info[:suffix]
160
167
  info[:digits] = -Math.log10(info[:round]).floor if info[:digits].nil?
161
168
  info[:digits] = 0 if info[:digits] < 0
162
-
169
+
163
170
  info
164
171
  end
165
-
172
+
166
173
  # Given the three-letter ISO 4217 code of a currency and a BigDecimal value, returns the
167
174
  # value formatted as an UTF-8 string, ready for human consumption.
168
175
  #
@@ -170,17 +177,17 @@ module Invoicing
170
177
  # as decimal separator and the comma as thousands separator.
171
178
  def format_value(currency_code, value, options={})
172
179
  info = currency_info(currency_code, options)
173
-
180
+
174
181
  negative = false
175
182
  if value < 0
176
183
  negative = true
177
184
  value = -value
178
185
  end
179
-
186
+
180
187
  value = "%.#{info[:digits]}f" % value
181
188
  while value.sub!(/(\d+)(\d\d\d)/, '\1,\2'); end
182
189
  value.sub!(/^\-/, '') # avoid displaying minus zero
183
-
190
+
184
191
  formatted = if ['', nil].include? info[:symbol]
185
192
  value
186
193
  elsif info[:space]
@@ -188,26 +195,26 @@ module Invoicing
188
195
  else
189
196
  info[:suffix] ? "#{value}#{info[:symbol]}" : "#{info[:symbol]}#{value}"
190
197
  end
191
-
198
+
192
199
  if negative
193
200
  # default is to use proper unicode minus sign
194
201
  formatted = (options[:negative] == :brackets) ? "(#{formatted})" : (
195
202
  (options[:negative] == :hyphen) ? "-#{formatted}" : "\xE2\x88\x92#{formatted}"
196
203
  )
197
204
  end
198
- formatted
205
+ formatted.force_encoding("utf-8")
199
206
  end
200
207
  end
201
208
  end
202
209
 
203
210
 
204
211
  class ClassInfo < Invoicing::ClassInfo::Base #:nodoc:
205
-
212
+
206
213
  def initialize(model_class, previous_info, args)
207
214
  super
208
215
  new_args.each{|attr| generate_attrs(attr)}
209
216
  end
210
-
217
+
211
218
  # Generates the getter and setter method for attribute +attr+.
212
219
  def generate_attrs(attr)
213
220
  model_class.class_eval do
@@ -215,23 +222,23 @@ module Invoicing
215
222
  currency_info = currency_value_class_info.currency_info_for(self)
216
223
  return read_attribute(attr) if currency_info.nil?
217
224
  round_factor = BigDecimal(currency_info[:round].to_s)
218
-
225
+
219
226
  value = currency_value_class_info.attr_conversion_input(self, attr)
220
227
  value.nil? ? nil : (value / round_factor).round * round_factor
221
228
  end
222
-
229
+
223
230
  define_method("#{attr}=") do |new_value|
224
231
  write_attribute(attr, new_value)
225
232
  end
226
-
233
+
227
234
  define_method("#{attr}_formatted") do |*args|
228
235
  options = args.first || {}
229
236
  value_as_float = begin
230
- Kernel.Float(send("#{attr}_before_type_cast"))
237
+ Kernel.Float(send("#{attr}_before_type_cast"))
231
238
  rescue ArgumentError, TypeError
232
239
  nil
233
240
  end
234
-
241
+
235
242
  if value_as_float.nil?
236
243
  ''
237
244
  else
@@ -240,7 +247,7 @@ module Invoicing
240
247
  end
241
248
  end
242
249
  end
243
-
250
+
244
251
  # Returns the value of the currency code column of +object+, if available; otherwise the
245
252
  # default currency code (set by the <tt>:currency_code</tt> option), if available; +nil+ if all
246
253
  # else fails.
@@ -251,12 +258,12 @@ module Invoicing
251
258
  all_options[:currency_code]
252
259
  end
253
260
  end
254
-
261
+
255
262
  # Returns a hash of information about the currency used by model +object+.
256
263
  def currency_info_for(object)
257
264
  ::Invoicing::CurrencyValue::Formatter.currency_info(currency_of(object), all_options)
258
265
  end
259
-
266
+
260
267
  # Formats a numeric value as a nice currency string in UTF-8 encoding.
261
268
  # +object+ is the model object carrying the value (used to determine the currency).
262
269
  def format_value(object, value, options={})
@@ -267,16 +274,16 @@ module Invoicing
267
274
  end
268
275
  ::Invoicing::CurrencyValue::Formatter.format_value(currency_of(object), value, options)
269
276
  end
270
-
277
+
271
278
  # If other modules have registered callbacks for the event of reading a rounded attribute,
272
279
  # they are executed here. +attr+ is the name of the attribute being read.
273
280
  def attr_conversion_input(object, attr)
274
281
  value = nil
275
-
282
+
276
283
  if callback = all_options[:conversion_input]
277
284
  value = object.send(callback, attr)
278
285
  end
279
-
286
+
280
287
  unless value
281
288
  raw_value = object.read_attribute(attr)
282
289
  value = BigDecimal.new(raw_value.to_s) unless raw_value.nil?
@@ -12,14 +12,14 @@ module Invoicing
12
12
  # extend Invoicing::FindSubclasses
13
13
  # def self.needs_refrigeration; false; end
14
14
  # end
15
- #
15
+ #
16
16
  # class Food < Product; end
17
17
  # class Bread < Food; end
18
18
  # class Yoghurt < Food
19
19
  # def self.needs_refrigeration; true; end
20
20
  # end
21
21
  # class GreekYoghurt < Yoghurt; end
22
- #
22
+ #
23
23
  # class Drink < Product; end
24
24
  # class SoftDrink < Drink; end
25
25
  # class Smoothie < Drink
@@ -72,11 +72,11 @@ module Invoicing
72
72
  # <tt>Class#inherited</tt> method; we use this to gather together a list of subclasses. Of course,
73
73
  # we won't necessarily know about every class in the world which may subclass our class; in
74
74
  # particular, <tt>Class#inherited</tt> won't be called until that subclass is loaded.
75
- #
75
+ #
76
76
  # If you're including the Ruby files with the subclass definitions using +require+, we will learn
77
- # about subclasses as soon as they are defined. However, if class loading is delayed until a
77
+ # about subclasses as soon as they are defined. However, if class loading is delayed until a
78
78
  # class is first used (for example, <tt>ActiveSupport::Dependencies</tt> does this with model
79
- # objects in Rails projects), we could run into a situation where we don't yet know about all
79
+ # objects in Rails projects), we could run into a situation where we don't yet know about all
80
80
  # subclasses used in a project at the point where we need to process a class method condition.
81
81
  # This would cause us to omit some objects we should have found.
82
82
  #
@@ -89,7 +89,7 @@ module Invoicing
89
89
  # to be on the safe side you can ensure all subclasses are loaded when your application
90
90
  # initialises -- but that's not completely DRY ;-)
91
91
  module FindSubclasses
92
-
92
+
93
93
  # Overrides <tt>ActiveRecord::Base.sanitize_sql_hash_for_conditions</tt> since this is the method
94
94
  # used to transform a hash of conditions into an SQL query fragment. This overriding method
95
95
  # searches for class method conditions in the hash and transforms them into a condition on the
@@ -103,14 +103,14 @@ module Invoicing
103
103
  # {:my_class_method => false} # known_subclasses.reject{|cls| cls.my_class_method }
104
104
  def sanitize_sql_hash_for_conditions(attrs, table_name = quoted_table_name)
105
105
  new_attrs = {}
106
-
106
+
107
107
  attrs.each_pair do |attr, value|
108
108
  attr = attr_base = attr.to_s
109
109
  attr_table_name = table_name
110
110
 
111
111
  # Extract table name from qualified attribute names
112
112
  attr_table_name, attr_base = attr.split('.', 2) if attr.include?('.')
113
-
113
+
114
114
  if columns_hash.include?(attr_base) || ![self.table_name, quoted_table_name].include?(attr_table_name)
115
115
  new_attrs[attr] = value # Condition on a table column, or another table -- pass through unmodified
116
116
  else
@@ -125,10 +125,35 @@ module Invoicing
125
125
 
126
126
  super(new_attrs, table_name)
127
127
  end
128
-
128
+
129
+ def expand_hash_conditions_for_aggregates(attrs)
130
+ new_attrs = {}
131
+
132
+ attrs.each_pair do |attr, value|
133
+ attr = attr_base = attr.to_s
134
+ attr_table_name = table_name
135
+
136
+ # Extract table name from qualified attribute names
137
+ attr_table_name, attr_base = attr.split('.', 2) if attr.include?('.')
138
+
139
+ if columns_hash.include?(attr_base) || ![self.table_name, quoted_table_name].include?(attr_table_name)
140
+ new_attrs[attr] = value # Condition on a table column, or another table -- pass through unmodified
141
+ else
142
+ begin
143
+ matching_classes = select_matching_subclasses(attr_base, value)
144
+ new_attrs["#{self.table_name}.#{inheritance_column}"] = matching_classes.map{|cls| cls.name.to_s}
145
+ rescue NoMethodError
146
+ new_attrs[attr] = value # If the class method doesn't exist, fall back to passing condition through unmodified
147
+ end
148
+ end
149
+ end
150
+
151
+ super(new_attrs)
152
+ end
153
+
129
154
  # Returns a list of those classes within +known_subclasses+ which match a condition
130
155
  # <tt>method_name => value</tt>. May raise +NoMethodError+ if a class object does not
131
- # respond to +method_name+.
156
+ # respond to +method_name+.
132
157
  def select_matching_subclasses(method_name, value, table = table_name, type_column = inheritance_column)
133
158
  known_subclasses(table, type_column).select do |cls|
134
159
  returned = cls.send(method_name)
@@ -139,28 +164,28 @@ module Invoicing
139
164
  end
140
165
  end
141
166
  end
142
-
167
+
143
168
  # Ruby callback which is invoked when a subclass is created. We use this to build a list of known
144
169
  # subclasses.
145
170
  def inherited(subclass)
146
171
  remember_subclass subclass
147
172
  super
148
173
  end
149
-
174
+
150
175
  # Add +subclass+ to the list of know subclasses of this class.
151
176
  def remember_subclass(subclass)
152
177
  @known_subclasses ||= [self]
153
178
  @known_subclasses << subclass unless @known_subclasses.include? subclass
154
179
  self.superclass.remember_subclass(subclass) if self.superclass.respond_to? :remember_subclass
155
180
  end
156
-
181
+
157
182
  # Return the list of all known subclasses of this class, if necessary checking the database for
158
183
  # classes which have not yet been loaded.
159
184
  def known_subclasses(table = table_name, type_column = inheritance_column)
160
185
  load_all_subclasses_found_in_database(table, type_column)
161
186
  @known_subclasses ||= [self]
162
187
  end
163
-
188
+
164
189
  private
165
190
  # Query the database for all qualified class names found in the +type_column+ column
166
191
  # (called +type+ by default), and check that classes of that name have been loaded by the Ruby
@@ -190,4 +215,4 @@ module Invoicing
190
215
  end
191
216
  end
192
217
  end
193
- end
218
+ end
@@ -1,3 +1,7 @@
1
+ require "active_support/concern"
2
+ require "invoicing/ledger_item/render_html"
3
+ require "invoicing/ledger_item/render_ubl"
4
+
1
5
  module Invoicing
2
6
  # = Ledger item objects
3
7
  #
@@ -285,7 +289,8 @@ module Invoicing
285
289
  # included though). If you're chaining scopes it would be advantageous
286
290
  # to put this one close to the beginning of your scope chain.
287
291
  module LedgerItem
288
-
292
+ extend ActiveSupport::Concern
293
+
289
294
  module ActMethods
290
295
  # Declares that the current class is a model for ledger items (i.e. invoices, credit notes and
291
296
  # payment notes).
@@ -302,98 +307,33 @@ module Invoicing
302
307
  # acts_as_ledger_item :total_amount => :gross_amount
303
308
  def acts_as_ledger_item(*args)
304
309
  Invoicing::ClassInfo.acts_as(Invoicing::LedgerItem, self, args)
305
-
310
+
306
311
  info = ledger_item_class_info
307
312
  return unless info.previous_info.nil? # Called for the first time?
308
-
309
- before_validation :calculate_total_amount
310
-
313
+
311
314
  # Set the 'amount' columns to act as currency values
312
315
  acts_as_currency_value(info.method(:total_amount), info.method(:tax_amount),
313
316
  :currency => info.method(:currency), :value_for_formatting => :value_for_formatting)
314
-
315
- extend Invoicing::FindSubclasses
317
+
318
+ extend Invoicing::FindSubclasses
316
319
  include Invoicing::LedgerItem::RenderHTML
317
320
  include Invoicing::LedgerItem::RenderUBL
318
-
319
- # Dynamically created named scopes
320
- named_scope :sent_by, lambda{ |sender_id|
321
- { :conditions => {info.method(:sender_id) => sender_id} }
322
- }
323
-
324
- named_scope :received_by, lambda{ |recipient_id|
325
- { :conditions => {info.method(:recipient_id) => recipient_id} }
326
- }
327
-
328
- named_scope :sent_or_received_by, lambda{ |sender_or_recipient_id|
329
- sender_col = connection.quote_column_name(info.method(:sender_id))
330
- recipient_col = connection.quote_column_name(info.method(:recipient_id))
331
- { :conditions => ["#{sender_col} = ? OR #{recipient_col} = ?",
332
- sender_or_recipient_id, sender_or_recipient_id] }
333
- }
334
-
335
- named_scope :in_effect, :conditions => {info.method(:status) => ['closed', 'cleared']}
336
-
337
- named_scope :open_or_pending, :conditions => {info.method(:status) => ['open', 'pending']}
338
-
339
- named_scope :due_at, lambda{ |date|
340
- due_date = connection.quote_column_name(info.method(:due_date))
341
- {:conditions => ["#{due_date} <= ? OR #{due_date} IS NULL", date]}
342
- }
343
-
344
- named_scope :sorted, lambda{|column|
345
- column = ledger_item_class_info.method(column).to_s
346
- if column_names.include?(column)
347
- {:order => "#{connection.quote_column_name(column)}, #{connection.quote_column_name(primary_key)}"}
348
- else
349
- {:order => connection.quote_column_name(primary_key)}
350
- end
351
- }
352
-
353
- named_scope :exclude_empty_invoices, lambda{
354
- line_items_assoc_id = info.method(:line_items).to_sym
355
- line_items_refl = reflections[line_items_assoc_id]
356
- line_items_table = line_items_refl.quoted_table_name
357
-
358
- # e.g. `ledger_items`.`id`
359
- ledger_items_id = quoted_table_name + "." + connection.quote_column_name(primary_key)
360
-
361
- # e.g. `line_items`.`id`
362
- line_items_id = line_items_table + "." +
363
- connection.quote_column_name(line_items_refl.klass.primary_key)
364
-
365
- # e.g. `line_items`.`ledger_item_id`
366
- ledger_item_foreign_key = line_items_table + "." + connection.quote_column_name(
367
- line_items_refl.klass.send(:line_item_class_info).method(:ledger_item_id))
368
-
369
- payment_classes = select_matching_subclasses(:is_payment, true).map{|c| c.name}
370
- is_payment_class = merge_conditions({info.method(:type) => payment_classes})
371
-
372
- subquery = construct_finder_sql(
373
- :select => "#{quoted_table_name}.*, COUNT(#{line_items_id}) AS number_of_line_items",
374
- :joins => "LEFT JOIN #{line_items_table} ON #{ledger_item_foreign_key} = #{ledger_items_id}",
375
- :group => Invoicing::ConnectionAdapterExt.group_by_all_columns(self)
376
- )
377
-
378
- {:from => "(#{subquery}) AS #{quoted_table_name}",
379
- :conditions => "number_of_line_items > 0 OR #{is_payment_class}"}
380
- }
381
321
  end # def acts_as_ledger_item
382
-
322
+
383
323
  # Synonym for <tt>acts_as_ledger_item :subtype => :invoice</tt>. All options other than
384
324
  # <tt>:subtype</tt> are passed on to +acts_as_ledger_item+. You should apply
385
325
  # +acts_as_invoice+ only to a model which is a subclass of an +acts_as_ledger_item+ type.
386
326
  def acts_as_invoice(options={})
387
327
  acts_as_ledger_item(options.clone.update({:subtype => :invoice}))
388
328
  end
389
-
329
+
390
330
  # Synonym for <tt>acts_as_ledger_item :subtype => :credit_note</tt>. All options other than
391
331
  # <tt>:subtype</tt> are passed on to +acts_as_ledger_item+. You should apply
392
332
  # +acts_as_credit_note+ only to a model which is a subclass of an +acts_as_ledger_item+ type.
393
333
  def acts_as_credit_note(options={})
394
334
  acts_as_ledger_item(options.clone.update({:subtype => :credit_note}))
395
335
  end
396
-
336
+
397
337
  # Synonym for <tt>acts_as_ledger_item :subtype => :payment</tt>. All options other than
398
338
  # <tt>:subtype</tt> are passed on to +acts_as_ledger_item+. You should apply
399
339
  # +acts_as_payment+ only to a model which is a subclass of an +acts_as_ledger_item+ type.
@@ -401,7 +341,73 @@ module Invoicing
401
341
  acts_as_ledger_item(options.clone.update({:subtype => :payment}))
402
342
  end
403
343
  end # module ActMethods
404
-
344
+
345
+ included do
346
+ before_validation :calculate_total_amount
347
+
348
+ # Dynamically created named scopes
349
+ scope :sent_by, lambda { |sender_id|
350
+ where(ledger_item_class_info.method(:sender_id) => sender_id)
351
+ }
352
+
353
+ scope :received_by, lambda {|recipient_id|
354
+ where(ledger_item_class_info.method(:recipient_id) => recipient_id)
355
+ }
356
+
357
+ scope :sent_or_received_by, lambda { |sender_or_recipient_id|
358
+ sender_col = connection.quote_column_name(ledger_item_class_info.method(:sender_id))
359
+ recipient_col = connection.quote_column_name(ledger_item_class_info.method(:recipient_id))
360
+
361
+ where(["#{sender_col} = ? OR #{recipient_col} = ?",
362
+ sender_or_recipient_id, sender_or_recipient_id])
363
+ }
364
+
365
+ scope :in_effect, lambda {
366
+ where(ledger_item_class_info.method(:status) => ['closed', 'cleared'])
367
+ }
368
+
369
+ scope :open_or_pending, lambda {
370
+ where(ledger_item_class_info.method(:status) => ['open', 'pending'])
371
+ }
372
+
373
+ scope :due_at, lambda { |date|
374
+ due_date = connection.quote_column_name(ledger_item_class_info.method(:due_date))
375
+ where(["#{due_date} <= ? OR #{due_date} IS NULL", date])
376
+ }
377
+
378
+ scope :sorted, lambda { |column|
379
+ column = ledger_item_class_info.method(column).to_s
380
+ if column_names.include?(column)
381
+ order("#{connection.quote_column_name(column)}, #{connection.quote_column_name(primary_key)}")
382
+ else
383
+ order(connection.quote_column_name(primary_key))
384
+ end
385
+ }
386
+
387
+ scope :exclude_empty_invoices, lambda {
388
+ line_items_assoc_id = ledger_item_class_info.method(:line_items).to_sym
389
+ line_items_refl = reflections[line_items_assoc_id]
390
+ line_items_table = line_items_refl.quoted_table_name
391
+
392
+ # e.g. `ledger_items`.`id`
393
+ ledger_items_id = quoted_table_name + "." + connection.quote_column_name(primary_key)
394
+
395
+ # e.g. `line_items`.`id`
396
+ line_items_id = line_items_table + "." +
397
+ connection.quote_column_name(line_items_refl.klass.primary_key)
398
+
399
+ # e.g. `line_items`.`ledger_item_id`
400
+ ledger_item_foreign_key = line_items_table + "." + connection.quote_column_name(
401
+ line_items_refl.klass.send(:line_item_class_info).method(:ledger_item_id))
402
+
403
+ payment_classes = select_matching_subclasses(:is_payment, true).map{|c| c.name}
404
+ is_payment_class = merge_conditions({ledger_item_class_info.method(:type) => payment_classes})
405
+
406
+ joins("LEFT JOIN #{line_items_table} ON #{ledger_item_foreign_key} = #{ledger_items_id}").
407
+ where("(#{ledger_item_foreign_key} IS NULL) OR #{is_payment_class}")
408
+ }
409
+ end
410
+
405
411
  # Overrides the default constructor of <tt>ActiveRecord::Base</tt> when +acts_as_ledger_item+
406
412
  # is called. If the +uuid+ gem is installed, this constructor creates a new UUID and assigns
407
413
  # it to the +uuid+ property when a new ledger item model object is created.
@@ -413,7 +419,7 @@ module Invoicing
413
419
  write_attribute(info.method(:uuid), info.uuid_generator.generate)
414
420
  end
415
421
  end
416
-
422
+
417
423
  # Calculate sum of net_amount and tax_amount across all line items, and assign it to total_amount;
418
424
  # calculate sum of tax_amount across all line items, and assign it to tax_amount.
419
425
  # Called automatically as a +before_validation+ callback. If the LedgerItem subtype is +payment+
@@ -423,25 +429,26 @@ module Invoicing
423
429
  return if self.class.is_payment && line_items.empty?
424
430
 
425
431
  net_total = tax_total = BigDecimal('0')
426
-
432
+
427
433
  line_items.each do |line|
428
434
  info = line.send(:line_item_class_info)
429
-
435
+
430
436
  # Make sure ledger_item association is assigned -- the CurrencyValue
431
437
  # getters depend on it to fetch the currency
432
438
  info.set(line, :ledger_item, self)
433
439
  line.valid? # Ensure any before_validation hooks are called
434
-
440
+
435
441
  net_amount = info.get(line, :net_amount)
436
442
  tax_amount = info.get(line, :tax_amount)
437
443
  net_total += net_amount unless net_amount.nil?
438
444
  tax_total += tax_amount unless tax_amount.nil?
439
445
  end
440
-
446
+
441
447
  ledger_item_class_info.set(self, :total_amount, net_total + tax_total)
442
448
  ledger_item_class_info.set(self, :tax_amount, tax_total)
449
+ return net_total
443
450
  end
444
-
451
+
445
452
  # We don't actually implement anything using +method_missing+ at the moment, but use it to
446
453
  # generate slightly more useful error messages in certain cases.
447
454
  def method_missing(method_id, *args)
@@ -461,13 +468,13 @@ module Invoicing
461
468
  tax_amount = ledger_item_class_info.get(self, :tax_amount)
462
469
  (total_amount && tax_amount) ? (total_amount - tax_amount) : nil
463
470
  end
464
-
471
+
465
472
  # +net_amount+ formatted in human-readable form using the ledger item's currency.
466
473
  def net_amount_formatted
467
474
  format_currency_value(net_amount)
468
475
  end
469
-
470
-
476
+
477
+
471
478
  # You must overwrite this method in subclasses of +Invoice+, +CreditNote+ and +Payment+ so that it returns
472
479
  # details of the party sending the document. See +sender_id+ above for a detailed interpretation of
473
480
  # sender and receiver.
@@ -501,28 +508,28 @@ module Invoicing
501
508
  def sender_details
502
509
  raise 'overwrite this method'
503
510
  end
504
-
511
+
505
512
  # You must overwrite this method in subclasses of +Invoice+, +CreditNote+ and +Payment+ so that it returns
506
513
  # details of the party receiving the document. See +recipient_id+ above for a detailed interpretation of
507
514
  # sender and receiver. See +sender_details+ for a list of fields to return in the hash.
508
515
  def recipient_details
509
516
  raise 'overwrite this method'
510
517
  end
511
-
518
+
512
519
  # Returns +true+ if this document was sent by the user with ID +user_id+. If the argument is +nil+
513
520
  # (indicating yourself), this also returns +true+ if <tt>sender_details[:is_self]</tt>.
514
521
  def sent_by?(user_id)
515
522
  (ledger_item_class_info.get(self, :sender_id) == user_id) ||
516
523
  !!(user_id.nil? && ledger_item_class_info.get(self, :sender_details)[:is_self])
517
524
  end
518
-
525
+
519
526
  # Returns +true+ if this document was received by the user with ID +user_id+. If the argument is +nil+
520
527
  # (indicating yourself), this also returns +true+ if <tt>recipient_details[:is_self]</tt>.
521
528
  def received_by?(user_id)
522
529
  (ledger_item_class_info.get(self, :recipient_id) == user_id) ||
523
530
  !!(user_id.nil? && ledger_item_class_info.get(self, :recipient_details)[:is_self])
524
531
  end
525
-
532
+
526
533
  # Returns a boolean which specifies whether this transaction should be recorded as a debit (+true+)
527
534
  # or a credit (+false+) on a particular ledger. Unless you know what you are doing, you probably
528
535
  # do not need to touch this method.
@@ -560,8 +567,8 @@ module Invoicing
560
567
  value = -value if (options[:credit] == :negative) && !debit?(options[:self_id])
561
568
  value
562
569
  end
563
-
564
-
570
+
571
+
565
572
  module ClassMethods
566
573
  # Returns +true+ if this type of ledger item should be recorded as a debit when the party
567
574
  # viewing the account is the sender of the document, and recorded as a credit when
@@ -578,22 +585,22 @@ module Invoicing
578
585
  else nil
579
586
  end
580
587
  end
581
-
588
+
582
589
  # Returns +true+ if this type of ledger item is a +invoice+ subtype, and +false+ otherwise.
583
590
  def is_invoice
584
591
  ledger_item_class_info.subtype == :invoice
585
592
  end
586
-
593
+
587
594
  # Returns +true+ if this type of ledger item is a +credit_note+ subtype, and +false+ otherwise.
588
595
  def is_credit_note
589
596
  ledger_item_class_info.subtype == :credit_note
590
597
  end
591
-
598
+
592
599
  # Returns +true+ if this type of ledger item is a +payment+ subtype, and +false+ otherwise.
593
600
  def is_payment
594
601
  ledger_item_class_info.subtype == :payment
595
602
  end
596
-
603
+
597
604
  # Returns a summary of the customer or supplier account between two parties identified
598
605
  # by +self_id+ (the party from whose perspective the account is seen, 'you') and +other_id+
599
606
  # ('them', your supplier/customer). The return value is a hash with ISO 4217 currency codes
@@ -627,7 +634,7 @@ module Invoicing
627
634
  info = ledger_item_class_info
628
635
  self_id = self_id.to_i
629
636
  other_id = [nil, ''].include?(other_id) ? nil : other_id.to_i
630
-
637
+
631
638
  if other_id.nil?
632
639
  result = {}
633
640
  # Sum over all others, grouped by currency
@@ -641,16 +648,13 @@ module Invoicing
641
648
  end
642
649
  end
643
650
  result
644
-
645
651
  else
646
652
  conditions = {info.method(:sender_id) => [self_id, other_id],
647
653
  info.method(:recipient_id) => [self_id, other_id]}
648
- with_scope(:find => {:conditions => conditions}) do
649
- account_summaries(self_id, options)[other_id] || {}
650
- end
654
+ where(conditions).account_summaries(self_id, options)[other_id] || {}
651
655
  end
652
656
  end
653
-
657
+
654
658
  # Returns a summary account status for all customers or suppliers with which a particular party
655
659
  # has dealings. Takes into account all +closed+ invoices/credit notes and all +cleared+ payments
656
660
  # which have +self_id+ as their +sender_id+ or +recipient_id+. Returns a hash whose keys are the
@@ -678,44 +682,38 @@ module Invoicing
678
682
  def account_summaries(self_id, options={})
679
683
  info = ledger_item_class_info
680
684
  ext = Invoicing::ConnectionAdapterExt
681
- scope = scope(:find)
682
-
685
+
683
686
  debit_classes = select_matching_subclasses(:debit_when_sent_by_self, true, self.table_name, self.inheritance_column).map{|c| c.name}
684
687
  credit_classes = select_matching_subclasses(:debit_when_sent_by_self, false, self.table_name, self.inheritance_column).map{|c| c.name}
685
- debit_when_sent = merge_conditions({info.method(:sender_id) => self_id, info.method(:type) => debit_classes})
686
- debit_when_received = merge_conditions({info.method(:recipient_id) => self_id, info.method(:type) => credit_classes})
687
- credit_when_sent = merge_conditions({info.method(:sender_id) => self_id, info.method(:type) => credit_classes})
688
- credit_when_received = merge_conditions({info.method(:recipient_id) => self_id, info.method(:type) => debit_classes})
688
+
689
+ # rails 3 idiocricies. in case of STI, type of base class is nil. Need special handling
690
+ debit_when_sent = merge_conditions(inheritance_condition(debit_classes), info.method(:sender_id) => self_id)
691
+ debit_when_received = merge_conditions(inheritance_condition(credit_classes), info.method(:recipient_id) => self_id)
692
+ credit_when_sent = merge_conditions(inheritance_condition(credit_classes), info.method(:sender_id) => self_id)
693
+ credit_when_received = merge_conditions(inheritance_condition(debit_classes), info.method(:recipient_id) => self_id)
689
694
 
690
695
  cols = {}
691
696
  [:total_amount, :sender_id, :recipient_id, :status, :currency].each do |col|
692
697
  cols[col] = connection.quote_column_name(info.method(col))
693
698
  end
694
-
699
+
695
700
  sender_is_self = merge_conditions({info.method(:sender_id) => self_id})
696
701
  recipient_is_self = merge_conditions({info.method(:recipient_id) => self_id})
697
702
  other_id_column = ext.conditional_function(sender_is_self, cols[:recipient_id], cols[:sender_id])
698
- accept_status = sanitize_sql_hash_for_conditions(info.method(:status) => (options[:with_status] || %w(closed cleared)))
703
+ accept_status = merge_conditions(info.method(:status) => (options[:with_status] || %w(closed cleared)))
699
704
  filter_conditions = "#{accept_status} AND (#{sender_is_self} OR #{recipient_is_self})"
700
705
 
701
- sql = "SELECT #{other_id_column} AS other_id, #{cols[:currency]} AS currency, " +
706
+ sql = select("#{other_id_column} AS other_id, #{cols[:currency]} AS currency, " +
702
707
  "SUM(#{ext.conditional_function(debit_when_sent, cols[:total_amount], 0)}) AS sales, " +
703
708
  "SUM(#{ext.conditional_function(debit_when_received, cols[:total_amount], 0)}) AS purchase_payments, " +
704
709
  "SUM(#{ext.conditional_function(credit_when_sent, cols[:total_amount], 0)}) AS sale_receipts, " +
705
- "SUM(#{ext.conditional_function(credit_when_received, cols[:total_amount], 0)}) AS purchases " +
706
- "FROM #{(scope && scope[:from]) || quoted_table_name} "
707
-
708
- # Structure borrowed from ActiveRecord::Base.construct_finder_sql
709
- add_joins!(sql, nil, scope)
710
- add_conditions!(sql, filter_conditions, scope)
711
-
712
- sql << " GROUP BY other_id, currency"
713
-
714
- add_order!(sql, nil, scope)
715
- add_limit!(sql, {}, scope)
716
- add_lock!(sql, {}, scope)
717
-
718
- rows = connection.select_all(sql)
710
+ "SUM(#{ext.conditional_function(credit_when_received, cols[:total_amount], 0)}) AS purchases ")
711
+
712
+ sql = sql.where(filter_conditions)
713
+ sql = sql.group("other_id, currency")
714
+
715
+ # add order, limit, and lock from outside
716
+ rows = connection.execute(sql.to_sql).to_a
719
717
 
720
718
  results = {}
721
719
  rows.each do |row|
@@ -723,16 +721,16 @@ module Invoicing
723
721
  other_id = row[:other_id].to_i
724
722
  currency = row[:currency].to_sym
725
723
  summary = {:balance => BigDecimal('0'), :currency => currency}
726
-
724
+
727
725
  {:sales => 1, :purchases => -1, :sale_receipts => -1, :purchase_payments => 1}.each_pair do |field, factor|
728
- summary[field] = BigDecimal(row[field])
726
+ summary[field] = BigDecimal(row[field].to_s)
729
727
  summary[:balance] += BigDecimal(factor.to_s) * summary[field]
730
728
  end
731
-
729
+
732
730
  results[other_id] ||= {}
733
731
  results[other_id][currency] = AccountSummary.new summary
734
732
  end
735
-
733
+
736
734
  results
737
735
  end
738
736
 
@@ -750,7 +748,7 @@ module Invoicing
750
748
  sender_recipient_to_ledger_item_ids = {}
751
749
  result_map = {}
752
750
  info = ledger_item_class_info
753
-
751
+
754
752
  # Find the most recent occurrence of each ID, first in the sender_id column, then in recipient_id
755
753
  [:sender_id, :recipient_id].each do |column|
756
754
  column = info.method(column)
@@ -758,19 +756,19 @@ module Invoicing
758
756
  sql = "SELECT MAX(#{primary_key}) AS id, #{quoted_column} AS ref FROM #{quoted_table_name} WHERE "
759
757
  sql << merge_conditions({column => sender_recipient_ids})
760
758
  sql << " GROUP BY #{quoted_column}"
761
-
759
+
762
760
  ActiveRecord::Base.connection.select_all(sql).each do |row|
763
761
  sender_recipient_to_ledger_item_ids[row['ref'].to_i] = row['id'].to_i
764
762
  end
765
-
763
+
766
764
  sender_recipient_ids -= sender_recipient_to_ledger_item_ids.keys
767
765
  end
768
-
766
+
769
767
  # Load all the ledger items needed to get one representative of each name
770
768
  find(sender_recipient_to_ledger_item_ids.values.uniq).each do |ledger_item|
771
769
  sender_id = info.get(ledger_item, :sender_id)
772
770
  recipient_id = info.get(ledger_item, :recipient_id)
773
-
771
+
774
772
  if sender_recipient_to_ledger_item_ids.include? sender_id
775
773
  details = info.get(ledger_item, :sender_details)
776
774
  result_map[sender_id] = details[:name]
@@ -780,25 +778,48 @@ module Invoicing
780
778
  result_map[recipient_id] = details[:name]
781
779
  end
782
780
  end
783
-
781
+
784
782
  result_map
785
783
  end
786
-
784
+
785
+ def inheritance_condition(classes)
786
+ segments = []
787
+ segments << sanitize_sql(inheritance_column => classes)
788
+
789
+ if classes.include?(self.to_s) && self.new.send(inheritance_column).nil?
790
+ segments << sanitize_sql(type: nil)
791
+ end
792
+
793
+ "(#{segments.join(') OR (')})" unless segments.empty?
794
+ end
795
+
796
+ def merge_conditions(*conditions)
797
+ segments = []
798
+
799
+ conditions.each do |condition|
800
+ unless condition.blank?
801
+ sql = sanitize_sql(condition)
802
+ segments << sql unless sql.blank?
803
+ end
804
+ end
805
+
806
+ "(#{segments.join(') AND (')})" unless segments.empty?
807
+ end
787
808
  end # module ClassMethods
788
-
789
-
809
+
810
+
790
811
  # Very simple class for representing the sum of all sales, purchases and payments on
791
812
  # an account.
792
813
  class AccountSummary #:nodoc:
793
814
  NUM_FIELDS = [:sales, :purchases, :sale_receipts, :purchase_payments, :balance]
794
815
  attr_reader *([:currency] + NUM_FIELDS)
795
-
816
+
796
817
  def initialize(hash)
797
818
  @currency = hash[:currency]; @sales = hash[:sales]; @purchases = hash[:purchases]
798
819
  @sale_receipts = hash[:sale_receipts]; @purchase_payments = hash[:purchase_payments]
799
820
  @balance = hash[:balance]
800
821
  end
801
-
822
+
802
823
  def method_missing(name, *args)
803
824
  if name.to_s =~ /(.*)_formatted$/
804
825
  ::Invoicing::CurrencyValue::Formatter.format_value(currency, send($1))
@@ -806,13 +827,13 @@ module Invoicing
806
827
  super
807
828
  end
808
829
  end
809
-
830
+
810
831
  def +(other)
811
832
  hash = {:currency => currency}
812
833
  NUM_FIELDS.each {|field| hash[field] = send(field) + other.send(field) }
813
834
  AccountSummary.new hash
814
835
  end
815
-
836
+
816
837
  def to_s
817
838
  NUM_FIELDS.map do |field|
818
839
  val = send("#{field}_formatted")
@@ -820,24 +841,24 @@ module Invoicing
820
841
  end.join('; ')
821
842
  end
822
843
  end
823
-
824
-
844
+
845
+
825
846
  # Stores state in the ActiveRecord class object
826
847
  class ClassInfo < Invoicing::ClassInfo::Base #:nodoc:
827
848
  attr_reader :subtype, :uuid_generator
828
-
849
+
829
850
  def initialize(model_class, previous_info, args)
830
851
  super
831
852
  @subtype = all_options[:subtype]
832
-
853
+
833
854
  begin # try to load the UUID gem
834
855
  require 'uuid'
835
856
  @uuid_generator = UUID.new
836
- rescue LoadError, NameError # silently ignore if gem not found
857
+ rescue LoadError, NameError # silently ignore if gem not found
837
858
  @uuid_generator = nil
838
859
  end
839
860
  end
840
-
861
+
841
862
  # Allow methods generated by +CurrencyValue+ to be renamed as well
842
863
  def method(name)
843
864
  if name.to_s =~ /^(.*)_formatted$/