invoicing 0.1.0 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- data.tar.gz.sig +0 -0
- data/History.txt +21 -0
- data/{Manifest → Manifest.txt} +13 -11
- data/PostInstall.txt +10 -0
- data/README.rdoc +55 -0
- data/Rakefile +30 -68
- data/lib/invoicing/currency_value.rb +113 -37
- data/lib/invoicing/ledger_item.rb +147 -28
- data/lib/invoicing/ledger_item/render_html.rb +3 -2
- data/lib/invoicing/ledger_item/render_ubl.rb +3 -2
- data/lib/invoicing/version.rb +1 -1
- data/script/console +10 -0
- data/script/destroy +14 -0
- data/script/generate +14 -0
- data/tasks/rcov.rake +4 -0
- data/test/currency_value_test.rb +25 -0
- data/test/ledger_item_test.rb +112 -30
- metadata +61 -50
- metadata.gz.sig +3 -0
- data/CHANGELOG +0 -3
- data/README +0 -48
- data/invoicing.gemspec +0 -41
- data/website/curvycorners.js +0 -1
- data/website/screen.css +0 -149
- data/website/template.html.erb +0 -43
data.tar.gz.sig
ADDED
Binary file
|
data/History.txt
ADDED
@@ -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
|
data/{Manifest → Manifest.txt}
RENAMED
@@ -1,29 +1,35 @@
|
|
1
|
-
|
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
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
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
|
data/PostInstall.txt
ADDED
@@ -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
|
+
|
data/README.rdoc
ADDED
@@ -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
|
2
|
-
require '
|
1
|
+
%w[rubygems rake rake/clean fileutils newgem rubigen].each { |f| require f }
|
2
|
+
require File.dirname(__FILE__) + '/lib/invoicing'
|
3
3
|
|
4
|
-
#
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
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
|
-
|
24
|
-
|
25
|
-
|
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
|
-
|
62
|
-
task :
|
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
|
-
|
137
|
-
|
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
|
-
|
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+.
|
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
|
-
|
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
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
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
|
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
|
-
#
|
586
|
-
#
|
587
|
-
#
|
588
|
-
#
|
589
|
-
#
|
590
|
-
#
|
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
|
-
|
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
|
-
|
601
|
-
|
602
|
-
|
603
|
-
|
604
|
-
|
605
|
-
|
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 =>
|
618
|
-
# # 3 => { :EUR =>
|
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
|
|