invoicing 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|