invoicing 0.2.1 → 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/LICENSE +1 -0
- data/README.md +57 -0
- data/Rakefile +16 -37
- data/lib/invoicing.rb +20 -10
- data/lib/invoicing/cached_record.rb +9 -6
- data/lib/invoicing/class_info.rb +34 -34
- data/lib/invoicing/connection_adapter_ext.rb +4 -4
- data/lib/invoicing/countries/uk.rb +6 -6
- data/lib/invoicing/currency_value.rb +39 -32
- data/lib/invoicing/find_subclasses.rb +40 -15
- data/lib/invoicing/ledger_item.rb +166 -145
- data/lib/invoicing/ledger_item/pdf_generator.rb +108 -0
- data/lib/invoicing/ledger_item/render_html.rb +76 -73
- data/lib/invoicing/ledger_item/render_ubl.rb +37 -35
- data/lib/invoicing/line_item.rb +43 -38
- data/lib/invoicing/price.rb +1 -1
- data/lib/invoicing/tax_rate.rb +3 -6
- data/lib/invoicing/taxable.rb +37 -32
- data/lib/invoicing/time_dependent.rb +40 -40
- data/lib/invoicing/version.rb +4 -4
- data/lib/rails/generators/invoicing/invoicing_generator.rb +14 -0
- data/lib/rails/generators/invoicing/ledger_item/ledger_item_generator.rb +17 -0
- data/lib/rails/generators/invoicing/ledger_item/templates/migration.rb +25 -0
- data/lib/rails/generators/invoicing/ledger_item/templates/model.rb +5 -0
- data/lib/rails/generators/invoicing/line_item/line_item_generator.rb +17 -0
- data/lib/rails/generators/invoicing/line_item/templates/migration.rb +20 -0
- data/lib/rails/generators/invoicing/line_item/templates/model.rb +5 -0
- data/lib/rails/generators/invoicing/tax_rate/tax_rate_generator.rb +17 -0
- data/lib/rails/generators/invoicing/tax_rate/templates/migration.rb +14 -0
- data/lib/rails/generators/invoicing/tax_rate/templates/model.rb +3 -0
- metadata +110 -153
- data.tar.gz.sig +0 -1
- data/History.txt +0 -31
- data/Manifest.txt +0 -62
- data/PostInstall.txt +0 -10
- data/README.rdoc +0 -58
- data/script/console +0 -10
- data/script/destroy +0 -14
- data/script/generate +0 -14
- data/tasks/rcov.rake +0 -4
- data/test/cached_record_test.rb +0 -100
- data/test/class_info_test.rb +0 -253
- data/test/connection_adapter_ext_test.rb +0 -79
- data/test/currency_value_test.rb +0 -209
- data/test/find_subclasses_test.rb +0 -120
- data/test/fixtures/README +0 -7
- data/test/fixtures/cached_record.sql +0 -22
- data/test/fixtures/class_info.sql +0 -28
- data/test/fixtures/currency_value.sql +0 -29
- data/test/fixtures/find_subclasses.sql +0 -43
- data/test/fixtures/ledger_item.sql +0 -39
- data/test/fixtures/line_item.sql +0 -33
- data/test/fixtures/price.sql +0 -4
- data/test/fixtures/tax_rate.sql +0 -4
- data/test/fixtures/taxable.sql +0 -14
- data/test/fixtures/time_dependent.sql +0 -35
- data/test/ledger_item_test.rb +0 -444
- data/test/line_item_test.rb +0 -139
- data/test/models/README +0 -4
- data/test/models/test_subclass_in_another_file.rb +0 -3
- data/test/models/test_subclass_not_in_database.rb +0 -6
- data/test/price_test.rb +0 -9
- data/test/ref-output/creditnote3.html +0 -82
- data/test/ref-output/creditnote3.xml +0 -89
- data/test/ref-output/invoice1.html +0 -93
- data/test/ref-output/invoice1.xml +0 -111
- data/test/ref-output/invoice2.html +0 -86
- data/test/ref-output/invoice2.xml +0 -98
- data/test/ref-output/invoice_null.html +0 -36
- data/test/render_html_test.rb +0 -70
- data/test/render_ubl_test.rb +0 -44
- data/test/setup.rb +0 -37
- data/test/tax_rate_test.rb +0 -9
- data/test/taxable_test.rb +0 -180
- data/test/test_helper.rb +0 -72
- data/test/time_dependent_test.rb +0 -180
- metadata.gz.sig +0 -4
@@ -1,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
|
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
|
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
|
-
|
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
|
-
|
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
|
-
|
686
|
-
|
687
|
-
|
688
|
-
|
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 =
|
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 = "
|
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
|
-
|
707
|
-
|
708
|
-
|
709
|
-
|
710
|
-
|
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$/
|