invoicing 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (62) hide show
  1. data/CHANGELOG +3 -0
  2. data/LICENSE +20 -0
  3. data/Manifest +60 -0
  4. data/README +48 -0
  5. data/Rakefile +75 -0
  6. data/invoicing.gemspec +41 -0
  7. data/lib/invoicing.rb +9 -0
  8. data/lib/invoicing/cached_record.rb +107 -0
  9. data/lib/invoicing/class_info.rb +187 -0
  10. data/lib/invoicing/connection_adapter_ext.rb +44 -0
  11. data/lib/invoicing/countries/uk.rb +24 -0
  12. data/lib/invoicing/currency_value.rb +212 -0
  13. data/lib/invoicing/find_subclasses.rb +193 -0
  14. data/lib/invoicing/ledger_item.rb +718 -0
  15. data/lib/invoicing/ledger_item/render_html.rb +515 -0
  16. data/lib/invoicing/ledger_item/render_ubl.rb +268 -0
  17. data/lib/invoicing/line_item.rb +246 -0
  18. data/lib/invoicing/price.rb +9 -0
  19. data/lib/invoicing/tax_rate.rb +9 -0
  20. data/lib/invoicing/taxable.rb +355 -0
  21. data/lib/invoicing/time_dependent.rb +388 -0
  22. data/lib/invoicing/version.rb +21 -0
  23. data/test/cached_record_test.rb +100 -0
  24. data/test/class_info_test.rb +253 -0
  25. data/test/connection_adapter_ext_test.rb +71 -0
  26. data/test/currency_value_test.rb +184 -0
  27. data/test/find_subclasses_test.rb +120 -0
  28. data/test/fixtures/README +7 -0
  29. data/test/fixtures/cached_record.sql +22 -0
  30. data/test/fixtures/class_info.sql +28 -0
  31. data/test/fixtures/currency_value.sql +29 -0
  32. data/test/fixtures/find_subclasses.sql +43 -0
  33. data/test/fixtures/ledger_item.sql +39 -0
  34. data/test/fixtures/line_item.sql +33 -0
  35. data/test/fixtures/price.sql +4 -0
  36. data/test/fixtures/tax_rate.sql +4 -0
  37. data/test/fixtures/taxable.sql +14 -0
  38. data/test/fixtures/time_dependent.sql +35 -0
  39. data/test/ledger_item_test.rb +352 -0
  40. data/test/line_item_test.rb +139 -0
  41. data/test/models/README +4 -0
  42. data/test/models/test_subclass_in_another_file.rb +3 -0
  43. data/test/models/test_subclass_not_in_database.rb +6 -0
  44. data/test/price_test.rb +9 -0
  45. data/test/ref-output/creditnote3.html +82 -0
  46. data/test/ref-output/creditnote3.xml +89 -0
  47. data/test/ref-output/invoice1.html +93 -0
  48. data/test/ref-output/invoice1.xml +111 -0
  49. data/test/ref-output/invoice2.html +86 -0
  50. data/test/ref-output/invoice2.xml +98 -0
  51. data/test/ref-output/invoice_null.html +36 -0
  52. data/test/render_html_test.rb +69 -0
  53. data/test/render_ubl_test.rb +32 -0
  54. data/test/setup.rb +37 -0
  55. data/test/tax_rate_test.rb +9 -0
  56. data/test/taxable_test.rb +180 -0
  57. data/test/test_helper.rb +48 -0
  58. data/test/time_dependent_test.rb +180 -0
  59. data/website/curvycorners.js +1 -0
  60. data/website/screen.css +149 -0
  61. data/website/template.html.erb +43 -0
  62. 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,4 @@
1
+ -- For consistency with line_item.sql:
2
+ -- 1 is 100.00 at tax rate 1
3
+ -- 2 is 50.00 at tax rate 2
4
+ -- 3 is 864.20 at tax rate null
@@ -0,0 +1,4 @@
1
+ -- For consistency with line_item.sql:
2
+ -- 1 is at 15%
3
+ -- 2 is at 0%
4
+ -- 3 is not applicable (null)
@@ -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