Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Introduce ActiveRecord like methods to Quickbooks Models #330

Open
wants to merge 6 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 56 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:


Expand Down Expand Up @@ -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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems like it should be a heading level 3, e.g. ###


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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This doesn't seem like a great example because you shouldn't have multiple customers with the same id.

Quickbooks::Model::Customer.where(id: 170, skip_pagination: true) #=> runs the conditional query and returns all customers having id equal to 170 without pagination
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here, would be better to use a more realistic value to demonstrate the filtering. I'm not clear what "without pagination" means? The pagination will happen behind inside that method call right?


# 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

Expand Down Expand Up @@ -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
Expand Down
26 changes: 26 additions & 0 deletions lib/configuration.rb
Original file line number Diff line number Diff line change
@@ -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
2 changes: 2 additions & 0 deletions lib/quickbooks-ruby.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
113 changes: 113 additions & 0 deletions lib/quickbooks/model/active_record_scaffold.rb
Original file line number Diff line number Diff line change
@@ -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 = {})
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

initialize_service(options).query(query, options).entries
end

def first
all.first
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should this really be calling all? seems wasteful if you just want one record

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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

seems like this should do a limit 1 to avoid fetching and parsing records that aren't used.

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
1 change: 1 addition & 0 deletions lib/quickbooks/model/base_model.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ class BaseModel
include ActiveModel::Validations
include Validator
include ROXML
include ActiveRecordScaffold

xml_convention :camelcase

Expand Down
6 changes: 4 additions & 2 deletions lib/quickbooks/service/base_service.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion lib/quickbooks/service/change_service.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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?

Expand Down
2 changes: 1 addition & 1 deletion lib/quickbooks/util/query_builder.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down