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,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$/