quickbooks-ruby-oauth2 0.6.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/faraday/middleware/gzip.rb +72 -0
- data/lib/quickbooks/model/access_token_response.rb +29 -0
- data/lib/quickbooks/model/account.rb +78 -0
- data/lib/quickbooks/model/account_based_expense_line_detail.rb +15 -0
- data/lib/quickbooks/model/attachable.rb +33 -0
- data/lib/quickbooks/model/attachable_ref.rb +22 -0
- data/lib/quickbooks/model/base_model.rb +120 -0
- data/lib/quickbooks/model/base_model_json.rb +16 -0
- data/lib/quickbooks/model/base_reference.rb +23 -0
- data/lib/quickbooks/model/batch_request.rb +45 -0
- data/lib/quickbooks/model/batch_response.rb +33 -0
- data/lib/quickbooks/model/bill.rb +45 -0
- data/lib/quickbooks/model/bill_line_item.rb +42 -0
- data/lib/quickbooks/model/bill_payment.rb +40 -0
- data/lib/quickbooks/model/bill_payment_check.rb +12 -0
- data/lib/quickbooks/model/bill_payment_credit_card.rb +10 -0
- data/lib/quickbooks/model/bill_payment_line_item.rb +21 -0
- data/lib/quickbooks/model/budget.rb +27 -0
- data/lib/quickbooks/model/budget_line_item.rb +15 -0
- data/lib/quickbooks/model/change_data_capture.rb +42 -0
- data/lib/quickbooks/model/change_model.rb +10 -0
- data/lib/quickbooks/model/check_payment.rb +11 -0
- data/lib/quickbooks/model/class.rb +31 -0
- data/lib/quickbooks/model/company_info.rb +69 -0
- data/lib/quickbooks/model/credit_card_payment.rb +17 -0
- data/lib/quickbooks/model/credit_memo.rb +59 -0
- data/lib/quickbooks/model/credit_memo_change.rb +8 -0
- data/lib/quickbooks/model/custom_field.rb +13 -0
- data/lib/quickbooks/model/customer.rb +76 -0
- data/lib/quickbooks/model/customer_change.rb +8 -0
- data/lib/quickbooks/model/definition.rb +46 -0
- data/lib/quickbooks/model/delivery_info.rb +8 -0
- data/lib/quickbooks/model/department.rb +31 -0
- data/lib/quickbooks/model/deposit.rb +44 -0
- data/lib/quickbooks/model/deposit_line_detail.rb +17 -0
- data/lib/quickbooks/model/deposit_line_item.rb +35 -0
- data/lib/quickbooks/model/description_line_detail.rb +7 -0
- data/lib/quickbooks/model/discount_line_detail.rb +15 -0
- data/lib/quickbooks/model/discount_override.rb +13 -0
- data/lib/quickbooks/model/document_numbering.rb +18 -0
- data/lib/quickbooks/model/effective_tax_rate.rb +15 -0
- data/lib/quickbooks/model/email_address.rb +19 -0
- data/lib/quickbooks/model/employee.rb +47 -0
- data/lib/quickbooks/model/entity.rb +16 -0
- data/lib/quickbooks/model/entity_ref.rb +8 -0
- data/lib/quickbooks/model/estimate.rb +76 -0
- data/lib/quickbooks/model/exchange_rate.rb +23 -0
- data/lib/quickbooks/model/fault.rb +16 -0
- data/lib/quickbooks/model/global_tax_calculation.rb +13 -0
- data/lib/quickbooks/model/group_line_detail.rb +13 -0
- data/lib/quickbooks/model/has_line_items.rb +14 -0
- data/lib/quickbooks/model/invoice.rb +107 -0
- data/lib/quickbooks/model/invoice_change.rb +9 -0
- data/lib/quickbooks/model/invoice_group_line_detail.rb +14 -0
- data/lib/quickbooks/model/invoice_line_item.rb +88 -0
- data/lib/quickbooks/model/item.rb +87 -0
- data/lib/quickbooks/model/item_based_expense_line_detail.rb +20 -0
- data/lib/quickbooks/model/item_change.rb +8 -0
- data/lib/quickbooks/model/item_group_detail.rb +9 -0
- data/lib/quickbooks/model/item_group_line.rb +20 -0
- data/lib/quickbooks/model/journal_entry.rb +36 -0
- data/lib/quickbooks/model/journal_entry_line_detail.rb +27 -0
- data/lib/quickbooks/model/line.rb +95 -0
- data/lib/quickbooks/model/line_ex.rb +9 -0
- data/lib/quickbooks/model/linked_transaction.rb +9 -0
- data/lib/quickbooks/model/markup_info.rb +12 -0
- data/lib/quickbooks/model/meta_data.rb +24 -0
- data/lib/quickbooks/model/name_value.rb +8 -0
- data/lib/quickbooks/model/other_contact_info.rb +8 -0
- data/lib/quickbooks/model/payment.rb +43 -0
- data/lib/quickbooks/model/payment_change.rb +8 -0
- data/lib/quickbooks/model/payment_line_detail.rb +12 -0
- data/lib/quickbooks/model/payment_method.rb +25 -0
- data/lib/quickbooks/model/physical_address.rb +40 -0
- data/lib/quickbooks/model/preferences.rb +49 -0
- data/lib/quickbooks/model/purchase.rb +57 -0
- data/lib/quickbooks/model/purchase_line_item.rb +44 -0
- data/lib/quickbooks/model/purchase_order.rb +50 -0
- data/lib/quickbooks/model/purchase_tax_rate_list.rb +11 -0
- data/lib/quickbooks/model/refund_receipt.rb +62 -0
- data/lib/quickbooks/model/refund_receipt_change.rb +8 -0
- data/lib/quickbooks/model/report.rb +52 -0
- data/lib/quickbooks/model/sales_item_line_detail.rb +16 -0
- data/lib/quickbooks/model/sales_receipt.rb +65 -0
- data/lib/quickbooks/model/sales_tax_rate_list.rb +11 -0
- data/lib/quickbooks/model/sub_total_line_detail.rb +13 -0
- data/lib/quickbooks/model/tax_agency.rb +18 -0
- data/lib/quickbooks/model/tax_code.rb +21 -0
- data/lib/quickbooks/model/tax_line.rb +15 -0
- data/lib/quickbooks/model/tax_line_detail.rb +16 -0
- data/lib/quickbooks/model/tax_rate.rb +28 -0
- data/lib/quickbooks/model/tax_rate_detail.rb +15 -0
- data/lib/quickbooks/model/tax_rate_detail_line.rb +23 -0
- data/lib/quickbooks/model/tax_service.rb +52 -0
- data/lib/quickbooks/model/telephone_number.rb +13 -0
- data/lib/quickbooks/model/term.rb +28 -0
- data/lib/quickbooks/model/time_activity.rb +79 -0
- data/lib/quickbooks/model/transaction_tax_detail.rb +12 -0
- data/lib/quickbooks/model/transfer.rb +21 -0
- data/lib/quickbooks/model/upload.rb +35 -0
- data/lib/quickbooks/model/validator.rb +10 -0
- data/lib/quickbooks/model/vendor.rb +53 -0
- data/lib/quickbooks/model/vendor_change.rb +8 -0
- data/lib/quickbooks/model/vendor_credit.rb +40 -0
- data/lib/quickbooks/model/web_site_address.rb +16 -0
- data/lib/quickbooks/service/access_token.rb +38 -0
- data/lib/quickbooks/service/account.rb +17 -0
- data/lib/quickbooks/service/attachable.rb +12 -0
- data/lib/quickbooks/service/base_service.rb +445 -0
- data/lib/quickbooks/service/base_service_json.rb +39 -0
- data/lib/quickbooks/service/batch.rb +11 -0
- data/lib/quickbooks/service/bill.rb +16 -0
- data/lib/quickbooks/service/bill_payment.rb +16 -0
- data/lib/quickbooks/service/budget.rb +12 -0
- data/lib/quickbooks/service/change_data_capture.rb +24 -0
- data/lib/quickbooks/service/change_service.rb +28 -0
- data/lib/quickbooks/service/class.rb +23 -0
- data/lib/quickbooks/service/company_info.rb +12 -0
- data/lib/quickbooks/service/credit_memo.rb +16 -0
- data/lib/quickbooks/service/credit_memo_change.rb +16 -0
- data/lib/quickbooks/service/customer.rb +18 -0
- data/lib/quickbooks/service/customer_change.rb +16 -0
- data/lib/quickbooks/service/department.rb +23 -0
- data/lib/quickbooks/service/deposit.rb +16 -0
- data/lib/quickbooks/service/employee.rb +24 -0
- data/lib/quickbooks/service/estimate.rb +22 -0
- data/lib/quickbooks/service/exchange_rate.rb +25 -0
- data/lib/quickbooks/service/invoice.rb +45 -0
- data/lib/quickbooks/service/invoice_change.rb +16 -0
- data/lib/quickbooks/service/item.rb +32 -0
- data/lib/quickbooks/service/item_change.rb +16 -0
- data/lib/quickbooks/service/journal_entry.rb +16 -0
- data/lib/quickbooks/service/payment.rb +29 -0
- data/lib/quickbooks/service/payment_change.rb +16 -0
- data/lib/quickbooks/service/payment_method.rb +30 -0
- data/lib/quickbooks/service/preferences.rb +12 -0
- data/lib/quickbooks/service/purchase.rb +16 -0
- data/lib/quickbooks/service/purchase_order.rb +16 -0
- data/lib/quickbooks/service/refund_receipt.rb +16 -0
- data/lib/quickbooks/service/refund_receipt_change.rb +16 -0
- data/lib/quickbooks/service/reports.rb +33 -0
- data/lib/quickbooks/service/responses/methods.rb +17 -0
- data/lib/quickbooks/service/responses/oauth1_http_response.rb +42 -0
- data/lib/quickbooks/service/responses/oauth2_http_response.rb +43 -0
- data/lib/quickbooks/service/responses/oauth_http_response.rb +21 -0
- data/lib/quickbooks/service/sales_receipt.rb +46 -0
- data/lib/quickbooks/service/service_crud.rb +75 -0
- data/lib/quickbooks/service/service_crud_json.rb +31 -0
- data/lib/quickbooks/service/tax_agency.rb +12 -0
- data/lib/quickbooks/service/tax_code.rb +12 -0
- data/lib/quickbooks/service/tax_rate.rb +12 -0
- data/lib/quickbooks/service/tax_service.rb +11 -0
- data/lib/quickbooks/service/term.rb +17 -0
- data/lib/quickbooks/service/time_activity.rb +16 -0
- data/lib/quickbooks/service/transfer.rb +16 -0
- data/lib/quickbooks/service/upload.rb +39 -0
- data/lib/quickbooks/service/vendor.rb +24 -0
- data/lib/quickbooks/service/vendor_change.rb +16 -0
- data/lib/quickbooks/service/vendor_credit.rb +16 -0
- data/lib/quickbooks/util/collection.rb +18 -0
- data/lib/quickbooks/util/http_encoding_helper.rb +54 -0
- data/lib/quickbooks/util/logging.rb +19 -0
- data/lib/quickbooks/util/multipart.rb +85 -0
- data/lib/quickbooks/util/name_entity.rb +85 -0
- data/lib/quickbooks/util/query_builder.rb +35 -0
- data/lib/quickbooks/version.rb +5 -0
- data/lib/quickbooks-ruby.rb +255 -0
- metadata +378 -0
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# === Business Rules
|
|
2
|
+
# * The VendorRef attribute must be specified.
|
|
3
|
+
# * At lease one Line with Line.Amount must be specified.
|
|
4
|
+
|
|
5
|
+
module Quickbooks
|
|
6
|
+
module Model
|
|
7
|
+
class VendorCredit < BaseModel
|
|
8
|
+
include GlobalTaxCalculation
|
|
9
|
+
include HasLineItems
|
|
10
|
+
|
|
11
|
+
#== Constants
|
|
12
|
+
REST_RESOURCE = 'vendorcredit'
|
|
13
|
+
XML_COLLECTION_NODE = "VendorCredit"
|
|
14
|
+
XML_NODE = "VendorCredit"
|
|
15
|
+
|
|
16
|
+
xml_accessor :id, :from => 'Id'
|
|
17
|
+
xml_accessor :sync_token, :from => 'SyncToken', :as => Integer
|
|
18
|
+
xml_accessor :meta_data, :from => 'MetaData', :as => MetaData
|
|
19
|
+
xml_accessor :doc_number, :from => 'DocNumber'
|
|
20
|
+
xml_accessor :txn_date, :from => 'TxnDate', :as => Date
|
|
21
|
+
xml_accessor :private_note, :from => 'PrivateNote'
|
|
22
|
+
|
|
23
|
+
xml_accessor :line_items, :from => 'Line', :as => [PurchaseLineItem]
|
|
24
|
+
xml_accessor :department_ref, :from => 'DepartmentRef', :as => BaseReference
|
|
25
|
+
xml_accessor :ap_account_ref, :from => 'APAccountRef', :as => BaseReference
|
|
26
|
+
xml_accessor :vendor_ref, :from => 'VendorRef', :as => BaseReference
|
|
27
|
+
xml_accessor :total, :from => 'TotalAmt', :as => BigDecimal, :to_xml => Proc.new { |val| val.to_f }
|
|
28
|
+
|
|
29
|
+
xml_accessor :currency_ref, :from => 'CurrencyRef', :as => BaseReference
|
|
30
|
+
xml_accessor :exchange_rate, :from => 'ExchangeRate', :as => BigDecimal, :to_xml => to_xml_big_decimal
|
|
31
|
+
|
|
32
|
+
reference_setters
|
|
33
|
+
|
|
34
|
+
#== This adds aliases for backwards compatability to old attributes names
|
|
35
|
+
alias_method :total_amount, :total
|
|
36
|
+
alias_method :total_amount=, :total=
|
|
37
|
+
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
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,38 @@
|
|
|
1
|
+
module Quickbooks
|
|
2
|
+
module Service
|
|
3
|
+
class AccessToken < BaseService
|
|
4
|
+
|
|
5
|
+
RENEW_URL = "https://appcenter.intuit.com/api/v1/connection/reconnect"
|
|
6
|
+
DISCONNECT_URL = "https://appcenter.intuit.com/api/v1/connection/disconnect"
|
|
7
|
+
|
|
8
|
+
# https://developer.intuit.com/docs/0025_quickbooksapi/0053_auth_auth/oauth_management_api#Reconnect
|
|
9
|
+
def renew
|
|
10
|
+
result = nil
|
|
11
|
+
response = do_http_get(RENEW_URL)
|
|
12
|
+
if response
|
|
13
|
+
code = response.code.to_i
|
|
14
|
+
if code == 200
|
|
15
|
+
result = Quickbooks::Model::AccessTokenResponse.from_xml(response.plain_body)
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
result
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# https://developer.intuit.com/docs/0025_quickbooksapi/0053_auth_auth/oauth_management_api#Disconnect
|
|
23
|
+
def disconnect
|
|
24
|
+
result = nil
|
|
25
|
+
response = do_http_get(DISCONNECT_URL)
|
|
26
|
+
if response
|
|
27
|
+
code = response.code.to_i
|
|
28
|
+
if code == 200
|
|
29
|
+
result = Quickbooks::Model::AccessTokenResponse.from_xml(response.plain_body)
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
result
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -0,0 +1,445 @@
|
|
|
1
|
+
module Quickbooks
|
|
2
|
+
module Service
|
|
3
|
+
class BaseService
|
|
4
|
+
include Quickbooks::Util::Logging
|
|
5
|
+
include ServiceCrud
|
|
6
|
+
|
|
7
|
+
attr_accessor :company_id
|
|
8
|
+
attr_accessor :oauth
|
|
9
|
+
attr_reader :base_uri
|
|
10
|
+
attr_reader :last_response_body
|
|
11
|
+
attr_reader :last_response_xml
|
|
12
|
+
|
|
13
|
+
XML_NS = %{xmlns="http://schema.intuit.com/finance/v3"}
|
|
14
|
+
HTTP_CONTENT_TYPE = 'application/xml'
|
|
15
|
+
HTTP_ACCEPT = 'application/xml'
|
|
16
|
+
HTTP_ACCEPT_ENCODING = 'gzip, deflate'
|
|
17
|
+
BASE_DOMAIN = 'quickbooks.api.intuit.com'
|
|
18
|
+
SANDBOX_DOMAIN = 'sandbox-quickbooks.api.intuit.com'
|
|
19
|
+
|
|
20
|
+
def initialize(attributes = {})
|
|
21
|
+
domain = Quickbooks.sandbox_mode ? SANDBOX_DOMAIN : BASE_DOMAIN
|
|
22
|
+
@base_uri = "https://#{domain}/v3/company"
|
|
23
|
+
attributes.each {|key, value| public_send("#{key}=", value) }
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def access_token=(token)
|
|
27
|
+
@oauth = token
|
|
28
|
+
rebuild_connection!
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def company_id=(company_id)
|
|
32
|
+
@company_id = company_id
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# realm & company are synonymous
|
|
36
|
+
def realm_id=(company_id)
|
|
37
|
+
@company_id = company_id
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def oauth_v1?
|
|
41
|
+
@oauth.is_a? OAuth::AccessToken
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def oauth_v2?
|
|
45
|
+
@oauth.is_a? OAuth2::AccessToken
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# [OAuth2] The default Faraday connection does not have gzip or multipart support.
|
|
49
|
+
# We need to reset the existing connection and build a new one.
|
|
50
|
+
def rebuild_connection!
|
|
51
|
+
return unless oauth_v2?
|
|
52
|
+
@oauth.client.connection = nil
|
|
53
|
+
@oauth.client.connection.build do |builder|
|
|
54
|
+
builder.use :gzip
|
|
55
|
+
builder.request :multipart
|
|
56
|
+
builder.request :url_encoded
|
|
57
|
+
builder.adapter :net_http
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def url_for_resource(resource)
|
|
62
|
+
"#{url_for_base}/#{resource}"
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def url_for_base
|
|
66
|
+
raise MissingRealmError.new unless @company_id
|
|
67
|
+
"#{@base_uri}/#{@company_id}"
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def is_json?
|
|
71
|
+
self.class::HTTP_CONTENT_TYPE == "application/json"
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def is_pdf?
|
|
75
|
+
self.class::HTTP_CONTENT_TYPE == "application/pdf"
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def default_model_query
|
|
79
|
+
"SELECT * FROM #{self.class.name.split("::").last}"
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def url_for_query(query = nil, start_position = 1, max_results = 20, options = {})
|
|
83
|
+
query ||= default_model_query
|
|
84
|
+
query = "#{query} STARTPOSITION #{start_position} MAXRESULTS #{max_results}"
|
|
85
|
+
|
|
86
|
+
"#{url_for_base}/query?query=#{CGI.escape(query)}"
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
private
|
|
90
|
+
|
|
91
|
+
def parse_xml(xml)
|
|
92
|
+
@last_response_xml = Nokogiri::XML(xml)
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def valid_xml_document(xml)
|
|
96
|
+
%Q{<?xml version="1.0" encoding="utf-8"?>\n#{xml.strip}}
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# A single object response is the same as a collection response except
|
|
100
|
+
# it just has a single main element
|
|
101
|
+
def fetch_object(model, url, params = {})
|
|
102
|
+
raise ArgumentError, "missing model to instantiate" if model.nil?
|
|
103
|
+
response = do_http_get(url, params)
|
|
104
|
+
collection = parse_collection(response, model)
|
|
105
|
+
if collection.is_a?(Quickbooks::Collection)
|
|
106
|
+
collection.entries.first
|
|
107
|
+
else
|
|
108
|
+
nil
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def fetch_collection(query, model, options = {})
|
|
113
|
+
page = options.fetch(:page, 1)
|
|
114
|
+
per_page = options.fetch(:per_page, 20)
|
|
115
|
+
|
|
116
|
+
start_position = ((page - 1) * per_page) + 1 # page=2, per_page=10 then we want to start at 11
|
|
117
|
+
max_results = per_page
|
|
118
|
+
|
|
119
|
+
response = do_http_get(url_for_query(query, start_position, max_results))
|
|
120
|
+
|
|
121
|
+
parse_collection(response, model)
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def parse_collection(response, model)
|
|
125
|
+
if response
|
|
126
|
+
collection = Quickbooks::Collection.new
|
|
127
|
+
xml = @last_response_xml
|
|
128
|
+
begin
|
|
129
|
+
results = []
|
|
130
|
+
|
|
131
|
+
query_response = xml.xpath("//xmlns:IntuitResponse/xmlns:QueryResponse")[0]
|
|
132
|
+
if query_response
|
|
133
|
+
|
|
134
|
+
start_pos_attr = query_response.attributes['startPosition']
|
|
135
|
+
if start_pos_attr
|
|
136
|
+
collection.start_position = start_pos_attr.value.to_i
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
max_results_attr = query_response.attributes['maxResults']
|
|
140
|
+
if max_results_attr
|
|
141
|
+
collection.max_results = max_results_attr.value.to_i
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
total_count_attr = query_response.attributes['totalCount']
|
|
145
|
+
if total_count_attr
|
|
146
|
+
collection.total_count = total_count_attr.value.to_i
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
path_to_nodes = "//xmlns:IntuitResponse//xmlns:#{model::XML_NODE}"
|
|
151
|
+
collection.count = xml.xpath(path_to_nodes).count
|
|
152
|
+
if collection.count > 0
|
|
153
|
+
xml.xpath(path_to_nodes).each do |xa|
|
|
154
|
+
results << model.from_xml(xa)
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
collection.entries = results
|
|
159
|
+
rescue => ex
|
|
160
|
+
raise Quickbooks::IntuitRequestException.new("Error parsing XML: #{ex.message}")
|
|
161
|
+
end
|
|
162
|
+
collection
|
|
163
|
+
else
|
|
164
|
+
nil
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
# Given an IntuitResponse which is expected to wrap a single
|
|
169
|
+
# Entity node, e.g.
|
|
170
|
+
# <IntuitResponse xmlns="http://schema.intuit.com/finance/v3" time="2013-11-16T10:26:42.762-08:00">
|
|
171
|
+
# <Customer domain="QBO" sparse="false">
|
|
172
|
+
# <Id>1</Id>
|
|
173
|
+
# ...
|
|
174
|
+
# </Customer>
|
|
175
|
+
# </IntuitResponse>
|
|
176
|
+
def parse_singular_entity_response(model, xml, node_xpath_prefix = nil)
|
|
177
|
+
xmldoc = Nokogiri(xml)
|
|
178
|
+
prefix = node_xpath_prefix || model::XML_NODE
|
|
179
|
+
xmldoc.xpath("//xmlns:IntuitResponse/xmlns:#{prefix}")[0]
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
# A successful delete request returns a XML packet like:
|
|
183
|
+
# <IntuitResponse xmlns="http://schema.intuit.com/finance/v3" time="2013-04-23T08:30:33.626-07:00">
|
|
184
|
+
# <Payment domain="QBO" status="Deleted">
|
|
185
|
+
# <Id>8748</Id>
|
|
186
|
+
# </Payment>
|
|
187
|
+
# </IntuitResponse>
|
|
188
|
+
def parse_singular_entity_response_for_delete(model, xml)
|
|
189
|
+
xmldoc = Nokogiri(xml)
|
|
190
|
+
xmldoc.xpath("//xmlns:IntuitResponse/xmlns:#{model::XML_NODE}[@status='Deleted']").length == 1
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
def do_http_post(url, body = "", params = {}, headers = {}) # throws IntuitRequestException
|
|
194
|
+
url = add_query_string_to_url(url, params)
|
|
195
|
+
do_http(:post, url, body, headers)
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
def do_http_get(url, params = {}, headers = {}) # throws IntuitRequestException
|
|
199
|
+
url = add_query_string_to_url(url, params)
|
|
200
|
+
do_http(:get, url, {}, headers)
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
def do_http_raw_get(url, params = {}, headers = {})
|
|
204
|
+
url = add_query_string_to_url(url, params)
|
|
205
|
+
unless headers.has_key?('Content-Type')
|
|
206
|
+
headers['Content-Type'] = self.class::HTTP_CONTENT_TYPE
|
|
207
|
+
end
|
|
208
|
+
unless headers.has_key?('Accept')
|
|
209
|
+
headers['Accept'] = self.class::HTTP_ACCEPT
|
|
210
|
+
end
|
|
211
|
+
unless headers.has_key?('Accept-Encoding')
|
|
212
|
+
headers['Accept-Encoding'] = HTTP_ACCEPT_ENCODING
|
|
213
|
+
end
|
|
214
|
+
raw_response = oauth_get(url, headers)
|
|
215
|
+
Quickbooks::Service::Responses::OAuthHttpResponse.wrap(raw_response)
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
def do_http_file_upload(uploadIO, url, metadata = nil)
|
|
219
|
+
headers = {
|
|
220
|
+
'Content-Type' => 'multipart/form-data'
|
|
221
|
+
}
|
|
222
|
+
body = {}
|
|
223
|
+
body['file_content_0'] = uploadIO
|
|
224
|
+
|
|
225
|
+
if metadata
|
|
226
|
+
standalone_prefix = '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>'
|
|
227
|
+
meta_data_xml = "#{standalone_prefix}\n#{metadata.to_xml_ns.to_s}"
|
|
228
|
+
param_part = UploadIO.new(StringIO.new(meta_data_xml), "application/xml")
|
|
229
|
+
body['file_metadata_0'] = param_part
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
do_http(:upload, url, body, headers)
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
def do_http(method, url, body, headers) # throws IntuitRequestException
|
|
236
|
+
if @oauth.nil?
|
|
237
|
+
raise "OAuth client has not been initialized. Initialize with setter access_token="
|
|
238
|
+
end
|
|
239
|
+
unless headers.has_key?('Content-Type')
|
|
240
|
+
headers['Content-Type'] = self.class::HTTP_CONTENT_TYPE
|
|
241
|
+
end
|
|
242
|
+
unless headers.has_key?('Accept')
|
|
243
|
+
headers['Accept'] = self.class::HTTP_ACCEPT
|
|
244
|
+
end
|
|
245
|
+
unless headers.has_key?('Accept-Encoding')
|
|
246
|
+
headers['Accept-Encoding'] = HTTP_ACCEPT_ENCODING
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
log "------ QUICKBOOKS-RUBY REQUEST ------"
|
|
250
|
+
log "METHOD = #{method}"
|
|
251
|
+
log "RESOURCE = #{url}"
|
|
252
|
+
log_request_body(body)
|
|
253
|
+
log "REQUEST HEADERS = #{headers.inspect}"
|
|
254
|
+
|
|
255
|
+
raw_response = case method
|
|
256
|
+
when :get
|
|
257
|
+
oauth_get(url, headers)
|
|
258
|
+
when :post
|
|
259
|
+
oauth_post(url, body, headers)
|
|
260
|
+
when :upload
|
|
261
|
+
oauth_post_with_multipart(url, body, headers)
|
|
262
|
+
else
|
|
263
|
+
raise "Do not know how to perform that HTTP operation"
|
|
264
|
+
end
|
|
265
|
+
response = Quickbooks::Service::Responses::OAuthHttpResponse.wrap(raw_response)
|
|
266
|
+
check_response(response, :request => body)
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
def oauth_get(url, headers)
|
|
270
|
+
if oauth_v1?
|
|
271
|
+
@oauth.get(url, headers)
|
|
272
|
+
elsif oauth_v2?
|
|
273
|
+
@oauth.get(url, headers: headers, raise_errors: false)
|
|
274
|
+
else
|
|
275
|
+
raise InvalidOauthAccessTokenObject.new(@oauth)
|
|
276
|
+
end
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
def oauth_post(url, body, headers)
|
|
280
|
+
if oauth_v1?
|
|
281
|
+
@oauth.post(url, body, headers)
|
|
282
|
+
elsif oauth_v2?
|
|
283
|
+
@oauth.post(url, headers: headers, body: body, raise_errors: false)
|
|
284
|
+
else
|
|
285
|
+
raise InvalidOauthAccessTokenObject.new(@oauth)
|
|
286
|
+
end
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
def oauth_post_with_multipart(url, body, headers)
|
|
290
|
+
raw_response = if oauth_v1?
|
|
291
|
+
oauth.post_with_multipart(url, body, headers)
|
|
292
|
+
elsif oauth_v2?
|
|
293
|
+
oauth.post_with_multipart(url, headers: headers, body: body, raise_errors: false)
|
|
294
|
+
else
|
|
295
|
+
raise InvalidOauthAccessTokenObject.new(@oauth)
|
|
296
|
+
end
|
|
297
|
+
response = Quickbooks::Service::Responses::OAuthHttpResponse.wrap(raw_response)
|
|
298
|
+
check_response(response, :request => body)
|
|
299
|
+
end
|
|
300
|
+
|
|
301
|
+
def add_query_string_to_url(url, params)
|
|
302
|
+
if params.is_a?(Hash) && !params.empty?
|
|
303
|
+
url + "?" + params.collect { |k| "#{k.first}=#{k.last}" }.join("&")
|
|
304
|
+
else
|
|
305
|
+
url
|
|
306
|
+
end
|
|
307
|
+
end
|
|
308
|
+
|
|
309
|
+
def check_response(response, options = {})
|
|
310
|
+
log "------ QUICKBOOKS-RUBY RESPONSE ------"
|
|
311
|
+
log "RESPONSE CODE = #{response.code}"
|
|
312
|
+
if response.respond_to?(:headers)
|
|
313
|
+
log "RESPONSE HEADERS = #{response.headers}"
|
|
314
|
+
end
|
|
315
|
+
log_response_body(response)
|
|
316
|
+
status = response.code.to_i
|
|
317
|
+
case status
|
|
318
|
+
when 200
|
|
319
|
+
# even HTTP 200 can contain an error, so we always have to peek for an Error
|
|
320
|
+
if response_is_error?
|
|
321
|
+
parse_and_raise_exception(options)
|
|
322
|
+
else
|
|
323
|
+
response
|
|
324
|
+
end
|
|
325
|
+
when 302
|
|
326
|
+
raise "Unhandled HTTP Redirect"
|
|
327
|
+
when 401
|
|
328
|
+
raise Quickbooks::AuthorizationFailure
|
|
329
|
+
when 403
|
|
330
|
+
message = parse_intuit_error[:message]
|
|
331
|
+
if message.include?('ThrottleExceeded')
|
|
332
|
+
raise Quickbooks::ThrottleExceeded, message
|
|
333
|
+
end
|
|
334
|
+
raise Quickbooks::Forbidden, message
|
|
335
|
+
when 404
|
|
336
|
+
raise Quickbooks::NotFound
|
|
337
|
+
when 413
|
|
338
|
+
raise Quickbooks::RequestTooLarge
|
|
339
|
+
when 400, 500
|
|
340
|
+
parse_and_raise_exception(options)
|
|
341
|
+
when 429
|
|
342
|
+
message = parse_intuit_error[:message]
|
|
343
|
+
raise Quickbooks::TooManyRequests, message
|
|
344
|
+
when 503, 504
|
|
345
|
+
raise Quickbooks::ServiceUnavailable
|
|
346
|
+
else
|
|
347
|
+
raise "HTTP Error Code: #{status}, Msg: #{response.plain_body}"
|
|
348
|
+
end
|
|
349
|
+
end
|
|
350
|
+
|
|
351
|
+
def log_response_body(response)
|
|
352
|
+
log "RESPONSE BODY:"
|
|
353
|
+
if is_json?
|
|
354
|
+
log ">>>>#{response.plain_body.inspect}"
|
|
355
|
+
parse_json(response.plain_body)
|
|
356
|
+
elsif is_pdf?
|
|
357
|
+
log("BODY is a PDF : not dumping")
|
|
358
|
+
else
|
|
359
|
+
log(log_xml(response.plain_body))
|
|
360
|
+
parse_xml(response.plain_body)
|
|
361
|
+
end
|
|
362
|
+
end
|
|
363
|
+
|
|
364
|
+
def log_request_body(body)
|
|
365
|
+
log "REQUEST BODY:"
|
|
366
|
+
if is_json?
|
|
367
|
+
log(body.inspect)
|
|
368
|
+
elsif is_pdf?
|
|
369
|
+
log("BODY is a PDF : not dumping")
|
|
370
|
+
else
|
|
371
|
+
#multipart request for uploads arrive here in a Hash with UploadIO vals
|
|
372
|
+
if body.is_a?(Hash)
|
|
373
|
+
body.each do |k,v|
|
|
374
|
+
log('BODY PART:')
|
|
375
|
+
val_content = v.inspect
|
|
376
|
+
if v.is_a?(UploadIO)
|
|
377
|
+
if v.content_type == 'application/xml'
|
|
378
|
+
if v.io.is_a?(StringIO)
|
|
379
|
+
val_content = log_xml(v.io.string)
|
|
380
|
+
end
|
|
381
|
+
end
|
|
382
|
+
end
|
|
383
|
+
log("#{k}: #{val_content}")
|
|
384
|
+
end
|
|
385
|
+
else
|
|
386
|
+
log(log_xml(body))
|
|
387
|
+
end
|
|
388
|
+
end
|
|
389
|
+
end
|
|
390
|
+
|
|
391
|
+
def parse_and_raise_exception(options = {})
|
|
392
|
+
err = parse_intuit_error
|
|
393
|
+
ex = Quickbooks::IntuitRequestException.new("#{err[:message]}:\n\t#{err[:detail]}")
|
|
394
|
+
ex.code = err[:code]
|
|
395
|
+
ex.detail = err[:detail]
|
|
396
|
+
ex.type = err[:type]
|
|
397
|
+
if is_json?
|
|
398
|
+
ex.request_json = options[:request]
|
|
399
|
+
else
|
|
400
|
+
ex.request_xml = options[:request]
|
|
401
|
+
end
|
|
402
|
+
raise ex
|
|
403
|
+
end
|
|
404
|
+
|
|
405
|
+
def response_is_error?
|
|
406
|
+
begin
|
|
407
|
+
@last_response_xml.xpath("//xmlns:IntuitResponse/xmlns:Fault")[0] != nil
|
|
408
|
+
rescue Nokogiri::XML::XPath::SyntaxError => exception
|
|
409
|
+
#puts @last_response_xml.to_xml.to_s
|
|
410
|
+
#puts "WTF: #{exception.inspect}:#{exception.backtrace.join("\n")}"
|
|
411
|
+
true
|
|
412
|
+
end
|
|
413
|
+
end
|
|
414
|
+
|
|
415
|
+
def parse_intuit_error
|
|
416
|
+
error = {:message => "", :detail => "", :type => nil, :code => 0}
|
|
417
|
+
fault = @last_response_xml.xpath("//xmlns:IntuitResponse/xmlns:Fault")[0]
|
|
418
|
+
if fault
|
|
419
|
+
error[:type] = fault.attributes['type'].value
|
|
420
|
+
|
|
421
|
+
error_element = fault.xpath("//xmlns:Error")[0]
|
|
422
|
+
if error_element
|
|
423
|
+
code_attr = error_element.attributes['code']
|
|
424
|
+
if code_attr
|
|
425
|
+
error[:code] = code_attr.value
|
|
426
|
+
end
|
|
427
|
+
element_attr = error_element.attributes['element']
|
|
428
|
+
if code_attr
|
|
429
|
+
error[:element] = code_attr.value
|
|
430
|
+
end
|
|
431
|
+
error[:message] = error_element.xpath("//xmlns:Message").text
|
|
432
|
+
error[:detail] = error_element.xpath("//xmlns:Detail").text
|
|
433
|
+
end
|
|
434
|
+
end
|
|
435
|
+
|
|
436
|
+
error
|
|
437
|
+
rescue Nokogiri::XML::XPath::SyntaxError => exception
|
|
438
|
+
error[:detail] = @last_response_xml.to_s
|
|
439
|
+
|
|
440
|
+
error
|
|
441
|
+
end
|
|
442
|
+
|
|
443
|
+
end
|
|
444
|
+
end
|
|
445
|
+
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
module Quickbooks
|
|
2
|
+
module Service
|
|
3
|
+
class BaseServiceJSON < BaseService
|
|
4
|
+
include ServiceCrudJSON
|
|
5
|
+
HTTP_CONTENT_TYPE = 'application/json'
|
|
6
|
+
HTTP_ACCEPT = 'application/json'
|
|
7
|
+
attr_reader :last_response_json
|
|
8
|
+
|
|
9
|
+
private
|
|
10
|
+
|
|
11
|
+
def parse_json(json)
|
|
12
|
+
@last_response_json = json
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def response_is_error?
|
|
16
|
+
@last_response_json['Fault'].present?
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def parse_intuit_error
|
|
20
|
+
error = {:message => "", :detail => "", :type => nil, :code => 0}
|
|
21
|
+
resp = JSON.parse(@last_response_json)
|
|
22
|
+
fault = resp['Fault']
|
|
23
|
+
if fault.present?
|
|
24
|
+
error[:type] = fault['type'] if fault.has_key?('type')
|
|
25
|
+
if fault_error = fault['Error'].first
|
|
26
|
+
error[:message] = fault_error['Message']
|
|
27
|
+
error[:detail] = fault_error['Detail']
|
|
28
|
+
error[:code] = fault_error['code']
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
error
|
|
32
|
+
rescue Exception => exception
|
|
33
|
+
error[:detail] = @last_response_json.to_s
|
|
34
|
+
error
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
module Quickbooks
|
|
2
|
+
module Service
|
|
3
|
+
class Batch < BaseService
|
|
4
|
+
|
|
5
|
+
def make_request(entity, options = {})
|
|
6
|
+
response = do_http_post(url_for_resource('batch'), valid_xml_document(entity.to_xml_ns), options[:query])
|
|
7
|
+
Quickbooks::Model::BatchResponse.from_xml(response.plain_body)
|
|
8
|
+
end
|
|
9
|
+
end
|
|
10
|
+
end
|
|
11
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
module Quickbooks
|
|
2
|
+
module Service
|
|
3
|
+
class ChangeDataCapture < BaseService
|
|
4
|
+
|
|
5
|
+
def url_for_query(entity_list, query=nil)
|
|
6
|
+
q = entity_list.join(",")
|
|
7
|
+
q = "#{q}&#{query}" if query.present?
|
|
8
|
+
return "#{url_for_base}/cdc?entities=#{q}"
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def since(entity_list, timestamp)
|
|
12
|
+
do_http_get(url_for_query(entity_list, "changedSince=#{URI.encode_www_form_component(timestamp.iso8601)}"))
|
|
13
|
+
model.new(:xml => @last_response_xml)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
private
|
|
17
|
+
|
|
18
|
+
def model
|
|
19
|
+
Quickbooks::Model::ChangeDataCapture
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|