xeroizer 2.15.3 → 2.15.5

Sign up to get free protection for your applications and to get access to all the features.
@@ -21,13 +21,15 @@ module Xeroizer
21
21
 
22
22
  has_many :tracking, :model_name => 'TrackingCategoryChild'
23
23
 
24
- # Swallow assignment of attributes that should only be calculated automatically.
25
- def line_amount=(value); raise SettingTotalDirectlyNotSupported.new(:line_amount); end
26
-
24
+ def line_amount=(line_amount)
25
+ @line_amount_set = true
26
+ attributes[:line_amount] = line_amount
27
+ end
28
+
27
29
  # Calculate the line_total (if there is a quantity and unit_amount).
28
30
  # Description-only lines have been allowed since Xero V2.09.
29
31
  def line_amount(summary_only = false)
30
- return attributes[:line_amount] if summary_only
32
+ return attributes[:line_amount] if summary_only || @line_amount_set
31
33
 
32
34
  BigDecimal((quantity * unit_amount).to_s).round(2) if quantity && unit_amount
33
35
  end
@@ -12,24 +12,24 @@ 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
19
19
  set_possible_primary_keys :manual_journal_id
20
20
  list_contains_summary_only true
21
21
 
22
- guid :manual_journal_id
23
- date :date
24
- string :status
25
- string :line_amount_types
26
- string :narration
27
- string :url
28
- string :external_link_provider_name # only seems to be read-only at the moment
29
- boolean :show_on_cash_basis_reports
30
- datetime :updated_date_utc, :api_name => 'UpdatedDateUTC'
22
+ guid :manual_journal_id
23
+ date :date
24
+ string :status
25
+ string :line_amount_types
26
+ string :narration
27
+ string :url
28
+ string :external_link_provider_name # only seems to be read-only at the moment
29
+ boolean :show_on_cash_basis_reports
30
+ datetime_utc :updated_date_utc, :api_name => 'UpdatedDateUTC'
31
31
 
32
- has_many :journal_lines, :model_name => 'ManualJournalLine'
32
+ has_many :journal_lines, :model_name => 'ManualJournalLine'
33
33
 
34
34
  validates_presence_of :narration
35
35
  validates_associated :journal_lines
@@ -39,4 +39,4 @@ module Xeroizer
39
39
  end
40
40
 
41
41
  end
42
- end
42
+ end
@@ -12,17 +12,17 @@ module Xeroizer
12
12
 
13
13
  set_primary_key :payment_id
14
14
 
15
- guid :payment_id
16
- date :date
17
- decimal :amount
18
- decimal :currency_rate
19
- string :payment_type
20
- string :status
21
- string :reference
22
- datetime :updated_date_utc, :api_name => 'UpdatedDateUTC'
15
+ guid :payment_id
16
+ date :date
17
+ decimal :amount
18
+ decimal :currency_rate
19
+ string :payment_type
20
+ string :status
21
+ string :reference
22
+ datetime_utc :updated_date_utc, :api_name => 'UpdatedDateUTC'
23
23
 
24
- belongs_to :account
25
- belongs_to :invoice
24
+ belongs_to :account
25
+ belongs_to :invoice
26
26
 
27
27
  def invoice_id
28
28
  invoice.id if invoice
@@ -39,4 +39,4 @@ module Xeroizer
39
39
  end
40
40
 
41
41
  end
42
- end
42
+ end
@@ -11,6 +11,7 @@ module Xeroizer
11
11
 
12
12
  string :name
13
13
  string :tax_type
14
+ string :status
14
15
  boolean :can_apply_to_assets
15
16
  boolean :can_apply_to_equity
16
17
  boolean :can_apply_to_expenses
@@ -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)
@@ -8,14 +8,19 @@ module Xeroizer
8
8
  include ClassLevelInheritableAttributes
9
9
  class_inheritable_attributes :api_controller_name
10
10
 
11
- class InvaidPermissionError < StandardError; end
11
+ module InvaidPermissionError; end
12
+ class InvalidPermissionError < StandardError
13
+ include InvaidPermissionError
14
+ end
12
15
  ALLOWED_PERMISSIONS = [:read, :write, :update]
13
16
  class_inheritable_attributes :permissions
14
17
 
15
18
  class_inheritable_attributes :xml_root_name
16
19
  class_inheritable_attributes :optional_xml_root_name
17
20
  class_inheritable_attributes :xml_node_name
18
-
21
+
22
+ DEFAULT_RECORDS_PER_BATCH_SAVE = 2000
23
+
19
24
  include BaseModelHttpProxy
20
25
 
21
26
  attr_reader :application
