xeroizer-float 2.15.3.13 → 2.15.3.14

Sign up to get free protection for your applications and to get access to all the features.
data/Gemfile CHANGED
@@ -3,6 +3,7 @@ source "http://rubygems.org"
3
3
  gem 'builder', '>= 2.1.2'
4
4
  gem 'oauth', '0.4.5'
5
5
  gem 'activesupport'
6
+ gem 'tzinfo'
6
7
  gem 'nokogiri'
7
8
  gem 'i18n'
8
9
 
data/Gemfile.lock CHANGED
@@ -35,6 +35,7 @@ GEM
35
35
  test-unit (2.4.8)
36
36
  turn (0.9.4)
37
37
  ansi
38
+ tzinfo (0.3.35)
38
39
  yard (0.7.5)
39
40
 
40
41
  PLATFORMS
@@ -54,4 +55,5 @@ DEPENDENCIES
54
55
  shoulda
55
56
  test-unit
56
57
  turn
58
+ tzinfo
57
59
  yard
data/LICENSE.txt CHANGED
@@ -1,4 +1,4 @@
1
- Copyright (c) 2011 Wayne Robinson
1
+ Copyright (c) 2013 Wayne Robinson
2
2
 
3
3
  Permission is hereby granted, free of charge, to any person obtaining
