invoicing 0.1.0 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
Binary file
@@ -0,0 +1,21 @@
1
+ == 0.2.0 2009-04-20
2
+
3
+ * 4 major enhancements:
4
+ * New associated gem invoicing_generator for generating an invoicing component in a Rails project
5
+ (script/generate invoicing_ledger)
6
+ * Generated controller and views for rendering statements and ledger
7
+ * Comes with a nice default stylesheet out of the box
8
+ * More flexible formatting of currency values
9
+ * 1 bugfix:
10
+ * Accidental overwriting of total_amount value on payment LedgerItems without LineItems
11
+
12
+ == 0.1.0 2009-02-10
13
+
14
+ * 2 major enhancements:
15
+ * Core API is now usable
16
+ * RCov reports 100% test coverage
17
+
18
+ == 0.0.1 2009-01-05
19
+
20
+ * 1 major enhancement:
21
+ * Initial public release
@@ -1,29 +1,35 @@
1
- CHANGELOG
1
+ History.txt
2
+ LICENSE
3
+ Manifest.txt
4
+ PostInstall.txt
5
+ README.rdoc
6
+ Rakefile
7
+ lib/invoicing.rb
2
8
  lib/invoicing/cached_record.rb
3
9
  lib/invoicing/class_info.rb
4
10
  lib/invoicing/connection_adapter_ext.rb
5
11
  lib/invoicing/countries/uk.rb
6
12
  lib/invoicing/currency_value.rb
7
13
  lib/invoicing/find_subclasses.rb
14
+ lib/invoicing/ledger_item.rb
8
15
  lib/invoicing/ledger_item/render_html.rb
9
16
  lib/invoicing/ledger_item/render_ubl.rb
10
- lib/invoicing/ledger_item.rb
11
17
  lib/invoicing/line_item.rb
12
18
  lib/invoicing/price.rb
13
19
  lib/invoicing/tax_rate.rb
14
20
  lib/invoicing/taxable.rb
15
21
  lib/invoicing/time_dependent.rb
16
22
  lib/invoicing/version.rb
17
- lib/invoicing.rb
18
- LICENSE
19
- Manifest
20
- Rakefile
21
- README
23
+ script/console
24
+ script/destroy
25
+ script/generate
26
+ tasks/rcov.rake
22
27
  test/cached_record_test.rb
23
28
  test/class_info_test.rb
24
29
  test/connection_adapter_ext_test.rb
25
30
  test/currency_value_test.rb
26
31
  test/find_subclasses_test.rb
32
+ test/fixtures/README
27
33
  test/fixtures/cached_record.sql
28
34
  test/fixtures/class_info.sql
29
35
  test/fixtures/currency_value.sql
@@ -31,7 +37,6 @@ test/fixtures/find_subclasses.sql
31
37
  test/fixtures/ledger_item.sql
32
38
  test/fixtures/line_item.sql
33
39
  test/fixtures/price.sql
34
- test/fixtures/README
35
40
  test/fixtures/tax_rate.sql
36
41
  test/fixtures/taxable.sql
37
42
  test/fixtures/time_dependent.sql
@@ -55,6 +60,3 @@ test/tax_rate_test.rb
55
60
  test/taxable_test.rb
56
61
  test/test_helper.rb
57
62
  test/time_dependent_test.rb
