effective_qb_online 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|