xeroizer 0.3.5 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/.bundle/config +2 -2
- data/Gemfile +5 -0
- data/Rakefile +17 -1
- data/VERSION +1 -1
- data/lib/xeroizer.rb +5 -1
- data/lib/xeroizer/configuration.rb +19 -0
- data/lib/xeroizer/generic_application.rb +2 -1
- data/lib/xeroizer/logging.rb +8 -0
- data/lib/xeroizer/models/account.rb +2 -1
- data/lib/xeroizer/models/bank_account.rb +12 -0
- data/lib/xeroizer/models/bank_transaction.rb +74 -0
- data/lib/xeroizer/models/invoice.rb +17 -12
- data/lib/xeroizer/models/item.rb +3 -3
- data/lib/xeroizer/models/item_purchase_details.rb +19 -0
- data/lib/xeroizer/models/{item_purchase_sale_details.rb → item_sales_details.rb} +5 -3
- data/lib/xeroizer/models/line_amount_type.rb +11 -0
- data/lib/xeroizer/models/line_item.rb +2 -12
- data/lib/xeroizer/models/line_item_sum.rb +21 -0
- data/lib/xeroizer/models/payment.rb +2 -6
- data/lib/xeroizer/oauth.rb +1 -1
- data/lib/xeroizer/record/base.rb +21 -2
- data/lib/xeroizer/record/validation_helper.rb +14 -2
- data/lib/xeroizer/record/validators/block_validator.rb +22 -0
- data/lib/xeroizer/record/validators/validator.rb +14 -5
- data/lib/xeroizer/record/xml_helper.rb +24 -7
- data/test/acceptance/about_creating_bank_transactions_test.rb +162 -0
- data/test/acceptance/about_fetching_bank_transactions_test.rb +56 -0
- data/test/acceptance/acceptance_test.rb +53 -0
- data/test/acceptance/bank_transaction_reference_data.rb +31 -0
- data/test/test_helper.rb +11 -1
- data/test/unit/models/bank_transaction_model_parsing_test.rb +131 -0
- data/test/unit/models/bank_transaction_test.rb +47 -0
- data/test/unit/models/bank_transaction_validation_test.rb +87 -0
- data/test/unit/models/contact_test.rb +2 -2
- data/test/unit/models/credit_note_test.rb +2 -2
- data/test/unit/models/invoice_test.rb +43 -17
- data/test/unit/models/line_item_sum_test.rb +24 -0
- data/test/unit/models/line_item_test.rb +54 -0
- data/test/unit/oauth_config_test.rb +20 -0
- data/test/unit/oauth_test.rb +1 -1
- data/test/unit/private_application_test.rb +2 -2
- data/test/unit/record/base_model_test.rb +2 -2
- data/test/unit/record/base_test.rb +38 -1
- data/test/unit/record/block_validator_test.rb +125 -0
- data/test/unit/record/model_definition_test.rb +2 -2
- data/test/unit/record/parse_where_hash_test.rb +2 -2
- data/test/unit/record/record_association_test.rb +1 -1
- data/test/unit/record/validators_test.rb +51 -3
- data/test/unit/record_definition_test.rb +2 -2
- data/test/unit/report_definition_test.rb +2 -2
- data/test/unit/report_test.rb +1 -1
- data/xeroizer.gemspec +60 -6
- metadata +124 -66
- data/lib/.DS_Store +0 -0
@@ -14,6 +14,7 @@ module Xeroizer
|
|
14
14
|
# Adds a validator config for each attribute specified in args.
|
15
15
|
def validates_with_validator(validator, args)
|
16
16
|
options = args.extract_options!
|
17
|
+
|
17
18
|
self.validators ||= []
|
18
19
|
args.flatten.each do | attribute |
|
19
20
|
self.validators << validator.new(attribute, options)
|
@@ -31,7 +32,18 @@ module Xeroizer
|
|
31
32
|
def validates_presence_of(*args)
|
32
33
|
validates_with_validator(Validator::PresenceOfValidator, args)
|
33
34
|
end
|
34
|
-
|
35
|
+
|
36
|
+
def validates(*args, &block)
|
37
|
+
fail "Block required" unless block_given?
|
38
|
+
|
39
|
+
if args.last.is_a? Hash
|
40
|
+
args.last[:block] = block
|
41
|
+
else
|
42
|
+
args << { :block => block }
|
43
|
+
end
|
44
|
+
|
45
|
+
validates_with_validator(Validator::BlockValidator, args)
|
46
|
+
end
|
35
47
|
end
|
36
48
|
|
37
49
|
module InstanceMethods
|
@@ -56,4 +68,4 @@ module Xeroizer
|
|
56
68
|
|
57
69
|
end
|
58
70
|
end
|
59
|
-
end
|
71
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
module Xeroizer
|
2
|
+
module Record
|
3
|
+
class Validator
|
4
|
+
class BlockValidator < Validator
|
5
|
+
def valid?(record)
|
6
|
+
fail "No block provided" unless options[:block]
|
7
|
+
|
8
|
+
result = record.instance_eval &options[:block]
|
9
|
+
|
10
|
+
record.errors << [attribute, message] unless result == true
|
11
|
+
end
|
12
|
+
|
13
|
+
private
|
14
|
+
|
15
|
+
def message
|
16
|
+
supplied_message = options[:message] || ""
|
17
|
+
supplied_message.empty? ? "block condition failed" : supplied_message
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -12,12 +12,21 @@ module Xeroizer
|
|
12
12
|
end
|
13
13
|
|
14
14
|
def validate(record)
|
15
|
-
run_validator
|
16
|
-
run_validator = false if options[:if] && !options[:if].call(record)
|
17
|
-
run_validator = false if options[:unless] && options[:unless].call(record)
|
18
|
-
valid?(record) if run_validator
|
15
|
+
valid?(record) if run_validator?(record, options)
|
19
16
|
end
|
20
|
-
|
17
|
+
|
18
|
+
def run_validator?(record, options)
|
19
|
+
return false if options[:if] && !condition?(record, options[:if])
|
20
|
+
return false if options[:unless] && condition?(record, options[:unless])
|
21
|
+
true
|
22
|
+
end
|
23
|
+
|
24
|
+
def condition?(record, condition)
|
25
|
+
return condition.call(record) if condition.respond_to? :call
|
26
|
+
return record.send(condition) if condition.is_a? Symbol
|
27
|
+
raise "Validation condition must be a Symbol or an Object that responds to call"
|
28
|
+
end
|
29
|
+
|
21
30
|
end
|
22
31
|
|
23
32
|
end
|
@@ -56,17 +56,34 @@ module Xeroizer
|
|
56
56
|
|
57
57
|
# Turn a record into its XML representation.
|
58
58
|
def to_xml(b = Builder::XmlMarkup.new(:indent => 2))
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
59
|
+
optional_root_tag(parent.class.xml_root_name, b) do |b|
|
60
|
+
b.tag!(parent.class.xml_node_name || parent.model_name) {
|
61
|
+
attributes.each do | key, value |
|
62
|
+
field = self.class.fields[key]
|
63
|
+
value = self.send(key) if field[:calculated]
|
64
|
+
xml_value_from_field(b, field, value) unless value.nil?
|
65
|
+
end
|
66
|
+
}
|
67
|
+
end
|
66
68
|
end
|
67
69
|
|
68
70
|
protected
|
69
71
|
|
72
|
+
# Add top-level root name if required.
|
73
|
+
# E.g. Payments need specifying in the form:
|
74
|
+
# <Payments>
|
75
|
+
# <Payment>
|
76
|
+
# ...
|
77
|
+
# </Payment>
|
78
|
+
# </Payments>
|
79
|
+
def optional_root_tag(root_name, b, &block)
|
80
|
+
if root_name
|
81
|
+
b.tag!(root_name) { |b| yield(b) }
|
82
|
+
else
|
83
|
+
yield(b)
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
70
87
|
# Format a attribute for use in the XML passed to Xero.
|
71
88
|
def xml_value_from_field(b, field, value)
|
72
89
|
case field[:type]
|
@@ -0,0 +1,162 @@
|
|
1
|
+
require "test_helper"
|
2
|
+
require "acceptance_test"
|
3
|
+
|
4
|
+
class AboutCreatingBankTransactions < Test::Unit::TestCase
|
5
|
+
include AcceptanceTest
|
6
|
+
|
7
|
+
let :client do
|
8
|
+
Xeroizer::PrivateApplication.new(@consumer_key, @consumer_secret, @key_file)
|
9
|
+
end
|
10
|
+
|
11
|
+
def setup
|
12
|
+
super
|
13
|
+
all_accounts = client.Account.all
|
14
|
+
@account = all_accounts.select{|account| account.status == "ACTIVE" && account.type == "REVENUE"}.first
|
15
|
+
@bank_account = all_accounts.select{|account| account.status == "ACTIVE" && account.type == "BANK"}.first
|
16
|
+
end
|
17
|
+
|
18
|
+
can "create a new SPEND bank transaction" do
|
19
|
+
new_transaction = client.BankTransaction.build(
|
20
|
+
:type => "SPEND",
|
21
|
+
:contact => { :name => "Jazz Kang" },
|
22
|
+
:line_items => any_line_items(@account),
|
23
|
+
:bank_account => { :code => @bank_account.code }
|
24
|
+
)
|
25
|
+
|
26
|
+
assert new_transaction.save, "Save failed with the following errors: #{new_transaction.errors.inspect}"
|
27
|
+
assert_exists new_transaction
|
28
|
+
end
|
29
|
+
|
30
|
+
can "update a SPEND bank transaction, for example by setting its status" do
|
31
|
+
new_transaction = client.BankTransaction.build(
|
32
|
+
:type => "SPEND",
|
33
|
+
:contact => { :name => "Jazz Kang" },
|
34
|
+
:line_items => any_line_items(@account),
|
35
|
+
:bank_account => { :code => @bank_account.code }
|
36
|
+
)
|
37
|
+
|
38
|
+
assert new_transaction.save, "Save failed with the following errors: #{new_transaction.errors.inspect}"
|
39
|
+
|
40
|
+
assert_exists new_transaction
|
41
|
+
|
42
|
+
the_new_type = "RECEIVE"
|
43
|
+
|
44
|
+
expected_id = new_transaction.id
|
45
|
+
|
46
|
+
new_transaction.type = the_new_type
|
47
|
+
|
48
|
+
assert new_transaction.save, "Update failed with the following errors: #{new_transaction.errors.inspect}"
|
49
|
+
|
50
|
+
assert_equal expected_id, new_transaction.id, "Expected the id to be the same because it has been updated"
|
51
|
+
|
52
|
+
refreshed_bank_transaction = client.BankTransaction.find expected_id
|
53
|
+
|
54
|
+
assert_equal the_new_type, refreshed_bank_transaction.type,
|
55
|
+
"Expected the bank transaction to've had its type updated"
|
56
|
+
end
|
57
|
+
|
58
|
+
can "update a bank transaction by adding line items provided you calculate the tax_amount correctly" do
|
59
|
+
new_transaction = client.BankTransaction.build(
|
60
|
+
:type => "SPEND",
|
61
|
+
:contact => { :name => "Jazz Kang" },
|
62
|
+
:line_items => any_line_items(@account),
|
63
|
+
:bank_account => { :code => @bank_account.code },
|
64
|
+
:line_amount_types => "Exclusive"
|
65
|
+
)
|
66
|
+
|
67
|
+
assert new_transaction.save, "Save failed with the following errors: #{new_transaction.errors.inspect}"
|
68
|
+
assert_exists new_transaction
|
69
|
+
|
70
|
+
expected_id = new_transaction.id
|
71
|
+
|
72
|
+
tax_rate = get_tax_rate(@account.tax_type).effective_rate
|
73
|
+
|
74
|
+
unit_price = BigDecimal("1337.00")
|
75
|
+
|
76
|
+
the_new_line_items = [
|
77
|
+
{
|
78
|
+
:description => "Burrito skin",
|
79
|
+
:quantity => 1,
|
80
|
+
:unit_amount => unit_price,
|
81
|
+
:account_code => @account.code,
|
82
|
+
:tax_type => @account.tax_type,
|
83
|
+
:tax_amount => get_exclusive_tax(unit_price, tax_rate)
|
84
|
+
}
|
85
|
+
]
|
86
|
+
|
87
|
+
new_transaction.line_items = the_new_line_items
|
88
|
+
|
89
|
+
assert new_transaction.save, "Update failed with the following errors: #{new_transaction.errors.inspect}"
|
90
|
+
|
91
|
+
refreshed_bank_transaction = client.BankTransaction.find expected_id
|
92
|
+
|
93
|
+
assert_equal expected_id, new_transaction.id,
|
94
|
+
"Expected the id to be the same because it has been updated"
|
95
|
+
|
96
|
+
assert_equal 1, refreshed_bank_transaction.line_items.size,
|
97
|
+
"Expected the bank transaction to've had its line items updated to just one"
|
98
|
+
|
99
|
+
the_first_line_item = refreshed_bank_transaction.line_items.first
|
100
|
+
|
101
|
+
assert_equal "Burrito skin", the_first_line_item.description,
|
102
|
+
"Expected the bank transaction to've had its line items updated, " +
|
103
|
+
"but the first one's description does not match: #{the_first_line_item.inspect}"
|
104
|
+
end
|
105
|
+
|
106
|
+
def get_inclusive_tax(amount, tax_rate)
|
107
|
+
inclusive_tax = amount * (1 - (100/(100 + tax_rate)))
|
108
|
+
BigDecimal(inclusive_tax.to_s).round(2)
|
109
|
+
end
|
110
|
+
|
111
|
+
def get_exclusive_tax(amount, tax_rate)
|
112
|
+
exclusive_tax = amount * (tax_rate/100)
|
113
|
+
BigDecimal(exclusive_tax.to_s).round(2)
|
114
|
+
end
|
115
|
+
|
116
|
+
def get_tax_rate tax_type
|
117
|
+
@all_tax_types ||= client.TaxRate.all
|
118
|
+
@all_tax_types.select{|tax_rate| tax_rate.tax_type == tax_type}.first
|
119
|
+
end
|
120
|
+
|
121
|
+
can "create a new RECEIVE bank transaction" do
|
122
|
+
new_transaction = client.BankTransaction.build(
|
123
|
+
:type => "RECEIVE",
|
124
|
+
:contact => { :name => "Jazz Kang" },
|
125
|
+
:line_items => any_line_items(@account),
|
126
|
+
:bank_account => { :code => @bank_account.code }
|
127
|
+
)
|
128
|
+
|
129
|
+
assert new_transaction.save, "Save failed with the following errors: #{new_transaction.errors.inspect}"
|
130
|
+
assert_exists new_transaction
|
131
|
+
end
|
132
|
+
|
133
|
+
it "treats line item unit_amounts as tax EXCLUSIVE"
|
134
|
+
must "not set the tax_amount manually on line items"
|
135
|
+
|
136
|
+
def assert_exists(bank_transaction)
|
137
|
+
assert_not_nil bank_transaction.id,
|
138
|
+
"Cannot check for exitence unless the bank transaction has non-null identifier"
|
139
|
+
assert_not_nil client.BankTransaction.find bank_transaction.id
|
140
|
+
end
|
141
|
+
|
142
|
+
def any_line_items(account)
|
143
|
+
[{
|
144
|
+
:description => "Clingfilm bike shorts",
|
145
|
+
:quantity => 1,
|
146
|
+
:unit_amount => "17.00",
|
147
|
+
:account_code => account.code,
|
148
|
+
:tax_type => account.tax_type
|
149
|
+
}]
|
150
|
+
end
|
151
|
+
|
152
|
+
it "fails with RuntimeError when you try and create a new bank account" do
|
153
|
+
new_account = client.Account.build(
|
154
|
+
:name => "Example bank account",
|
155
|
+
:code => "ACC-001"
|
156
|
+
)
|
157
|
+
|
158
|
+
assert_raise RuntimeError do
|
159
|
+
new_account.save
|
160
|
+
end
|
161
|
+
end
|
162
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
require "test_helper"
|
2
|
+
require "acceptance_test"
|
3
|
+
require "bank_transaction_reference_data"
|
4
|
+
|
5
|
+
class AboutFetchingBankTransactions < Test::Unit::TestCase
|
6
|
+
include AcceptanceTest
|
7
|
+
|
8
|
+
def client
|
9
|
+
@client ||= Xeroizer::PrivateApplication.new(@consumer_key, @consumer_secret, @key_file)
|
10
|
+
end
|
11
|
+
|
12
|
+
context "when requesting all bank transactions (i.e., without filter)" do
|
13
|
+
setup do
|
14
|
+
@the_first_bank_transaction = client.BankTransaction.all.first
|
15
|
+
end
|
16
|
+
|
17
|
+
it "returns line items empty" do
|
18
|
+
assert_empty(@the_first_bank_transaction.line_items, "Expected line items to've been excluded")
|
19
|
+
end
|
20
|
+
|
21
|
+
it "returns contact with name and and id ONLY (no addresses or phones)" do
|
22
|
+
the_contact = @the_first_bank_transaction.contact
|
23
|
+
assert_not_nil(the_contact.contact_id, "Expected contact id to be present")
|
24
|
+
assert_not_nil(the_contact.name, "Expected contact name to be present")
|
25
|
+
assert_empty the_contact.phones, "Expected empty contact phones"
|
26
|
+
assert_empty the_contact.addresses, "Expected empty contact addresses"
|
27
|
+
end
|
28
|
+
|
29
|
+
it "returns the bank account" do
|
30
|
+
assert_not_nil @the_first_bank_transaction.bank_account
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
context "when requesting a single bank transaction for example" do
|
35
|
+
setup do
|
36
|
+
@a_new_bank_transaction = BankTransactionReferenceData.new(client).bank_transaction
|
37
|
+
end
|
38
|
+
|
39
|
+
it "returns contact with addresses and phones" do
|
40
|
+
single_bank_transaction = client.BankTransaction.find @a_new_bank_transaction.id
|
41
|
+
|
42
|
+
assert_not_empty single_bank_transaction.contact.addresses,
|
43
|
+
"expected the contact's addresses to have been included"
|
44
|
+
|
45
|
+
assert_not_empty single_bank_transaction.contact.phones,
|
46
|
+
"expected the contact's phone numbers to have been included"
|
47
|
+
end
|
48
|
+
|
49
|
+
it "returns full line item details" do
|
50
|
+
single_bank_transaction = client.BankTransaction.find @a_new_bank_transaction.id
|
51
|
+
|
52
|
+
assert_not_empty single_bank_transaction.line_items,
|
53
|
+
"expected the bank transaction's line items to have been included"
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
module AcceptanceTest
|
2
|
+
class << self
|
3
|
+
def included(klass)
|
4
|
+
klass.class_eval do
|
5
|
+
def self.log_to_console
|
6
|
+
Xeroizer::Logging.const_set :Log, Xeroizer::Logging::StdOutLog
|
7
|
+
end
|
8
|
+
|
9
|
+
def self.no_log
|
10
|
+
Xeroizer::Logging.const_set :Log, Xeroizer::Logging::DevNullLog
|
11
|
+
end
|
12
|
+
|
13
|
+
def self.let(symbol, &block)
|
14
|
+
return unless block_given?
|
15
|
+
|
16
|
+
unless respond_to? symbol
|
17
|
+
define_method symbol, do
|
18
|
+
cached_method_result = instance_variable_get ivar_name = "@#{symbol}"
|
19
|
+
instance_variable_set(ivar_name, instance_eval(&block)) if cached_method_result.nil?
|
20
|
+
instance_variable_get ivar_name
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def setup
|
29
|
+
config = load_config_from_file || load_config_from_env
|
30
|
+
|
31
|
+
@key_file = config.key_file
|
32
|
+
@consumer_key = config.consumer_key
|
33
|
+
@consumer_secret = config.consumer_secret
|
34
|
+
end
|
35
|
+
|
36
|
+
private
|
37
|
+
|
38
|
+
def load_config_from_file
|
39
|
+
the_file_name = ".oauth"
|
40
|
+
|
41
|
+
return nil unless File.exists? the_file_name
|
42
|
+
|
43
|
+
Xeroizer::OAuthConfig.load IO.read the_file_name
|
44
|
+
end
|
45
|
+
|
46
|
+
def load_config_from_env
|
47
|
+
assert_not_nil ENV["CONSUMER_KEY"], "No CONSUMER_KEY environment variable specified."
|
48
|
+
assert_not_nil ENV["CONSUMER_SECRET"], "No CONSUMER_SECRET environment variable specified."
|
49
|
+
assert_not_nil ENV["KEY_FILE"], "No KEY_FILE environment variable specified."
|
50
|
+
assert File.exists?(ENV["KEY_FILE"]), "The file <#{ENV["KEY_FILE"]}> does not exist."
|
51
|
+
OAuthCredentials.new ENV["CONSUMER_KEY"], ENV["CONSUMER_SECRET"], ENV["KEY_FILE"]
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
class BankTransactionReferenceData
|
2
|
+
def initialize(client); @client = client; end
|
3
|
+
def bank_transaction; @bank_transaction ||= new_bank_transaction; end
|
4
|
+
|
5
|
+
private
|
6
|
+
|
7
|
+
def new_bank_transaction
|
8
|
+
all_accounts = @client.Account.all
|
9
|
+
|
10
|
+
account = all_accounts.select{|account| account.status == "ACTIVE" && account.type == "REVENUE"}.first
|
11
|
+
bank_account = all_accounts.select{|account| account.status == "ACTIVE" && account.type == "BANK"}.first
|
12
|
+
|
13
|
+
result = @client.BankTransaction.build(
|
14
|
+
:type => "SPEND",
|
15
|
+
:contact => { :name => "Jazz Kang" },
|
16
|
+
:line_items => [
|
17
|
+
:item_code => "Clingfilm bike shorts",
|
18
|
+
:description => "Bike shorts made of clear, unbreathable material",
|
19
|
+
:quantity => 1,
|
20
|
+
:unit_amount => 39.99,
|
21
|
+
:account_code => account.code,
|
22
|
+
:tax_type => account.tax_type
|
23
|
+
],
|
24
|
+
:bank_account => { :code => bank_account.code }
|
25
|
+
)
|
26
|
+
|
27
|
+
fail("Expected save to have succeeded, but it failed. #{result.errors.inspect}") unless result.save
|
28
|
+
|
29
|
+
result
|
30
|
+
end
|
31
|
+
end
|
data/test/test_helper.rb
CHANGED
@@ -7,9 +7,11 @@ require 'pp'
|
|
7
7
|
|
8
8
|
require File.dirname(__FILE__) + '/../lib/xeroizer.rb'
|
9
9
|
|
10
|
+
$: << File.join(File.dirname(__FILE__), "acceptance")
|
11
|
+
|
10
12
|
module TestHelper
|
11
13
|
|
12
|
-
# The integration tests can be run against the Xero test environment. You
|
14
|
+
# The integration tests can be run against the Xero test environment. You must have a company set up in the test
|
13
15
|
# environment, and you must have set up a customer key for that account.
|
14
16
|
#
|
15
17
|
# You can then run the tests against the test environment using the commands (linux or mac):
|
@@ -55,3 +57,11 @@ module TestHelper
|
|
55
57
|
end
|
56
58
|
|
57
59
|
end
|
60
|
+
|
61
|
+
Shoulda::ClassMethods.class_eval do
|
62
|
+
%w{it must can}.each do |m|
|
63
|
+
alias_method m, :should
|
64
|
+
end
|
65
|
+
|
66
|
+
alias_method :must_eventually, :should_eventually
|
67
|
+
end
|