effective_qb_online 0.1.0
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/MIT-LICENSE +20 -0
- data/README.md +96 -0
- data/Rakefile +18 -0
- data/app/assets/config/effective_qb_online_manifest.js +3 -0
- data/app/assets/javascripts/effective_qb_online/base.js +0 -0
- data/app/assets/javascripts/effective_qb_online.js +1 -0
- data/app/assets/stylesheets/effective_qb_online/base.scss +0 -0
- data/app/assets/stylesheets/effective_qb_online.scss +1 -0
- data/app/controllers/admin/qb_online_controller.rb +20 -0
- data/app/controllers/admin/qb_realms_controller.rb +11 -0
- data/app/controllers/admin/qb_receipts_controller.rb +17 -0
- data/app/controllers/effective/qb_oauth_controller.rb +52 -0
- data/app/datatables/admin/effective_qb_receipts_datatable.rb +41 -0
- data/app/helpers/effective_qb_online_helper.rb +14 -0
- data/app/jobs/qb_sync_order_job.rb +13 -0
- data/app/models/effective/qb_api.rb +208 -0
- data/app/models/effective/qb_realm.rb +37 -0
- data/app/models/effective/qb_receipt.rb +98 -0
- data/app/models/effective/qb_receipt_item.rb +26 -0
- data/app/models/effective/qb_sales_receipt.rb +101 -0
- data/app/views/admin/qb_online/_company.html.haml +31 -0
- data/app/views/admin/qb_online/_test_credentials.html.haml +15 -0
- data/app/views/admin/qb_online/index.html.haml +14 -0
- data/app/views/admin/qb_online/instructions.html.haml +18 -0
- data/app/views/admin/qb_receipts/_form.html.haml +40 -0
- data/app/views/admin/qb_receipts/edit.html.haml +4 -0
- data/app/views/admin/qb_receipts/new.html.haml +4 -0
- data/config/effective_qb_online.rb +23 -0
- data/config/routes.rb +25 -0
- data/db/migrate/01_create_effective_qb_online.rb.erb +40 -0
- data/db/seeds.rb +1 -0
- data/lib/effective_qb_online/engine.rb +17 -0
- data/lib/effective_qb_online/version.rb +3 -0
- data/lib/effective_qb_online.rb +55 -0
- data/lib/generators/effective_qb_online/install_generator.rb +32 -0
- data/lib/generators/templates/effective_qb_online_mailer_preview.rb +4 -0
- data/lib/tasks/effective_qb_online_tasks.rake +8 -0
- metadata +262 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 41c564325389352ce139af8d840c39d3f5ffaa132efd7f8bb72c5d7fe28927af
|
4
|
+
data.tar.gz: 505c8393cf0e7a06b6a687ccebda7c510f9590a248cc111e4c524027cb9b8163
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 06fd5e50780c61176aaf4df670888855cd7590a5fb9f949d2a5d50686e2c077e8719c1a454327d2c6f82721ecf596fd0ed2f0c8d1ce4a056293d9f40c1bd11c5
|
7
|
+
data.tar.gz: da8ba5c9871f8988a4e2d74393779ebd9b0b4e97c5fa4b8d65304d264556cfe1d1d1078c4d0ec3d049d48d6232d416651054f52a8beb1a27450037cee52576fe
|
data/MIT-LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright 2022 Code and Effect Inc.
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,96 @@
|
|
1
|
+
# Effective Quickbooks Online
|
2
|
+
|
3
|
+
Create Quickbooks Online SalesReceipts for purchased effective orders.
|
4
|
+
|
5
|
+
## Getting Started
|
6
|
+
|
7
|
+
This requires Rails 6+ and Twitter Bootstrap 4 and just works with Devise.
|
8
|
+
|
9
|
+
Please first install the [effective_datatables](https://github.com/code-and-effect/effective_datatables) gem.
|
10
|
+
|
11
|
+
Please download and install the [Twitter Bootstrap4](http://getbootstrap.com)
|
12
|
+
|
13
|
+
Add to your Gemfile:
|
14
|
+
|
15
|
+
```ruby
|
16
|
+
gem 'haml-rails' # or try using gem 'hamlit-rails'
|
17
|
+
gem 'effective_qb_online'
|
18
|
+
```
|
19
|
+
|
20
|
+
Run the bundle command to install it:
|
21
|
+
|
22
|
+
```console
|
23
|
+
bundle install
|
24
|
+
```
|
25
|
+
|
26
|
+
Then run the generator:
|
27
|
+
|
28
|
+
```ruby
|
29
|
+
rails generate effective_qb_online:install
|
30
|
+
```
|
31
|
+
|
32
|
+
The generator will install an initializer which describes all configuration options and creates a database migration.
|
33
|
+
|
34
|
+
If you want to tweak the table names, manually adjust both the configuration file and the migration now.
|
35
|
+
|
36
|
+
Then migrate the database:
|
37
|
+
|
38
|
+
```ruby
|
39
|
+
rake db:migrate
|
40
|
+
```
|
41
|
+
|
42
|
+
```
|
43
|
+
Add a link to the admin menu:
|
44
|
+
|
45
|
+
```haml
|
46
|
+
- if can? :admin, :effective_qb_online
|
47
|
+
= nav_link_to 'Quickbooks Online', effective_qb_online.admin_quickbooks_path
|
48
|
+
```
|
49
|
+
|
50
|
+
and visit `/admin/quickbooks`.
|
51
|
+
|
52
|
+
## Authorization
|
53
|
+
|
54
|
+
All authorization checks are handled via the effective_resources gem found in the `config/initializers/effective_resources.rb` file.
|
55
|
+
|
56
|
+
## Permissions
|
57
|
+
|
58
|
+
The permissions you actually want to define are as follows (using CanCan):
|
59
|
+
|
60
|
+
```ruby
|
61
|
+
if user.admin?
|
62
|
+
can :admin, :effective_qb_online
|
63
|
+
|
64
|
+
can(crud, Effective::QbRealm)
|
65
|
+
can(crud + [:skip, :sync], Effective::QbReceipt) { |receipt| !receipt.completed? }
|
66
|
+
end
|
67
|
+
```
|
68
|
+
|
69
|
+
## Configuring Quickbooks Company
|
70
|
+
|
71
|
+
This gem has only been tested with a Canadian Quickbooks Online store.
|
72
|
+
|
73
|
+
It has GST, HST and Tax Exempt tax codes and rates set up ahead of time by Quickbooks.
|
74
|
+
|
75
|
+
## License
|
76
|
+
|
77
|
+
MIT License. Copyright [Code and Effect Inc.](http://www.codeandeffect.com/)
|
78
|
+
|
79
|
+
## Testing
|
80
|
+
|
81
|
+
There are tests, but the access token and refresh token doesn't work well.
|
82
|
+
|
83
|
+
You must visit /admin/quickbooks and copy & paste the test credentials into ~/.env
|
84
|
+
|
85
|
+
```ruby
|
86
|
+
rails test
|
87
|
+
```
|
88
|
+
|
89
|
+
## Contributing
|
90
|
+
|
91
|
+
1. Fork it
|
92
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
93
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
94
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
95
|
+
5. Bonus points for test coverage
|
96
|
+
6. Create new Pull Request
|
data/Rakefile
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
require "bundler/setup"
|
2
|
+
|
3
|
+
APP_RAKEFILE = File.expand_path("test/dummy/Rakefile", __dir__)
|
4
|
+
load "rails/tasks/engine.rake"
|
5
|
+
|
6
|
+
load "rails/tasks/statistics.rake"
|
7
|
+
|
8
|
+
require "bundler/gem_tasks"
|
9
|
+
|
10
|
+
require "rake/testtask"
|
11
|
+
|
12
|
+
Rake::TestTask.new(:test) do |t|
|
13
|
+
t.libs << 'test'
|
14
|
+
t.pattern = 'test/**/*_test.rb'
|
15
|
+
t.verbose = false
|
16
|
+
end
|
17
|
+
|
18
|
+
task default: :test
|
File without changes
|
@@ -0,0 +1 @@
|
|
1
|
+
//= require_tree ./effective_qb_online
|
File without changes
|
@@ -0,0 +1 @@
|
|
1
|
+
@import 'effective_qb_online/base';
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module Admin
|
2
|
+
class QbOnlineController < ApplicationController
|
3
|
+
before_action(:authenticate_user!) if defined?(Devise)
|
4
|
+
before_action { EffectiveResources.authorize!(self, :admin, :effective_qb_online) }
|
5
|
+
|
6
|
+
include Effective::CrudController
|
7
|
+
|
8
|
+
page_title 'Quickbooks Online'
|
9
|
+
|
10
|
+
# /admin/quickbooks
|
11
|
+
def index
|
12
|
+
@api = EffectiveQbOnline.api
|
13
|
+
|
14
|
+
authorize! :index, Effective::QbRealm
|
15
|
+
|
16
|
+
render(@api.present? ? 'index' : 'instructions')
|
17
|
+
end
|
18
|
+
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,11 @@
|
|
1
|
+
module Admin
|
2
|
+
class QbRealmsController < ApplicationController
|
3
|
+
before_action(:authenticate_user!) if defined?(Devise)
|
4
|
+
before_action { EffectiveResources.authorize!(self, :admin, :effective_qb_online) }
|
5
|
+
|
6
|
+
include Effective::CrudController
|
7
|
+
|
8
|
+
on :save, redirect: -> { effective_qb_online.admin_quickbooks_path }
|
9
|
+
|
10
|
+
end
|
11
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module Admin
|
2
|
+
class QbReceiptsController < ApplicationController
|
3
|
+
before_action(:authenticate_user!) if defined?(Devise)
|
4
|
+
before_action { EffectiveResources.authorize!(self, :admin, :effective_qb_online) }
|
5
|
+
|
6
|
+
include Effective::CrudController
|
7
|
+
|
8
|
+
on :skip, redirect: -> { effective_qb_online.admin_quickbooks_path }
|
9
|
+
|
10
|
+
on :sync, redirect: -> {
|
11
|
+
resource.completed? ? effective_qb_online.admin_quickbooks_path : :edit
|
12
|
+
}
|
13
|
+
|
14
|
+
submit :sync, 'Save and Sync'
|
15
|
+
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
# http://caaa.test:3000/quickbooks/oauth/authorize
|
2
|
+
|
3
|
+
module Effective
|
4
|
+
class QbOauthController < ApplicationController
|
5
|
+
before_action(:authenticate_user!) if defined?(Devise)
|
6
|
+
|
7
|
+
# Any user that has priviledges with the Quickbooks Online company could authenticate
|
8
|
+
# But we assume this user also has admin priviledges on our site
|
9
|
+
# This should only be done once anyway
|
10
|
+
before_action { EffectiveResources.authorize!(self, :admin, :effective_qb_online) }
|
11
|
+
|
12
|
+
def authorize
|
13
|
+
grant_url = EffectiveQbOnline.oauth2_client.auth_code.authorize_url(
|
14
|
+
redirect_uri: redirect_uri,
|
15
|
+
response_type: 'code',
|
16
|
+
state: SecureRandom.hex(12),
|
17
|
+
scope: 'com.intuit.quickbooks.accounting'
|
18
|
+
)
|
19
|
+
|
20
|
+
redirect_to(grant_url)
|
21
|
+
end
|
22
|
+
|
23
|
+
# This matches the Quickbooks Redirect URI and we have to set it up ahead of time.
|
24
|
+
def callback
|
25
|
+
return unless params[:code].present? && params[:realmId].present? && params[:state].present?
|
26
|
+
|
27
|
+
token = EffectiveQbOnline.oauth2_client.auth_code.get_token(params[:code], redirect_uri: redirect_uri)
|
28
|
+
return unless token
|
29
|
+
|
30
|
+
realm = Effective::QbRealm.all.first_or_initialize
|
31
|
+
|
32
|
+
realm.update!(
|
33
|
+
realm_id: params[:realmId],
|
34
|
+
access_token: token.token,
|
35
|
+
refresh_token: token.refresh_token,
|
36
|
+
access_token_expires_at: Time.at(token.expires_at),
|
37
|
+
refresh_token_expires_at: (Time.at(token.expires_at) + 100.days)
|
38
|
+
)
|
39
|
+
|
40
|
+
flash[:success] = 'Successfully connected with Quickbooks Online'
|
41
|
+
|
42
|
+
redirect_to(effective_qb_online.admin_quickbooks_path)
|
43
|
+
end
|
44
|
+
|
45
|
+
private
|
46
|
+
|
47
|
+
def redirect_uri
|
48
|
+
effective_qb_online.quickbooks_oauth_callback_url
|
49
|
+
end
|
50
|
+
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
module Admin
|
2
|
+
class EffectiveQbReceiptsDatatable < Effective::Datatable
|
3
|
+
datatable do
|
4
|
+
order :updated_at
|
5
|
+
|
6
|
+
col :updated_at, visible: false
|
7
|
+
col :created_at, visible: false
|
8
|
+
col :id, visible: false
|
9
|
+
|
10
|
+
col :updated_at
|
11
|
+
|
12
|
+
col :order, search: :string
|
13
|
+
|
14
|
+
col :sales_receipt_id, label: 'QB Sales Receipt' do |receipt|
|
15
|
+
if receipt.sales_receipt_id.present?
|
16
|
+
link_to("Sales Receipt", api.sales_receipt_url(receipt.sales_receipt_id), target: '_blank')
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
col :customer_id, label: 'QB Customer' do |receipt|
|
21
|
+
if receipt.sales_receipt_id.present?
|
22
|
+
link_to("Customer", api.customer_url(receipt.customer_id), target: '_blank')
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
col :status
|
27
|
+
col :result
|
28
|
+
|
29
|
+
actions_col
|
30
|
+
end
|
31
|
+
|
32
|
+
collection do
|
33
|
+
Effective::QbReceipt.deep.all
|
34
|
+
end
|
35
|
+
|
36
|
+
def api
|
37
|
+
@api ||= EffectiveQbOnline.api
|
38
|
+
end
|
39
|
+
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
module EffectiveQbOnlineHelper
|
2
|
+
def qb_receipt_effective_orders_collection(qb_receipt)
|
3
|
+
raise('expected a non-persisted Effective::QbReceipt') unless qb_receipt.kind_of?(Effective::QbReceipt) && qb_receipt.new_record?
|
4
|
+
|
5
|
+
existing = Effective::QbReceipt.select('order_id')
|
6
|
+
orders = Effective::Order.purchased.sorted.includes(:user).where.not(id: existing)
|
7
|
+
|
8
|
+
orders.map do |order|
|
9
|
+
label = "#{order} - #{order.user} - #{price_to_currency(order.total)}"
|
10
|
+
[label, order.to_param]
|
11
|
+
end
|
12
|
+
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
class QbSyncOrderJob < ApplicationJob
|
2
|
+
queue_as :default
|
3
|
+
|
4
|
+
def perform(order)
|
5
|
+
raise('expected a purchased Effective::Order') unless order.kind_of?(Effective::Order) && order.purchased?
|
6
|
+
|
7
|
+
puts "Starting QB Sync Order Job for order #{order}"
|
8
|
+
|
9
|
+
qb_receipt = Effective::QbReceipt.create_from_order!(order)
|
10
|
+
qb_receipt.sync!
|
11
|
+
end
|
12
|
+
|
13
|
+
end
|
@@ -0,0 +1,208 @@
|
|
1
|
+
# The Quickbooks namespace comes from quickbooks-ruby gem
|
2
|
+
# https://github.com/ruckus/quickbooks-ruby
|
3
|
+
|
4
|
+
module Effective
|
5
|
+
class QbApi
|
6
|
+
attr_accessor :realm
|
7
|
+
|
8
|
+
def initialize(realm:)
|
9
|
+
raise('expected an Effective::QbRealm') unless realm.kind_of?(Effective::QbRealm)
|
10
|
+
@realm = realm
|
11
|
+
end
|
12
|
+
|
13
|
+
def app_url
|
14
|
+
Quickbooks.sandbox_mode ? 'https://app.sandbox.qbo.intuit.com/app' : 'https://app.qbo.intuit.com/app'
|
15
|
+
end
|
16
|
+
|
17
|
+
def sales_receipt_url(obj)
|
18
|
+
"#{app_url}/salesreceipt?txnId=#{obj.try(:sales_receipt_id) || obj.try(:id) || obj}"
|
19
|
+
end
|
20
|
+
|
21
|
+
def customer_url(obj)
|
22
|
+
"#{app_url}/customerdetail?nameId=#{obj.try(:customer_id) || obj.try(:id) || obj}"
|
23
|
+
end
|
24
|
+
|
25
|
+
def price_to_amount(price)
|
26
|
+
raise('Expected an Integer price') unless price.kind_of?(Integer)
|
27
|
+
(price / 100.0).round(2)
|
28
|
+
end
|
29
|
+
|
30
|
+
def build_address(address)
|
31
|
+
raise('Expected a Effective::Address') unless address.kind_of?(Effective::Address)
|
32
|
+
|
33
|
+
Quickbooks::Model::PhysicalAddress.new(
|
34
|
+
line1: address.address1,
|
35
|
+
line2: address.address2,
|
36
|
+
line3: address.try(:address3),
|
37
|
+
city: address.city,
|
38
|
+
country: address.country,
|
39
|
+
country_sub_division_code: address.country_code,
|
40
|
+
postal_code: address.postal_code
|
41
|
+
)
|
42
|
+
end
|
43
|
+
|
44
|
+
# Singular
|
45
|
+
def company_info
|
46
|
+
with_service('CompanyInfo') { |service| service.fetch_by_id(realm.realm_id) }
|
47
|
+
end
|
48
|
+
|
49
|
+
def accounts
|
50
|
+
with_service('Account') { |service| service.all }
|
51
|
+
end
|
52
|
+
|
53
|
+
# Only accounts we can use for the Deposit to Account setting
|
54
|
+
def accounts_collection
|
55
|
+
accounts
|
56
|
+
.select { |account| ['Bank', 'Other Current Asset'].include?(account.account_type) }
|
57
|
+
.sort_by { |account| [account.account_type, account.name] }
|
58
|
+
.map { |account| [account.name, account.id, account.account_type] }
|
59
|
+
.group_by(&:last)
|
60
|
+
end
|
61
|
+
|
62
|
+
def items
|
63
|
+
with_service('Item') { |service| service.all }
|
64
|
+
end
|
65
|
+
|
66
|
+
def items_collection
|
67
|
+
items
|
68
|
+
.reject { |item| item.type == 'Category' }
|
69
|
+
.sort_by { |item| [item.type, item.name] }
|
70
|
+
.map { |item| [item.name, item.id, item.type] }
|
71
|
+
.group_by(&:last)
|
72
|
+
end
|
73
|
+
|
74
|
+
def payment_methods
|
75
|
+
with_service('PaymentMethod') { |service| service.all }
|
76
|
+
end
|
77
|
+
|
78
|
+
def payment_methods_collection
|
79
|
+
payment_methods.sort_by(&:name).map { |payment_method| [payment_method.name, payment_method.id] }
|
80
|
+
end
|
81
|
+
|
82
|
+
def find_or_create_customer(user:)
|
83
|
+
find_customer(user: user) || create_customer(user: user)
|
84
|
+
end
|
85
|
+
|
86
|
+
def find_customer(user:)
|
87
|
+
raise('expected a user that responds to email') unless user.respond_to?(:email)
|
88
|
+
|
89
|
+
with_service('Customer') do |service|
|
90
|
+
# Find by email
|
91
|
+
customer = service.find_by(:PrimaryEmailAddr, user.email)&.first
|
92
|
+
|
93
|
+
# Find by given name and family name
|
94
|
+
customer ||= if user.respond_to?(:first_name) && user.respond_to?(:last_name)
|
95
|
+
service.query("SELECT * FROM Customer WHERE GivenName LIKE '#{scrub(user.first_name)}' AND FamilyName LIKE '#{scrub(user.last_name)}'")&.first
|
96
|
+
end
|
97
|
+
|
98
|
+
# Find by display name
|
99
|
+
customer || service.find_by(:display_name, scrub(user.to_s))&.first
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
def create_customer(user:)
|
104
|
+
raise('expected a user that responds to email') unless user.respond_to?(:email)
|
105
|
+
|
106
|
+
with_service('Customer') do |service|
|
107
|
+
customer = Quickbooks::Model::Customer.new(
|
108
|
+
primary_email_address: Quickbooks::Model::EmailAddress.new(user.email),
|
109
|
+
display_name: scrub(user.to_s),
|
110
|
+
given_name: scrub(user.try(:first_name)),
|
111
|
+
family_name: scrub(user.try(:last_name))
|
112
|
+
)
|
113
|
+
|
114
|
+
service.create(customer)
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
def delete_customer(customer:)
|
119
|
+
with_service('Customer') { |service| service.delete(customer) }
|
120
|
+
end
|
121
|
+
|
122
|
+
def create_sales_receipt(sales_receipt:)
|
123
|
+
with_service('SalesReceipt') { |service| service.create(sales_receipt) }
|
124
|
+
end
|
125
|
+
|
126
|
+
def find_sales_receipt(id:)
|
127
|
+
with_service('SalesReceipt') { |service| service.find_by(:id, id) }
|
128
|
+
end
|
129
|
+
|
130
|
+
def tax_codes
|
131
|
+
with_service('TaxCode') { |service| service.all }
|
132
|
+
end
|
133
|
+
|
134
|
+
def tax_rates
|
135
|
+
with_service('TaxRate') { |service| service.all }
|
136
|
+
end
|
137
|
+
|
138
|
+
# Returns a Hash of BigNumeral Tax Rate => TaxCode Object
|
139
|
+
# { 0.0 => Quickbooks::Model::TaxCode }
|
140
|
+
def taxes_collection
|
141
|
+
rates = tax_rates()
|
142
|
+
codes = tax_codes()
|
143
|
+
|
144
|
+
# Find Exempt 0.0
|
145
|
+
exempt = codes.find do |code|
|
146
|
+
rate_id = code.sales_tax_rate_list.tax_rate_detail.first&.tax_rate_ref&.value
|
147
|
+
rate = rates.find { |rate| rate.id == rate_id } if rate_id
|
148
|
+
|
149
|
+
code.name.downcase.include?('exempt') && rate && rate.rate_value == 0.0
|
150
|
+
end
|
151
|
+
|
152
|
+
exempt = [['0.0', exempt]] if exempt.present?
|
153
|
+
|
154
|
+
# Find The rest
|
155
|
+
tax_codes = codes.map do |code|
|
156
|
+
rate_id = code.sales_tax_rate_list.tax_rate_detail.first&.tax_rate_ref&.value
|
157
|
+
rate = rates.find { |rate| rate.id == rate_id } if rate_id
|
158
|
+
|
159
|
+
[rate.rate_value.to_s, code] if rate && (exempt.blank? || rate.rate_value > 0.0)
|
160
|
+
end
|
161
|
+
|
162
|
+
(Array(exempt) + tax_codes.compact).to_h
|
163
|
+
end
|
164
|
+
|
165
|
+
private
|
166
|
+
|
167
|
+
def with_service(name, &block)
|
168
|
+
klass = "Quickbooks::Service::#{name}".constantize
|
169
|
+
|
170
|
+
with_authenticated_request do |access_token|
|
171
|
+
service = klass.new(company_id: realm.realm_id, access_token: access_token)
|
172
|
+
yield(service)
|
173
|
+
end
|
174
|
+
end
|
175
|
+
|
176
|
+
def with_authenticated_request(max_attempts: 3, &block)
|
177
|
+
attempts = 0
|
178
|
+
|
179
|
+
begin
|
180
|
+
token = OAuth2::AccessToken.new(EffectiveQbOnline.oauth2_client, realm.access_token, refresh_token: realm.refresh_token)
|
181
|
+
yield(token)
|
182
|
+
rescue OAuth2::Error, Quickbooks::AuthorizationFailure => e
|
183
|
+
puts "Quickbooks OAuth Error: #{e.message}"
|
184
|
+
|
185
|
+
attempts += 1
|
186
|
+
raise "unable to refresh Quickbooks OAuth2 token" if attempts >= max_attempts
|
187
|
+
|
188
|
+
# Refresh
|
189
|
+
refreshed = token.refresh!
|
190
|
+
|
191
|
+
realm.update!(
|
192
|
+
access_token: refreshed.token,
|
193
|
+
refresh_token: refreshed.refresh_token,
|
194
|
+
access_token_expires_at: Time.at(refreshed.expires_at),
|
195
|
+
refresh_token_expires_at: (Time.at(refreshed.expires_at) + 100.days)
|
196
|
+
)
|
197
|
+
|
198
|
+
retry
|
199
|
+
end
|
200
|
+
end
|
201
|
+
|
202
|
+
def scrub(value)
|
203
|
+
return nil unless value.present?
|
204
|
+
value.gsub(':', '')
|
205
|
+
end
|
206
|
+
|
207
|
+
end
|
208
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
# One Quickbooks Realm / Company
|
2
|
+
|
3
|
+
module Effective
|
4
|
+
class QbRealm < ActiveRecord::Base
|
5
|
+
|
6
|
+
effective_resource do
|
7
|
+
# As per Quickbooks oAuth
|
8
|
+
realm_id :string
|
9
|
+
|
10
|
+
access_token :text
|
11
|
+
access_token_expires_at :datetime
|
12
|
+
|
13
|
+
refresh_token :text
|
14
|
+
refresh_token_expires_at :datetime
|
15
|
+
|
16
|
+
# Set on /admin/quickbooks
|
17
|
+
deposit_to_account_id :string
|
18
|
+
payment_method_id :string
|
19
|
+
|
20
|
+
timestamps
|
21
|
+
end
|
22
|
+
|
23
|
+
validates :realm_id, presence: true
|
24
|
+
validates :realm_id, uniqueness: true, if: -> { new_record? }
|
25
|
+
|
26
|
+
validates :access_token, presence: true
|
27
|
+
validates :access_token_expires_at, presence: true
|
28
|
+
|
29
|
+
validates :refresh_token, presence: true
|
30
|
+
validates :refresh_token_expires_at, presence: true
|
31
|
+
|
32
|
+
def to_s
|
33
|
+
'Quickbooks Online Settings'
|
34
|
+
end
|
35
|
+
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,98 @@
|
|
1
|
+
module Effective
|
2
|
+
class QbReceipt < ActiveRecord::Base
|
3
|
+
belongs_to :order, class_name: 'Effective::Order'
|
4
|
+
|
5
|
+
log_changes if respond_to?(:log_changes)
|
6
|
+
|
7
|
+
has_many :qb_receipt_items, inverse_of: :qb_receipt, dependent: :delete_all
|
8
|
+
accepts_nested_attributes_for :qb_receipt_items
|
9
|
+
|
10
|
+
acts_as_statused(:todo, :completed, :errored, :skipped)
|
11
|
+
|
12
|
+
effective_resource do
|
13
|
+
# QuickBooks Customer
|
14
|
+
customer_id :string
|
15
|
+
|
16
|
+
# Quickbooks Online SalesReceipt id, once sync'd
|
17
|
+
sales_receipt_id :string
|
18
|
+
|
19
|
+
# Any error message from our sync
|
20
|
+
result :text
|
21
|
+
|
22
|
+
# Acts as Statused
|
23
|
+
status :string
|
24
|
+
status_steps :text
|
25
|
+
|
26
|
+
timestamps
|
27
|
+
end
|
28
|
+
|
29
|
+
scope :deep, -> { includes(order: :user, qb_receipt_items: [order_item: :purchasable]) }
|
30
|
+
|
31
|
+
# Create a QbReceiptItem for each OrderItem
|
32
|
+
before_validation(if: -> { order.present? }) do
|
33
|
+
order.order_items.each { |order_item| qb_receipt_item(order_item: order_item) }
|
34
|
+
end
|
35
|
+
|
36
|
+
validates :qb_receipt_items, presence: true
|
37
|
+
|
38
|
+
with_options(if: -> { completed? }) do
|
39
|
+
validates :customer_id, presence: true
|
40
|
+
validates :sales_receipt_id, presence: true
|
41
|
+
end
|
42
|
+
|
43
|
+
# Create a QbReceipt from an Effective::Order
|
44
|
+
def self.create_from_order!(order)
|
45
|
+
raise('Expected a purchased Effective::Order') unless order.kind_of?(Effective::Order) && order.purchased?
|
46
|
+
Effective::QbReceipt.where(order: order).first_or_create
|
47
|
+
end
|
48
|
+
|
49
|
+
def to_s
|
50
|
+
order.to_s
|
51
|
+
end
|
52
|
+
|
53
|
+
# Find or build
|
54
|
+
def qb_receipt_item(order_item:)
|
55
|
+
qb_receipt_items.find { |item| item.order_item == order_item } ||
|
56
|
+
qb_receipt_items.build(order_item: order_item)
|
57
|
+
end
|
58
|
+
|
59
|
+
def sync!(force: false)
|
60
|
+
raise('Already created SalesReceipt with Quickbooks Online') if sales_receipt_id.present? && !force
|
61
|
+
save!
|
62
|
+
|
63
|
+
api = EffectiveQbOnline.api
|
64
|
+
|
65
|
+
begin
|
66
|
+
sales_receipt = Effective::QbSalesReceipt.build_from_receipt!(self, api: api)
|
67
|
+
sales_receipt = api.create_sales_receipt(sales_receipt: sales_receipt)
|
68
|
+
|
69
|
+
# Sanity check
|
70
|
+
if (expected = api.price_to_amount(order.total)) != sales_receipt.total
|
71
|
+
raise("A Quickbooks Online Sales Receipt has been created with an unexpected total. Quickbooks total is #{sales_receipt.total} but we expected #{expected}. Please adjust the Sales Receipt on Quickbooks")
|
72
|
+
end
|
73
|
+
|
74
|
+
assign_attributes(result: 'completed successfully', sales_receipt_id: sales_receipt.id)
|
75
|
+
complete!
|
76
|
+
rescue => e
|
77
|
+
assign_attributes(result: e.message)
|
78
|
+
error!
|
79
|
+
end
|
80
|
+
|
81
|
+
true
|
82
|
+
end
|
83
|
+
|
84
|
+
def skip!
|
85
|
+
assign_attributes(result: 'skipped')
|
86
|
+
skipped!
|
87
|
+
end
|
88
|
+
|
89
|
+
def complete!
|
90
|
+
completed!
|
91
|
+
end
|
92
|
+
|
93
|
+
def error!
|
94
|
+
errored!
|
95
|
+
end
|
96
|
+
|
97
|
+
end
|
98
|
+
end
|