4
4
  a copy of this software and associated documentation files (the
@@ -18,3 +18,24 @@ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
18
  LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
19
  OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
20
  WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21
+
22
+ -----------------------------------------------------------------------
23
+
24
+ Part of the HTTP communication and OAuth authentication library were
25
+ provided by the https://github.com/tlconnor/xero_gateway gem created by
26
+ Tim Connor (github.com/tlconnor) and Nik Wakelin (github.com/nikz).
27
+ The license for this Gem is included below:
28
+
29
+ Copyright (c) 2008 Tim Connor <tlconnor@gmail.com>
30
+
31
+ Permission to use, copy, modify, and/or distribute this software for any
32
+ purpose with or without fee is hereby granted, provided that the above
33
+ copyright notice and this permission notice appear in all copies.
34
+
35
+ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
36
+ WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
37
+ MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
38
+ ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
39
+ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
40
+ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
41
+ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
data/README.md CHANGED
@@ -6,7 +6,7 @@ Xeroizer API Library ![Project status](http://stillmaintained.com/waynerobinson/
6
6
  **Github**: [https://github.com/waynerobinson/xeroizer](https://github.com/waynerobinson/xeroizer)
7
7
  **Author**: Wayne Robinson [http://www.wayne-robinson.com](http://www.wayne-robinson.com)
8
8
  **Contributors**: See Contributors section below
9
- **Copyright**: 2007-2010
9
+ **Copyright**: 2007-2013
10
10
  **License**: MIT License
11
11
 
12
12
  Introduction
@@ -435,9 +435,30 @@ contact.save
435
435
  Have a look at the models in `lib/xeroizer/models/` to see the valid attributes, associations and
436
436
  minimum validation requirements for each of the record types.
437
437
 
438
+ ### Bulk Creates & Updates
439
+
440
+ Xero has a hard daily limit on the number of API requests you can make (currently 1,000 requests
441
+ per account per day). To save on requests, you can batch creates and updates into a single PUT or
442
+ POST call, like so:
443
+
444
+ ```ruby
445
+ contact1 = xero.Contact.create(some_attributes)
446
+ xero.Contact.batch_save do
447
+ contact1.email_address = "foo@bar.com"
448
+ contact2 = xero.Contact.build(some_other_attributes)
449
+ contact3 = xero.Contact.build(some_more_attributes)
450
+ end
451
+ ```
452
+
453
+ `batch_save` will issue one PUT request for every 2,000 unsaved records built within its block, and one
454
+ POST request for evert 2,000 existing records that have been altered within its block. If any of the
455
+ unsaved records aren't valid, it'll return `false` before sending anything across the wire;
456
+ otherwise, it returns `true`. `batch_save` takes one optional argument: the number of records to
457
+ create/update per request. (Defaults to 2,000.)
458
+
438
459
  ### Errors
439
460
 
440
- If a record doesn't match it's internal validation requirements the `#save` method will return
461
+ If a record doesn't match its internal validation requirements, the `#save` method will return
441
462
  `false` and the `#errors` attribute will be populated with what went wrong.
442
463
 
443
464
  For example:
@@ -527,3 +548,9 @@ client = Xeroizer::PublicApplication.new(YOUR_OAUTH_CONSUMER_KEY,
527
548
  YOUR_OAUTH_CONSUMER_SECRET,
528
549
  :rate_limit_sleep => 2)
529
550
  ```
551
+
552
+
553
+ ### Contributors
554
+ Xeroizer was inspired by the https://github.com/tlconnor/xero_gateway gem created by Tim Connor
555
+ and Nik Wakelin and portions of the networking and authentication code are based completely off
556
+ this project. Copyright for these components remains held in the name of Tim Connor.
data/VERSION CHANGED
@@ -1 +1 @@
1
- 2.15.3.13
1
+ 2.15.3.14
@@ -1,12 +1,27 @@
1
+ # Copyright (c) 2008 Tim Connor <tlconnor@gmail.com>
2
+ #
3
+ # Permission to use, copy, modify, and/or distribute this software for any
4
+ # purpose with or without fee is hereby granted, provided that the above
5
+ # copyright notice and this permission notice appear in all copies.
6
+ #
7
+ # THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
8
+ # WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
9
+ # MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
10
+ # ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
11
+ # WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
12
+ # ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
13
+ # OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
14
+
1
15
  module Xeroizer
2
16
  class ApiException < StandardError
3
17
 
4
- attr_reader :type, :message, :xml, :request_body
18
+ attr_reader :type, :message, :xml, :parsed_xml, :request_body
5
19
 
6
- def initialize(type, message, xml, request_body)
20
+ def initialize(type, message, xml, parsed_xml, request_body)
7
21
  @type = type
8
22
  @message = message
9
23
  @xml = xml
24
+ @parsed_xml = parsed_xml
10
25
  @request_body = request_body
11
26
  end
12
27
 
data/lib/xeroizer/http.rb CHANGED
@@ -1,3 +1,17 @@
1
+ # Copyright (c) 2008 Tim Connor <tlconnor@gmail.com>
2
+ #
3
+ # Permission to use, copy, modify, and/or distribute this software for any
4
+ # purpose with or without fee is hereby granted, provided that the above
5
+ # copyright notice and this permission notice appear in all copies.
6
+ #
7
+ # THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
8
+ # WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
9
+ # MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
10
+ # ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
11
+ # WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
12
+ # ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
13
+ # OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
14
+
1
15
  module Xeroizer
2
16
  module Http
3
17
 
@@ -69,7 +83,7 @@ module Xeroizer
69
83
 
70
84
  begin
71
85
  attempts += 1
72
- logger.info("\n== [#{Time.now.to_s}] XeroGateway Request: #{uri.request_uri} ") if self.logger
86
+ logger.info("\n== [#{Time.now.to_s}] XeroGateway Request: #{method.to_s.upcase} #{uri.request_uri} ") if self.logger
73
87
 
74
88
  response = case method
75
89
  when :get then client.get(uri.request_uri, headers)
@@ -133,16 +147,17 @@ module Xeroizer
133
147
 
134
148
  # XeroGenericApplication API Exceptions *claim* to be UTF-16 encoded, but fail REXML/Iconv parsing...
135
149
  # So let's ignore that :)
136
- # raw_response.gsub! '<?xml version="1.0" encoding="utf-16"?>', ''
150
+ raw_response.gsub! '<?xml version="1.0" encoding="utf-16"?>', ''
137
151
 
138
152
  # doc = REXML::Document.new(raw_response, :ignore_whitespace_nodes => :all)
139
153
  doc = Nokogiri::XML(raw_response)
140
154
 
141
155
  if doc && doc.root && doc.root.name == "ApiException"
142
156
 
143
- raise ApiException.new(doc.root.xpath("Type").text,
144
- doc.root.xpath("Message").text,
157
+ raise ApiException.new(doc.root.xpath("Type").text,
158
+ doc.root.xpath("Message").text,
145
159
  raw_response,
160
+ doc,
146
161
  request_body)
147
162
 
148
163
  else
@@ -34,6 +34,9 @@ module Xeroizer
34
34
  boolean :is_reconciled
35
35
  string :status
36
36
 
37
+ string :currency_code
38
+ decimal :currency_rate, :calculated => true
39
+
37
40
  alias_method :reconciled?, :is_reconciled
38
41
 
39
42
  belongs_to :contact, :model_name => 'Contact'
@@ -55,6 +58,14 @@ module Xeroizer
55
58
  self.line_items.size > 0
56
59
  end
57
60
 
61
+ def currency_rate
62
+ if attributes[:currency_rate]
63
+ BigDecimal.new(attributes[:currency_rate])
64
+ else
65
+ BigDecimal.new('1.0')
66
+ end
67
+ end
68
+
58
69
  def sub_total=(value); raise SettingTotalDirectlyNotSupported.new(:sub_total); end
59
70
  def total_tax=(value); raise SettingTotalDirectlyNotSupported.new(:total_tax); end
60
71
  def total=(value); raise SettingTotalDirectlyNotSupported.new(:total); end
@@ -57,6 +57,7 @@ module Xeroizer
57
57
  decimal :total, :calculated => true
58
58
  datetime_utc :updated_date_utc, :api_name => 'UpdatedDateUTC'
59
59
  string :currency_code
60
+ decimal :currency_rate, :calculated => true
60
61
  datetime :fully_paid_on_date
61
62
  boolean :sent_to_contact
62
63
 
@@ -81,6 +82,14 @@ module Xeroizer
81
82
  def contact_id
82
83
  attributes[:contact] && attributes[:contact][:contact_id]
83
84
  end
85
+
86
+ def currency_rate
87
+ if attributes[:currency_rate]
88
+ BigDecimal.new(attributes[:currency_rate])
89
+ else
90
+ BigDecimal.new('1.0')
91
+ end
92
+ end
84
93
 
85
94
  # Swallow assignment of attributes that should only be calculated automatically.
86
95
  def sub_total=(value); raise SettingTotalDirectlyNotSupported.new(:sub_total); end
@@ -2,7 +2,10 @@ module Xeroizer
2
2
  module Record
3
3
 
4
4
  class InvoiceModel < BaseModel
5
-
5
+ # To create a new invoice, use the folowing
6
+ # $xero_client.Invoice.build(type: 'ACCREC', ..., contact: {name: 'Foo Bar'},...)
7
+ # Note that we are not making an api request to xero just to get the contact
8
+
6
9
  set_permissions :read, :write, :update
7
10
 
8
11
  public
@@ -62,6 +65,7 @@ module Xeroizer
62
65
  decimal :amount_credited
63
66
  datetime_utc :updated_date_utc, :api_name => 'UpdatedDateUTC'
64
67
  string :currency_code
68
+ decimal :currency_rate, :calculated => true
65
69
  datetime :fully_paid_on_date
66
70
  boolean :sent_to_contact
67
71
 
@@ -106,6 +110,14 @@ module Xeroizer
106
110
  def accounts_receivable?
107
111
  type == 'ACCREC'
108
112
  end
113
+
114
+ def currency_rate
115
+ if attributes[:currency_rate]
116
+ BigDecimal.new(attributes[:currency_rate])
117
+ else
118
+ BigDecimal.new('1.0')
119
+ end
120
+ end
109
121
 
110
122
  def sub_total=(sub_total)
111
123
  @sub_total_is_set = true
@@ -12,7 +12,7 @@ module Xeroizer
12
12
  JOURNAL_STATUS = {
13
13
  'DRAFT' => 'Draft',
14
14
  'POSTED' => 'Posted'
15
- } unless defined?(INVOICE_TYPE)
15
+ } unless defined?(JOURNAL_STATUS)
16
16
  JOURNAL_STATUSES = JOURNAL_STATUS.keys.sort
17
17
 
18
18
  set_primary_key :manual_journal_id
@@ -1,3 +1,17 @@
1
+ # Copyright (c) 2008 Tim Connor <tlconnor@gmail.com>
2
+ #
3
+ # Permission to use, copy, modify, and/or distribute this software for any
4
+ # purpose with or without fee is hereby granted, provided that the above
5
+ # copyright notice and this permission notice appear in all copies.
6
+ #
7
+ # THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
8
+ # WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
9
+ # MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
10
+ # ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
11
+ # WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
12
+ # ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
13
+ # OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
14
+
1
15
  module Xeroizer
2
16
 
3
17
  # Shamelessly taken from the XeroGateway library by Tim Connor which is shamelessly
@@ -50,6 +64,7 @@ module Xeroizer
50
64
  # @option options [String] :request_token_path base URL path for getting a RequestToken (default: "/oauth/RequestToken")
51
65
  # @option options [String] :signature_method method usd to sign requests (default: OAuth library default)
52
66
  # @option options [String] :site base site for API requests (default: "https://api.xero.com")
67
+ # @option options [IO] :http_debug_output filehandle to write HTTP traffic to
53
68
  # @option options [OpenSSL:X509::Certificate] :ssl_client_cert client-side SSL certificate to use for requests (used for PartnerApplication mode)
54
69
  # @option options [OpenSSL::PKey::RSA] :ssl_client_key client-side SSL private key to use for requests (used for PartnerApplication mode)
55
70
  def initialize(ctoken, csecret, options = {})
@@ -104,9 +119,9 @@ module Xeroizer
104
119
  })
105
120
  update_attributes_from_token(access_token)
106
121
  end
107
-
108
- private
109
-
122
+
123
+ private
124
+
110
125
  # Create an OAuth consumer with the SSL client key if specified in @consumer_options when
111
126
  # this instance was created.
112
127
  def create_consumer
@@ -116,8 +131,13 @@ module Xeroizer
116
131
  consumer.http.key = @consumer_options[:ssl_client_key]
117
132
  end
118
133
  consumer
134
+
135
+ if @consumer_options[:http_debug_output]
136
+ consumer.http.set_debug_output(@consumer_options[:http_debug_output])
137
+ end
138
+ consumer
119
139
  end
120
-
140
+
121
141
  # Update instance variables with those from the AccessToken.
122
142
  def update_attributes_from_token(access_token)
123
143
  @expires_at = Time.now + access_token.params[:oauth_expires_in].to_i
@@ -125,6 +145,5 @@ module Xeroizer
125
145
  @session_handle = access_token.params[:oauth_session_handle]
126
146
  @atoken, @asecret = access_token.token, access_token.secret
127
147
  end
128
-
129
148
  end
130
149
  end
@@ -51,11 +51,13 @@ module Xeroizer
51
51
  end
52
52
 
53
53
  def []=(attribute, value)
54
+ parent.mark_dirty(self) if parent
54
55
  self.send("#{attribute}=".to_sym, value)
55
56
  end
56
57
 
57
58
  def attributes=(new_attributes)
58
59
  return unless new_attributes.is_a?(Hash)
60
+ parent.mark_dirty(self) if parent
59
61
  new_attributes.each do | key, value |
60
62
  self.send("#{key}=".to_sym, value)
61
63
  end
@@ -84,6 +86,7 @@ module Xeroizer
84
86
  record = self.parent.find(self.id)
85
87
  @attributes = record.attributes if record
86
88
  @complete_record_downloaded = true
89
+ parent.mark_clean(self)
87
90
  self
88
91
  end
89
92
 
@@ -94,9 +97,30 @@ module Xeroizer
94
97
  else
95
98
  update
96
99
  end
100
+ saved!
101
+ end
102
+
103
+ def saved!
97
104
  @complete_record_downloaded = true
105
+ parent.mark_clean(self)
98
106
  true
99
107
  end
108
+
109
+ def to_json(*args)
110
+ to_h.to_json(*args)
111
+ end
112
+
113
+ # Deprecated
114
+ def as_json(options = {})
115
+ to_h.to_json
116
+ end
117
+
118
+ def to_h
119
+ attrs = self.attributes.reject {|k, v| k == :parent }.map do |k, v|
120
+ [k, v.kind_of?(Array) ? v.map(&:to_h) : (v.respond_to?(:to_h) ? v.to_h : v)]
121
+ end
122
+ Hash[attrs]
123
+ end
100
124
 
101
125
  def inspect
102
126
  attribute_string = self.attributes.collect do |attr, value|
@@ -112,7 +136,7 @@ module Xeroizer
112
136
  request = to_xml
113
137
  log "[CREATE SENT] (#{__FILE__}:#{__LINE__}) #{request}"
114
138
 
115
- response = parent.http_put(request)
139
+ response = parent.http_post(request)
116
140
  log "[CREATE RECEIVED] (#{__FILE__}:#{__LINE__}) #{response}"
117
141
 
118
142
  parse_save_response(response)
@@ -18,7 +18,9 @@ module Xeroizer
18
18
  class_inheritable_attributes :xml_root_name
19
19
  class_inheritable_attributes :optional_xml_root_name
20
20
  class_inheritable_attributes :xml_node_name
21
-
21
+
22
+ DEFAULT_RECORDS_PER_BATCH_SAVE = 2000
23
+
22
24
  include BaseModelHttpProxy
23
25
 
24
26
  attr_reader :application
@@ -89,7 +91,22 @@ module Xeroizer
89
91
 
90
92
  # Build a record with attributes set to the value of attributes.
91
93
  def build(attributes = {})
92
- model_class.build(attributes, self)
94
+ model_class.build(attributes, self).tap do |resource|
95
+ mark_dirty(resource)
96
+ end
97
+ end
98
+
99
+ def mark_dirty(resource)
100
+ if @allow_batch_operations
101
+ @objects[model_class] ||= {}
102
+ @objects[model_class][resource.object_id] ||= resource
103
+ end
104
+ end
105
+
106
+ def mark_clean(resource)
107
+ if @objects and @objects[model_class]
108
+ @objects[model_class].delete(resource.object_id)
109
+ end
93
110
  end
94
111
 
95
112
  # Create (build and save) a record with attributes set to the value of attributes.
@@ -122,7 +139,37 @@ module Xeroizer
122
139
  result.complete_record_downloaded = true if result
123
140
  result
124
141
  end
125
-
142
+
143
+ def batch_save(chunk_size = DEFAULT_RECORDS_PER_BATCH_SAVE)
144
+ @objects = {}
145
+ @allow_batch_operations = true
146
+
147
+ yield
148
+
149
+ if @objects[model_class]
150
+ objects = @objects[model_class].values.compact
151
+ return false unless objects.all?(&:valid?)
152
+ actions = objects.group_by {|o| o.new_record? ? :http_put : :http_post }
153
+ actions.each_pair do |http_method, records|
154
+ records.each_slice(chunk_size) do |some_records|
155
+ request = to_bulk_xml(some_records)
156
+ response = parse_response(self.send(http_method, request, {:summarizeErrors => false}))
157
+ response.response_items.each_with_index do |record, i|
158
+ if record and record.is_a?(model_class)
159
+ some_records[i].attributes = record.attributes
160
+ some_records[i].errors = record.errors
161
+ some_records[i].saved!
162
+ end
163
+ end
164
+ end
165
+ end
166
+ end
167
+
168
+ @objects = {}
169
+ @allow_batch_operations = false
170
+ true
171
+ end
172
+
126
173
  def parse_response(response_xml, options = {})
127
174
  Response.parse(response_xml, options) do | response, elements, response_model_name |
128
175
  if model_name == response_model_name
@@ -131,17 +178,40 @@ module Xeroizer
131
178
  end
132
179
  end
133
180
  end
134
-
181
+
135
182
  protected
136
-
183
+
137
184
  # Parse the records part of the XML response and builds model instances as necessary.
138
185
  def parse_records(response, elements)
139
186
  elements.each do | element |
140
- response.response_items << model_class.build_from_node(element, self)
187
+ new_record = model_class.build_from_node(element, self)
188
+ if element.attribute('status').try(:value) == 'ERROR'
189
+ new_record.errors = []
190
+ element.xpath('.//ValidationError').each do |err|
191
+ new_record.errors << err.text.gsub(/^\s+/, '').gsub(/\s+$/, '')
192
+ end
193
+ end
194
+ response.response_items << new_record
141
195
  end
142
196
  end
143
-
144
- end
145
-
197
+
198
+ def to_bulk_xml(records, builder = Builder::XmlMarkup.new(:indent => 2))
199
+ tag = (self.class.optional_xml_root_name || model_name).pluralize
200
+ builder.tag!(tag) do
201
+ records.each {|r| r.to_xml(builder) }
202
+ end
203
+ end
204
+
205
+ # Parse the response from a create/update request.
206
+ def parse_save_response(response_xml)
207
+ response = parse_response(response_xml)
208
+ record = response.response_items.first if response.response_items.is_a?(Array)
209
+ if record && record.is_a?(self.class)
210
+ @attributes = record.attributes
211
+ end
212
+ self
213
+ end
214
+ end
215
+
146
216
  end
147
217
  end
@@ -73,6 +73,7 @@ module Xeroizer
73
73
 
74
74
  unless options[:skip_writer]
75
75
  define_method "#{internal_field_name}=".to_sym do | value |
76
+ parent.mark_dirty(self) if parent
76
77
  @attributes[field_name] = value
77
78
  end
78
79
  end
@@ -89,6 +90,7 @@ module Xeroizer
89
90
 
90
91
  # Sets the value of the Xero primary key for this record if it exists.
91
92
  def id=(new_id)
93
+ parent.mark_dirty(self) if parent
92
94
  self[self.class.primary_key_name] = new_id
93
95
  end
94
96
 
@@ -47,7 +47,8 @@ module Xeroizer
47
47
  end
48
48
  end
49
49
  end
50
-
50
+
51
+ parent.mark_clean(record)
51
52
  record
52
53
  end
53
54
 
@@ -87,7 +88,7 @@ module Xeroizer
87
88
  end
88
89
  end
89
90
 
90
- # Format a attribute for use in the XML passed to Xero.
91
+ # Format an attribute for use in the XML passed to Xero.
91
92
  def xml_value_from_field(b, field, value)
92
93
  case field[:type]
93
94
  when :guid then b.tag!(field[:api_name], value)
@@ -1,3 +1,17 @@
1
+ # Copyright (c) 2008 Tim Connor <tlconnor@gmail.com>
2
+ #
3
+ # Permission to use, copy, modify, and/or distribute this software for any
4
+ # purpose with or without fee is hereby granted, provided that the above
5
+ # copyright notice and this permission notice appear in all copies.
6
+ #
7
+ # THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
8
+ # WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
9
+ # MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
10
+ # ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
11
+ # WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
12
+ # ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
13
+ # OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
14
+
1
15
  module Xeroizer
2
16
  class Response
3
17
 
@@ -48,6 +48,6 @@ module AcceptanceTest
48
48
  assert_not_nil ENV["CONSUMER_SECRET"], "No CONSUMER_SECRET environment variable specified."
49
49
  assert_not_nil ENV["KEY_FILE"], "No KEY_FILE environment variable specified."
50
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"]
51
+ Xeroizer::OAuthCredentials.new ENV["CONSUMER_KEY"], ENV["CONSUMER_SECRET"], ENV["KEY_FILE"]
52
52
  end
53
53
  end
@@ -0,0 +1,48 @@
1
+ require "test_helper"
2
+ require "acceptance_test"
3
+
4
+ class BulkOperationsTest < Test::Unit::TestCase
5
+ include AcceptanceTest
6
+
7
+ def random_name
8
+ "test-person-#{rand 1000000000}"
9
+ end
10
+
11
+ def setup
12
+ super
13
+ @client = Xeroizer::PrivateApplication.new(@consumer_key, @consumer_secret, @key_file)
14
+ end
15
+
16
+ can "create multiple invoices at once" do
17
+ c1, c2 = nil, nil
18
+ assert_true(
19
+ @client.Contact.batch_save do
20
+ c1 = @client.Contact.build(name: random_name)
21
+ c2 = @client.Contact.build(name: random_name)
22
+ end
23
+ )
24
+ [c1, c2].each {|c| assert_false c.new_record? }
25
+ end
26
+
27
+ can "create and update new records in bulk" do
28
+ c1, c2 = nil, nil
29
+ assert_true(
30
+ @client.Contact.batch_save do
31
+ c1 = @client.Contact.create(name: random_name)
32
+ c1.email_address = "foo@bar.com"
33
+ c2 = @client.Contact.build(name: random_name)
34
+ end
35
+ )
36
+ [c1, c2].each {|c| assert_false c.new_record? }
37
+ c1.download_complete_record!
38
+ assert_equal c1.email_address, "foo@bar.com"
39
+ end
40
+
41
+ can "return false from #batch_save if validation fails" do
42
+ assert_false(
43
+ @client.Contact.batch_save do
44
+ @client.Contact.build(email_address: "guy-with-no-name@example.com")
45
+ end
46
+ )
47
+ end
48
+ end
@@ -0,0 +1,52 @@
1
+ <Response xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
2
+ <Id>8ce97e9e-31d2-457a-a75d-5e359ffac103</Id>
3
+ <Status>OK</Status>
4
+ <ProviderName>Xero API Previewer</ProviderName>
5
+ <DateTimeUTC>2013-05-20T11:54:50.9949578Z</DateTimeUTC>
6
+ <BankTransactions>
7
+ <BankTransaction>
8
+ <Contact>
9
+ <ContactID>a852a44c-3d8f-4c4b-a628-3a2c2121b9b1</ContactID>
10
+ <Name>Bank West</Name>
11
+ </Contact>
12
+ <Date>2013-05-16T00:00:00</Date>
13
+ <Status>AUTHORISED</Status>
14
+ <LineAmountTypes>Inclusive</LineAmountTypes>
15
+ <SubTotal>1000.00</SubTotal>
16
+ <TotalTax>0.00</TotalTax>
17
+ <Total>1000.00</Total>
18
+ <UpdatedDateUTC>2013-05-16T15:45:07.563</UpdatedDateUTC>
19
+ <CurrencyCode>GBP</CurrencyCode>
20
+ <BankTransactionID>ee4c4d5a-437d-4369-b152-848bd932f431</BankTransactionID>
21
+ <BankAccount>
22
+ <AccountID>bd9e85e0-0478-433d-ae9f-0b3c4f04bfe4</AccountID>
23
+ <Code>090</Code>
24
+ <Name>Business Bank Account</Name>
25
+ </BankAccount>
26
+ <Type>SPEND</Type>
27
+ <IsReconciled>false</IsReconciled>
28
+ </BankTransaction>
29
+ <BankTransaction>
30
+ <Contact>
31
+ <ContactID>d69ed9cd-f98a-4604-aced-54a1611554b2</ContactID>
32
+ <Name>Foo</Name>
33
+ </Contact>
34
+ <Date>2013-05-20T00:00:00</Date>
35
+ <Status>AUTHORISED</Status>
36
+ <LineAmountTypes>Inclusive</LineAmountTypes>
37
+ <SubTotal>123.00</SubTotal>
38
+ <TotalTax>0.00</TotalTax>
39
+ <Total>123.00</Total>
40
+ <UpdatedDateUTC>2013-05-20T11:39:41.34</UpdatedDateUTC>
41
+ <CurrencyCode>USD</CurrencyCode>
42
+ <BankTransactionID>8644472b-ea4e-4e5a-9987-f79250ece35e</BankTransactionID>
43
+ <BankAccount>
44
+ <AccountID>66c28ab3-05c2-4db7-9066-4357b8099fd8</AccountID>
45
+ <Name>FooBar</Name>
46
+ </BankAccount>
47
+ <Type>SPEND</Type>
48
+ <IsReconciled>false</IsReconciled>
49
+ <CurrencyRate>1.519520</CurrencyRate>
50
+ </BankTransaction>
51
+ </BankTransactions>
52
+ </Response>
@@ -17,7 +17,8 @@
17
17
  <TotalTax>19.90</TotalTax>
18
18
  <Total>218.90</Total>
19
19
  <UpdatedDateUTC>2008-09-16T09:40:09.467</UpdatedDateUTC>
20
- <CurrencyCode>AUD</CurrencyCode>
20
+ <CurrencyCode>USD</CurrencyCode>
21
+ <CurrencyRate>1.5619</CurrencyRate>
21
22
  <FullyPaidOnDate>2011-03-07T00:00:00</FullyPaidOnDate>
22
23
  <Type>ACCPAYCREDIT</Type>
23
24
  <CreditNoteID>7df8949c-b71f-40c0-bbcf-39f2f450f286</CreditNoteID>
@@ -166,4 +167,4 @@
166
167
  <SentToContact>false</SentToContact>
167
168
  </CreditNote>
168
169
  </CreditNotes>
169
- </Response>
170
+ </Response>
@@ -17,7 +17,8 @@
17
17
  <TotalTax>13.50</TotalTax>
18
18
  <Total>148.50</Total>
19
19
  <UpdatedDateUTC>2008-09-16T10:28:51.5</UpdatedDateUTC>
20
- <CurrencyCode>AUD</CurrencyCode>
20
+ <CurrencyCode>USD</CurrencyCode>
21
+ <CurrencyRate>1.516990</CurrencyRate>
21
22
  <FullyPaidOnDate>2011-02-24T00:00:00</FullyPaidOnDate>
22
23
  <Type>ACCPAY</Type>
23
24
  <InvoiceID>0032f627-3156-4d30-9b1c-4d3b994dc921</InvoiceID>
@@ -1896,4 +1897,4 @@
1896
1897
  <SentToContact>false</SentToContact>
1897
1898
  </Invoice>
1898
1899
  </Invoices>
1899
- </Response>
1900
+ </Response>
@@ -0,0 +1,65 @@
1
+ <Response xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
2
+ <Id>3465b325-f509-4323-9d1c-31b112aaae06</Id>
3
+ <Status>OK</Status>
4
+ <ProviderName>Xero API Previewer</ProviderName>
5
+ <DateTimeUTC>2013-05-20T11:57:54.000904Z</DateTimeUTC>
6
+ <BankTransactions>
7
+ <BankTransaction>
8
+ <Contact>
9
+ <ContactID>d69ed9cd-f98a-4604-aced-54a1611554b2</ContactID>
10
+ <ContactStatus>ACTIVE</ContactStatus>
11
+ <Name>Foo</Name>
12
+ <Addresses>
13
+ <Address>
14
+ <AddressType>POBOX</AddressType>
15
+ </Address>
16
+ <Address>
17
+ <AddressType>STREET</AddressType>
18
+ </Address>
19
+ </Addresses>
20
+ <Phones>
21
+ <Phone>
22
+ <PhoneType>DDI</PhoneType>
23
+ </Phone>
24
+ <Phone>
25
+ <PhoneType>DEFAULT</PhoneType>
26
+ </Phone>
27
+ <Phone>
28
+ <PhoneType>FAX</PhoneType>
29
+ </Phone>
30
+ <Phone>
31
+ <PhoneType>MOBILE</PhoneType>
32
+ </Phone>
33
+ </Phones>
34
+ <UpdatedDateUTC>2013-05-20T11:39:41.34</UpdatedDateUTC>
35
+ <DefaultCurrency>USD</DefaultCurrency>
36
+ </Contact>
37
+ <Date>2013-05-20T00:00:00</Date>
38
+ <Status>AUTHORISED</Status>
39
+ <LineAmountTypes>Inclusive</LineAmountTypes>
40
+ <LineItems>
41
+ <LineItem>
42
+ <UnitAmount>123.00</UnitAmount>
43
+ <TaxType>NONE</TaxType>
44
+ <TaxAmount>0.00</TaxAmount>
45
+ <LineAmount>123.00</LineAmount>
46
+ <AccountCode>320</AccountCode>
47
+ <Quantity>1.0000</Quantity>
48
+ </LineItem>
49
+ </LineItems>
50
+ <SubTotal>123.00</SubTotal>
51
+ <TotalTax>0.00</TotalTax>
52
+ <Total>123.00</Total>
53
+ <UpdatedDateUTC>2013-05-20T11:39:41.34</UpdatedDateUTC>
54
+ <CurrencyCode>USD</CurrencyCode>
55
+ <BankTransactionID>8644472b-ea4e-4e5a-9987-f79250ece35e</BankTransactionID>
56
+ <BankAccount>
57
+ <AccountID>66c28ab3-05c2-4db7-9066-4357b8099fd8</AccountID>
58
+ <Name>FooBar</Name>
59
+ </BankAccount>
60
+ <Type>SPEND</Type>
61
+ <IsReconciled>false</IsReconciled>
62
+ <CurrencyRate>1.519520</CurrencyRate>
63
+ </BankTransaction>
64
+ </BankTransactions>
65
+ </Response>
@@ -0,0 +1,72 @@
1
+ <Response xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
2
+ <Id>3f444db0-9293-4b4a-bd2c-b2b59ee51442</Id>
3
+ <Status>OK</Status>
4
+ <ProviderName>Xero API Previewer</ProviderName>
5
+ <DateTimeUTC>2013-05-20T11:56:54.1897372Z</DateTimeUTC>
6
+ <BankTransactions>
7
+ <BankTransaction>
8
+ <Contact>
9
+ <ContactID>a852a44c-3d8f-4c4b-a628-3a2c2121b9b1</ContactID>
10
+ <ContactStatus>ACTIVE</ContactStatus>
11
+ <Name>Bank West</Name>
12
+ <Addresses>
13
+ <Address>
14
+ <AddressType>POBOX</AddressType>
15
+ <AddressLine1>Procurement Services</AddressLine1>
16
+ <AddressLine2>GPO 1234</AddressLine2>
17
+ <City>Pinehaven</City>
18
+ <PostalCode>PI98 7HV</PostalCode>
19
+ </Address>
20
+ <Address>
21
+ <AddressType>STREET</AddressType>
22
+ </Address>
23
+ </Addresses>
24
+ <Phones>
25
+ <Phone>
26
+ <PhoneType>MOBILE</PhoneType>
27
+ </Phone>
28
+ <Phone>
29
+ <PhoneType>DEFAULT</PhoneType>
30
+ <PhoneNumber>2023456</PhoneNumber>
31
+ <PhoneAreaCode>02</PhoneAreaCode>
32
+ </Phone>
33
+ <Phone>
34
+ <PhoneType>FAX</PhoneType>
35
+ </Phone>
36
+ <Phone>
37
+ <PhoneType>DDI</PhoneType>
38
+ </Phone>
39
+ </Phones>
40
+ <UpdatedDateUTC>2013-05-12T04:24:12.347</UpdatedDateUTC>
41
+ </Contact>
42
+ <Date>2013-05-16T00:00:00</Date>
43
+ <Status>AUTHORISED</Status>
44
+ <LineAmountTypes>Inclusive</LineAmountTypes>
45
+ <LineItems>
46
+ <LineItem>
47
+ <Description>Loan Repayment</Description>
48
+ <UnitAmount>1000.00</UnitAmount>
49
+ <TaxType>NONE</TaxType>
50
+ <TaxAmount>0.00</TaxAmount>
51
+ <LineAmount>1000.00</LineAmount>
52
+ <AccountCode>900</AccountCode>
53
+ <Quantity>1.0000</Quantity>
54
+ </LineItem>
55
+ </LineItems>
56
+ <SubTotal>1000.00</SubTotal>
57
+ <TotalTax>0.00</TotalTax>
58
+ <Total>1000.00</Total>
59
+ <UpdatedDateUTC>2013-05-16T15:45:07.563</UpdatedDateUTC>
60
+ <CurrencyCode>GBP</CurrencyCode>
61
+ <BankTransactionID>ee4c4d5a-437d-4369-b152-848bd932f431</BankTransactionID>
62
+ <BankAccount>
63
+ <AccountID>bd9e85e0-0478-433d-ae9f-0b3c4f04bfe4</AccountID>
64
+ <Code>090</Code>
65
+ <Name>Business Bank Account</Name>
66
+ </BankAccount>
67
+ <Type>SPEND</Type>
68
+ <IsReconciled>false</IsReconciled>
69
+ <CurrencyRate>1.000000</CurrencyRate>
70
+ </BankTransaction>
71
+ </BankTransactions>
72
+ </Response>
@@ -7,7 +7,7 @@ base_path = File.expand_path(File.dirname(__FILE__))
7
7
  if ARGV[3]
8
8
  models = [ARGV[3]]
9
9
  else
10
- models = %w(Account BrandingTheme Contact CreditNote Currency Employee Invoice Item ManualJournal Organisation Payment TaxRate TrackingCategory)
10
+ models = %w(Account BankTransaction BrandingTheme Contact CreditNote Currency Employee Invoice Item ManualJournal Organisation Payment TaxRate TrackingCategory)
11
11
  end
12
12
 
13
13
  models.each do | model_name |
@@ -17,7 +17,7 @@ models.each do | model_name |
17
17
  records = model.all
18
18
  File.open("#{base_path}/#{model_name.underscore.pluralize}.xml", "w") { | fp | fp.write model.response.response_xml }
19
19
 
20
- if %w(Contact CreditNote Invoice ManualJournal).include?(model_name)
20
+ if %w(BankTransaction Contact CreditNote Invoice ManualJournal).include?(model_name)
21
21
  records.each do | summary_record |
22
22
  record = model.find(summary_record.id)
23
23
  File.open("#{base_path}/records/#{model_name.underscore}-#{record.id}.xml", "w") { | fp | fp.write model.response.response_xml }
@@ -26,4 +26,4 @@ models.each do | model_name |
26
26
  record = model.find(records.first.id)
27
27
  File.open("#{base_path}/#{model_name.underscore.singularize}.xml", "w") { | fp | fp.write model.response.response_xml }
28
28
  end
29
- end
29
+ end
@@ -1,11 +1,14 @@
1
1
  require "test_helper"
2
2
 
3
3
  class BankTransactionTest < Test::Unit::TestCase
4
+ include TestHelper
4
5
  include Xeroizer::Record
5
6
 
6
7
  def setup
7
8
  fake_parent = Class.new do
8
9
  attr_accessor :application
10
+ def mark_dirty(*args)
11
+ end
9
12
  end.new
10
13
 
11
14
  the_line_items = [
@@ -15,6 +18,9 @@ class BankTransactionTest < Test::Unit::TestCase
15
18
 
16
19
  @the_bank_transaction = BankTransaction.new fake_parent
17
20
  @the_bank_transaction.line_items = the_line_items
21
+
22
+ @client = Xeroizer::PublicApplication.new(CONSUMER_KEY, CONSUMER_SECRET)
23
+ mock_api('BankTransactions')
18
24
  end
19
25
 
20
26
  context "given a bank_transaction with line_amount_types set to \"Exclusive\"" do
@@ -44,4 +50,26 @@ class BankTransactionTest < Test::Unit::TestCase
44
50
  assert_equal "1.0", @the_bank_transaction.sub_total.to_s
45
51
  end
46
52
  end
53
+
54
+ context "bank transaction totals" do
55
+
56
+ should "large-scale testing from API XML" do
57
+ bank_transactions = @client.BankTransaction.all
58
+ bank_transactions.each do | bank_transaction |
59
+ assert(!!bank_transaction.attributes[:currency_code], "Doesn't have currency code in attributes")
60
+ assert(!!bank_transaction.currency_code, "Doesn't have currency code in model")
61
+ assert(!!bank_transaction.currency_rate, "Doesn't have currency rate in model")
62
+
63
+
64
+ assert_equal(bank_transaction.attributes[:currency_code], bank_transaction.currency_code)
65
+
66
+ if bank_transaction.attributes[:currency_rate]
67
+ assert_equal(bank_transaction.attributes[:currency_rate], bank_transaction.currency_rate)
68
+ else
69
+ assert_equal(1.0, bank_transaction.currency_rate)
70
+ end
71
+ end
72
+ end
73
+
74
+ end
47
75
  end
@@ -26,6 +26,15 @@ class CreditNoteTest < Test::Unit::TestCase
26
26
  should "large-scale testing from API XML" do
27
27
  credit_notes = @client.CreditNote.all
28
28
  credit_notes.each do | credit_note |
29
+ assert_equal(credit_note.attributes[:currency_code], credit_note.currency_code)
30
+
31
+ if credit_note.attributes[:currency_rate]
32
+ assert_equal(credit_note.attributes[:currency_rate], credit_note.currency_rate)
33
+ else
34
+ assert_equal(1.0, credit_note.currency_rate)
35
+ end
36
+
37
+
29
38
  assert_equal(credit_note.attributes[:sub_total], credit_note.sub_total)
30
39
  assert_equal(credit_note.attributes[:total_tax], credit_note.total_tax)
31
40
  assert_equal(credit_note.attributes[:total], credit_note.total)
@@ -44,6 +44,13 @@ class InvoiceTest < Test::Unit::TestCase
44
44
  should "large-scale testing from API XML" do
45
45
  invoices = @client.Invoice.all
46
46
  invoices.each do | invoice |
47
+ assert_equal(invoice.attributes[:currency_code], invoice.currency_code)
48
+ if invoice.attributes[:currency_rate]
49
+ assert_equal(invoice.attributes[:currency_rate], invoice.currency_rate)
50
+ else
51
+ assert_equal(1.0, invoice.currency_rate)
52
+ end
53
+
47
54
  assert_equal(invoice.attributes[:sub_total], invoice.sub_total)
48
55
  assert_equal(invoice.attributes[:total_tax], invoice.total_tax)
49
56
  assert_equal(invoice.attributes[:total], invoice.total)
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: xeroizer-float
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.15.3.13
4
+ version: 2.15.3.14
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -10,7 +10,7 @@ authors:
10
10
  autorequire:
11
11
  bindir: bin
12
12
  cert_chain: []
13
- date: 2013-02-21 00:00:00.000000000 Z
13
+ date: 2013-05-20 00:00:00.000000000 Z
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
16
16
  name: builder
@@ -60,6 +60,22 @@ dependencies:
60
60
  - - ! '>='
61
61
  - !ruby/object:Gem::Version
62
62
  version: '0'
63
+ - !ruby/object:Gem::Dependency
64
+ name: tzinfo
65
+ requirement: !ruby/object:Gem::Requirement
66
+ none: false
67
+ requirements:
68
+ - - ! '>='
69
+ - !ruby/object:Gem::Version
70
+ version: '0'
71
+ type: :runtime
72
+ prerelease: false
73
+ version_requirements: !ruby/object:Gem::Requirement
74
+ none: false
75
+ requirements:
76
+ - - ! '>='
77
+ - !ruby/object:Gem::Version
78
+ version: '0'
63
79
  - !ruby/object:Gem::Dependency
64
80
  name: nokogiri
65
81
  requirement: !ruby/object:Gem::Requirement
@@ -278,8 +294,10 @@ files:
278
294
  - test/acceptance/about_fetching_bank_transactions_test.rb
279
295
  - test/acceptance/acceptance_test.rb
280
296
  - test/acceptance/bank_transaction_reference_data.rb
297
+ - test/acceptance/bulk_operations_test.rb
281
298
  - test/stub_responses/accounts.xml
282
299
  - test/stub_responses/api_exception.xml
300
+ - test/stub_responses/bank_transactions.xml
283
301
  - test/stub_responses/bogus_oauth_error
284
302
  - test/stub_responses/branding_themes.xml
285
303
  - test/stub_responses/contact.xml
@@ -304,6 +322,8 @@ files:
304
322
  - test/stub_responses/organisations.xml
305
323
  - test/stub_responses/payments.xml
306
324
  - test/stub_responses/rate_limit_exceeded
325
+ - test/stub_responses/records/bank_transaction-8644472b-ea4e-4e5a-9987-f79250ece35e.xml
326
+ - test/stub_responses/records/bank_transaction-ee4c4d5a-437d-4369-b152-848bd932f431.xml
307
327
  - test/stub_responses/records/contact-043892a1-aef1-4c18-88d8-b8ccb6d31466.xml
308
328
  - test/stub_responses/records/contact-09664078-efe2-4a88-89a5-67eac9b0047b.xml
309
329
  - test/stub_responses/records/contact-0a4cf37b-a1a8-4753-9ee2-f9207f63a8ff.xml