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.
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