@@ -40,7 +45,7 @@ module Xeroizer
40
45
  def set_permissions(*args)
41
46
  self.permissions = {}
42
47
  args.each do | permission |
43
- raise InvaidPermissionError.new("Permission #{permission} is invalid.") unless ALLOWED_PERMISSIONS.include?(permission)
48
+ raise InvalidPermissionError.new("Permission #{permission} is invalid.") unless ALLOWED_PERMISSIONS.include?(permission)
44
49
  self.permissions[permission] = true
45
50
  end
46
51
  end
@@ -86,7 +91,22 @@ module Xeroizer
86
91
 
87
92
  # Build a record with attributes set to the value of attributes.
88
93
  def build(attributes = {})
89
- 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
90
110
  end
91
111
 
92
112
  # Create (build and save) a record with attributes set to the value of attributes.
@@ -119,7 +139,37 @@ module Xeroizer
119
139
  result.complete_record_downloaded = true if result
120
140
  result
121
141
  end
122
-
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
+
123
173
  def parse_response(response_xml, options = {})
124
174
  Response.parse(response_xml, options) do | response, elements, response_model_name |
125
175
  if model_name == response_model_name
@@ -128,17 +178,40 @@ module Xeroizer
128
178
  end
129
179
  end
130
180
  end
131
-
181
+
132
182
  protected
133
-
183
+
134
184
  # Parse the records part of the XML response and builds model instances as necessary.
135
185
  def parse_records(response, elements)
136
186
  elements.each do | element |
137
- 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
138
195
  end
139
196
  end
140
-
141
- end
142
-
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
+
143
216
  end
144
217
  end
@@ -17,7 +17,8 @@ module Xeroizer
17
17
  def parse_params(options)
18
18
  params = {}
19
19
  params[:ModifiedAfter] = options[:modified_since] if options[:modified_since]
20
- params[:OrderBy] = options[:order] if options[:order]
20
+ params[:order] = options[:order] if options[:order]
21
+
21
22
  if options[:where]
22
23
  params[:where] = case options[:where]
23
24
  when String then options[:where]
@@ -42,6 +42,7 @@ module Xeroizer
42
42
  def decimal(field_name, options = {}); define_simple_attribute(field_name, :decimal, options, 0.0); end
43
43
  def date(field_name, options = {}); define_simple_attribute(field_name, :date, options); end
44
44
  def datetime(field_name, options = {}); define_simple_attribute(field_name, :datetime, options); end
45
+ def datetime_utc(field_name, options = {}); define_simple_attribute(field_name, :datetime_utc, options); end
45
46
 
46
47
  def guid(field_name, options = {})
47
48
  # Ensure all automated Id conversions are changed to ID.
@@ -72,6 +73,7 @@ module Xeroizer
72
73
 
73
74
  unless options[:skip_writer]
74
75
  define_method "#{internal_field_name}=".to_sym do | value |
76
+ parent.mark_dirty(self) if parent
75
77
  @attributes[field_name] = value
76
78
  end
77
79
  end
@@ -88,6 +90,7 @@ module Xeroizer
88
90
 
89
91
  # Sets the value of the Xero primary key for this record if it exists.
90
92
  def id=(new_id)
93
+ parent.mark_dirty(self) if parent
91
94
  self[self.class.primary_key_name] = new_id
92
95
  end
93
96
 
@@ -1,3 +1,5 @@
1
+ require 'active_support/time'
2
+
1
3
  module Xeroizer
2
4
  module Record
3
5
  module XmlHelper
@@ -23,6 +25,7 @@ module Xeroizer
23
25
  when :decimal then BigDecimal.new(element.text)
24
26
  when :date then Date.parse(element.text)
25
27
  when :datetime then Time.parse(element.text)
28
+ when :datetime_utc then ActiveSupport::TimeZone['UTC'].parse(element.text).utc.round(3)
26
29
  when :belongs_to
27
30
  model_name = field[:model_name] ? field[:model_name].to_sym : element.name.to_sym
28
31
  Xeroizer::Record.const_get(model_name).build_from_node(element, parent)
@@ -44,7 +47,8 @@ module Xeroizer
44
47
  end
45
48
  end
46
49
  end
47
-
50
+
51
+ parent.mark_clean(record)
48
52
  record
49
53
  end
50
54
 
@@ -84,7 +88,7 @@ module Xeroizer
84
88
  end
85
89
  end
86
90
 
87
- # Format a attribute for use in the XML passed to Xero.
91
+ # Format an attribute for use in the XML passed to Xero.
88
92
  def xml_value_from_field(b, field, value)
89
93
  case field[:type]
90
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