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
@@ -0,0 +1,26 @@
|
|
1
|
+
module Effective
|
2
|
+
class QbReceiptItem < ActiveRecord::Base
|
3
|
+
belongs_to :qb_receipt
|
4
|
+
belongs_to :order_item
|
5
|
+
|
6
|
+
log_changes(to: :qb_receipt) if respond_to?(:log_changes)
|
7
|
+
|
8
|
+
effective_resource do
|
9
|
+
# Will be blank when first created. Populated by QbSalesReceipt.build_from_receipt!
|
10
|
+
item_id :string
|
11
|
+
|
12
|
+
timestamps
|
13
|
+
end
|
14
|
+
|
15
|
+
scope :deep, -> { includes(:qb_receipt, :order_item) }
|
16
|
+
|
17
|
+
def to_s
|
18
|
+
item_id.presence || 'New Qb Receipt Item'
|
19
|
+
end
|
20
|
+
|
21
|
+
def order_item_qb_name
|
22
|
+
item_id || order_item.purchasable.try(:qb_item_id) || order_item.purchasable.try(:qb_item_name)
|
23
|
+
end
|
24
|
+
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,101 @@
|
|
1
|
+
module Effective
|
2
|
+
class QbSalesReceipt
|
3
|
+
|
4
|
+
# Build the Quickbooks SalesReceipt from a QbReceipt
|
5
|
+
def self.build_from_receipt!(receipt, api: nil)
|
6
|
+
raise('Expected a persisted Effective::QbReceipt') unless receipt.kind_of?(Effective::QbReceipt) && receipt.persisted?
|
7
|
+
|
8
|
+
api ||= EffectiveQbOnline.api
|
9
|
+
raise('Expected a connected Quickbooks API') unless api.present?
|
10
|
+
|
11
|
+
order = receipt.order
|
12
|
+
raise('Expected a purchased Effective::Order') unless order.purchased?
|
13
|
+
|
14
|
+
user = order.user
|
15
|
+
raise('Expected a user with an email') unless user.respond_to?(:email)
|
16
|
+
|
17
|
+
realm = api.realm
|
18
|
+
raise('Missing Deposit to Account') unless realm.deposit_to_account_id.present?
|
19
|
+
raise('Missing Payment Method') unless realm.payment_method_id.present?
|
20
|
+
|
21
|
+
taxes = api.taxes_collection
|
22
|
+
raise("Missing Tax Code for tax rate #{order.tax_rate.presence || 'blank'}") unless taxes[order.tax_rate.to_s].present?
|
23
|
+
raise("Missing Tax Code for tax exempt 0.0 rate") unless taxes['0.0'].present?
|
24
|
+
|
25
|
+
# Find and validate items
|
26
|
+
items = api.items()
|
27
|
+
|
28
|
+
receipt.qb_receipt_items.each do |receipt_item|
|
29
|
+
purchasable = receipt_item.order_item.purchasable
|
30
|
+
raise("Expected a purchasable for Effective::OrderItem #{receipt_item.order_item.id}") unless purchasable.present?
|
31
|
+
|
32
|
+
# Find item by receipt item
|
33
|
+
item = items.find { |item| [item.id, item.name].include?(receipt_item.item_id) }
|
34
|
+
|
35
|
+
# Find item by purchasable qb_item_id and qb_item_name
|
36
|
+
item ||= begin
|
37
|
+
purchasable_id_name = [purchasable.try(:qb_item_id), purchasable.try(:qb_item_name)]
|
38
|
+
items.find { |item| ([item.id, item.name] & purchasable_id_name).present? }
|
39
|
+
end
|
40
|
+
|
41
|
+
if item.blank?
|
42
|
+
raise("Unknown Quickbooks Item for #{purchasable} (#{purchasable.class.name} ##{purchasable.id})")
|
43
|
+
end
|
44
|
+
|
45
|
+
receipt_item.update!(item_id: item.id)
|
46
|
+
end
|
47
|
+
|
48
|
+
# Find or build customer
|
49
|
+
if receipt.customer_id.blank?
|
50
|
+
customer = api.find_or_create_customer(user: user)
|
51
|
+
receipt.update!(customer_id: customer.id)
|
52
|
+
end
|
53
|
+
|
54
|
+
# Receipt
|
55
|
+
sales_receipt = Quickbooks::Model::SalesReceipt.new(
|
56
|
+
customer_id: receipt.customer_id,
|
57
|
+
deposit_to_account_id: api.realm.deposit_to_account_id, # The ID of the Account Entity you want hte SalesReceipt to be deposited to
|
58
|
+
payment_method_id: api.realm.payment_method_id, # The ID of the PaymentMethod Entity
|
59
|
+
payment_ref_number: order.to_param, # Optional payment reference number/string
|
60
|
+
txn_date: order.purchased_at.to_date,
|
61
|
+
customer_memo: order.note_to_buyer,
|
62
|
+
private_note: order.note_internal,
|
63
|
+
bill_email: Quickbooks::Model::EmailAddress.new(order.email),
|
64
|
+
email_status: 'EmailSent'
|
65
|
+
)
|
66
|
+
|
67
|
+
# Allows Quickbooks to auto-generate the transaction number
|
68
|
+
sales_receipt.auto_doc_number!
|
69
|
+
|
70
|
+
# Addresses
|
71
|
+
sales_receipt.bill_address = api.build_address(order.billing_address) if order.billing_address.present?
|
72
|
+
sales_receipt.ship_address = api.build_address(order.shipping_address) if order.shipping_address.present?
|
73
|
+
|
74
|
+
# Line Items
|
75
|
+
tax_code = taxes[order.tax_rate.to_s]
|
76
|
+
tax_exempt = taxes['0.0']
|
77
|
+
|
78
|
+
receipt.qb_receipt_items.each do |receipt_item|
|
79
|
+
order_item = receipt_item.order_item
|
80
|
+
line_item = Quickbooks::Model::Line.new(amount: api.price_to_amount(order_item.subtotal), description: order_item.name)
|
81
|
+
|
82
|
+
line_item.sales_item! do |line|
|
83
|
+
line.item_id = receipt_item.item_id
|
84
|
+
line.tax_code_id = (order_item.tax_exempt? ? tax_exempt.id : tax_code.id)
|
85
|
+
|
86
|
+
line.unit_price = api.price_to_amount(order_item.price)
|
87
|
+
line.quantity = order_item.quantity
|
88
|
+
end
|
89
|
+
|
90
|
+
sales_receipt.line_items << line_item
|
91
|
+
end
|
92
|
+
|
93
|
+
# Double check
|
94
|
+
raise("Invalid SalesReceipt generated for Effective::Order #{order.id}") unless sales_receipt.valid?
|
95
|
+
|
96
|
+
# Return a Quickbooks::Model::SalesReceipt that is ready to create
|
97
|
+
sales_receipt
|
98
|
+
end
|
99
|
+
|
100
|
+
end
|
101
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
%table.table.table-sm
|
2
|
+
- company_info = api.company_info
|
3
|
+
|
4
|
+
%tbody
|
5
|
+
%tr
|
6
|
+
%td Name
|
7
|
+
%td= company_info.company_name
|
8
|
+
|
9
|
+
%tr
|
10
|
+
%td Country
|
11
|
+
%td= company_info.country
|
12
|
+
|
13
|
+
%tr
|
14
|
+
%td OAuth2 Connection
|
15
|
+
%td= link_to('Reconnect to Quickbooks Online', effective_qb_online.quickbooks_oauth_path)
|
16
|
+
|
17
|
+
%tr
|
18
|
+
%td Deposit to Account
|
19
|
+
%td
|
20
|
+
= effective_form_with(model: [:admin, api.realm], engine: true) do |f|
|
21
|
+
.row
|
22
|
+
.col= f.select :deposit_to_account_id, api.accounts_collection, grouped: true, label: false
|
23
|
+
.col= f.save
|
24
|
+
|
25
|
+
%tr
|
26
|
+
%td Payment Method
|
27
|
+
%td
|
28
|
+
= effective_form_with(model: [:admin, api.realm], engine: true) do |f|
|
29
|
+
.row
|
30
|
+
.col= f.select :payment_method_id, api.payment_methods_collection, label: false
|
31
|
+
.col= f.save
|
@@ -0,0 +1,15 @@
|
|
1
|
+
- realm = api.realm
|
2
|
+
|
3
|
+
%p
|
4
|
+
QB_REALM_ID=#{realm.realm_id}
|
5
|
+
%br
|
6
|
+
QB_ACCESS_TOKEN=#{realm.access_token}
|
7
|
+
%br
|
8
|
+
QB_REFRESH_TOKEN=#{realm.refresh_token}
|
9
|
+
%br
|
10
|
+
QB_DEPOSIT_TO_ACCOUNT=#{realm.deposit_to_account_id}
|
11
|
+
%br
|
12
|
+
QB_PAYMENT_METHOD=#{realm.payment_method_id}
|
13
|
+
|
14
|
+
%p
|
15
|
+
%small Copy these values into effective_qb_online/.env to run the test suite
|
@@ -0,0 +1,14 @@
|
|
1
|
+
%h1.effective-admin-heading= @page_title
|
2
|
+
|
3
|
+
- if Rails.env.development?
|
4
|
+
= card('Test Credentials') do
|
5
|
+
= render('test_credentials', api: @api)
|
6
|
+
|
7
|
+
= card('Company') do
|
8
|
+
= render('company', api: @api)
|
9
|
+
|
10
|
+
= card('Sales Receipts') do
|
11
|
+
.text-right.mb-2
|
12
|
+
= link_to 'New Sales Receipt', effective_qb_online.new_admin_qb_receipt_path, class: 'btn btn-primary btn-sm'
|
13
|
+
|
14
|
+
= render_datatable(Admin::EffectiveQbReceiptsDatatable.new)
|
@@ -0,0 +1,18 @@
|
|
1
|
+
%h1.effective-page-title= @page_title
|
2
|
+
|
3
|
+
%p Welcome to the Quickbooks Online first run instructions!
|
4
|
+
|
5
|
+
%p
|
6
|
+
1.) Ask Code & Effect to add a Redirect URI
|
7
|
+
to the Quickbooks Online Effective Orders application.
|
8
|
+
|
9
|
+
%ul
|
10
|
+
%li The uri is: #{effective_qb_online.quickbooks_oauth_callback_url}
|
11
|
+
|
12
|
+
%p
|
13
|
+
2.) Then
|
14
|
+
= link_to('click here', effective_qb_online.quickbooks_oauth_path)
|
15
|
+
and sign in to the Quickbooks Online company you wish to connect.
|
16
|
+
|
17
|
+
%p
|
18
|
+
3.) Once connected, you will be be returned to this website. All done!
|
@@ -0,0 +1,40 @@
|
|
1
|
+
= effective_form_with(model: [:admin, qb_receipt], engine: true) do |f|
|
2
|
+
|
3
|
+
- if f.object.new_record?
|
4
|
+
= f.select :order_id, qb_receipt_effective_orders_collection(f.object), required: true,
|
5
|
+
label: 'Unsynchronized purchased order',
|
6
|
+
hint: "If you don't see your purchased order in the list, it already has an existing sales receipt."
|
7
|
+
|
8
|
+
- if f.object.persisted?
|
9
|
+
= f.static_field :order
|
10
|
+
|
11
|
+
= f.static_field :updated_at
|
12
|
+
|
13
|
+
= f.static_field :status
|
14
|
+
= f.static_field :result, label: 'Quickbooks Online Result'
|
15
|
+
|
16
|
+
%table.table.table-sm
|
17
|
+
%thead
|
18
|
+
%tr
|
19
|
+
%th Id
|
20
|
+
%th Order Item
|
21
|
+
%th Existing Name
|
22
|
+
%th Quickbooks Online Item
|
23
|
+
|
24
|
+
%tbody
|
25
|
+
- items_collection = EffectiveQbOnline.api.items_collection
|
26
|
+
- items = items_collection.flatten(2)
|
27
|
+
|
28
|
+
= f.fields_for :qb_receipt_items do |fi|
|
29
|
+
%tr
|
30
|
+
%td= fi.object.order_item_id
|
31
|
+
%td= fi.object.order_item
|
32
|
+
%td
|
33
|
+
- if fi.object.item_id.present?
|
34
|
+
- existing = items.find { |(name, id)| id == fi.object.item_id }&.first
|
35
|
+
|
36
|
+
= existing || fi.object.order_item_qb_name || '-'
|
37
|
+
|
38
|
+
%td= fi.select :item_id, items_collection, grouped: true, label: false
|
39
|
+
|
40
|
+
= f.submit 'Save and Sync'
|
@@ -0,0 +1,23 @@
|
|
1
|
+
EffectiveQbOnline.setup do |config|
|
2
|
+
config.qb_realms_table_name = :qb_realms
|
3
|
+
config.qb_receipts_table_name = :qb_receipts
|
4
|
+
config.qb_receipt_items_table_name = :qb_receipt_items
|
5
|
+
|
6
|
+
# Layout Settings
|
7
|
+
# Configure the Layout per controller, or all at once
|
8
|
+
# config.layout = { application: 'application', admin: 'admin' }
|
9
|
+
|
10
|
+
# Quickbooks Online Application
|
11
|
+
# Client and Seceret
|
12
|
+
config.oauth_client_id = ENV['QUICKBOOKS_ONLINE_OAUTH_CLIENT_ID']
|
13
|
+
config.oauth_client_secret = ENV['QUICKBOOKS_ONLINE_OAUTH_CLIENT_SECRET']
|
14
|
+
|
15
|
+
# Quickbooks API
|
16
|
+
# https://github.com/ruckus/quickbooks-ruby
|
17
|
+
Quickbooks.sandbox_mode = (ENV['QUICKBOOKS_ONLINE_SANDBOX'].to_s == 'true')
|
18
|
+
|
19
|
+
# Effective Orders
|
20
|
+
# Add the following to your config/intializers/effective_orders.rb
|
21
|
+
# config.use_effective_qb_online = true
|
22
|
+
|
23
|
+
end
|
data/config/routes.rb
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
Rails.application.routes.draw do
|
4
|
+
mount EffectiveQbOnline::Engine => '/', as: 'effective_qb_online'
|
5
|
+
end
|
6
|
+
|
7
|
+
EffectiveQbOnline::Engine.routes.draw do
|
8
|
+
# Public routes
|
9
|
+
scope module: 'effective' do
|
10
|
+
get '/quickbooks/oauth/authorize', to: 'qb_oauth#authorize', as: :quickbooks_oauth
|
11
|
+
get '/quickbooks/oauth/callback', to: 'qb_oauth#callback', as: :quickbooks_oauth_callback
|
12
|
+
end
|
13
|
+
|
14
|
+
namespace :admin do
|
15
|
+
resources :qb_realms, only: [:edit, :update]
|
16
|
+
|
17
|
+
resources :qb_receipts, except: [:show, :destroy] do
|
18
|
+
post :skip, on: :member
|
19
|
+
post :sync, on: :member
|
20
|
+
end
|
21
|
+
|
22
|
+
get '/quickbooks', to: 'qb_online#index', as: :quickbooks
|
23
|
+
end
|
24
|
+
|
25
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
class CreateEffectiveQbOnline < ActiveRecord::Migration[6.1]
|
2
|
+
def change
|
3
|
+
create_table <%= @qb_realms_table_name %> do |t|
|
4
|
+
t.string :realm_id
|
5
|
+
|
6
|
+
t.integer :deposit_to_account_id
|
7
|
+
t.integer :payment_method_id
|
8
|
+
|
9
|
+
t.text :access_token
|
10
|
+
t.datetime :access_token_expires_at
|
11
|
+
|
12
|
+
t.text :refresh_token
|
13
|
+
t.datetime :refresh_token_expires_at
|
14
|
+
|
15
|
+
t.timestamps
|
16
|
+
end
|
17
|
+
|
18
|
+
create_table <%= @qb_receipts_table_name %> do |t|
|
19
|
+
t.integer :order_id
|
20
|
+
t.integer :customer_id
|
21
|
+
|
22
|
+
t.text :result
|
23
|
+
|
24
|
+
t.string :status
|
25
|
+
t.text :status_steps
|
26
|
+
|
27
|
+
t.timestamps
|
28
|
+
end
|
29
|
+
|
30
|
+
create_table <%= @qb_receipt_items_name %> do |t|
|
31
|
+
t.integer :qb_receipt_id
|
32
|
+
t.integer :order_item_id
|
33
|
+
|
34
|
+
t.integer :item_id
|
35
|
+
|
36
|
+
t.timestamps
|
37
|
+
end
|
38
|
+
|
39
|
+
end
|
40
|
+
end
|
data/db/seeds.rb
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
puts "Running effective_qb_online seeds"
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module EffectiveQbOnline
|
2
|
+
class Engine < ::Rails::Engine
|
3
|
+
engine_name 'effective_qb_online'
|
4
|
+
|
5
|
+
# Set up our default configuration options.
|
6
|
+
initializer 'effective_qb_online.defaults', before: :load_config_initializers do |app|
|
7
|
+
eval File.read("#{config.root}/config/effective_qb_online.rb")
|
8
|
+
end
|
9
|
+
|
10
|
+
# Include acts_as_addressable concern and allow any ActiveRecord object to call it
|
11
|
+
initializer 'effective_qb_online.active_record' do |app|
|
12
|
+
ActiveSupport.on_load :active_record do
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
require 'quickbooks-ruby'
|
2
|
+
require 'effective_resources'
|
3
|
+
require 'effective_datatables'
|
4
|
+
require 'effective_qb_online/engine'
|
5
|
+
require 'effective_qb_online/version'
|
6
|
+
|
7
|
+
module EffectiveQbOnline
|
8
|
+
def self.config_keys
|
9
|
+
[
|
10
|
+
:qb_realms_table_name, :qb_receipts_table_name, :qb_receipt_items_table_name,
|
11
|
+
:oauth_client_id, :oauth_client_secret,
|
12
|
+
:layout
|
13
|
+
]
|
14
|
+
end
|
15
|
+
|
16
|
+
include EffectiveGem
|
17
|
+
|
18
|
+
def self.oauth2_client
|
19
|
+
OAuth2::Client.new(
|
20
|
+
oauth_client_id,
|
21
|
+
oauth_client_secret,
|
22
|
+
site: 'https://appcenter.intuit.com/connect/oauth2',
|
23
|
+
authorize_url: 'https://appcenter.intuit.com/connect/oauth2',
|
24
|
+
token_url: 'https://oauth.platform.intuit.com/oauth2/v1/tokens/bearer'
|
25
|
+
)
|
26
|
+
end
|
27
|
+
|
28
|
+
def self.api(realm: nil)
|
29
|
+
realm ||= Effective::QbRealm.first
|
30
|
+
return nil if realm.blank?
|
31
|
+
|
32
|
+
Effective::QbApi.new(realm: realm)
|
33
|
+
end
|
34
|
+
|
35
|
+
def self.sync_order!(order, perform_now: false)
|
36
|
+
raise 'expected a purchased Effective::Order' unless order.kind_of?(Effective::Order) && order.purchased?
|
37
|
+
|
38
|
+
if perform_now
|
39
|
+
qb_receipt = Effective::QbReceipt.create_from_order!(order)
|
40
|
+
qb_receipt.sync!
|
41
|
+
else
|
42
|
+
QbSyncOrderJob.perform_later(order)
|
43
|
+
end
|
44
|
+
|
45
|
+
true
|
46
|
+
end
|
47
|
+
|
48
|
+
def self.skip_order!(order)
|
49
|
+
raise 'expected a purchased Effective::Order' unless order.kind_of?(Effective::Order) && order.purchased?
|
50
|
+
|
51
|
+
qb_receipt = Effective::QbReceipt.create_from_order!(order)
|
52
|
+
qb_receipt.skip!
|
53
|
+
end
|
54
|
+
|
55
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
module EffectiveMemberships
|
2
|
+
module Generators
|
3
|
+
class InstallGenerator < Rails::Generators::Base
|
4
|
+
include Rails::Generators::Migration
|
5
|
+
|
6
|
+
desc 'Creates an EffectiveQbOnline initializer in your application.'
|
7
|
+
|
8
|
+
source_root File.expand_path('../../templates', __FILE__)
|
9
|
+
|
10
|
+
def self.next_migration_number(dirname)
|
11
|
+
if not ActiveRecord::Base.timestamped_migrations
|
12
|
+
Time.new.utc.strftime("%Y%m%d%H%M%S")
|
13
|
+
else
|
14
|
+
'%.3d' % (current_migration_number(dirname) + 1)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def copy_initializer
|
19
|
+
template ('../' * 3) + 'config/effective_qb_online.rb', 'config/initializers/effective_qb_online.rb'
|
20
|
+
end
|
21
|
+
|
22
|
+
def create_migration_file
|
23
|
+
@qb_realms_table_name = ':' + EffectiveQbOnline.qb_realms_table_name.to_s
|
24
|
+
@qb_receipts_table_name = ':' + EffectiveQbOnline.qb_receipts_table_name.to_s
|
25
|
+
@qb_receipt_items_table_name = ':' + EffectiveQbOnline.qb_receipt_items_table_name.to_s
|
26
|
+
|
27
|
+
migration_template ('../' * 3) + 'db/migrate/01_create_effective_qb_online.rb.erb', 'db/migrate/create_effective_qb_online.rb'
|
28
|
+
end
|
29
|
+
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|