invoicing 0.1.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/CHANGELOG +3 -0
- data/LICENSE +20 -0
- data/Manifest +60 -0
- data/README +48 -0
- data/Rakefile +75 -0
- data/invoicing.gemspec +41 -0
- data/lib/invoicing.rb +9 -0
- data/lib/invoicing/cached_record.rb +107 -0
- data/lib/invoicing/class_info.rb +187 -0
- data/lib/invoicing/connection_adapter_ext.rb +44 -0
- data/lib/invoicing/countries/uk.rb +24 -0
- data/lib/invoicing/currency_value.rb +212 -0
- data/lib/invoicing/find_subclasses.rb +193 -0
- data/lib/invoicing/ledger_item.rb +718 -0
- data/lib/invoicing/ledger_item/render_html.rb +515 -0
- data/lib/invoicing/ledger_item/render_ubl.rb +268 -0
- data/lib/invoicing/line_item.rb +246 -0
- data/lib/invoicing/price.rb +9 -0
- data/lib/invoicing/tax_rate.rb +9 -0
- data/lib/invoicing/taxable.rb +355 -0
- data/lib/invoicing/time_dependent.rb +388 -0
- data/lib/invoicing/version.rb +21 -0
- data/test/cached_record_test.rb +100 -0
- data/test/class_info_test.rb +253 -0
- data/test/connection_adapter_ext_test.rb +71 -0
- data/test/currency_value_test.rb +184 -0
- data/test/find_subclasses_test.rb +120 -0
- data/test/fixtures/README +7 -0
- data/test/fixtures/cached_record.sql +22 -0
- data/test/fixtures/class_info.sql +28 -0
- data/test/fixtures/currency_value.sql +29 -0
- data/test/fixtures/find_subclasses.sql +43 -0
- data/test/fixtures/ledger_item.sql +39 -0
- data/test/fixtures/line_item.sql +33 -0
- data/test/fixtures/price.sql +4 -0
- data/test/fixtures/tax_rate.sql +4 -0
- data/test/fixtures/taxable.sql +14 -0
- data/test/fixtures/time_dependent.sql +35 -0
- data/test/ledger_item_test.rb +352 -0
- data/test/line_item_test.rb +139 -0
- data/test/models/README +4 -0
- data/test/models/test_subclass_in_another_file.rb +3 -0
- data/test/models/test_subclass_not_in_database.rb +6 -0
- data/test/price_test.rb +9 -0
- data/test/ref-output/creditnote3.html +82 -0
- data/test/ref-output/creditnote3.xml +89 -0
- data/test/ref-output/invoice1.html +93 -0
- data/test/ref-output/invoice1.xml +111 -0
- data/test/ref-output/invoice2.html +86 -0
- data/test/ref-output/invoice2.xml +98 -0
- data/test/ref-output/invoice_null.html +36 -0
- data/test/render_html_test.rb +69 -0
- data/test/render_ubl_test.rb +32 -0
- data/test/setup.rb +37 -0
- data/test/tax_rate_test.rb +9 -0
- data/test/taxable_test.rb +180 -0
- data/test/test_helper.rb +48 -0
- data/test/time_dependent_test.rb +180 -0
- data/website/curvycorners.js +1 -0
- data/website/screen.css +149 -0
- data/website/template.html.erb +43 -0
- metadata +180 -0
@@ -0,0 +1,120 @@
|
|
1
|
+
require File.join(File.dirname(__FILE__), 'test_helper.rb')
|
2
|
+
|
3
|
+
# Associated with TestBaseclass
|
4
|
+
|
5
|
+
class FindSubclassesAssociate < ActiveRecord::Base
|
6
|
+
end
|
7
|
+
|
8
|
+
|
9
|
+
# Primary hierarchy of classes for testing.
|
10
|
+
|
11
|
+
class TestBaseclass < ActiveRecord::Base
|
12
|
+
set_table_name 'find_subclasses_records'
|
13
|
+
set_inheritance_column 'type_name' # usually left as default 'type'. rename to test renaming
|
14
|
+
belongs_to :associate, :foreign_key => 'associate_id', :class_name => 'FindSubclassesAssociate'
|
15
|
+
named_scope :with_coolness, lambda{|factor| {:conditions => {:coolness_factor => factor}}}
|
16
|
+
extend Invoicing::FindSubclasses
|
17
|
+
def self.coolness_factor; 3; end
|
18
|
+
end
|
19
|
+
|
20
|
+
class TestSubclass < TestBaseclass
|
21
|
+
|
22
|
+
end
|
23
|
+
|
24
|
+
class TestSubSubclass < TestSubclass
|
25
|
+
def self.coolness_factor; 5; end
|
26
|
+
end
|
27
|
+
|
28
|
+
module TestModule
|
29
|
+
class TestInsideModuleSubclass < TestBaseclass
|
30
|
+
def self.coolness_factor; nil; end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
class TestOutsideModuleSubSubclass < TestModule::TestInsideModuleSubclass
|
35
|
+
def self.coolness_factor; 999; end
|
36
|
+
end
|
37
|
+
|
38
|
+
|
39
|
+
# This class' table contains non-existent subclass names, to test errors
|
40
|
+
|
41
|
+
class SomeSillySuperclass < ActiveRecord::Base
|
42
|
+
extend Invoicing::FindSubclasses
|
43
|
+
set_table_name 'find_subclasses_non_existent'
|
44
|
+
end
|
45
|
+
|
46
|
+
|
47
|
+
#####################
|
48
|
+
|
49
|
+
class FindSubclassesTest < Test::Unit::TestCase
|
50
|
+
|
51
|
+
def test_known_subclasses
|
52
|
+
# All subclasses of TestBaseclass except for TestSubclassNotInDatabase
|
53
|
+
expected = ['TestBaseclass', 'TestModule::TestInsideModuleSubclass', 'TestOutsideModuleSubSubclass',
|
54
|
+
'TestSubSubclass', 'TestSubclass', 'TestSubclassInAnotherFile']
|
55
|
+
assert_equal expected, TestBaseclass.known_subclasses.map{|cls| cls.name}.sort
|
56
|
+
end
|
57
|
+
|
58
|
+
def test_known_subclasses_for_subtype
|
59
|
+
expected = ['TestSubSubclass', 'TestSubclass']
|
60
|
+
assert_equal expected, TestSubclass.known_subclasses.map{|cls| cls.name}.sort
|
61
|
+
end
|
62
|
+
|
63
|
+
def test_error_when_unknown_type_is_encountered
|
64
|
+
assert_raise ActiveRecord::SubclassNotFound do
|
65
|
+
SomeSillySuperclass.known_subclasses
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
def test_class_method_condition_in_find
|
70
|
+
assert_equal [1, 2, 4], TestBaseclass.all(:conditions => {:coolness_factor => 3}).map{|r| r.id}.sort
|
71
|
+
end
|
72
|
+
|
73
|
+
def test_class_method_condition_in_named_scope
|
74
|
+
assert_equal [6], TestBaseclass.with_coolness(999).map{|r| r.id}
|
75
|
+
end
|
76
|
+
|
77
|
+
def test_class_method_condition_combined_with_column_condition_as_string_list
|
78
|
+
assert_equal [2, 4], TestBaseclass.with_coolness(3).all(:conditions => ["value LIKE ?", 'B%']).map{|r| r.id}.sort
|
79
|
+
end
|
80
|
+
|
81
|
+
def test_class_method_condition_combined_with_column_condition_as_hash
|
82
|
+
assert_equal [1], TestBaseclass.scoped(:conditions => {:value => 'Mooo!', :coolness_factor => 3}).all.map{|r| r.id}
|
83
|
+
end
|
84
|
+
|
85
|
+
def test_class_method_condition_combined_with_column_condition_on_joined_table_expressed_as_string
|
86
|
+
conditions = {'find_subclasses_associates.value' => 'Cool stuff', 'find_subclasses_records.coolness_factor' => 3}
|
87
|
+
assert_equal [1], TestBaseclass.all(:joins => :associate, :conditions => conditions).map{|r| r.id}
|
88
|
+
end
|
89
|
+
|
90
|
+
def test_class_method_condition_combined_with_column_condition_on_joined_table_expressed_as_hash
|
91
|
+
conditions = {:find_subclasses_associates => {:value => 'Cool stuff'},
|
92
|
+
:find_subclasses_records => {:coolness_factor => 3}}
|
93
|
+
assert_equal [1], TestBaseclass.all(:joins => :associate, :conditions => conditions).map{|r| r.id}
|
94
|
+
end
|
95
|
+
|
96
|
+
def test_class_method_condition_with_same_table_name
|
97
|
+
conditions = {'find_subclasses_records.value' => 'Baaa!', 'find_subclasses_records.coolness_factor' => 3}
|
98
|
+
assert_equal [2, 4], TestBaseclass.all(:conditions => conditions).map{|r| r.id}.sort
|
99
|
+
end
|
100
|
+
|
101
|
+
def test_class_method_condition_with_list_of_alternatives
|
102
|
+
assert_equal [3, 6], TestBaseclass.all(:conditions => {:coolness_factor => [5, 999]}).map{|r| r.id}.sort
|
103
|
+
end
|
104
|
+
|
105
|
+
def test_class_method_condition_with_range_of_alternatives
|
106
|
+
assert_equal [1, 2, 3, 4, 6], TestBaseclass.all(:conditions => {:coolness_factor => 1..1000}).map{|r| r.id}.sort
|
107
|
+
end
|
108
|
+
|
109
|
+
def test_class_method_condition_invoked_on_subclass
|
110
|
+
assert_equal [2], TestSubclass.with_coolness(3).all.map{|r| r.id}
|
111
|
+
end
|
112
|
+
|
113
|
+
def test_class_method_condition_false_type_coercion
|
114
|
+
assert_equal [5], TestBaseclass.find(:all, :conditions => {:coolness_factor => false}).map{|r| r.id}
|
115
|
+
end
|
116
|
+
|
117
|
+
def test_class_method_condition_true_type_coercion
|
118
|
+
assert_equal [1, 2, 3, 4, 6], TestBaseclass.all(:conditions => {:coolness_factor => true}).map{|r| r.id}.sort
|
119
|
+
end
|
120
|
+
end
|
@@ -0,0 +1,7 @@
|
|
1
|
+
The SQL files in this folder are executed once on the current ActiveRecord database connection
|
2
|
+
before the tests are run. Use them to set up a database schema and any contents (fixtures)
|
3
|
+
required by the tests. Tests are run in transactions and rolled back, so the database should
|
4
|
+
be restored back to the state defined in these files after each test.
|
5
|
+
|
6
|
+
It's important that all tables are created with option ENGINE=InnoDB, otherwise MySQL creates
|
7
|
+
MyISAM tables which do not support transactions.
|
@@ -0,0 +1,22 @@
|
|
1
|
+
DROP TABLE IF EXISTS cached_records;
|
2
|
+
|
3
|
+
CREATE TABLE cached_records (
|
4
|
+
id2 int primary key auto_increment,
|
5
|
+
value varchar(255)
|
6
|
+
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;
|
7
|
+
|
8
|
+
INSERT INTO cached_records(id2, value) values(1, 'One'), (2, 'Two');
|
9
|
+
|
10
|
+
ALTER SEQUENCE cached_records_id2_seq start 1000;
|
11
|
+
|
12
|
+
|
13
|
+
DROP TABLE IF EXISTS refers_to_cached_records;
|
14
|
+
|
15
|
+
CREATE TABLE refers_to_cached_records (
|
16
|
+
id int primary key auto_increment,
|
17
|
+
cached_record_id int
|
18
|
+
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;
|
19
|
+
|
20
|
+
INSERT INTO refers_to_cached_records(id, cached_record_id) values(1, 1), (2, 1), (3, NULL);
|
21
|
+
|
22
|
+
ALTER SEQUENCE refers_to_cached_records_id_seq start 1000;
|
@@ -0,0 +1,28 @@
|
|
1
|
+
DROP TABLE IF EXISTS class_info_test_records;
|
2
|
+
|
3
|
+
CREATE TABLE class_info_test_records (
|
4
|
+
id int primary key auto_increment,
|
5
|
+
value int,
|
6
|
+
type varchar(255)
|
7
|
+
);
|
8
|
+
|
9
|
+
INSERT INTO class_info_test_records (id, value, type) values
|
10
|
+
(1, 2, 'ClassInfoTestRecord'),
|
11
|
+
(2, 3, 'ClassInfoTestSubclass'),
|
12
|
+
(3, 3, 'ClassInfoTestSubclass2'),
|
13
|
+
(4, 3, 'ClassInfoTestSubSubclass');
|
14
|
+
|
15
|
+
ALTER SEQUENCE class_info_test_records_id_seq start 1000;
|
16
|
+
|
17
|
+
|
18
|
+
DROP TABLE IF EXISTS class_info_test2_records;
|
19
|
+
|
20
|
+
CREATE TABLE class_info_test2_records (
|
21
|
+
id int primary key auto_increment,
|
22
|
+
value int,
|
23
|
+
okapi varchar(255)
|
24
|
+
);
|
25
|
+
|
26
|
+
INSERT INTO class_info_test2_records(id, value, okapi) values(1, 1, 'OKAPI!');
|
27
|
+
|
28
|
+
ALTER SEQUENCE class_info_test2_records_id_seq start 1000;
|
@@ -0,0 +1,29 @@
|
|
1
|
+
DROP TABLE IF EXISTS currency_value_records;
|
2
|
+
|
3
|
+
CREATE TABLE currency_value_records (
|
4
|
+
id int primary key auto_increment,
|
5
|
+
currency_code varchar(3),
|
6
|
+
amount decimal(20,4),
|
7
|
+
tax_amount decimal(20,4)
|
8
|
+
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;
|
9
|
+
|
10
|
+
INSERT INTO currency_value_records(id, currency_code, amount, tax_amount) values
|
11
|
+
(1, 'GBP', 123.45, NULL),
|
12
|
+
(2, 'EUR', 98765432, 0.02),
|
13
|
+
(3, 'CNY', 5432, 0),
|
14
|
+
(4, 'JPY', 8888, 123),
|
15
|
+
(5, 'XXX', 123, NULL);
|
16
|
+
|
17
|
+
ALTER SEQUENCE currency_value_records_id_seq start 1000;
|
18
|
+
|
19
|
+
|
20
|
+
DROP TABLE IF EXISTS no_currency_column_records;
|
21
|
+
|
22
|
+
CREATE TABLE no_currency_column_records (
|
23
|
+
id int primary key auto_increment,
|
24
|
+
amount decimal(20,4)
|
25
|
+
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;
|
26
|
+
|
27
|
+
INSERT INTO no_currency_column_records(id, amount) values(1, '95.15');
|
28
|
+
|
29
|
+
ALTER SEQUENCE no_currency_column_records_id_seq start 1000;
|
@@ -0,0 +1,43 @@
|
|
1
|
+
DROP TABLE IF EXISTS find_subclasses_records;
|
2
|
+
|
3
|
+
CREATE TABLE find_subclasses_records (
|
4
|
+
id int primary key auto_increment,
|
5
|
+
value varchar(255),
|
6
|
+
type_name varchar(255),
|
7
|
+
associate_id int
|
8
|
+
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;
|
9
|
+
|
10
|
+
INSERT INTO find_subclasses_records(id, value, associate_id, type_name) values
|
11
|
+
(1, 'Mooo!', 1, 'TestBaseclass'),
|
12
|
+
(2, 'Baaa!', NULL, 'TestSubclass'),
|
13
|
+
(3, 'Mooo!', NULL, 'TestSubSubclass'),
|
14
|
+
(4, 'Baaa!', NULL, 'TestSubclassInAnotherFile'),
|
15
|
+
(5, 'Mooo!', 1, 'TestModule::TestInsideModuleSubclass'),
|
16
|
+
(6, 'Baaa!', 1, 'TestOutsideModuleSubSubclass');
|
17
|
+
|
18
|
+
ALTER SEQUENCE find_subclasses_records_id_seq start 1000;
|
19
|
+
|
20
|
+
|
21
|
+
DROP TABLE IF EXISTS find_subclasses_associates;
|
22
|
+
|
23
|
+
CREATE TABLE find_subclasses_associates (
|
24
|
+
id int primary key auto_increment,
|
25
|
+
value varchar(255)
|
26
|
+
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;
|
27
|
+
|
28
|
+
INSERT INTO find_subclasses_associates (id, value) values(1, 'Cool stuff');
|
29
|
+
|
30
|
+
ALTER SEQUENCE find_subclasses_associates_id_seq start 1000;
|
31
|
+
|
32
|
+
|
33
|
+
DROP TABLE IF EXISTS find_subclasses_non_existent;
|
34
|
+
|
35
|
+
CREATE TABLE find_subclasses_non_existent (
|
36
|
+
id int primary key auto_increment,
|
37
|
+
value varchar(255),
|
38
|
+
type varchar(255)
|
39
|
+
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;
|
40
|
+
|
41
|
+
INSERT INTO find_subclasses_non_existent(id, value, type) values(1, 'Badger', 'SurelyThereIsNoClassWithThisName');
|
42
|
+
|
43
|
+
ALTER SEQUENCE find_subclasses_non_existent_id_seq start 1000;
|
@@ -0,0 +1,39 @@
|
|
1
|
+
DROP TABLE IF EXISTS ledger_item_records;
|
2
|
+
|
3
|
+
CREATE TABLE ledger_item_records (
|
4
|
+
id2 int primary key auto_increment,
|
5
|
+
type2 varchar(255) not null,
|
6
|
+
sender_id2 int,
|
7
|
+
recipient_id2 int,
|
8
|
+
identifier2 varchar(255),
|
9
|
+
issue_date2 datetime,
|
10
|
+
currency2 varchar(5),
|
11
|
+
total_amount2 decimal(20,4),
|
12
|
+
tax_amount2 decimal(20,4),
|
13
|
+
status2 varchar(100),
|
14
|
+
period_start2 datetime,
|
15
|
+
period_end2 datetime,
|
16
|
+
uuid2 varchar(40),
|
17
|
+
due_date2 datetime,
|
18
|
+
created_at datetime,
|
19
|
+
updated_at datetime
|
20
|
+
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;
|
21
|
+
|
22
|
+
|
23
|
+
INSERT INTO ledger_item_records
|
24
|
+
(id2, type2, sender_id2, recipient_id2, identifier2, issue_date2, currency2, total_amount2, tax_amount2, status2, period_start2, period_end2, uuid2, due_date2, created_at, updated_at) values
|
25
|
+
(1, 'MyInvoice', 1, 2, '1', '2008-06-30', 'GBP', 315.00, 15.00, 'closed', '2008-06-01', '2008-07-01', '30f4f680-d1b9-012b-48a5-0017f22d32c0', '2008-07-30', '2008-06-02 12:34:00', '2008-07-01 00:00:00'),
|
26
|
+
(2, 'InvoiceSubtype', 2, 1, '12-ASDF', '2009-01-01', 'GBP', 141.97, 18.52, 'closed', '2008-01-01', '2009-01-01', 'fe4d20a0-d1b9-012b-48a5-0017f22d32c0', '2009-01-31', '2008-12-25 00:00:00', '2008-12-26 00:00:00'),
|
27
|
+
(3, 'MyCreditNote', 1, 2, 'putain!', '2008-07-13', 'GBP', -57.50, -7.50, 'closed', '2008-06-01', '2008-07-01', '671a05d0-d1ba-012b-48a5-0017f22d32c0', NULL, '2008-07-13 09:13:14', '2008-07-13 09:13:14'),
|
28
|
+
(4, 'MyPayment', 1, 2, '14BC4E0F', '2008-07-06', 'GBP', 256.50, 0.00, 'cleared', NULL, NULL, 'cfdf2ae0-d1ba-012b-48a5-0017f22d32c0', NULL, '2008-07-06 01:02:03', '2008-07-06 02:03:04'),
|
29
|
+
(5, 'MyLedgerItem', 2, 3, NULL, '2007-04-23', 'USD', 432.10, NULL, 'closed', NULL, NULL, 'f6d6a700-d1ae-012b-48a5-0017f22d32c0', '2011-02-27', '2008-01-01 00:00:00', '2008-01-01 00:00:00'),
|
30
|
+
(6, 'CorporationTaxLiability', 4, 1, 'OMGWTFBBQ', '2009-01-01', 'GBP', 666666.66, NULL, 'closed', '2008-01-01', '2009-01-01', '7273c000-d1bb-012b-48a5-0017f22d32c0', '2009-04-23', '2009-01-23 00:00:00', '2009-01-23 00:00:00'),
|
31
|
+
(7, 'MyPayment', 1, 2, 'nonsense', '2009-01-23', 'GBP', 1000000.00, 0.00, 'failed', NULL, NULL, 'af488310-d1bb-012b-48a5-0017f22d32c0', NULL, '2009-01-23 00:00:00', '2009-01-23 00:00:00'),
|
32
|
+
(8, 'MyPayment', 1, 2, '1quid', '2008-12-23', 'GBP', 1.00, 0.00, 'pending', NULL, NULL, 'df733560-d1bb-012b-48a5-0017f22d32c0', NULL, '2009-12-23 00:00:00', '2009-12-23 00:00:00'),
|
33
|
+
(9, 'MyInvoice', 1, 2, '9', '2009-01-23', 'GBP', 11.50, 1.50, 'open', '2009-01-01', '2008-02-01', 'e5b0dac0-d1bb-012b-48a5-0017f22d32c0', '2009-02-01', '2009-12-23 00:00:00', '2009-12-23 00:00:00'),
|
34
|
+
(10,'MyInvoice', 1, 2, 'a la con', '2009-01-23', 'GBP', 432198.76, 4610.62, 'cancelled', '2008-12-01', '2009-01-01', 'eb167b10-d1bb-012b-48a5-0017f22d32c0', NULL, '2009-12-23 00:00:00', '2009-12-23 00:00:00'),
|
35
|
+
(11,'MyInvoice', 1, 2, 'no_lines', '2009-01-24', 'GBP', NULL, NULL, 'closed', '2009-01-23', '2009-01-24', '9ed54a00-d99f-012b-592c-0017f22d32c0', '2009-01-25', '2009-01-24 23:59:59', '2009-01-24 23:59:59');
|
36
|
+
|
37
|
+
-- Invoice 10 is set to not add up correctly; total_amount is 0.01 too little to test error handling
|
38
|
+
|
39
|
+
ALTER SEQUENCE ledger_item_records_id2_seq start 1000;
|
@@ -0,0 +1,33 @@
|
|
1
|
+
DROP TABLE IF EXISTS line_item_records;
|
2
|
+
|
3
|
+
CREATE TABLE line_item_records (
|
4
|
+
id2 int primary key auto_increment,
|
5
|
+
type2 varchar(255),
|
6
|
+
ledger_item_id2 int not null,
|
7
|
+
net_amount2 decimal(20,4),
|
8
|
+
tax_amount2 decimal(20,4),
|
9
|
+
uuid2 varchar(40),
|
10
|
+
tax_point2 datetime,
|
11
|
+
tax_rate_id2 int,
|
12
|
+
price_id2 int,
|
13
|
+
quantity2 decimal(10,5),
|
14
|
+
creator_id2 int,
|
15
|
+
created_at datetime,
|
16
|
+
updated_at datetime
|
17
|
+
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;
|
18
|
+
|
19
|
+
|
20
|
+
-- Can you spot which make of computer I have?
|
21
|
+
|
22
|
+
INSERT INTO line_item_records
|
23
|
+
(id2, type2, ledger_item_id2, net_amount2, tax_amount2, uuid2, tax_point2, tax_rate_id2, price_id2, quantity2, creator_id2, created_at, updated_at) values
|
24
|
+
(1, 'SuperLineItem', 1, 100.00, 15.00, '0cc659f0-cfac-012b-481d-0017f22d32c0', '2008-06-30', 1, 1, 1, 42, '2008-06-30 12:34:56', '2008-06-30 12:34:56'),
|
25
|
+
(2, 'SubLineItem', 1, 200.00, 0, '0cc65e20-cfac-012b-481d-0017f22d32c0', '2008-06-25', 2, 2, 4, 42, '2008-06-30 21:43:56', '2008-06-30 21:43:56'),
|
26
|
+
(3, 'OtherLineItem', 2, 123.45, 18.52, '0cc66060-cfac-012b-481d-0017f22d32c0', '2009-01-01', 1, NULL, 1, 43, '2008-12-25 00:00:00', '2008-12-26 00:00:00'),
|
27
|
+
(4, 'UntaxedLineItem', 5, 432.10, NULL, '0cc662a0-cfac-012b-481d-0017f22d32c0', '2007-04-23', NULL, 3, NULL, 99, '2007-04-03 12:34:00', '2007-04-03 12:34:00'),
|
28
|
+
(5, 'SuperLineItem', 3, -50.00, -7.50, 'eab28cf0-d1b4-012b-48a5-0017f22d32c0', '2008-07-13', 1, 1, 0.5, 42, '2008-07-13 09:13:14', '2008-07-13 09:13:14'),
|
29
|
+
(6, 'OtherLineItem', 6, 666666.66, NULL, 'b5e66b50-d1b9-012b-48a5-0017f22d32c0', '2009-01-01', 3, NULL, 0, 666, '2009-01-23 00:00:00', '2009-01-23 00:00:00'),
|
30
|
+
(7, 'SubLineItem', 9, 10.00, 1.50, '6f362040-d1be-012b-48a5-0017f22d32c0', '2009-01-31', 1, 1, 0.1, NULL, '2009-12-23 00:00:00', '2009-12-23 00:00:00'),
|
31
|
+
(8, 'SubLineItem', 10, 427588.15, 4610.62, '3d12c020-d1bf-012b-48a5-0017f22d32c0', '2009-01-31', NULL, NULL, NULL, 42, '2009-12-23 00:00:00', '2009-12-23 00:00:00');
|
32
|
+
|
33
|
+
ALTER SEQUENCE line_item_records_id2_seq start 1000;
|
@@ -0,0 +1,14 @@
|
|
1
|
+
DROP TABLE IF EXISTS taxable_records;
|
2
|
+
|
3
|
+
CREATE TABLE taxable_records (
|
4
|
+
id int primary key auto_increment,
|
5
|
+
currency_code varchar(3),
|
6
|
+
amount decimal(20,4),
|
7
|
+
gross_amount decimal(20,4),
|
8
|
+
tax_factor decimal(10,9)
|
9
|
+
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;
|
10
|
+
|
11
|
+
INSERT INTO taxable_records(id, currency_code, amount, gross_amount, tax_factor) values
|
12
|
+
(1, 'GBP', 123.45, 141.09, 0.142857143);
|
13
|
+
|
14
|
+
ALTER SEQUENCE taxable_records_id_seq start 1000;
|
@@ -0,0 +1,35 @@
|
|
1
|
+
DROP TABLE IF EXISTS time_dependent_records;
|
2
|
+
|
3
|
+
CREATE TABLE time_dependent_records (
|
4
|
+
id2 int primary key auto_increment,
|
5
|
+
valid_from2 datetime not null,
|
6
|
+
valid_until2 datetime,
|
7
|
+
replaced_by_id2 int,
|
8
|
+
value2 varchar(255) not null,
|
9
|
+
is_default2 tinyint(1) not null
|
10
|
+
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;
|
11
|
+
|
12
|
+
-- 2008 -> 2009 -> 2010 -> 2011
|
13
|
+
--
|
14
|
+
-- 1 -> none
|
15
|
+
-- 2 -> 3 -> 4
|
16
|
+
-- 5 -> 3
|
17
|
+
-- none -> 6* -> 7* -> none
|
18
|
+
-- 8 -> 9*
|
19
|
+
-- 10
|
20
|
+
--
|
21
|
+
-- * = default
|
22
|
+
|
23
|
+
INSERT INTO time_dependent_records(id2, valid_from2, valid_until2, replaced_by_id2, value2, is_default2) values
|
24
|
+
( 1, '2008-01-01 00:00:00', '2009-01-01 00:00:00', NULL, 'One', 0), -- false
|
25
|
+
( 2, '2008-01-01 00:00:00', '2009-01-01 00:00:00', 3, 'Two', 0), -- false
|
26
|
+
( 3, '2009-01-01 00:00:00', '2010-01-01 00:00:00', 4, 'Three', 0), -- false
|
27
|
+
( 4, '2010-01-01 00:00:00', NULL, NULL, 'Four', 0), -- false
|
28
|
+
( 5, '2008-01-01 00:00:00', '2009-01-01 00:00:00', 3, 'Five', 0), -- false
|
29
|
+
( 6, '2009-01-01 00:00:00', '2010-01-01 00:00:00', 7, 'Six', 1), -- true
|
30
|
+
( 7, '2010-01-01 00:00:00', '2011-01-01 00:00:00', NULL, 'Seven', 1), -- true
|
31
|
+
( 8, '2008-01-01 00:00:00', '2011-01-01 00:00:00', 9, 'Eight', 0), -- false
|
32
|
+
( 9, '2011-01-01 00:00:00', NULL, NULL, 'Nine', 1), -- true
|
33
|
+
(10, '2008-01-01 00:00:00', NULL, NULL, 'Ten', 0); -- false
|
34
|
+
|
35
|
+
ALTER SEQUENCE time_dependent_records_id2_seq start 1000;
|
@@ -0,0 +1,352 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require File.join(File.dirname(__FILE__), 'test_helper.rb')
|
4
|
+
|
5
|
+
####### Helper stuff
|
6
|
+
|
7
|
+
module LedgerItemMethods
|
8
|
+
RENAMED_METHODS = {
|
9
|
+
:id => :id2, :type => :type2, :sender_id => :sender_id2, :recipient_id => :recipient_id2,
|
10
|
+
:sender_details => :sender_details2, :recipient_details => :recipient_details2,
|
11
|
+
:identifier => :identifier2, :issue_date => :issue_date2, :currency => :currency2,
|
12
|
+
:total_amount => :total_amount2, :tax_amount => :tax_amount2, :status => :status2,
|
13
|
+
:description => :description2, :period_start => :period_start2,
|
14
|
+
:period_end => :period_end2, :uuid => :uuid2, :due_date => :due_date2,
|
15
|
+
:line_items => :line_items2
|
16
|
+
}
|
17
|
+
|
18
|
+
def user_id_to_details_hash(user_id)
|
19
|
+
case user_id
|
20
|
+
when 1, nil
|
21
|
+
{:is_self => true, :name => 'Unlimited Limited', :contact_name => "Mr B. Badger",
|
22
|
+
:address => "The Sett\n5 Badger Lane\n", :city => "Badgertown", :state => "",
|
23
|
+
:postal_code => "Badger999", :country => "England", :country_code => "GB",
|
24
|
+
:tax_number => "123456789"}
|
25
|
+
when 2
|
26
|
+
{:name => 'Lovely Customer Inc.', :contact_name => "Fred",
|
27
|
+
:address => "The pasture", :city => "Mootown", :state => "Cow Kingdom",
|
28
|
+
:postal_code => "MOOO", :country => "Scotland", :country_code => "GB",
|
29
|
+
:tax_number => "987654321"}
|
30
|
+
when 3
|
31
|
+
{:name => 'I drink milk', :address => "Guzzle guzzle", :city => "Cheesetown",
|
32
|
+
:postal_code => "12345", :country => "United States", :country_code => "US"}
|
33
|
+
when 4
|
34
|
+
{:name => 'The taxman', :address => "ALL YOUR EARNINGS\r\n\tARE BELONG TO US",
|
35
|
+
:city => 'Cumbernauld', :state => 'North Lanarkshire', :postal_code => "",
|
36
|
+
:country => 'United Kingdom'}
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def sender_details2
|
41
|
+
user_id_to_details_hash(sender_id2)
|
42
|
+
end
|
43
|
+
|
44
|
+
def recipient_details2
|
45
|
+
user_id_to_details_hash(recipient_id2)
|
46
|
+
end
|
47
|
+
|
48
|
+
def description2
|
49
|
+
"#{type2} #{id2}"
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
|
54
|
+
####### Classes for use in the tests
|
55
|
+
|
56
|
+
class MyLedgerItem < ActiveRecord::Base
|
57
|
+
set_primary_key 'id2'
|
58
|
+
set_inheritance_column 'type2'
|
59
|
+
set_table_name 'ledger_item_records'
|
60
|
+
include LedgerItemMethods
|
61
|
+
acts_as_ledger_item RENAMED_METHODS
|
62
|
+
has_many :line_items2, :class_name => 'SuperLineItem', :foreign_key => 'ledger_item_id2'
|
63
|
+
end
|
64
|
+
|
65
|
+
class MyInvoice < MyLedgerItem
|
66
|
+
acts_as_ledger_item :subtype => :invoice
|
67
|
+
end
|
68
|
+
|
69
|
+
class InvoiceSubtype < MyInvoice
|
70
|
+
end
|
71
|
+
|
72
|
+
class MyCreditNote < MyLedgerItem
|
73
|
+
acts_as_credit_note
|
74
|
+
end
|
75
|
+
|
76
|
+
class MyPayment < MyLedgerItem
|
77
|
+
acts_as_payment
|
78
|
+
end
|
79
|
+
|
80
|
+
class CorporationTaxLiability < MyLedgerItem
|
81
|
+
def self.debit_when_sent_by_self
|
82
|
+
true
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
class UUIDNotPresentLedgerItem < ActiveRecord::Base
|
87
|
+
set_primary_key 'id2'
|
88
|
+
set_inheritance_column 'type2'
|
89
|
+
set_table_name 'ledger_item_records'
|
90
|
+
include LedgerItemMethods
|
91
|
+
|
92
|
+
def get_class_info
|
93
|
+
ledger_item_class_info
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
class OverwrittenMethodsNotPresentLedgerItem < ActiveRecord::Base
|
98
|
+
set_primary_key 'id2'
|
99
|
+
set_inheritance_column 'type2'
|
100
|
+
set_table_name 'ledger_item_records'
|
101
|
+
acts_as_invoice LedgerItemMethods::RENAMED_METHODS
|
102
|
+
end
|
103
|
+
|
104
|
+
|
105
|
+
####### The actual tests
|
106
|
+
|
107
|
+
class LedgerItemTest < Test::Unit::TestCase
|
108
|
+
|
109
|
+
def test_total_amount_is_currency_value
|
110
|
+
record = MyLedgerItem.find(5)
|
111
|
+
assert_equal '$432.10', record.total_amount2_formatted
|
112
|
+
end
|
113
|
+
|
114
|
+
def test_tax_amount_is_currency_value
|
115
|
+
record = MyInvoice.find(1)
|
116
|
+
assert_equal '£15.00', record.tax_amount2_formatted
|
117
|
+
end
|
118
|
+
|
119
|
+
def test_net_amount
|
120
|
+
assert_equal BigDecimal('300'), MyInvoice.find(1).net_amount
|
121
|
+
end
|
122
|
+
|
123
|
+
def test_net_amount_nil
|
124
|
+
assert_nil MyInvoice.new.net_amount
|
125
|
+
end
|
126
|
+
|
127
|
+
def test_net_amount_formatted
|
128
|
+
assert_equal '£300.00', MyInvoice.find(1).net_amount_formatted
|
129
|
+
end
|
130
|
+
|
131
|
+
def test_sent_by_nil_is_treated_as_self
|
132
|
+
assert MyInvoice.find(1).sent_by?(nil)
|
133
|
+
assert MyCreditNote.find(3).sent_by?(nil)
|
134
|
+
end
|
135
|
+
|
136
|
+
def test_received_by_nil_is_treated_as_self
|
137
|
+
assert InvoiceSubtype.find(2).received_by?(nil)
|
138
|
+
assert CorporationTaxLiability.find(6).received_by?(nil)
|
139
|
+
end
|
140
|
+
|
141
|
+
def test_invoice_from_self_is_debit
|
142
|
+
record = MyInvoice.find(1)
|
143
|
+
assert_kind_of MyInvoice, record
|
144
|
+
assert record.debit?(1)
|
145
|
+
assert record.debit?(nil)
|
146
|
+
end
|
147
|
+
|
148
|
+
def test_invoice_to_self_is_credit
|
149
|
+
record = InvoiceSubtype.find(2)
|
150
|
+
assert_kind_of MyInvoice, record
|
151
|
+
assert !record.debit?(1)
|
152
|
+
assert !record.debit?(nil)
|
153
|
+
end
|
154
|
+
|
155
|
+
def test_invoice_to_customer_is_seen_as_credit_by_customer
|
156
|
+
assert !MyInvoice.find(1).debit?(2)
|
157
|
+
end
|
158
|
+
|
159
|
+
def test_invoice_from_supplier_is_seen_as_debit_by_supplier
|
160
|
+
assert InvoiceSubtype.find(2).debit?(2)
|
161
|
+
end
|
162
|
+
|
163
|
+
def test_credit_note_from_self_is_debit
|
164
|
+
record = MyCreditNote.find(3)
|
165
|
+
assert_kind_of MyCreditNote, record
|
166
|
+
assert record.debit?(nil)
|
167
|
+
assert record.debit?(1)
|
168
|
+
end
|
169
|
+
|
170
|
+
def test_credit_note_to_customer_is_seen_as_credit_by_customer
|
171
|
+
assert !MyCreditNote.find(3).debit?(2)
|
172
|
+
end
|
173
|
+
|
174
|
+
def test_payment_receipt_from_self_is_credit
|
175
|
+
record = MyPayment.find(4)
|
176
|
+
assert_kind_of MyPayment, record
|
177
|
+
assert !record.debit?(1)
|
178
|
+
assert !record.debit?(nil)
|
179
|
+
end
|
180
|
+
|
181
|
+
def test_payment_receipt_to_customer_is_seen_as_debit_by_customer
|
182
|
+
assert MyPayment.find(4).debit?(2)
|
183
|
+
end
|
184
|
+
|
185
|
+
def test_cannot_determine_debit_status_for_uninvolved_party
|
186
|
+
assert_raise ArgumentError do
|
187
|
+
MyInvoice.find(1).debit?(3)
|
188
|
+
end
|
189
|
+
end
|
190
|
+
|
191
|
+
def test_assign_uuid_to_new_record
|
192
|
+
record = MyInvoice.new
|
193
|
+
begin
|
194
|
+
UUID
|
195
|
+
uuid_gem_available = true
|
196
|
+
rescue NameError
|
197
|
+
uuid_gem_available = false
|
198
|
+
end
|
199
|
+
if uuid_gem_available
|
200
|
+
assert_match /^[0-9a-f]{8}\-[0-9a-f]{4}\-[0-9a-f]{4}\-[0-9a-f]{4}\-[0-9a-f]{12}$/, record.uuid2
|
201
|
+
else
|
202
|
+
assert record.uuid2.blank?
|
203
|
+
puts "Warning: uuid gem not installed -- not testing UUID generation"
|
204
|
+
end
|
205
|
+
end
|
206
|
+
|
207
|
+
def test_uuid_gem_not_present
|
208
|
+
begin
|
209
|
+
real_uuid = Object.send(:remove_const, :UUID)
|
210
|
+
UUIDNotPresentLedgerItem.acts_as_ledger_item(LedgerItemMethods::RENAMED_METHODS)
|
211
|
+
assert_nil UUIDNotPresentLedgerItem.new.get_class_info.uuid_generator
|
212
|
+
ensure
|
213
|
+
Object.send(:const_set, :UUID, real_uuid)
|
214
|
+
end
|
215
|
+
end
|
216
|
+
|
217
|
+
def test_must_overwrite_sender_details
|
218
|
+
assert_raise RuntimeError do
|
219
|
+
OverwrittenMethodsNotPresentLedgerItem.new.sender_details
|
220
|
+
end
|
221
|
+
end
|
222
|
+
|
223
|
+
def test_must_overwrite_recipient_details
|
224
|
+
assert_raise RuntimeError do
|
225
|
+
OverwrittenMethodsNotPresentLedgerItem.new.recipient_details
|
226
|
+
end
|
227
|
+
end
|
228
|
+
|
229
|
+
def test_must_provide_line_items_association
|
230
|
+
assert_raise RuntimeError do
|
231
|
+
OverwrittenMethodsNotPresentLedgerItem.new.line_items
|
232
|
+
end
|
233
|
+
end
|
234
|
+
|
235
|
+
def test_calculate_total_amount_for_new_invoice
|
236
|
+
invoice = MyInvoice.new(:currency2 => 'USD')
|
237
|
+
invoice.line_items2 << SuperLineItem.new(:net_amount2 => 100, :tax_amount2 => 15)
|
238
|
+
invoice.line_items2 << SubLineItem.new(:net_amount2 => 10)
|
239
|
+
invoice.valid?
|
240
|
+
assert_equal BigDecimal('125'), invoice.total_amount2
|
241
|
+
assert_equal BigDecimal('15'), invoice.tax_amount2
|
242
|
+
end
|
243
|
+
|
244
|
+
def test_calculate_total_amount_for_updated_invoice
|
245
|
+
invoice = MyInvoice.find(9)
|
246
|
+
invoice.line_items2 << SuperLineItem.new(:net_amount2 => 10, :tax_amount2 => 1.5)
|
247
|
+
invoice.save!
|
248
|
+
assert_equal([{'total_amount2' => '23.0000', 'tax_amount2' => '3.0000'}],
|
249
|
+
ActiveRecord::Base.connection.select_all("SELECT total_amount2, tax_amount2 FROM ledger_item_records WHERE id2=9"))
|
250
|
+
end
|
251
|
+
|
252
|
+
def test_line_items_error
|
253
|
+
assert_raise RuntimeError do
|
254
|
+
MyInvoice.find(1).line_items # not line_items2
|
255
|
+
end
|
256
|
+
end
|
257
|
+
|
258
|
+
def test_find_invoice_subclasses
|
259
|
+
assert_equal %w(InvoiceSubtype MyInvoice), MyLedgerItem.select_matching_subclasses(:is_invoice, true).map{|c| c.name}.sort
|
260
|
+
end
|
261
|
+
|
262
|
+
def test_find_credit_note_subclasses
|
263
|
+
assert_equal %w(MyCreditNote), MyLedgerItem.select_matching_subclasses(:is_credit_note, true).map{|c| c.name}
|
264
|
+
end
|
265
|
+
|
266
|
+
def test_find_payment_subclasses
|
267
|
+
assert_equal %w(MyPayment), MyLedgerItem.select_matching_subclasses(:is_payment, true).map{|c| c.name}
|
268
|
+
end
|
269
|
+
|
270
|
+
def test_account_summary
|
271
|
+
summary = {:GBP => {:sales => BigDecimal('257.50'), :purchases => BigDecimal('141.97'),
|
272
|
+
:sale_receipts => BigDecimal('256.50'), :purchase_payments => BigDecimal('0.00'),
|
273
|
+
:balance => BigDecimal('-140.97')}}
|
274
|
+
assert_equal summary, MyLedgerItem.account_summary(1, 2)
|
275
|
+
end
|
276
|
+
|
277
|
+
def test_account_summary_with_scope
|
278
|
+
summary = {:GBP => {:sales => BigDecimal('257.50'), :purchases => BigDecimal('0.00'),
|
279
|
+
:sale_receipts => BigDecimal('256.50'), :purchase_payments => BigDecimal('0.00'),
|
280
|
+
:balance => BigDecimal('1.00')}}
|
281
|
+
conditions = ['issue_date2 >= ? AND issue_date2 < ?', DateTime.parse('2008-01-01'), DateTime.parse('2009-01-01')]
|
282
|
+
assert_equal summary, MyLedgerItem.scoped(:conditions => conditions).account_summary(1, 2)
|
283
|
+
end
|
284
|
+
|
285
|
+
def test_account_summaries
|
286
|
+
summaries = {
|
287
|
+
1 => {:GBP => {:balance => BigDecimal('140.97'), :sales => BigDecimal('141.97'),
|
288
|
+
:purchases => BigDecimal('257.50'), :sale_receipts => BigDecimal('0.00'),
|
289
|
+
:purchase_payments => BigDecimal('256.50')}
|
290
|
+
},
|
291
|
+
3 => {:USD => {:balance => BigDecimal('-432.10'), :sales => BigDecimal('0.00'),
|
292
|
+
:purchases => BigDecimal('0.00'), :sale_receipts => BigDecimal('432.10'),
|
293
|
+
:purchase_payments => BigDecimal('0.00')}
|
294
|
+
}
|
295
|
+
}
|
296
|
+
assert_equal summaries, MyLedgerItem.account_summaries(2)
|
297
|
+
end
|
298
|
+
|
299
|
+
def test_account_summaries_with_scope
|
300
|
+
summaries = {
|
301
|
+
1 => {:GBP => {:balance => BigDecimal('-315.00'), :sales => BigDecimal('0.00'),
|
302
|
+
:purchases => BigDecimal('315.00'), :sale_receipts => BigDecimal('0.00'),
|
303
|
+
:purchase_payments => BigDecimal('0.00')}
|
304
|
+
},
|
305
|
+
3 => {:USD => {:balance => BigDecimal('-432.10'), :sales => BigDecimal('0.00'),
|
306
|
+
:purchases => BigDecimal('0.00'), :sale_receipts => BigDecimal('432.10'),
|
307
|
+
:purchase_payments => BigDecimal('0.00')}
|
308
|
+
}
|
309
|
+
}
|
310
|
+
conditions = {:conditions => ['issue_date2 < ?', DateTime.parse('2008-07-01')]}
|
311
|
+
assert_equal summaries, MyLedgerItem.scoped(conditions).account_summaries(2)
|
312
|
+
end
|
313
|
+
|
314
|
+
def test_sent_by_scope
|
315
|
+
assert_equal [2,5], MyLedgerItem.sent_by(2).map{|i| i.id}.sort
|
316
|
+
end
|
317
|
+
|
318
|
+
def test_received_by_scope
|
319
|
+
assert_equal [1,3,4,7,8,9,10,11], MyLedgerItem.received_by(2).map{|i| i.id}.sort
|
320
|
+
end
|
321
|
+
|
322
|
+
def test_sent_or_received_by_scope
|
323
|
+
assert_equal [1,2,3,4,5,7,8,9,10,11], MyLedgerItem.sent_or_received_by(2).map{|i| i.id}.sort
|
324
|
+
end
|
325
|
+
|
326
|
+
def test_in_effect_scope
|
327
|
+
assert_equal [1,2,3,4,5,6,7,8,9,10,11], MyLedgerItem.all.map{|i| i.id}.sort
|
328
|
+
assert_equal [1,2,3,4,5,6,11], MyLedgerItem.in_effect.map{|i| i.id}.sort
|
329
|
+
end
|
330
|
+
|
331
|
+
def test_open_or_pending_scope
|
332
|
+
assert_equal [8,9], MyLedgerItem.open_or_pending.map{|i| i.id}.sort
|
333
|
+
end
|
334
|
+
|
335
|
+
def test_due_at_scope
|
336
|
+
assert_equal [1,3,4,7,8,10,11], MyLedgerItem.due_at(DateTime.parse('2009-01-30')).map{|i| i.id}.sort
|
337
|
+
assert_equal [1,2,3,4,7,8,10,11], MyLedgerItem.due_at(DateTime.parse('2009-01-31')).map{|i| i.id}.sort
|
338
|
+
end
|
339
|
+
|
340
|
+
def test_sorted_scope
|
341
|
+
assert_equal [5,1,4,3,8,2,6,7,9,10,11], MyLedgerItem.sorted(:issue_date).map{|i| i.id}
|
342
|
+
end
|
343
|
+
|
344
|
+
def test_sorted_scope_with_non_existent_column
|
345
|
+
assert_equal [1,2,3,4,5,6,7,8,9,10,11], MyLedgerItem.sorted(:this_column_does_not_exist).map{|i| i.id}
|
346
|
+
end
|
347
|
+
|
348
|
+
def test_exclude_empty_invoices_scope
|
349
|
+
assert_equal [1,2,3,4,5,6,7,8,9,10], MyLedgerItem.exclude_empty_invoices.map{|i| i.id}.sort
|
350
|
+
end
|
351
|
+
|
352
|
+
end
|