quickbooks-ruby 0.0.1
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.
- checksums.yaml +7 -0
- data/lib/quickbooks.rb +81 -0
- data/lib/quickbooks/http_encoding_helper.rb +49 -0
- data/lib/quickbooks/model/base_model.rb +41 -0
- data/lib/quickbooks/model/custom_field.rb +9 -0
- data/lib/quickbooks/model/customer.rb +118 -0
- data/lib/quickbooks/model/customer_ref.rb +21 -0
- data/lib/quickbooks/model/discount_override.rb +15 -0
- data/lib/quickbooks/model/email_address.rb +19 -0
- data/lib/quickbooks/model/invoice.rb +96 -0
- data/lib/quickbooks/model/invoice_item_ref.rb +21 -0
- data/lib/quickbooks/model/invoice_line_item.rb +45 -0
- data/lib/quickbooks/model/item.rb +99 -0
- data/lib/quickbooks/model/linked_transaction.rb +9 -0
- data/lib/quickbooks/model/meta_data.rb +24 -0
- data/lib/quickbooks/model/payment_line_detail.rb +10 -0
- data/lib/quickbooks/model/physical_address.rb +24 -0
- data/lib/quickbooks/model/sales_item_line_detail.rb +14 -0
- data/lib/quickbooks/model/sub_total_line_detail.rb +11 -0
- data/lib/quickbooks/model/telephone_number.rb +7 -0
- data/lib/quickbooks/model/web_site_address.rb +16 -0
- data/lib/quickbooks/service/base_service.rb +317 -0
- data/lib/quickbooks/service/customer.rb +23 -0
- data/lib/quickbooks/service/invoice.rb +21 -0
- data/lib/quickbooks/service/item.rb +22 -0
- data/lib/quickbooks/service/service_crud.rb +61 -0
- data/lib/quickbooks/util/logging.rb +9 -0
- data/lib/quickbooks/version.rb +5 -0
- metadata +224 -0
@@ -0,0 +1,21 @@
|
|
1
|
+
module Quickbooks
|
2
|
+
module Model
|
3
|
+
class InvoiceItemRef < BaseModel
|
4
|
+
xml_convention :camelcase
|
5
|
+
xml_accessor :name, :from => '@name' # Attribute with name 'name'
|
6
|
+
xml_accessor :value, :from => :content
|
7
|
+
|
8
|
+
def initialize(value = nil)
|
9
|
+
self.value = value
|
10
|
+
end
|
11
|
+
|
12
|
+
def to_i
|
13
|
+
self.value.to_i
|
14
|
+
end
|
15
|
+
|
16
|
+
def to_s
|
17
|
+
self.value.to_s
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
module Quickbooks
|
2
|
+
module Model
|
3
|
+
class InvoiceLineItem < BaseModel
|
4
|
+
|
5
|
+
#== Constants
|
6
|
+
SALES_LINE_ITEM_DETAIL = 'SalesItemLineDetail'
|
7
|
+
SUB_TOTAL_LINE_DETAIL = 'SubTotalLineDetail'
|
8
|
+
PAYMENT_LINE_DETAIL = 'PaymentLineDetail'
|
9
|
+
|
10
|
+
xml_accessor :id, :from => 'Id', :as => Integer
|
11
|
+
xml_accessor :line_num, :from => 'LineNum', :as => Integer
|
12
|
+
xml_accessor :description, :from => 'Description'
|
13
|
+
xml_accessor :amount, :from => 'Amount', :as => Float
|
14
|
+
xml_accessor :detail_type, :from => 'DetailType'
|
15
|
+
|
16
|
+
#== Various detail types
|
17
|
+
xml_accessor :sales_line_item_detail, :from => 'SalesItemLineDetail', :as => Quickbooks::Model::SalesItemLineDetail
|
18
|
+
xml_accessor :sub_total_line_detail, :from => 'SubTotalLineDetail', :as => Quickbooks::Model::SubTotalLineDetail
|
19
|
+
xml_accessor :payment_line_detail, :from => 'PaymentLineDetail', :as => Quickbooks::Model::PaymentLineDetail
|
20
|
+
|
21
|
+
def sales_item?
|
22
|
+
detail_type.to_s == SALES_LINE_ITEM_DETAIL
|
23
|
+
end
|
24
|
+
|
25
|
+
def sub_total_item?
|
26
|
+
detail_type.to_s == SUB_TOTAL_LINE_DETAIL
|
27
|
+
end
|
28
|
+
|
29
|
+
def sales_item!
|
30
|
+
self.detail_type = SALES_LINE_ITEM_DETAIL
|
31
|
+
self.sales_line_item_detail = Quickbooks::Model::SalesItemLineDetail.new
|
32
|
+
|
33
|
+
yield self.sales_line_item_detail if block_given?
|
34
|
+
end
|
35
|
+
|
36
|
+
def payment_item!
|
37
|
+
self.detail_type = PAYMENT_LINE_DETAIL
|
38
|
+
self.payment_line_detail = Quickbooks::Model::PaymentLineDetail.new
|
39
|
+
|
40
|
+
yield self.payment_line_detail if block_given?
|
41
|
+
end
|
42
|
+
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,99 @@
|
|
1
|
+
# Business Rules
|
2
|
+
# * The item name must be unique.
|
3
|
+
# * Service item types must have IncomeAccountRef.
|
4
|
+
# * Service item types must have ExpenseAccountRef.
|
5
|
+
|
6
|
+
module Quickbooks
|
7
|
+
module Model
|
8
|
+
class Item < BaseModel
|
9
|
+
|
10
|
+
XML_COLLECTION_NODE = "Item"
|
11
|
+
XML_NODE = "Item"
|
12
|
+
REST_RESOURCE = 'item'
|
13
|
+
|
14
|
+
INVENTORY_TYPE = 'Inventory'
|
15
|
+
NON_INVENTORY_TYPE = 'Non Inventory'
|
16
|
+
SERVICE_TYPE = 'Service'
|
17
|
+
ITEM_TYPES = [INVENTORY_TYPE, NON_INVENTORY_TYPE, SERVICE_TYPE]
|
18
|
+
|
19
|
+
xml_name 'Item'
|
20
|
+
xml_accessor :id, :from => 'Id', :as => Integer
|
21
|
+
xml_accessor :sync_token, :from => 'SyncToken', :as => Integer
|
22
|
+
xml_accessor :meta_data, :from => 'MetaData', :as => Quickbooks::Model::MetaData
|
23
|
+
xml_accessor :attachable_ref, :from => 'AttachableRef', :as => Integer
|
24
|
+
xml_accessor :name, :from => 'Name'
|
25
|
+
xml_accessor :description, :from => 'Description'
|
26
|
+
xml_accessor :active, :from => 'Active'
|
27
|
+
xml_accessor :parent_ref, :from => 'ParentRef', :as => Integer
|
28
|
+
xml_accessor :sub_item, :from => 'SubItem'
|
29
|
+
xml_accessor :unit_price, :from => 'UnitPrice', :as => Float
|
30
|
+
xml_accessor :rate_percent, :from => 'RatePercent', :as => Float
|
31
|
+
xml_accessor :type, :from => 'Type'
|
32
|
+
xml_accessor :taxable, :from => 'Taxable'
|
33
|
+
xml_accessor :asset_account_ref, :from => 'AssetAccountRef', :as => Integer
|
34
|
+
xml_accessor :income_account_ref, :from => 'IncomeAccountRef', :as => Integer
|
35
|
+
xml_accessor :purchase_desc, :from => 'PurchaseDesc'
|
36
|
+
xml_accessor :purchase_cost, :from => 'PurchaseCost', :as => Float
|
37
|
+
xml_accessor :expense_account_ref, :from => 'ExpenseAccountRef', :as => Integer
|
38
|
+
xml_accessor :quantity_on_hand, :from => 'QtyOnHand', :as => Float
|
39
|
+
xml_accessor :sales_tax_code_ref, :from => 'SalesTaxCodeRef', :as => Integer
|
40
|
+
xml_accessor :purchase_tax_code_ref, :from => 'PurchaseTaxCodeRef', :as => Integer
|
41
|
+
xml_accessor :track_quantity_on_hand, :from => 'TrackQtyOnHand'
|
42
|
+
xml_accessor :asset_account, :from => 'AssetAccount', :as => Integer
|
43
|
+
xml_accessor :level, :from => 'Level', :as => Integer
|
44
|
+
xml_accessor :sales_tax_included, :from => 'SalesTaxIncluded'
|
45
|
+
xml_accessor :purchase_tax_included, :from => 'PurchaseTaxIncluded'
|
46
|
+
|
47
|
+
validates_length_of :name, :minimum => 1
|
48
|
+
validates_inclusion_of :type, :in => ITEM_TYPES
|
49
|
+
|
50
|
+
def initialize
|
51
|
+
self.type = INVENTORY_TYPE
|
52
|
+
super
|
53
|
+
end
|
54
|
+
|
55
|
+
def active?
|
56
|
+
active.to_s == 'true'
|
57
|
+
end
|
58
|
+
|
59
|
+
def sub_item?
|
60
|
+
sub_item.to_s == 'true'
|
61
|
+
end
|
62
|
+
|
63
|
+
def taxable?
|
64
|
+
taxable.to_s == 'true'
|
65
|
+
end
|
66
|
+
|
67
|
+
def track_quantity_on_hand?
|
68
|
+
track_quantity_on_hand.to_s == 'true'
|
69
|
+
end
|
70
|
+
|
71
|
+
def sales_tax_included?
|
72
|
+
sales_tax_included.to_s == 'true'
|
73
|
+
end
|
74
|
+
|
75
|
+
def purchase_tax_included?
|
76
|
+
purchase_tax_included.to_s == 'true'
|
77
|
+
end
|
78
|
+
|
79
|
+
def valid_for_create?
|
80
|
+
valid?
|
81
|
+
errors.empty?
|
82
|
+
end
|
83
|
+
|
84
|
+
# To delete an object Intuit requires we provide Id and SyncToken fields
|
85
|
+
def valid_for_deletion?
|
86
|
+
return false if(id.nil? || sync_token.nil?)
|
87
|
+
id.to_i > 0 && !sync_token.to_s.empty? && sync_token.to_i >= 0
|
88
|
+
end
|
89
|
+
|
90
|
+
def valid_for_update?
|
91
|
+
if sync_token.nil?
|
92
|
+
errors.add(:sync_token, "Missing required attribute SyncToken for update")
|
93
|
+
end
|
94
|
+
errors.empty?
|
95
|
+
end
|
96
|
+
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
require 'time'
|
2
|
+
|
3
|
+
module Quickbooks
|
4
|
+
module Model
|
5
|
+
class MetaData < BaseModel
|
6
|
+
xml_accessor :create_time, :from => 'CreateTime', :as => Time
|
7
|
+
xml_accessor :last_updated_time, :from => 'LastUpdatedTime', :as => Time
|
8
|
+
|
9
|
+
def to_xml(options = {})
|
10
|
+
xml = %Q{<MetaData>}
|
11
|
+
xml = "#{xml}<CreateTime>#{formatted_date(create_time)}</CreateTime>"
|
12
|
+
xml = "#{xml}<LastUpdatedTime>#{formatted_date(last_updated_time)}</LastUpdatedTime></MetaData>"
|
13
|
+
xml
|
14
|
+
end
|
15
|
+
|
16
|
+
private
|
17
|
+
|
18
|
+
def formatted_date(datetime)
|
19
|
+
datetime.strftime('%Y-%m-%dT%H:%M:%S%z')
|
20
|
+
end
|
21
|
+
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,10 @@
|
|
1
|
+
module Quickbooks
|
2
|
+
module Model
|
3
|
+
class PaymentLineDetail < BaseModel
|
4
|
+
xml_accessor :item_ref, :from => 'ItemRef', :as => Integer
|
5
|
+
xml_accessor :class_ref, :from => 'ClassRef', :as => Integer
|
6
|
+
xml_accessor :balance, :from => 'Balance', :as => Float
|
7
|
+
xml_accessor :discount, :from => 'Discount', :as => Quickbooks::Model::DiscountOverride
|
8
|
+
end
|
9
|
+
end
|
10
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module Quickbooks
|
2
|
+
module Model
|
3
|
+
class PhysicalAddress < BaseModel
|
4
|
+
xml_accessor :id, :from => 'Id', :as => Integer
|
5
|
+
xml_accessor :line1, :from => 'Line1'
|
6
|
+
xml_accessor :line2, :from => 'Line2'
|
7
|
+
xml_accessor :line3, :from => 'Line3'
|
8
|
+
xml_accessor :line4, :from => 'Line4'
|
9
|
+
xml_accessor :line5, :from => 'Line5'
|
10
|
+
xml_accessor :city, :from => 'City'
|
11
|
+
xml_accessor :country, :from => 'Country'
|
12
|
+
xml_accessor :country_sub_division_code, :from => 'CountrySubDivisionCode'
|
13
|
+
xml_accessor :postal_code, :from => 'PostalCode'
|
14
|
+
xml_accessor :note, :from => 'Note'
|
15
|
+
xml_accessor :lat, :from => 'Lat', :as => Float
|
16
|
+
xml_accessor :lon, :from => 'Long', :as => Float
|
17
|
+
|
18
|
+
def zip
|
19
|
+
postal_code
|
20
|
+
end
|
21
|
+
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
module Quickbooks
|
2
|
+
module Model
|
3
|
+
class SalesItemLineDetail < BaseModel
|
4
|
+
xml_accessor :item_ref, :from => 'ItemRef', :as => Integer
|
5
|
+
xml_accessor :class_ref, :from => 'ClassRef', :as => Integer
|
6
|
+
xml_accessor :unit_price, :from => 'UnitPrice', :as => Float
|
7
|
+
xml_accessor :rate_percent, :from => 'RatePercent', :as => Float
|
8
|
+
xml_accessor :price_level_ref, :from => 'PriceLevelRef', :as => Integer
|
9
|
+
xml_accessor :quantity, :from => 'Qty', :as => Float
|
10
|
+
xml_accessor :tax_code_ref, :from => 'TaxCodeRef'
|
11
|
+
xml_accessor :service_date, :from => 'ServiceDate', :as => Date
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,11 @@
|
|
1
|
+
module Quickbooks
|
2
|
+
module Model
|
3
|
+
class SubTotalLineDetail < BaseModel
|
4
|
+
xml_accessor :item_ref, :from => 'ItemRef', :as => Integer
|
5
|
+
xml_accessor :class_ref, :from => 'ClassRef'
|
6
|
+
xml_accessor :unit_price, :from => 'UnitPrice', :as => Float
|
7
|
+
xml_accessor :quantity, :from => 'Qty', :as => Float
|
8
|
+
xml_accessor :tax_code_ref, :from => 'TaxCodeRef'
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
module Quickbooks
|
2
|
+
module Model
|
3
|
+
class WebSiteAddress < BaseModel
|
4
|
+
xml_accessor :uri, :from => 'URI'
|
5
|
+
|
6
|
+
def initialize(uri = nil)
|
7
|
+
self.uri = uri if uri
|
8
|
+
end
|
9
|
+
|
10
|
+
def to_xml(options = {})
|
11
|
+
return "" if uri.to_s.empty?
|
12
|
+
super(options)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,317 @@
|
|
1
|
+
#require 'rexml/document'
|
2
|
+
require 'uri'
|
3
|
+
require 'cgi'
|
4
|
+
|
5
|
+
class IntuitRequestException < StandardError
|
6
|
+
attr_accessor :message, :code, :detail, :type
|
7
|
+
def initialize(msg)
|
8
|
+
self.message = msg
|
9
|
+
super(msg)
|
10
|
+
end
|
11
|
+
end
|
12
|
+
class AuthorizationFailure < StandardError; end
|
13
|
+
|
14
|
+
module Quickbooks
|
15
|
+
module Service
|
16
|
+
class BaseService
|
17
|
+
include Quickbooks::Util::Logging
|
18
|
+
|
19
|
+
attr_accessor :company_id
|
20
|
+
attr_accessor :oauth
|
21
|
+
attr_reader :base_uri
|
22
|
+
attr_reader :last_response_body
|
23
|
+
attr_reader :last_response_xml
|
24
|
+
|
25
|
+
XML_NS = %{xmlns="http://schema.intuit.com/finance/v3"}
|
26
|
+
HTTP_CONTENT_TYPE = 'application/xml'
|
27
|
+
HTTP_ACCEPT = 'application/xml'
|
28
|
+
|
29
|
+
def initialize()
|
30
|
+
@base_uri = 'https://qb.sbfinance.intuit.com/v3/company'
|
31
|
+
end
|
32
|
+
|
33
|
+
def access_token=(token)
|
34
|
+
@oauth = token
|
35
|
+
end
|
36
|
+
|
37
|
+
def company_id=(company_id)
|
38
|
+
@company_id = company_id
|
39
|
+
end
|
40
|
+
|
41
|
+
# realm & company are synonymous
|
42
|
+
def realm_id=(company_id)
|
43
|
+
@company_id = company_id
|
44
|
+
end
|
45
|
+
|
46
|
+
def url_for_resource(resource)
|
47
|
+
"#{url_for_base}/#{resource}"
|
48
|
+
end
|
49
|
+
|
50
|
+
def url_for_base
|
51
|
+
"#{@base_uri}/#{@company_id}"
|
52
|
+
end
|
53
|
+
|
54
|
+
def url_for_query(query = "")
|
55
|
+
"#{url_for_base}/query?query=#{URI.encode(query)}"
|
56
|
+
end
|
57
|
+
|
58
|
+
private
|
59
|
+
|
60
|
+
def parse_xml(xml)
|
61
|
+
@last_response_xml =
|
62
|
+
begin
|
63
|
+
x = Nokogiri::XML(xml)
|
64
|
+
#x.document.remove_namespaces!
|
65
|
+
x
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
def valid_xml_document(xml)
|
70
|
+
%Q{<?xml version="1.0" encoding="utf-8"?>\n#{xml.strip}}
|
71
|
+
end
|
72
|
+
|
73
|
+
# A single object response is the same as a collection response except
|
74
|
+
# it just has a single main element
|
75
|
+
def fetch_object(model, url, params = {}, options = {})
|
76
|
+
raise ArgumentError, "missing model to instantiate" if model.nil?
|
77
|
+
response = do_http_get(url, params)
|
78
|
+
collection = parse_collection(response, model)
|
79
|
+
if collection.is_a?(Quickbooks::Collection)
|
80
|
+
collection.entries.first
|
81
|
+
else
|
82
|
+
nil
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
def fetch_collection(query, model, options = {})
|
87
|
+
page = options.fetch(:page, 1)
|
88
|
+
per_page = options.fetch(:per_page, 20)
|
89
|
+
|
90
|
+
if page == 1
|
91
|
+
start_position = 1
|
92
|
+
else
|
93
|
+
start_position = (page * per_page) + 1 # page=2, per_page=10 then we want to start at 11
|
94
|
+
end
|
95
|
+
|
96
|
+
max_results = (page * per_page)
|
97
|
+
|
98
|
+
query = "#{query} STARTPOSITION #{start_position} MAXRESULTS #{max_results}"
|
99
|
+
response = do_http_get(url_for_query(query))
|
100
|
+
|
101
|
+
parse_collection(response, model)
|
102
|
+
end
|
103
|
+
|
104
|
+
#
|
105
|
+
# def fetch_collection2(model, custom_field_query = nil, filters = [], page = 1, per_page = 20, sort = nil, options ={})
|
106
|
+
# raise ArgumentError, "missing model to instantiate" if model.nil?
|
107
|
+
#
|
108
|
+
# post_body_tags = []
|
109
|
+
#
|
110
|
+
# # pagination parameters must come first
|
111
|
+
# post_body_tags << "<StartPage>#{page}</StartPage>"
|
112
|
+
# post_body_tags << "<ChunkSize>#{per_page}</ChunkSize>"
|
113
|
+
#
|
114
|
+
# # ... followed by any filters
|
115
|
+
# if filters.is_a?(Array) && filters.length > 0
|
116
|
+
# filters = enforce_filter_order(filters).compact
|
117
|
+
# post_body_tags << filters.collect { |f| f.to_xml }
|
118
|
+
# post_body_tags.flatten!
|
119
|
+
# end
|
120
|
+
#
|
121
|
+
# if sort
|
122
|
+
# post_body_tags << sort.to_xml
|
123
|
+
# end
|
124
|
+
#
|
125
|
+
# post_body_tags << custom_field_query
|
126
|
+
#
|
127
|
+
# xml_query_tag = "#{model::XML_NODE}Query"
|
128
|
+
# body = %Q{<?xml version="1.0" encoding="utf-8"?>\n<#{xml_query_tag} xmlns="http://www.intuit.com/sb/cdm/v2">#{post_body_tags.join}</#{xml_query_tag}>}
|
129
|
+
#
|
130
|
+
# response = do_http_post(url_for_resource(model::REST_RESOURCE), body, {}, {'Content-Type' => 'text/xml'})
|
131
|
+
# parse_collection(response, model)
|
132
|
+
# end
|
133
|
+
|
134
|
+
def parse_collection(response, model)
|
135
|
+
if response
|
136
|
+
collection = Quickbooks::Collection.new
|
137
|
+
xml = @last_response_xml
|
138
|
+
begin
|
139
|
+
results = []
|
140
|
+
|
141
|
+
query_response = xml.xpath("//xmlns:IntuitResponse/xmlns:QueryResponse")[0]
|
142
|
+
if query_response
|
143
|
+
|
144
|
+
start_pos_attr = query_response.attributes['startPosition']
|
145
|
+
if start_pos_attr
|
146
|
+
collection.start_position = start_pos_attr.value.to_i
|
147
|
+
end
|
148
|
+
|
149
|
+
max_results_attr = query_response.attributes['maxResults']
|
150
|
+
if max_results_attr
|
151
|
+
collection.max_results = max_results_attr.value.to_i
|
152
|
+
end
|
153
|
+
|
154
|
+
total_count_attr = query_response.attributes['totalCount']
|
155
|
+
if total_count_attr
|
156
|
+
collection.total_count = total_count_attr.value.to_i
|
157
|
+
end
|
158
|
+
end
|
159
|
+
|
160
|
+
path_to_nodes = "//xmlns:IntuitResponse//xmlns:#{model::XML_NODE}"
|
161
|
+
collection.count = xml.xpath(path_to_nodes).count
|
162
|
+
if collection.count > 0
|
163
|
+
xml.xpath(path_to_nodes).each do |xa|
|
164
|
+
entry = model.from_xml(xa)
|
165
|
+
results << entry
|
166
|
+
end
|
167
|
+
end
|
168
|
+
collection.entries = results
|
169
|
+
rescue => ex
|
170
|
+
#log("Error parsing XML: #{ex.message}")
|
171
|
+
raise IntuitRequestException.new("Error parsing XML: #{ex.message}")
|
172
|
+
end
|
173
|
+
collection
|
174
|
+
else
|
175
|
+
nil
|
176
|
+
end
|
177
|
+
end
|
178
|
+
|
179
|
+
# Given an IntuitResponse which is expected to wrap a single
|
180
|
+
# Entity node, e.g.
|
181
|
+
# <IntuitResponse xmlns="http://schema.intuit.com/finance/v3" time="2013-11-16T10:26:42.762-08:00">
|
182
|
+
# <Customer domain="QBO" sparse="false">
|
183
|
+
# <Id>1</Id>
|
184
|
+
# ...
|
185
|
+
# </Customer>
|
186
|
+
# </IntuitResponse>
|
187
|
+
def parse_singular_entity_response(model, xml)
|
188
|
+
xmldoc = Nokogiri(xml)
|
189
|
+
xmldoc.xpath("//xmlns:IntuitResponse/xmlns:#{model::XML_NODE}")[0]
|
190
|
+
end
|
191
|
+
|
192
|
+
# A successful delete request returns a XML packet like:
|
193
|
+
# <IntuitResponse xmlns="http://schema.intuit.com/finance/v3" time="2013-04-23T08:30:33.626-07:00">
|
194
|
+
# <Payment domain="QBO" status="Deleted">
|
195
|
+
# <Id>8748</Id>
|
196
|
+
# </Payment>
|
197
|
+
# </IntuitResponse>
|
198
|
+
def parse_singular_entity_response_for_delete(model, xml)
|
199
|
+
xmldoc = Nokogiri(xml)
|
200
|
+
xmldoc.xpath("//xmlns:IntuitResponse/xmlns:#{model::XML_NODE}[@status='Deleted']").length == 1
|
201
|
+
end
|
202
|
+
|
203
|
+
def perform_write(model, body = "", params = {}, headers = {})
|
204
|
+
url = url_for_resource(model::REST_RESOURCE)
|
205
|
+
unless headers.has_key?('Content-Type')
|
206
|
+
headers['Content-Type'] = 'text/xml'
|
207
|
+
end
|
208
|
+
response = do_http_post(url, body.strip, params, headers)
|
209
|
+
|
210
|
+
result = nil
|
211
|
+
if response
|
212
|
+
case response.code.to_i
|
213
|
+
when 200
|
214
|
+
result = Quickbooks::Model::RestResponse.from_xml(response.body)
|
215
|
+
when 401
|
216
|
+
raise IntuitRequestException.new("Authorization failure: token timed out?")
|
217
|
+
when 404
|
218
|
+
raise IntuitRequestException.new("Resource Not Found: Check URL and try again")
|
219
|
+
end
|
220
|
+
end
|
221
|
+
result
|
222
|
+
end
|
223
|
+
|
224
|
+
def do_http_post(url, body = "", params = {}, headers = {}) # throws IntuitRequestException
|
225
|
+
url = add_query_string_to_url(url, params)
|
226
|
+
do_http(:post, url, body, headers)
|
227
|
+
end
|
228
|
+
|
229
|
+
def do_http_get(url, params = {}, headers = {}) # throws IntuitRequestException
|
230
|
+
do_http(:get, url, {}, headers)
|
231
|
+
end
|
232
|
+
|
233
|
+
def do_http(method, url, body, headers) # throws IntuitRequestException
|
234
|
+
if @oauth.nil?
|
235
|
+
raise "OAuth client has not been initialized. Initialize with setter access_token="
|
236
|
+
end
|
237
|
+
unless headers.has_key?('Content-Type')
|
238
|
+
headers.merge!({'Content-Type' => HTTP_CONTENT_TYPE})
|
239
|
+
end
|
240
|
+
# log "------ New Request ------"
|
241
|
+
# log "METHOD = #{method}"
|
242
|
+
# log "RESOURCE = #{url}"
|
243
|
+
# log "BODY(#{body.class}) = #{body == nil ? "<NIL>" : body.inspect}"
|
244
|
+
# log "HEADERS = #{headers.inspect}"
|
245
|
+
response = @oauth.request(method, url, body, headers)
|
246
|
+
check_response(response)
|
247
|
+
end
|
248
|
+
|
249
|
+
def add_query_string_to_url(url, params)
|
250
|
+
if params.is_a?(Hash) && !params.empty?
|
251
|
+
url + "?" + params.collect { |k| "#{k.first}=#{k.last}" }.join("&")
|
252
|
+
else
|
253
|
+
url
|
254
|
+
end
|
255
|
+
end
|
256
|
+
|
257
|
+
def check_response(response)
|
258
|
+
# puts "RESPONSE CODE = #{response.code}"
|
259
|
+
# puts "RESPONSE BODY = #{response.body}"
|
260
|
+
parse_xml(response.body)
|
261
|
+
status = response.code.to_i
|
262
|
+
case status
|
263
|
+
when 200
|
264
|
+
# even HTTP 200 can contain an error, so we always have to peek for an Error
|
265
|
+
if response_is_error?
|
266
|
+
parse_and_raise_exception
|
267
|
+
else
|
268
|
+
response
|
269
|
+
end
|
270
|
+
when 302
|
271
|
+
raise "Unhandled HTTP Redirect"
|
272
|
+
when 401
|
273
|
+
raise AuthorizationFailure
|
274
|
+
when 400, 500
|
275
|
+
parse_and_raise_exception
|
276
|
+
else
|
277
|
+
raise "HTTP Error Code: #{status}, Msg: #{response.body}"
|
278
|
+
end
|
279
|
+
end
|
280
|
+
|
281
|
+
def parse_and_raise_exception
|
282
|
+
err = parse_intuit_error
|
283
|
+
ex = IntuitRequestException.new(err[:message])
|
284
|
+
ex.code = err[:code]
|
285
|
+
ex.detail = err[:detail]
|
286
|
+
ex.type = err[:type]
|
287
|
+
|
288
|
+
raise ex
|
289
|
+
end
|
290
|
+
|
291
|
+
def response_is_error?
|
292
|
+
@last_response_xml.xpath("//xmlns:IntuitResponse/xmlns:Fault")[0] != nil
|
293
|
+
end
|
294
|
+
|
295
|
+
def parse_intuit_error
|
296
|
+
error = {:message => "", :detail => "", :type => nil, :code => 0}
|
297
|
+
fault = @last_response_xml.xpath("//xmlns:IntuitResponse/xmlns:Fault")[0]
|
298
|
+
if fault
|
299
|
+
error[:type] = fault.attributes['type'].value
|
300
|
+
|
301
|
+
error_element = fault.xpath("//xmlns:Error")[0]
|
302
|
+
if error_element
|
303
|
+
code_attr = error_element.attributes['code']
|
304
|
+
if code_attr
|
305
|
+
error[:code] = code_attr.value
|
306
|
+
end
|
307
|
+
error[:message] = error_element.xpath("//xmlns:Message").text
|
308
|
+
error[:detail] = error_element.xpath("//xmlns:Detail").text
|
309
|
+
end
|
310
|
+
end
|
311
|
+
|
312
|
+
error
|
313
|
+
end
|
314
|
+
|
315
|
+
end
|
316
|
+
end
|
317
|
+
end
|