58
- website/curvycorners.js
59
- website/screen.css
60
- website/template.html.erb
@@ -0,0 +1,10 @@
1
+
2
+ Thanks for trying the Ruby Invoicing Gem. Hope you find it useful.
3
+ Do please blog and tweet about it (tag your posts with #invgem).
4
+
5
+ Now please go to the root of your Rails project and type:
6
+ script/generate invoicing_ledger billing --currency=GBP
7
+ (replace GBP with your currency if you do not use Pounds Sterling).
8
+ For more information please go to http://ept.github.com/invoicing/
9
+
10
+
@@ -0,0 +1,55 @@
1
+ = Ruby Invoicing Framework
2
+
3
+ * {Ruby Invoicing Framework website}[http://ept.github.com/invoicing/]
4
+ * {API Reference Docs}[http://invoicing.rubyforge.org/doc/]
5
+ * {Browse the code on GitHub}[http://github.com/ept/invoicing/]
6
+ * {RubyForge project}[http://rubyforge.org/projects/invoicing/]
7
+ * Email: Martin Kleppmann <ept@rubyforge.org>
8
+
9
+ == DESCRIPTION
10
+
11
+ This is a framework for generating and displaying invoices (ideal for
12
+ commercial Rails apps). It allows for flexible business logic; provides tools
13
+ for tax handling, commission calculation etc. It aims to be both
14
+ developer-friendly and accountant-friendly.
15
+
16
+ The Ruby Invoicing Framework is based on
17
+ {ActiveRecord}[http://api.rubyonrails.org/classes/ActiveRecord/Base.html].
18
+
19
+ Please see {the website}[http://ept.github.com/invoicing/] for an introduction
20
+ to using Invoicing, and check the
21
+ {API reference}[http://invoicing.rubyforge.org/doc/] for in-depth details.
22
+
23
+ == FEATURES
24
+
25
+ * TODO
26
+
27
+ == REQUIREMENTS
28
+
29
+ * ActiveRecord >= 2.1
30
+ * Only MySQL and PostgreSQL databases are currently supported
31
+
32
+ == INSTALL
33
+
34
+ sudo gem install invoicing
35
+
36
+ == STATUS
37
+
38
+ So far, the Ruby Invoicing Framework has been tested with ActiveRecord 2.2.2,
39
+ MySQL 5.0.67 and PostgreSQL 8.3.5. We will be testing it across a wider
40
+ variety of versions soon.
41
+
42
+ == CREDITS
43
+
44
+ The Ruby invoicing framework originated as part of the website
45
+ {Bid for Wine}[http://www.bidforwine.co.uk], developed by Patrick Dietrich,
46
+ Conrad Irwin, Michael Arnold and Martin Kleppmann for Ept Computing Ltd.
47
+ It was extracted from the Bid for Wine codebase and substantially extended
48
+ by Martin Kleppmann.
49
+
50
+ == LICENSE
51
+
52
+ Copyright (c) 2009 Martin Kleppmann, Ept Computing Limited.
53
+
54
+ This gem is made publicly available under the terms of the MIT license.
55
+ See LICENSE and/or COPYING for details.
data/Rakefile CHANGED
@@ -1,75 +1,37 @@
1
- require 'rubygems'
2
- require 'echoe'
1
+ %w[rubygems rake rake/clean fileutils newgem rubigen].each { |f| require f }
2
+ require File.dirname(__FILE__) + '/lib/invoicing'
3
3
 
4
- # Add the project's top level directory and the lib directory to the Ruby search path
5
- $: << File.expand_path(File.join(File.dirname(__FILE__), "lib"))
6
- $: << File.expand_path(File.dirname(__FILE__))
7
-
8
- require 'invoicing'
9
-
10
- Echoe.new('invoicing', Invoicing::VERSION) do |p|
11
- p.summary = 'Ruby invoicing framework'
12
- p.description = 'Provides tools for applications which need to generate invoices for customers.'
13
- p.url = 'http://invoicing.rubyforge.org/'
14
- p.author = 'Martin Kleppmann'
15
- p.email = 'rubyforge@eptcomputing.com'
16
- p.dependencies = ['activerecord >=2.1.0', 'builder >= 2.0']
17
- p.docs_host = 'ept@rubyforge.org:/var/www/gforge-projects/'
18
- p.test_pattern = 'test/*_test.rb' # do not include test/models/*.rb
19
- p.rcov_options = "-x '/Library/'"
4
+ # Hoe calls Ruby with the "-w" set by default; unfortunately, ActiveRecord (at version 2.2.2
5
+ # at least) causes a lot of warnings internally, by no fault of our own, which clutters
6
+ # the output. Comment out the following four lines to see those warnings.
7
+ class Hoe
8
+ RUBY_FLAGS = ENV['RUBY_FLAGS'] || "-I#{%w(lib test).join(File::PATH_SEPARATOR)}" +
9
+ (RUBY_DEBUG ? " #{RUBY_DEBUG}" : '')
20
10
  end
21
11
 
12
+ # Generate all the Rake tasks
13
+ # Run 'rake -T' to see list of generated tasks (from gem root directory)
14
+ $hoe = Hoe.new('invoicing', Invoicing::VERSION) do |p|
15
+ p.developer('Martin Kleppmann', 'rubyforge@eptcomputing.com')
16
+ p.changes = p.paragraphs_of("History.txt", 0..1).join("\n\n")
17
+ p.post_install_message = 'PostInstall.txt'
18
+ p.rubyforge_name = p.name
19
+ p.extra_deps = [
20
+ ['activerecord', '>= 2.1.0'],
21
+ ['builder', '>= 2.0']
22
+ ]
23
+ p.extra_dev_deps = [
24
+ ['newgem', ">= #{::Newgem::VERSION}"]
25
+ #['invoicing_generator', "= #{Invoicing::VERSION}"] - causes a circular dependency in rubygems < 1.2
26
+ ]
22
27
 
23
- desc "Generate a new website from README file"
24
- task 'website' do
25
- require 'rubygems'
26
- require 'redcloth'
27
- require 'syntax/convertors/html'
28
- require 'erb'
29
-
30
- version = Invoicing::VERSION
31
- download = 'http://rubyforge.org/projects/invoicing'
32
-
33
- def convert_syntax(syntax, source)
34
- return Syntax::Convertors::HTML.for_syntax(syntax).convert(source).gsub(%r!^<pre>|</pre>$!,'')
35
- end
36
-
37
- template = ERB.new(File.open(File.join(File.dirname(__FILE__), '/website/template.html.erb')).read)
38
-
39
- title = nil
40
- body = nil
41
- File.open(File.join(File.dirname(__FILE__), '/README')) do |fsrc|
42
- title = fsrc.readline.gsub(/^[^ ]* /, '')
43
- body_text = fsrc.read
44
- syntax_items = []
45
- body_text.gsub!(%r!<(pre|code)[^>]*?syntax=['"]([^'"]+)[^>]*>(.*?)</\1>!m){
46
- ident = syntax_items.length
47
- element, syntax, source = $1, $2, $3
48
- syntax_items << "<#{element} class='syntax'>#{convert_syntax(syntax, source)}</#{element}>"
49
- "syntax-temp-#{ident}"
50
- }
51
- body = RedCloth.new(body_text).to_html
52
- body.gsub!(%r!(?:<pre><code>)?syntax-temp-(\d+)(?:</code></pre>)?!){ syntax_items[$1.to_i] }
53
- end
54
-
55
- File.open(File.join(File.dirname(__FILE__), '/website/index.html'), 'w') do |fout|
56
- fout.write(template.result(binding))
57
- end
28
+ p.test_globs = %w[test/*_test.rb] # do not include test/models/*.rb
29
+ p.clean_globs |= %w[**/.DS_Store tmp *.log coverage]
30
+ p.rsync_args = '-av --delete --ignore-errors'
58
31
  end
59
32
 
33
+ require 'newgem/tasks' # load /tasks/*.rake
34
+ Dir['tasks/**/*.rake'].each { |t| load t }
60
35
 
61
- desc "Generate and publish website"
62
- task :publish_website => :website do
63
- require 'net/sftp'
64
- upload_user = 'ept'
65
- upload_host = 'rubyforge.org'
66
- upload_dir = '/var/www/gforge-projects/invoicing'
67
- local_dir = File.join(File.dirname(__FILE__), 'website')
68
- Net::SFTP.start(upload_host, upload_user) do |sftp|
69
- for f in Dir.entries(local_dir)
70
- next if f =~ /^\./
71
- puts "Uploading #{f} to #{upload_user}@#{upload_host}:#{upload_dir}/#{f}"
72
- sftp.upload!(File.join(local_dir, f), "#{upload_dir}/#{f}")
73
- end
74
- end
75
- end
36
+ # Tasks to run by default
37
+ # task :default => [:spec, :features]
@@ -84,6 +84,31 @@ module Invoicing
84
84
  # validates_inclusion_of :currency_code, %w( USD GBP EUR JPY )
85
85
  # acts_as_currency_value :net_amount, :tax_amount, :currency => :currency_code
86
86
  # end
87
+ #
88
+ # You may also specify the <tt>:value_for_formatting</tt> option, passing it the name of a method on
89
+ # your model object. That method will be called when a CurrencyValue method with +_formatted+ suffix
90
+ # is called, and allows you to modify the numerical value before it is formatted into a string. An
91
+ # options hash is also passed. This can be useful, for example, if a value is stored positive but you
92
+ # want to display it as negative in certain circumstances depending on the view:
93
+ #
94
+ # class LedgerItem < ActiveRecord::Base
95
+ # acts_as_ledger_item
96
+ # acts_as_currency_value :total_amount, :tax_amount, :value_for_formatting => :value_for_formatting
97
+ #
98
+ # def value_for_formatting(value, options={})
99
+ # value *= -1 if options[:debit] == :negative && debit?(options[:self_id])
100
+ # value *= -1 if options[:credit] == :negative && !debit?(options[:self_id])
101
+ # value
102
+ # end
103
+ # end
104
+ #
105
+ # invoice = Invoice.find(1)
106
+ # invoice.total_amount_formatted :debit => :negative, :self_id => invoice.sender_id
107
+ # # => '$25.00'
108
+ # invoice.total_amount_formatted :debit => :negative, :self_id => invoice.recipient_id
109
+ # # => '-$25.00'
110
+ #
111
+ # (The example above is actually a real part of +LedgerItem+.)
87
112
  def acts_as_currency_value(*args)
88
113
  Invoicing::ClassInfo.acts_as(Invoicing::CurrencyValue, self, args)
89
114
 
@@ -94,8 +119,8 @@ module Invoicing
94
119
 
95
120
  # Format a numeric monetary value into a human-readable string, in the currency of the
96
121
  # current model object.
97
- def format_currency_value(value)
98
- currency_value_class_info.format_value(self, value)
122
+ def format_currency_value(value, options={})
123
+ currency_value_class_info.format_value(self, value, options)
99
124
  end
100
125
 
101
126
 
@@ -109,6 +134,73 @@ module Invoicing
109
134
  protected :write_back_currency_values
110
135
 
111
136
 
137
+ # Encapsulates the methods for formatting currency values in a human-friendly way.
138
+ # These methods do not depend on ActiveRecord and can thus also be called externally.
139
+ module Formatter
140
+ class << self
141
+
142
+ # Given the three-letter ISO 4217 code of a currency, returns a hash with useful bits of information:
143
+ # <tt>:code</tt>:: The ISO 4217 code of the currency.
144
+ # <tt>:round</tt>:: Smallest unit of the currency in normal use, to which values are rounded. Default is 0.01.
145
+ # <tt>:symbol</tt>:: Symbol or string usually used to denote the currency. Encoded as UTF-8. Default is ISO 4217 code.
146
+ # <tt>:suffix</tt>:: +true+ if the currency symbol appears after the number, +false+ if it appears before.
147
+ # <tt>:space</tt>:: Whether or not to leave a space between the number and the currency symbol.
148
+ # <tt>:digits</tt>:: Number of digits to display after the decimal point.
149
+ def currency_info(code, options={})
150
+ code = code.to_s.upcase
151
+ valid_options = [:symbol, :round, :suffix, :space, :digits]
152
+ info = {:code => code, :symbol => code, :round => 0.01, :suffix => nil, :space => nil, :digits => nil}
153
+ if ::Invoicing::CurrencyValue::CURRENCIES.has_key? code
154
+ info.update(::Invoicing::CurrencyValue::CURRENCIES[code])
155
+ end
156
+ options.each_pair {|key, value| info[key] = value if valid_options.include? key }
157
+
158
+ info[:suffix] ||= (info[:code] == info[:symbol]) && !info[:code].nil?
159
+ info[:space] ||= info[:suffix]
160
+ info[:digits] = -Math.log10(info[:round]).floor if info[:digits].nil?
161
+ info[:digits] = 0 if info[:digits] < 0
162
+
163
+ info
164
+ end
165
+
166
+ # Given the three-letter ISO 4217 code of a currency and a BigDecimal value, returns the
167
+ # value formatted as an UTF-8 string, ready for human consumption.
168
+ #
169
+ # FIXME: This method currently does not take locale into account -- it always uses the dot
170
+ # as decimal separator and the comma as thousands separator.
171
+ def format_value(currency_code, value, options={})
172
+ info = currency_info(currency_code, options)
173
+
174
+ negative = false
175
+ if value < 0
176
+ negative = true
177
+ value = -value
178
+ end
179
+
180
+ value = "%.#{info[:digits]}f" % value
181
+ while value.sub!(/(\d+)(\d\d\d)/, '\1,\2'); end
182
+ value.sub!(/^\-/, '') # avoid displaying minus zero
183
+
184
+ formatted = if ['', nil].include? info[:symbol]
185
+ value
186
+ elsif info[:space]
187
+ info[:suffix] ? "#{value} #{info[:symbol]}" : "#{info[:symbol]} #{value}"
188
+ else
189
+ info[:suffix] ? "#{value}#{info[:symbol]}" : "#{info[:symbol]}#{value}"
190
+ end
191
+
192
+ if negative
193
+ # default is to use proper unicode minus sign
194
+ formatted = (options[:negative] == :brackets) ? "(#{formatted})" : (
195
+ (options[:negative] == :hyphen) ? "-#{formatted}" : "\xE2\x88\x92#{formatted}"
196
+ )
197
+ end
198
+ formatted
199
+ end
200
+ end
201
+ end
202
+
203
+
112
204
  class ClassInfo < Invoicing::ClassInfo::Base #:nodoc:
113
205
 
114
206
  def initialize(model_class, previous_info, args)
@@ -132,11 +224,18 @@ module Invoicing
132
224
  write_attribute(attr, new_value)
133
225
  end
134
226
 
135
- define_method("#{attr}_formatted") do
136
- begin
137
- format_currency_value(Kernel.Float(send("#{attr}_before_type_cast")))
227
+ define_method("#{attr}_formatted") do |*args|
228
+ options = args.first || {}
229
+ value_as_float = begin
230
+ Kernel.Float(send("#{attr}_before_type_cast"))
138
231
  rescue ArgumentError, TypeError
139
- '' # if <attr>_before_type_cast could not be converted to float
232
+ nil
233
+ end
234
+
235
+ if value_as_float.nil?
236
+ ''
237
+ else
238
+ format_currency_value(value_as_float, options.merge({:method_name => attr}))
140
239
  end
141
240
  end
142
241
  end
@@ -153,43 +252,20 @@ module Invoicing
153
252
  end
154
253
  end
155
254
 
156
- # Returns a hash of information about the currency used by model +object+. Contains the following keys:
157
- # <tt>:code</tt>:: The ISO 4217 code of the currency.
158
- # <tt>:round</tt>:: Smallest unit of the currency in normal use, to which values are rounded. Default is 0.01.
159
- # <tt>:symbol</tt>:: Symbol or string usually used to denote the currency. Encoded as UTF-8. Default is ISO 4217 code.
160
- # <tt>:suffix</tt>:: +true+ if the currency symbol appears after the number, +false+ if it appears before.
161
- # <tt>:space</tt>:: Whether or not to leave a space between the number and the currency symbol.
162
- # <tt>:digits</tt>:: Number of digits to display after the decimal point.
255
+ # Returns a hash of information about the currency used by model +object+.
163
256
  def currency_info_for(object)
164
- valid_options = [:symbol, :round, :suffix, :space, :digits, :format]
165
- code = currency_of(object)
166
- info = {:code => code, :symbol => code, :round => 0.01, :suffix => nil, :space => nil, :digits => nil}
167
- if ::Invoicing::CurrencyValue::CURRENCIES.has_key? code
168
- info.update(::Invoicing::CurrencyValue::CURRENCIES[code])
169
- end
170
- all_options.each_pair {|key, value| info[key] = value if valid_options.include? key }
171
-
172
- info[:suffix] = true if info[:suffix].nil? && (info[:code] == info[:symbol]) && !info[:code].nil?
173
- info[:space] = true if info[:space].nil? && info[:suffix]
174
- info[:digits] = -Math.log10(info[:round]).floor if info[:digits].nil?
175
- info[:digits] = 0 if info[:digits] < 0
176
-
177
- info
257
+ ::Invoicing::CurrencyValue::Formatter.currency_info(currency_of(object), all_options)
178
258
  end
179
259
 
180
260
  # Formats a numeric value as a nice currency string in UTF-8 encoding.
181
261
  # +object+ is the model object carrying the value (used to determine the currency).
182
- def format_value(object, value)
183
- info = currency_info_for(object)
184
-
185
- # FIXME: take locale into account
186
- value = "%.#{info[:digits]}f" % value
187
- while value.sub!(/(\d+)(\d\d\d)/,'\1,\2'); end
188
- if info[:space]
189
- info[:suffix] ? "#{value} #{info[:symbol]}" : "#{info[:symbol]} #{value}"
190
- else
191
- info[:suffix] ? "#{value}#{info[:symbol]}" : "#{info[:symbol]}#{value}"
262
+ def format_value(object, value, options={})
263
+ options = all_options.merge(options).symbolize_keys
264
+ intercept = options[:value_for_formatting]
265
+ if intercept && object.respond_to?(intercept)
266
+ value = object.send(intercept, value, options)
192
267
  end
268
+ ::Invoicing::CurrencyValue::Formatter.format_value(currency_of(object), value, options)
193
269
  end
194
270
 
195
271
  # If other modules have registered callbacks for the event of reading a rounded attribute,
@@ -310,7 +310,7 @@ module Invoicing
310
310
 
311
311
  # Set the 'amount' columns to act as currency values
312
312
  acts_as_currency_value(info.method(:total_amount), info.method(:tax_amount),
313
- :currency => info.method(:currency))
313
+ :currency => info.method(:currency), :value_for_formatting => :value_for_formatting)
314
314
 
315
315
  extend Invoicing::FindSubclasses
316
316
  include Invoicing::LedgerItem::RenderHTML
@@ -416,10 +416,13 @@ module Invoicing
416
416
 
417
417
  # Calculate sum of net_amount and tax_amount across all line items, and assign it to total_amount;
418
418
  # calculate sum of tax_amount across all line items, and assign it to tax_amount.
419
- # Called automatically as a +before_validation+ callback.
419
+ # Called automatically as a +before_validation+ callback. If the LedgerItem subtype is +payment+
420
+ # and there are no line items then the total amount is not touched.
420
421
  def calculate_total_amount
421
- net_total = tax_total = BigDecimal('0')
422
422
  line_items = ledger_item_class_info.get(self, :line_items)
423
+ return if self.class.is_payment && line_items.empty?
424
+
425
+ net_total = tax_total = BigDecimal('0')
423
426
 
424
427
  line_items.each do |line|
425
428
  info = line.send(:line_item_class_info)
@@ -544,6 +547,17 @@ module Invoicing
544
547
  raise ArgumentError, "self_id #{self_id.inspect} is both sender and recipient" if sender_is_self && recipient_is_self
545
548
  self.class.debit_when_sent_by_self ? sender_is_self : recipient_is_self
546
549
  end
550
+
551
+ # Invoked internally when +total_amount_formatted+ or +tax_amount_formatted+ is called. Allows
552
+ # you to specify options like <tt>:debit => :negative, :self_id => 42</tt> meaning that if this
553
+ # ledger item is a debit as regarded from the point of view of +self_id+ then it should be
554
+ # displayed as a negative number. Note this only affects the output formatting, not the actual
555
+ # stored values.
556
+ def value_for_formatting(value, options={})
557
+ value = -value if (options[:debit] == :negative) && debit?(options[:self_id])
558
+ value = -value if (options[:credit] == :negative) && !debit?(options[:self_id])
559
+ value
560
+ end
547
561
 
548
562
 
549
563
  module ClassMethods
@@ -580,29 +594,52 @@ module Invoicing
580
594
 
581
595
  # Returns a summary of the customer or supplier account between two parties identified
582
596
  # by +self_id+ (the party from whose perspective the account is seen, 'you') and +other_id+
583
- # ('them', your supplier/customer). The return value is a hash with the following structure:
597
+ # ('them', your supplier/customer). The return value is a hash with ISO 4217 currency codes
598
+ # as keys (as symbols), and summary objects as values. An account using only one currency
599
+ # will have only one entry in the hash, but more complex accounts may have several.
600
+ #
601
+ # The summary object has the following methods:
584
602
  #
585
- # { :GBP => { # ISO 4217 currency code for the following figures
586
- # :sales => BigDecimal(...), # Sum of sales (invoices sent by self_id)
587
- # :purchases => BigDecimal(...), # Sum of purchases (invoices received by self_id)
588
- # :sale_receipts => BigDecimal(...), # Sum of payments received from customer
589
- # :purchase_payments => BigDecimal(...), # Sum of payments made to supplier
590
- # :balance => BigDecimal(...) # sales - purchases - sale_receipts + purchase_payments
591
- # },
592
- # :USD => { # Another block as above, if the account uses
593
- # ... # multiple currencies
594
- # }}
603
+ # currency => symbol # Same as the key of this hash entry
604
+ # sales => BigDecimal(...) # Sum of sales (invoices sent by self_id)
605
+ # purchases => BigDecimal(...) # Sum of purchases (invoices received by self_id)
606
+ # sale_receipts => BigDecimal(...) # Sum of payments received from customer
607
+ # purchase_payments => BigDecimal(...) # Sum of payments made to supplier
608
+ # balance => BigDecimal(...) # sales - purchases - sale_receipts + purchase_payments
595
609
  #
596
610
  # The <tt>:balance</tt> fields indicate any outstanding money owed on the account: the value is
597
611
  # positive if they owe you money, and negative if you owe them money.
598
- def account_summary(self_id, other_id)
612
+ #
613
+ # In addition, +acts_as_currency_value+ is set on the numeric fields, so you can use its
614
+ # convenience methods such as +summary.sales_formatted+.
615
+ #
616
+ # If +other_id+ is +nil+, this method aggregates the accounts of +self_id+ with *all* other
617
+ # parties.
618
+ def account_summary(self_id, other_id=nil)
599
619
  info = ledger_item_class_info
600
- conditions = {info.method(:sender_id) => [self_id, other_id],
601
- info.method(:recipient_id) => [self_id, other_id]}
602
-
603
- with_scope :find => {:conditions => conditions} do
604
- summaries = account_summaries(self_id)
605
- summaries[other_id] || {}
620
+ self_id = self_id.to_i
621
+ other_id = [nil, ''].include?(other_id) ? nil : other_id.to_i
622
+
623
+ if other_id.nil?
624
+ result = {}
625
+ # Sum over all others, grouped by currency
626
+ account_summaries(self_id).each_pair do |other_id, hash|
627
+ hash.each_pair do |currency, summary|
628
+ if result[currency]
629
+ result[currency] += summary
630
+ else
631
+ result[currency] = summary
632
+ end
633
+ end
634
+ end
635
+ result
636
+
637
+ else
638
+ conditions = {info.method(:sender_id) => [self_id, other_id],
639
+ info.method(:recipient_id) => [self_id, other_id]}
640
+ with_scope(:find => {:conditions => conditions}) do
641
+ account_summaries(self_id)[other_id] || {}
642
+ end
606
643
  end
607
644
  end
608
645
 
@@ -611,11 +648,11 @@ module Invoicing
611
648
  # which have +self_id+ as their +sender_id+ or +recipient_id+. Returns a hash whose keys are the
612
649
  # other party of each account (i.e. the value of +sender_id+ or +recipient_id+ which is not
613
650
  # +self_id+, as an integer), and whose values are again hashes, of the same form as returned by
614
- # +account_summary+:
651
+ # +account_summary+ (+summary+ objects as documented on +account_summary+):
615
652
  #
616
653
  # LedgerItem.account_summaries(1)
617
- # # => { 2 => { :USD => { :sales => ... }, :EUR => { :sales => ... } },
618
- # # 3 => { :EUR => { :sales => ... } } }
654
+ # # => { 2 => { :USD => summary, :EUR => summary },
655
+ # # 3 => { :EUR => summary } }
619
656
  #
620
657
  # If you want to further restrict the ledger items taken into account in this calculation (e.g.
621
658
  # include only data from a particular quarter) you can call this method within an ActiveRecord
@@ -646,8 +683,6 @@ module Invoicing
646
683
  other_id_column = ext.conditional_function(sender_is_self, cols[:recipient_id], cols[:sender_id])
647
684
  filter_conditions = "#{cols[:status]} IN ('closed','cleared') AND (#{sender_is_self} OR #{recipient_is_self})"
648
685
 
649
-
650
-
651
686
  sql = "SELECT #{other_id_column} AS other_id, #{cols[:currency]} AS currency, " +
652
687
  "SUM(#{ext.conditional_function(debit_when_sent, cols[:total_amount], 0)}) AS sales, " +
653
688
  "SUM(#{ext.conditional_function(debit_when_received, cols[:total_amount], 0)}) AS purchase_payments, " +
@@ -672,7 +707,7 @@ module Invoicing
672
707
  row.symbolize_keys!
673
708
  other_id = row[:other_id].to_i
674
709
  currency = row[:currency].to_sym
675
- summary = {:balance => BigDecimal('0')}
710
+ summary = {:balance => BigDecimal('0'), :currency => currency}
676
711
 
677
712
  {:sales => 1, :purchases => -1, :sale_receipts => -1, :purchase_payments => 1}.each_pair do |field, factor|
678
713
  summary[field] = BigDecimal(row[field])
@@ -680,11 +715,95 @@ module Invoicing
680
715
  end
681
716
 
682
717
  results[other_id] ||= {}
683
- results[other_id][currency] = summary
718
+ results[other_id][currency] = AccountSummary.new summary
684
719
  end
685
720
 
686
721
  results
687
722
  end
723
+
724
+ # Takes an array of IDs like those used in +sender_id+ and +recipient_id+, and returns a hash
725
+ # which maps each of these IDs (typecast to integer) to the <tt>:name</tt> field of the
726
+ # hash returned by +sender_details+ or +recipient_details+ for that ID. This is useful as it
727
+ # allows +LedgerItem+ to use human-readable names for people or organisations in its output,
728
+ # without depending on a particular implementation of the model objects used to store those
729
+ # entities.
730
+ #
731
+ # LedgerItem.sender_recipient_name_map [2, 4]
732
+ # => {2 => "Fast Flowers Ltd.", 4 => "Speedy Motors"}
733
+ def sender_recipient_name_map(*sender_recipient_ids)
734
+ sender_recipient_ids = sender_recipient_ids.flatten.map &:to_i
735
+ sender_recipient_to_ledger_item_ids = {}
736
+ result_map = {}
737
+ info = ledger_item_class_info
738
+
739
+ # Find the most recent occurrence of each ID, first in the sender_id column, then in recipient_id
740
+ [:sender_id, :recipient_id].each do |column|
741
+ column = info.method(column)
742
+ quoted_column = connection.quote_column_name(column)
743
+ sql = "SELECT MAX(#{primary_key}) AS id, #{quoted_column} AS ref FROM #{quoted_table_name} WHERE "
744
+ sql << merge_conditions({column => sender_recipient_ids})
745
+ sql << " GROUP BY #{quoted_column}"
746
+
747
+ ActiveRecord::Base.connection.select_all(sql).each do |row|
748
+ sender_recipient_to_ledger_item_ids[row['ref'].to_i] = row['id'].to_i
749
+ end
750
+
751
+ sender_recipient_ids -= sender_recipient_to_ledger_item_ids.keys
752
+ end
753
+
754
+ # Load all the ledger items needed to get one representative of each name
755
+ find(sender_recipient_to_ledger_item_ids.values.uniq).each do |ledger_item|
756
+ sender_id = info.get(ledger_item, :sender_id)
757
+ recipient_id = info.get(ledger_item, :recipient_id)
758
+
759
+ if sender_recipient_to_ledger_item_ids.include? sender_id
760
+ details = info.get(ledger_item, :sender_details)
761
+ result_map[sender_id] = details[:name]
762
+ end
763
+ if sender_recipient_to_ledger_item_ids.include? recipient_id
764
+ details = info.get(ledger_item, :recipient_details)
765
+ result_map[recipient_id] = details[:name]
766
+ end
767
+ end
768
+
769
+ result_map
770
+ end
771
+
772
+ end # module ClassMethods
773
+
774
+
775
+ # Very simple class for representing the sum of all sales, purchases and payments on
776
+ # an account.
777
+ class AccountSummary #:nodoc:
778
+ NUM_FIELDS = [:sales, :purchases, :sale_receipts, :purchase_payments, :balance]
779
+ attr_reader *([:currency] + NUM_FIELDS)
780
+
781
+ def initialize(hash)
782
+ @currency = hash[:currency]; @sales = hash[:sales]; @purchases = hash[:purchases]
783
+ @sale_receipts = hash[:sale_receipts]; @purchase_payments = hash[:purchase_payments]
784
+ @balance = hash[:balance]
785
+ end
786
+
787
+ def method_missing(name, *args)
788
+ if name.to_s =~ /(.*)_formatted$/
789
+ ::Invoicing::CurrencyValue::Formatter.format_value(currency, send($1))
790
+ else
791
+ super
792
+ end
793
+ end
794
+
795
+ def +(other)
796
+ hash = {:currency => currency}
797
+ NUM_FIELDS.each {|field| hash[field] = send(field) + other.send(field) }
798
+ AccountSummary.new hash
799
+ end
800
+
801
+ def to_s
802
+ NUM_FIELDS.map do |field|
803
+ val = send("#{field}_formatted")
804
+ "#{field} = #{val}"
805
+ end.join('; ')
806
+ end
688
807
  end
689
808
 
690
809