diff --git a/README.md b/README.md index 66c01527..4f7d500e 100644 --- a/README.md +++ b/README.md @@ -73,8 +73,14 @@ OAUTH_CONSUMER_SECRET = "OAUTH_CONSUMER_SECRET" :authorize_url => "https://appcenter.intuit.com/Connect/Begin", :access_token_path => "/oauth/v1/get_access_token" }) + +Quickbooks::Configuration.set_oauth_consumer(QB_OAUTH_CONSUMER) + +Quickbooks::Configuration.set_tokens_and_realm_id(token, secret, realm_id, :scope => :application) ``` +You can pass an optional key value pair as last argument to `set_tokens_and_realm_id` method. The key value pair can be `:scope => :application` or `:scope => :thread`. If you want to set the tokens, secret and realm_id on application then you need to pass `:scope => :application`. The default value for `:scope` is `:thread`. + To start the authentication flow with Intuit you include the Intuit Javascript and on a page of your choosing you present the "Connect to Quickbooks" button by including this XHTML: @@ -175,7 +181,41 @@ end ## Getting Started - Retrieving a list of Customers -The general approach is you first instantiate a `Service` object based on the entity you would like to retrieve. Lets retrieve a list of Customers: +There are two approaches to retrieve results from Quickbooks. However, you're encouraged to use the first approach as we'll be removing the second approach in the long run. + +###### Approach 1 + +Make sure that you've followed the steps mentioned in [Getting Started & Initiating Authentication Flow with Intuit](#getting-started--initiating-authentication-flow-with-intuit) before you proceed to use Approach 1. + +Then you can simply call `Quickbooks::Model::Customer.all` to retrieve a list of all customers. However, there is a default pagination applied and only 20 records are fetched per request. If you want to skip pagination and just want to fetch all the records then you can pass `skip_pagination: true` argument to `Quickbooks::Model::Customer.all`: +```ruby +Quickbooks::Model::Customer.all #=> Returns array of first 20 customers +Quickbooks::Model::Customer.all(nil, skip_pagination: true) #=> Returns array of all customers +``` +The first argument in the method is query and if you don't pass any query then the default query will be executed which is, in this case, `SELECT * FROM Customer` + +Quickbooks models behave exactly the same like ActiveRecord for querying purposes. That means that these models have other methods e.g: `where` `count` `find_by` `find` etc. Following are some examples: +```ruby + +# where +Quickbooks::Model::Customer.where('') #=> runs default query and returns first 20 customers +Quickbooks::Model::Customer.where(id: 170) #=> runs a conditional query and returns first 20 customers having id equal to 170 +Quickbooks::Model::Customer.where(id: 170, skip_pagination: true) #=> runs the conditional query and returns all customers having id equal to 170 without pagination + +# find_by +Quickbooks::Model::Customer.find_by('') #=> runs default query and returns first customer +Quickbooks::Model::Customer.find_by(id: 170) #=> runs a conditional query and returns first customer having id equal to 170 + +# find +Quickbooks::Model::Customer.find('') #=> runs default query and returns first customer +Quickbooks::Model::Customer.find(170) #=> runs a conditional query and returns first customer having id equal to 170 + +# count +Quickbooks::Model::Customer.count #=> returns total number of customers in your quickbooks account +``` + +###### Approach 2 +The second approach is you first instantiate a `Service` object based on the entity you would like to retrieve. Lets retrieve a list of Customers: ```ruby @@ -218,8 +258,21 @@ customers.query(query, :page => 2, :per_page => 25) ``` ### Querying in Batches -Often one needs to retrieve multiple pages of records of an Entity type -and loop over them all. Fortunately there is the `query_in_batches` collection method: +Often one needs to retrieve multiple pages of records of an Entity type and loop over them all. Again there are two approaches currently supported and first approach is encouraged to be used. + +###### Approach 1 + +Make sure that you've followed the steps mentioned in [Getting Started & Initiating Authentication Flow with Intuit](#getting-started--initiating-authentication-flow-with-intuit) before you proceed to use Approach 1. + +```ruby +Quickbooks::Model::Customer.query_in_batches(query, per_page: 1000) do |batch| + batch.each do |customer| + # ... + end +end +``` +###### Approach 2 +As of second approach, there is the `query_in_batches` collection method in each service: ```ruby query = nil diff --git a/lib/configuration.rb b/lib/configuration.rb new file mode 100644 index 00000000..6f32a469 --- /dev/null +++ b/lib/configuration.rb @@ -0,0 +1,26 @@ +module Quickbooks + class Configuration + class << self + attr_accessor :qb_oauth_consumer, :qb_token_scope, :qb_token, :qb_secret, :qb_realm_id + end + + def self.set_tokens_and_realm_id(token, secret, realm_id, options = {}) + options[:scope] ||= :thread + if options[:scope] == :thread + Thread.current[:token] = token + Thread.current[:secret] = secret + Thread.current[:realm_id] = realm_id + self.qb_token_scope = :thread + else + self.qb_token = token + self.qb_secret = secret + self.qb_realm_id = realm_id + self.qb_token_scope = :application + end + end + + def self.set_oauth_consumer(oauth_consumer) + self.qb_oauth_consumer = oauth_consumer + end + end +end \ No newline at end of file diff --git a/lib/quickbooks-ruby.rb b/lib/quickbooks-ruby.rb index f80e2c03..041866ee 100644 --- a/lib/quickbooks-ruby.rb +++ b/lib/quickbooks-ruby.rb @@ -13,10 +13,12 @@ require 'quickbooks/util/http_encoding_helper' require 'quickbooks/util/name_entity' require 'quickbooks/util/query_builder' +require 'configuration' #== Models require 'quickbooks/model/definition' require 'quickbooks/model/validator' +require 'quickbooks/model/active_record_scaffold' require 'quickbooks/model/base_model' require 'quickbooks/model/base_model_json' require 'quickbooks/model/base_reference' diff --git a/lib/quickbooks/model/active_record_scaffold.rb b/lib/quickbooks/model/active_record_scaffold.rb new file mode 100644 index 00000000..0cc3f50c --- /dev/null +++ b/lib/quickbooks/model/active_record_scaffold.rb @@ -0,0 +1,113 @@ +module Quickbooks + module Model + module ActiveRecordScaffold + + def self.included(base) + base.extend(ClassMethods) + end + + def save(options = {}) + if id.blank? + self.class.send(:initialize_service).create(self, options) + else + self.class.send(:initialize_service).update(self, options) + end + end + + module ClassMethods + def all(query = nil, options = {}) + initialize_service(options).query(query, options).entries + end + + def first + all.first + end + + def where(options = {}) + if options.is_a?(String) + all(build_string_query(options), {}) + elsif options.present? + all(build_hash_query(options.except(:skip_pagination)), options) + else + all(full_query, {}) + end + end + + def find_by(options = {}) + where(options).first + end + + def find(id) + find_by(id: id) + end + + def count + initialize_service.query("SELECT COUNT(*) FROM #{model}").total_count + end + + def query_in_batches(options = {}) + initialize_service(options).query_in_batches(query, options).entries + end + + private + + def initialize_service(options = {}) + if Quickbooks::Configuration.qb_oauth_consumer.blank? + raise "QB_OAUTH_CONSUMER not set. Please follow instructions at " + + "https://github.com/ruckus/quickbooks-ruby#getting-started--initiating-authentication-flow-with-intuit" + end + service = eval("Quickbooks::Service::#{model}").new + service.access_token = OAuth::AccessToken.new( + Quickbooks::Configuration.qb_oauth_consumer, options[:token] || get_token, + options[:secret] || get_secret + ) + service.company_id = options[:realm_id] || get_realm_id + service + end + + def build_string_query(query = nil) + unless query.downcase.starts_with?('select ') + query = query.present? ? "#{conditional_query} #{query}" : full_query + end + query + end + + def build_hash_query(options = {}) + query_builder = Quickbooks::Util::QueryBuilder.new + conditions = options.collect do |key, value| + if value.is_a?(Array) + query_builder.clause(key, 'IN', value) + else + query_builder.clause(key, '=', value) + end + end.join(' AND ') + conditional_query + conditions + end + + def conditional_query + "#{full_query} WHERE " + end + + def full_query + "SELECT * FROM #{model}" + end + + def model + self.name.split('::').last + end + + def get_token + Quickbooks::Configuration.qb_token_scope == :thread ? Thread.current[:token] : Quickbooks::Configuration.qb_token + end + + def get_secret + Quickbooks::Configuration.qb_token_scope == :thread ? Thread.current[:secret] : Quickbooks::Configuration.qb_secret + end + + def get_realm_id + Quickbooks::Configuration.qb_token_scope == :thread ? Thread.current[:realm_id] : Quickbooks::Configuration.qb_realm_id + end + end + end + end +end \ No newline at end of file diff --git a/lib/quickbooks/model/base_model.rb b/lib/quickbooks/model/base_model.rb index 0ebf36e7..f29b9580 100644 --- a/lib/quickbooks/model/base_model.rb +++ b/lib/quickbooks/model/base_model.rb @@ -5,6 +5,7 @@ class BaseModel include ActiveModel::Validations include Validator include ROXML + include ActiveRecordScaffold xml_convention :camelcase diff --git a/lib/quickbooks/service/base_service.rb b/lib/quickbooks/service/base_service.rb index 7654f837..d6d843e2 100644 --- a/lib/quickbooks/service/base_service.rb +++ b/lib/quickbooks/service/base_service.rb @@ -59,7 +59,9 @@ def default_model_query def url_for_query(query = nil, start_position = 1, max_results = 20, options = {}) query ||= default_model_query - query = "#{query} STARTPOSITION #{start_position} MAXRESULTS #{max_results}" + unless options[:skip_pagination] + query = "#{query} STARTPOSITION #{start_position} MAXRESULTS #{max_results}" + end "#{url_for_base}/query?query=#{URI.encode_www_form_component(query)}" end @@ -94,7 +96,7 @@ def fetch_collection(query, model, options = {}) start_position = ((page - 1) * per_page) + 1 # page=2, per_page=10 then we want to start at 11 max_results = per_page - response = do_http_get(url_for_query(query, start_position, max_results)) + response = do_http_get(url_for_query(query, start_position, max_results, options)) parse_collection(response, model) end diff --git a/lib/quickbooks/service/change_service.rb b/lib/quickbooks/service/change_service.rb index af704ea9..c9c8ae2d 100644 --- a/lib/quickbooks/service/change_service.rb +++ b/lib/quickbooks/service/change_service.rb @@ -2,7 +2,7 @@ module Quickbooks module Service class ChangeService < BaseService - def url_for_query(query = nil, start_position = 1, max_results = 20) + def url_for_query(query = nil, start_position = 1, max_results = 20, options = {}) q = entity q = "#{q}&#{query}" if query.present? diff --git a/lib/quickbooks/util/query_builder.rb b/lib/quickbooks/util/query_builder.rb index d024911b..48c9261e 100644 --- a/lib/quickbooks/util/query_builder.rb +++ b/lib/quickbooks/util/query_builder.rb @@ -19,7 +19,7 @@ def clause(field, operator, value) value = value.map{|v| v.to_s.gsub("'", "\\\\'") } else # escape single quotes with an escaped backslash - value = value.gsub("'", "\\\\'") + value = value.to_s.gsub("'", "\\\\'") end if operator.downcase == 'in' && value.is_a?(Array)