invoicing 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|
|