xeroizer 0.3.5 → 0.4.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|