Compare commits
38 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 332f07c93d | |||
| 54d4be9762 | |||
| f1e3c29c97 | |||
| 66d393a465 | |||
| 218d3392f0 | |||
| 0136d91cc3 | |||
| a95f0350d8 | |||
| 55c04b6585 | |||
| ea21bc362a | |||
| 117d92b879 | |||
| 440c8e4618 | |||
| 1344526f7f | |||
| 19acfbc76f | |||
| 9dfb27f0a4 | |||
| 51cd830710 | |||
| 956ba2ad46 | |||
| 3ae3107760 | |||
| 925d4b8bcf | |||
| ca6dbfd12d | |||
| 9ea03d0c6d | |||
| 6ad4929d53 | |||
| 446f419af0 | |||
| f3c5de82e0 | |||
| 56e24752cf | |||
| 255af13b20 | |||
| 02b4f1eb43 | |||
| 8c735d3921 | |||
| 70e6038215 | |||
| fc7501c4fe | |||
| 45b60cfea1 | |||
| 09313ad471 | |||
| 1b15aecbff | |||
| 2bea7dbc8d | |||
| 3468b5f236 | |||
| 1c431d14dc | |||
| 7234a70265 | |||
| a459d84b00 | |||
| 49d2ed8244 |
3
Gemfile
@@ -1,13 +1,12 @@
|
|||||||
source 'https://rubygems.org'
|
source 'https://rubygems.org'
|
||||||
|
|
||||||
gem 'quickbooks-ruby'
|
gem 'quickbooks-ruby'
|
||||||
gem 'oauth2'
|
gem 'oauth2', '1.4.7'
|
||||||
gem 'roxml'
|
gem 'roxml'
|
||||||
gem 'nhtsa_vin'
|
gem 'nhtsa_vin'
|
||||||
gem 'will_paginate'
|
gem 'will_paginate'
|
||||||
gem 'rails-jquery-autocomplete'
|
gem 'rails-jquery-autocomplete'
|
||||||
gem 'jquery-ui-rails'
|
gem 'jquery-ui-rails'
|
||||||
gem 'faraday_middleware', '1.2.0'
|
|
||||||
|
|
||||||
group :assets do
|
group :assets do
|
||||||
gem 'coffee-rails'
|
gem 'coffee-rails'
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ The goal of this project is to allow Redmine to connect with Quickbooks Online t
|
|||||||
|
|
||||||
Note: Although the core functionality is complete, this project is still under heavy development. I am still working on refining everthing and adding other features. Tags should be stable
|
Note: Although the core functionality is complete, this project is still under heavy development. I am still working on refining everthing and adding other features. Tags should be stable
|
||||||
|
|
||||||
Also worth metioning I am currently using this in a live production enviroment with no issues
|
Use tags Version 1.0.0 & up for Redmine 4+ and Version 0.8.1 for Redine 3
|
||||||
|
|
||||||
#### Features
|
#### Features
|
||||||
* Issues can be assigned to a `Customer` via drop down in the edit Issue form
|
* Issues can be assigned to a `Customer` via drop down in the edit Issue form
|
||||||
@@ -52,11 +52,6 @@ Also worth metioning I am currently using this in a live production enviroment w
|
|||||||
|
|
||||||
5. Assign an Employee to each of your users via the User Administration Page
|
5. Assign an Employee to each of your users via the User Administration Page
|
||||||
|
|
||||||
## Automatic Deploy
|
|
||||||
|
|
||||||
If you want the redmine server to be automaticly restarted after a git pull event add this hook to your git hook directory
|
|
||||||
https://gist.github.com/rickbarrette/3c999c7f37e321f9c60380de99e494f5
|
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
To enable automatic `Time Activity` entries for an Issue , you need only to assign a `Customer` to an Issue via drop downs in the issue creation/update form.
|
To enable automatic `Time Activity` entries for an Issue , you need only to assign a `Customer` to an Issue via drop downs in the issue creation/update form.
|
||||||
@@ -79,7 +74,7 @@ Note: After the inital synchronization, this plugin will recieve push notificati
|
|||||||
|
|
||||||
The MIT License (MIT)
|
The MIT License (MIT)
|
||||||
|
|
||||||
Copyright (c) 2020 rick barrette
|
Copyright (c) 2022 rick barrette
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 53 KiB After Width: | Height: | Size: 346 KiB |
BIN
Screenshots/plugin_cusomer_search.png
Normal file
|
After Width: | Height: | Size: 148 KiB |
BIN
Screenshots/plugin_customer_detail.png
Normal file
|
After Width: | Height: | Size: 303 KiB |
|
Before Width: | Height: | Size: 72 KiB After Width: | Height: | Size: 240 KiB |
|
Before Width: | Height: | Size: 106 KiB After Width: | Height: | Size: 512 KiB |
|
Before Width: | Height: | Size: 49 KiB |
@@ -28,7 +28,7 @@ class CustomersController < ApplicationController
|
|||||||
helper :timelog
|
helper :timelog
|
||||||
|
|
||||||
before_action :add_customer, :only => :new
|
before_action :add_customer, :only => :new
|
||||||
before_action :view_customer, :except => :new
|
before_action :view_customer, :except => [:new, :view]
|
||||||
skip_before_action :verify_authenticity_token, :check_if_login_required, :only => [:view]
|
skip_before_action :verify_authenticity_token, :check_if_login_required, :only => [:view]
|
||||||
|
|
||||||
default_search_scope :names
|
default_search_scope :names
|
||||||
@@ -89,7 +89,10 @@ class CustomersController < ApplicationController
|
|||||||
begin
|
begin
|
||||||
@customer = Customer.find_by_id(params[:id])
|
@customer = Customer.find_by_id(params[:id])
|
||||||
@vehicles = @customer.vehicles.paginate(:page => params[:page])
|
@vehicles = @customer.vehicles.paginate(:page => params[:page])
|
||||||
@issues = @customer.issues
|
@issues = @customer.issues.order(id: :desc)
|
||||||
|
@billing_address = address_to_s(@customer.billing_address)
|
||||||
|
@shipping_address = address_to_s(@customer.shipping_address)
|
||||||
|
@closed_issues = (@issues - @issues.open)
|
||||||
rescue ActiveRecord::RecordNotFound
|
rescue ActiveRecord::RecordNotFound
|
||||||
render_404
|
render_404
|
||||||
end
|
end
|
||||||
@@ -189,4 +192,18 @@ class CustomersController < ApplicationController
|
|||||||
found_non_zero
|
found_non_zero
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# format a quickbooks address to a human readable string
|
||||||
|
def address_to_s (address)
|
||||||
|
return if address.nil?
|
||||||
|
string = address.line1
|
||||||
|
string << "\n" + address.line2 if address.line2
|
||||||
|
string << "\n" + address.line3 if address.line3
|
||||||
|
string << "\n" + address.line4 if address.line4
|
||||||
|
string << "\n" + address.line5 if address.line5
|
||||||
|
string << " " + address.city
|
||||||
|
string << ", " + address.country_sub_division_code
|
||||||
|
string << " " + address.postal_code
|
||||||
|
return string
|
||||||
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -18,6 +18,10 @@ class QboController < ApplicationController
|
|||||||
before_action :require_user, :except => :qbo_webhook
|
before_action :require_user, :except => :qbo_webhook
|
||||||
skip_before_action :verify_authenticity_token, :check_if_login_required, :only => [:qbo_webhook]
|
skip_before_action :verify_authenticity_token, :check_if_login_required, :only => [:qbo_webhook]
|
||||||
|
|
||||||
|
def allowed_params
|
||||||
|
params.permit(:code, :state, :realmId, :id)
|
||||||
|
end
|
||||||
|
|
||||||
#
|
#
|
||||||
# Called when the QBO Top Menu us shown
|
# Called when the QBO Top Menu us shown
|
||||||
#
|
#
|
||||||
@@ -88,7 +92,7 @@ class QboController < ApplicationController
|
|||||||
# Quickbooks Webhook Callback
|
# Quickbooks Webhook Callback
|
||||||
def qbo_webhook
|
def qbo_webhook
|
||||||
|
|
||||||
logger.debug "Quickbooks is calling webhook"
|
logger.info "Quickbooks is calling webhook"
|
||||||
|
|
||||||
# check the payload
|
# check the payload
|
||||||
signature = request.headers['intuit-signature']
|
signature = request.headers['intuit-signature']
|
||||||
@@ -145,14 +149,14 @@ class QboController < ApplicationController
|
|||||||
render nothing: true, status: 400
|
render nothing: true, status: 400
|
||||||
end
|
end
|
||||||
|
|
||||||
logger.debug "Quickbooks webhook complete"
|
logger.info "Quickbooks webhook complete"
|
||||||
end
|
end
|
||||||
|
|
||||||
#
|
#
|
||||||
# Synchronizes the QboCustomer table with QBO
|
# Synchronizes the QboCustomer table with QBO
|
||||||
#
|
#
|
||||||
def sync
|
def sync
|
||||||
logger.debug "Syncing EVERYTHING"
|
logger.info "Syncing EVERYTHING"
|
||||||
# Update info in background
|
# Update info in background
|
||||||
Thread.new do
|
Thread.new do
|
||||||
if Qbo.exists?
|
if Qbo.exists?
|
||||||
|
|||||||
@@ -62,6 +62,8 @@ class VehiclesController < ApplicationController
|
|||||||
begin
|
begin
|
||||||
@vehicle = Vehicle.find_by_id(params[:id])
|
@vehicle = Vehicle.find_by_id(params[:id])
|
||||||
@vin = @vehicle.vin.scan(/.{1,9}/) if @vehicle.vin
|
@vin = @vehicle.vin.scan(/.{1,9}/) if @vehicle.vin
|
||||||
|
@issues = @vehicle.issues.order(id: :desc)
|
||||||
|
@closed_issues = (@issues - @issues.open)
|
||||||
rescue ActiveRecord::RecordNotFound
|
rescue ActiveRecord::RecordNotFound
|
||||||
render_404
|
render_404
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ class QboEstimate < ActiveRecord::Base
|
|||||||
|
|
||||||
# sync all estimates
|
# sync all estimates
|
||||||
def self.sync
|
def self.sync
|
||||||
|
logger.debug "Syncing ALL estimates"
|
||||||
estimates = get_base.all
|
estimates = get_base.all
|
||||||
estimates.each { |estimate|
|
estimates.each { |estimate|
|
||||||
process_estimate(estimate)
|
process_estimate(estimate)
|
||||||
@@ -35,6 +36,7 @@ class QboEstimate < ActiveRecord::Base
|
|||||||
|
|
||||||
# sync only one estimate
|
# sync only one estimate
|
||||||
def self.sync_by_id(id)
|
def self.sync_by_id(id)
|
||||||
|
logger.debug "Syncing estimate #{id}"
|
||||||
process_estimate(get_base.fetch_by_id(id))
|
process_estimate(get_base.fetch_by_id(id))
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -44,15 +46,18 @@ class QboEstimate < ActiveRecord::Base
|
|||||||
estimate = get_base.fetch_by_id(id)
|
estimate = get_base.fetch_by_id(id)
|
||||||
qbo_estimate = find_or_create_by(id: id)
|
qbo_estimate = find_or_create_by(id: id)
|
||||||
qbo_estimate.doc_number = estimate.doc_number
|
qbo_estimate.doc_number = estimate.doc_number
|
||||||
|
qbo_estimate.txn_date = estimate.txn_date
|
||||||
qbo_estimate.save!
|
qbo_estimate.save!
|
||||||
end
|
end
|
||||||
|
|
||||||
# process an estimate into the database
|
# process an estimate into the database
|
||||||
def self.process_estimate(estimate)
|
def self.process_estimate(estimate)
|
||||||
|
logger.info "Processing estimate #{estimate.id}"
|
||||||
qbo_estimate = find_or_create_by(id: estimate.id)
|
qbo_estimate = find_or_create_by(id: estimate.id)
|
||||||
qbo_estimate.doc_number = estimate.doc_number
|
qbo_estimate.doc_number = estimate.doc_number
|
||||||
qbo_estimate.customer_id = estimate.customer_ref.value
|
qbo_estimate.customer_id = estimate.customer_ref.value
|
||||||
qbo_estimate.id = estimate.id
|
qbo_estimate.id = estimate.id
|
||||||
|
qbo_estimate.txn_date = estimate.txn_date
|
||||||
qbo_estimate.save!
|
qbo_estimate.save!
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -63,4 +68,34 @@ class QboEstimate < ActiveRecord::Base
|
|||||||
return base.pdf(estimate)
|
return base.pdf(estimate)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Magic Method
|
||||||
|
# Maps Get/Set methods to QBO estimate object
|
||||||
|
def method_missing(sym, *arguments)
|
||||||
|
# Check to see if the method exists
|
||||||
|
if Quickbooks::Model::Estimate.method_defined?(sym)
|
||||||
|
# download details if required
|
||||||
|
pull unless @details
|
||||||
|
method_name = sym.to_s
|
||||||
|
# Setter
|
||||||
|
if method_name[-1, 1] == "="
|
||||||
|
@details.method(method_name).call(arguments[0])
|
||||||
|
# Getter
|
||||||
|
else
|
||||||
|
return @details.method(method_name).call
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
# pull the details
|
||||||
|
def pull
|
||||||
|
begin
|
||||||
|
raise Exception unless self.id
|
||||||
|
@details = Qbo.get_base(:estimate).fetch_by_id(self.id)
|
||||||
|
rescue Exception => e
|
||||||
|
@details = Quickbooks::Model::Estimate.new
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -10,13 +10,12 @@
|
|||||||
|
|
||||||
class QboInvoice < ActiveRecord::Base
|
class QboInvoice < ActiveRecord::Base
|
||||||
unloadable
|
unloadable
|
||||||
|
|
||||||
has_and_belongs_to_many :issues
|
has_and_belongs_to_many :issues
|
||||||
belongs_to :customer
|
belongs_to :customer
|
||||||
#attr_accessible :doc_number, :id
|
validates_presence_of :doc_number, :id, :customer_id, :txn_date
|
||||||
validates_presence_of :doc_number, :id
|
|
||||||
self.primary_key = :id
|
self.primary_key = :id
|
||||||
|
|
||||||
|
# Get the quickbooks-ruby base for invoice
|
||||||
def self.get_base
|
def self.get_base
|
||||||
Qbo.get_base(:invoice)
|
Qbo.get_base(:invoice)
|
||||||
end
|
end
|
||||||
@@ -29,13 +28,14 @@ class QboInvoice < ActiveRecord::Base
|
|||||||
query = "SELECT Id, DocNumber FROM Invoice"
|
query = "SELECT Id, DocNumber FROM Invoice"
|
||||||
query << " WHERE Metadata.LastUpdatedTime >= '#{last.iso8601}' " if last
|
query << " WHERE Metadata.LastUpdatedTime >= '#{last.iso8601}' " if last
|
||||||
|
|
||||||
|
# TODO actually do something with the above query
|
||||||
|
# .all() is never called since count is never initialized
|
||||||
if count == 0
|
if count == 0
|
||||||
invoices = get_base.all
|
invoices = get_base.all
|
||||||
else
|
else
|
||||||
invoices = get_base.query()
|
invoices = get_base.query()
|
||||||
end
|
end
|
||||||
|
|
||||||
# Update the invoice table
|
|
||||||
invoices.each { | invoice |
|
invoices.each { | invoice |
|
||||||
process_invoice invoice
|
process_invoice invoice
|
||||||
}
|
}
|
||||||
@@ -44,7 +44,6 @@ class QboInvoice < ActiveRecord::Base
|
|||||||
#sync by invoice ID
|
#sync by invoice ID
|
||||||
def self.sync_by_id(id)
|
def self.sync_by_id(id)
|
||||||
logger.debug "Syncing invoice #{id}"
|
logger.debug "Syncing invoice #{id}"
|
||||||
#update the information in the database
|
|
||||||
invoice = get_base.fetch_by_id(id)
|
invoice = get_base.fetch_by_id(id)
|
||||||
process_invoice invoice
|
process_invoice invoice
|
||||||
end
|
end
|
||||||
@@ -60,12 +59,7 @@ class QboInvoice < ActiveRecord::Base
|
|||||||
|
|
||||||
logger.debug "Attaching invoice #{invoice.id} to issue #{issue.id}"
|
logger.debug "Attaching invoice #{invoice.id} to issue #{issue.id}"
|
||||||
|
|
||||||
# Load the invoice into the database
|
|
||||||
qbo_invoice = QboInvoice.find_or_create_by(id: invoice.id)
|
qbo_invoice = QboInvoice.find_or_create_by(id: invoice.id)
|
||||||
qbo_invoice.doc_number = invoice.doc_number
|
|
||||||
qbo_invoice.id = invoice.id
|
|
||||||
qbo_invoice.customer_id = invoice.customer_ref
|
|
||||||
qbo_invoice.save!
|
|
||||||
|
|
||||||
unless issue.qbo_invoices.include?(qbo_invoice)
|
unless issue.qbo_invoices.include?(qbo_invoice)
|
||||||
issue.qbo_invoices << qbo_invoice
|
issue.qbo_invoices << qbo_invoice
|
||||||
@@ -75,10 +69,19 @@ class QboInvoice < ActiveRecord::Base
|
|||||||
compare_custom_fields(issue, invoice)
|
compare_custom_fields(issue, invoice)
|
||||||
end
|
end
|
||||||
|
|
||||||
# processes the invoice into the system
|
# processes the invoice into the database
|
||||||
def self.process_invoice(invoice)
|
def self.process_invoice(invoice)
|
||||||
logger.debug "Processing invoice"
|
logger.info "Processing invoice #{invoice.id}"
|
||||||
# Check the private notes
|
|
||||||
|
# Load the invoice into the database
|
||||||
|
qbo_invoice = QboInvoice.find_or_create_by(id: invoice.id)
|
||||||
|
qbo_invoice.doc_number = invoice.doc_number
|
||||||
|
qbo_invoice.id = invoice.id
|
||||||
|
qbo_invoice.customer_id = invoice.customer_ref
|
||||||
|
qbo_invoice.txn_date = invoice.txn_date
|
||||||
|
qbo_invoice.save!
|
||||||
|
|
||||||
|
# Scan the private notes for hashtags and attach to the applicable issues
|
||||||
if not invoice.private_note.nil?
|
if not invoice.private_note.nil?
|
||||||
invoice.private_note.scan(/#(\w+)/).flatten.each { |issue|
|
invoice.private_note.scan(/#(\w+)/).flatten.each { |issue|
|
||||||
attach_to_issue(Issue.find_by_id(issue.to_i), invoice)
|
attach_to_issue(Issue.find_by_id(issue.to_i), invoice)
|
||||||
@@ -95,17 +98,28 @@ class QboInvoice < ActiveRecord::Base
|
|||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# compares the custome fields on invoices & issues and updates the invoice as needed
|
||||||
|
#
|
||||||
|
# the issue here is when two or more issues share an invoice with the same custom field, but diffrent values
|
||||||
|
# this condions causes an infinite loop as the webhook is called when an invoice is updated
|
||||||
|
# TODO maybe add a cf_sync_confict flag to invoices
|
||||||
def self.compare_custom_fields(issue, invoice)
|
def self.compare_custom_fields(issue, invoice)
|
||||||
|
logger.debug "Comparing custom fields"
|
||||||
|
# TODO break if QboInvoice.find(invoice.id).cf_sync_confict
|
||||||
is_changed = false
|
is_changed = false
|
||||||
|
|
||||||
# update the invoive custom fields with infomation from the work ticket if available
|
# update the invoive custom fields with infomation from the issue if available
|
||||||
invoice.custom_fields.each { |cf|
|
invoice.custom_fields.each { |cf|
|
||||||
|
|
||||||
# TODO Add some hooks here
|
|
||||||
|
|
||||||
# VIN from the attached vehicle
|
# VIN from the attached vehicle
|
||||||
|
# TODO move this into seperate plugin
|
||||||
|
# TODO create hook for seperate plugin
|
||||||
begin
|
begin
|
||||||
if cf.name.eql? "VIN"
|
if cf.name.eql? "VIN"
|
||||||
|
# Only update if blank to prevent infite loops
|
||||||
|
# TODO check cf_sync_confict flag once implemented
|
||||||
|
if cf.string_value.to_s.blank?
|
||||||
|
logger.debug " VIN was blank, updating the invoice vin in quickbooks"
|
||||||
vin = Vehicle.find(issue.vehicles_id).vin
|
vin = Vehicle.find(issue.vehicles_id).vin
|
||||||
break if vin.nil?
|
break if vin.nil?
|
||||||
if not cf.string_value.to_s.eql? vin
|
if not cf.string_value.to_s.eql? vin
|
||||||
@@ -113,6 +127,8 @@ class QboInvoice < ActiveRecord::Base
|
|||||||
logger.debug "VIN has changed"
|
logger.debug "VIN has changed"
|
||||||
is_changed = true
|
is_changed = true
|
||||||
end
|
end
|
||||||
|
|
||||||
|
end
|
||||||
end
|
end
|
||||||
rescue
|
rescue
|
||||||
#do nothing
|
#do nothing
|
||||||
@@ -126,43 +142,55 @@ class QboInvoice < ActiveRecord::Base
|
|||||||
if not value.value.to_s.blank?
|
if not value.value.to_s.blank?
|
||||||
# Check to see if the value is diffrent
|
# Check to see if the value is diffrent
|
||||||
if not cf.string_value.to_s.eql? value.value.to_s
|
if not cf.string_value.to_s.eql? value.value.to_s
|
||||||
|
# update the custom field on the invoice
|
||||||
# Use the lowest Milage
|
|
||||||
if cf.name.eql? "Mileage In"
|
|
||||||
if cf.string_value.to_i > value.value.to_i or cf.string_value.blank?
|
|
||||||
cf.string_value = value.value.to_s
|
cf.string_value = value.value.to_s
|
||||||
is_changed = true
|
is_changed = true
|
||||||
end
|
end
|
||||||
# Use the max milage
|
|
||||||
elsif cf.name.eql? "Mileage Out"
|
|
||||||
if cf.string_value.to_i < value.value.to_i or cf.string_value.blank?
|
|
||||||
cf.string_value = value.value.to_s
|
|
||||||
is_changed = true
|
|
||||||
end
|
|
||||||
else
|
|
||||||
# Everything else
|
|
||||||
cf.string_value = value.value.to_s
|
|
||||||
is_changed = true
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
rescue
|
rescue
|
||||||
# Nothing to do here, there is no match
|
# Nothing to do here, there is no match
|
||||||
end
|
end
|
||||||
}
|
}
|
||||||
|
|
||||||
# TODO Add some hooks here
|
|
||||||
|
|
||||||
# Push updates
|
# Push updates
|
||||||
#invoice.sync_token += 1 if is_changed
|
|
||||||
begin
|
begin
|
||||||
logger.debug "Trying to update invoice"
|
logger.debug "Trying to update invoice"
|
||||||
get_base.update(invoice) if is_changed
|
get_base.update(invoice) if is_changed
|
||||||
rescue
|
rescue
|
||||||
# Do nothing, probaly too many vehicles on the invoice. This is a problem with how it's billed
|
# Do nothing, probaly custome field sync confict on the invoice.
|
||||||
|
# This is a problem with how it's billed
|
||||||
# TODO Add notes in memo area
|
# TODO Add notes in memo area
|
||||||
|
# TODO flag QboInvoice.cf_sync_confict here
|
||||||
logger.error "Failed to update invoice"
|
logger.error "Failed to update invoice"
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Magic Method
|
||||||
|
# Maps Get/Set methods to QBO invoice object
|
||||||
|
def method_missing(sym, *arguments)
|
||||||
|
# Check to see if the method exists
|
||||||
|
if Quickbooks::Model::Invoice.method_defined?(sym)
|
||||||
|
# download details if required
|
||||||
|
pull unless @details
|
||||||
|
method_name = sym.to_s
|
||||||
|
# Setter
|
||||||
|
if method_name[-1, 1] == "="
|
||||||
|
@details.method(method_name).call(arguments[0])
|
||||||
|
# Getter
|
||||||
|
else
|
||||||
|
return @details.method(method_name).call
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# pull the details from quickbooks
|
||||||
|
def pull
|
||||||
|
begin
|
||||||
|
raise Exception unless self.id
|
||||||
|
@details = Qbo.get_base(:invoice).fetch_by_id(self.id)
|
||||||
|
rescue Exception => e
|
||||||
|
@details = Quickbooks::Model::Invoice.new
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -17,17 +17,12 @@
|
|||||||
|
|
||||||
<tr>
|
<tr>
|
||||||
<th><%=t(:label_billing_address)%></th>
|
<th><%=t(:label_billing_address)%></th>
|
||||||
<td><%= customer.billing_address %></td>
|
<td><%= @billing_address %></td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
||||||
<tr>
|
<tr>
|
||||||
<th><%=t(:label_shipping_address)%></th>
|
<th><%=t(:label_shipping_address)%></th>
|
||||||
<td><%= customer.shipping_address %></td>
|
<td><%= @shipping_address %></td>
|
||||||
</tr>
|
|
||||||
|
|
||||||
<tr>
|
|
||||||
<th><%=t(:issues)%></th>
|
|
||||||
<td><%= customer.issues.count %></td>
|
|
||||||
</tr>
|
</tr>
|
||||||
|
|
||||||
<tr>
|
<tr>
|
||||||
@@ -35,11 +30,6 @@
|
|||||||
<td>$<%= customer.balance %></td>
|
<td>$<%= customer.balance %></td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
||||||
<tr>
|
|
||||||
<th><%=t(:label_balance_with_jobs)%></th>
|
|
||||||
<td>$<%= customer.balance_with_jobs %></td>
|
|
||||||
</tr>
|
|
||||||
|
|
||||||
<tr>
|
<tr>
|
||||||
<th><%=t(:field_notes)%></th>
|
<th><%=t(:field_notes)%></th>
|
||||||
<td><%= customer.notes %></td>
|
<td><%= customer.notes %></td>
|
||||||
|
|||||||
@@ -7,28 +7,28 @@
|
|||||||
<div class="clearfix">
|
<div class="clearfix">
|
||||||
<%=t(:label_display_name)%>
|
<%=t(:label_display_name)%>
|
||||||
<div class="input">
|
<div class="input">
|
||||||
<%= f.text_field :name, :required => true %>
|
<%= f.text_field :name, :required => true, :autocomplete => "off" %>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="clearfix">
|
<div class="clearfix">
|
||||||
<%=t(:label_primary_phone)%>
|
<%=t(:label_primary_phone)%>
|
||||||
<div class="input">
|
<div class="input">
|
||||||
<%= f.telephone_field :primary_phone %>
|
<%= f.telephone_field :primary_phone, :autocomplete => "off" %>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="clearfix">
|
<div class="clearfix">
|
||||||
<%=t(:label_mobile_phone)%>:
|
<%=t(:label_mobile_phone)%>:
|
||||||
<div class="input">
|
<div class="input">
|
||||||
<%= f.telephone_field :mobile_phone %>
|
<%= f.telephone_field :mobile_phone, :autocomplete => "off" %>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="clearfix">
|
<div class="clearfix">
|
||||||
<%=t(:label_email)%>:
|
<%=t(:label_email)%>:
|
||||||
<div class="input">
|
<div class="input">
|
||||||
<%= f.email_field :email %>
|
<%= f.email_field :email, :autocomplete => "off" %>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<%= form_tag(customers_path, :method => "get", id: "search-form") do %>
|
<%= form_tag(customers_path, :method => "get", id: "search-form") do %>
|
||||||
<%= text_field_tag :search, params[:search], placeholder: t(:label_search_customers) %>
|
<%= text_field_tag :search, params[:search], placeholder: t(:label_search_customers), :autocomplete => "off" %>
|
||||||
<%= submit_tag t(:label_search) %>
|
<%= submit_tag t(:label_search) %>
|
||||||
<% end %>
|
<% end %>
|
||||||
<%= button_to t(:label_new_customer), new_customer_path, method: :get%>
|
<%= button_to t(:label_new_customer), new_customer_path, method: :get%>
|
||||||
|
|||||||
@@ -2,15 +2,30 @@
|
|||||||
<br/>
|
<br/>
|
||||||
|
|
||||||
<div class="subject">
|
<div class="subject">
|
||||||
<div><h3><%=t(:label_details)%>:</h3></div>
|
<div><h4><%=t(:label_details)%>:</h4></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="attributes">
|
<div class="attributes">
|
||||||
|
|
||||||
<div class="splitcontent">
|
<div class="splitcontent">
|
||||||
|
|
||||||
<div class="splitcontentleft">
|
<div class="splitcontentleft">
|
||||||
<%= render :partial => 'customers/details', locals: {customer: @customer} %>
|
<%= render :partial => 'customers/details', locals: {customer: @customer} %>
|
||||||
|
|
||||||
|
<div class="splitcontent">
|
||||||
|
<div class="splitcontentleft">
|
||||||
|
<h4><%=t(:estimates)%>:</h4>
|
||||||
|
<%= render :partial => 'estimates/list', locals: {customer: @customer} %>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="splitcontentleft">
|
||||||
|
<h4><%=t(:label_invoices)%>:</h4>
|
||||||
|
<%= render :partial => 'invoices/list', locals: {customer: @customer} %>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="splitcontentleft">
|
<div class="splitcontentleft">
|
||||||
<h4><%=t(:field_vehicles)%>:</h4>
|
<h4><%=t(:field_vehicles)%>:</h4>
|
||||||
<%= render :partial => 'vehicles/list' %>
|
<%= render :partial => 'vehicles/list' %>
|
||||||
@@ -19,10 +34,10 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<br/>
|
<br/>
|
||||||
<h2><%=t(:label_open_issues)%>:</h2>
|
<h2><%=@issues.open.count%> <%=t(:label_open_issues)%>:</h2>
|
||||||
<%= render :partial => 'issues/list_simple', locals: {issues: @issues.open} %>
|
<%= render :partial => 'issues/list_simple', locals: {issues: @issues.open} %>
|
||||||
|
|
||||||
<h2><%=t(:label_closed_issues)%>:</h2>
|
<h2><%=@closed_issues.count%> <%=t(:label_closed_issues)%>:</h2>
|
||||||
<%= render :partial => 'issues/list_simple', locals: {issues: (@issues - @issues.open)} %>
|
<%= render :partial => 'issues/list_simple', locals: {issues: @closed_issues} %>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
11
app/views/estimates/_list.html.erb
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
<% if @customer.present? %>
|
||||||
|
|
||||||
|
<% @customer.qbo_estimates.order(doc_number: :desc).each do |estimate| %>
|
||||||
|
<div class="row">
|
||||||
|
<b><%= link_to "##{estimate.doc_number}", estimate_path(estimate), target: :_blank %></b> <%= estimate.txn_date %>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
|
||||||
|
<% else %>
|
||||||
|
<p><%=t(:label_no_estimates)%>.</p>
|
||||||
|
<% end %>
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
<%= form_tag("/qbo/estimate/doc", :method => "get", id: "est-search-form") do %>
|
<%= form_tag("/qbo/estimate/doc", :method => "get", id: "est-search-form") do %>
|
||||||
<%= text_field_tag :search, params[:search], placeholder: t(:label_search_estimates) %>
|
<%= text_field_tag :search, params[:search], placeholder: t(:label_search_estimates), :autocomplete => "off" %>
|
||||||
<%= submit_tag t(:label_search), :formtarget => "_blank" %>
|
<%= submit_tag t(:label_search), :formtarget => "_blank" %>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|||||||
11
app/views/invoices/_list.html.erb
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
<% if @customer.present? %>
|
||||||
|
|
||||||
|
<% @customer.qbo_invoices.order(doc_number: :desc).each do |invoice| %>
|
||||||
|
<div class="row">
|
||||||
|
<b><%= link_to "##{invoice.doc_number}", invoice_path(invoice), target: :_blank %></b> <%= invoice.txn_date %>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
|
||||||
|
<% else %>
|
||||||
|
<p><%=t(:label_no_invoices)%>.</p>
|
||||||
|
<% end %>
|
||||||
35
app/views/issues/_history.html.erb
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
<% reply_links = issue.notes_addable? -%>
|
||||||
|
<% for journal in journals %>
|
||||||
|
<div id="change-<%= journal.id %>" class="<%= journal.css_classes %>">
|
||||||
|
<div id="note-<%= journal.indice %>">
|
||||||
|
<div class="contextual">
|
||||||
|
<span class="journal-actions"><%= render_journal_actions(issue, journal, :reply_links => reply_links) %></span>
|
||||||
|
<a href="#note-<%= journal.indice %>" class="journal-link">#<%= journal.indice %></a>
|
||||||
|
</div>
|
||||||
|
<h4>
|
||||||
|
<%= avatar(journal.user, :size => "24") %>
|
||||||
|
<%= authoring journal.created_on, journal.user, :label => :label_updated_time_by %>
|
||||||
|
<%= render_private_notes_indicator(journal) %>
|
||||||
|
</h4>
|
||||||
|
|
||||||
|
<% if journal.details.any? %>
|
||||||
|
<ul class="details">
|
||||||
|
<% details_to_strings(journal.visible_details).each do |string| %>
|
||||||
|
<li><%= string %></li>
|
||||||
|
<% end %>
|
||||||
|
</ul>
|
||||||
|
<% if Setting.thumbnails_enabled? && (thumbnail_attachments = journal_thumbnail_attachments(journal)).any? %>
|
||||||
|
<div class="thumbnails">
|
||||||
|
<% thumbnail_attachments.each do |attachment| %>
|
||||||
|
<div><%= thumbnail_tag(attachment) %></div>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
<% end %>
|
||||||
|
<%= render_notes(issue, journal, :reply_links => reply_links) unless journal.notes.blank? %>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<%= call_hook(:view_issues_history_journal_bottom, { :journal => journal }) %>
|
||||||
|
<% end %>
|
||||||
|
|
||||||
|
<% heads_for_wiki_formatter if User.current.allowed_to?(:edit_issue_notes, issue.project) || User.current.allowed_to?(:edit_own_issue_notes, issue.project) %>
|
||||||
@@ -1,2 +1,6 @@
|
|||||||
|
<% if User.current.logged? %>
|
||||||
|
|
||||||
<%= render :partial => 'customers/sidebar' %>
|
<%= render :partial => 'customers/sidebar' %>
|
||||||
<%= render :partial => 'estimates/sidebar' %>
|
<%= render :partial => 'estimates/sidebar' %>
|
||||||
|
|
||||||
|
<% end %>
|
||||||
|
|||||||
@@ -21,11 +21,6 @@
|
|||||||
<td><%= vehicle.notes %></td>
|
<td><%= vehicle.notes %></td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
||||||
<tr>
|
|
||||||
<th> <%= t(:issues) %> </th>
|
|
||||||
<td><%= vehicle.issues.count %></td>
|
|
||||||
</tr>
|
|
||||||
|
|
||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
<%= button_to t(:label_edit), edit_vehicle_path(vehicle), method: :get%>
|
<%= button_to t(:label_edit), edit_vehicle_path(vehicle), method: :get%>
|
||||||
|
|||||||
@@ -14,21 +14,21 @@
|
|||||||
<div class="clearfix">
|
<div class="clearfix">
|
||||||
<%=t(:label_year)%>:
|
<%=t(:label_year)%>:
|
||||||
<div class="input">
|
<div class="input">
|
||||||
<%= f.number_field :year %>
|
<%= f.number_field :year, :autocomplete => "off" %>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="clearfix">
|
<div class="clearfix">
|
||||||
<%=t(:label_make)%>:
|
<%=t(:label_make)%>:
|
||||||
<div class="input">
|
<div class="input">
|
||||||
<%= f.text_field :make %>
|
<%= f.text_field :make, :autocomplete => "off" %>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="clearfix">
|
<div class="clearfix">
|
||||||
<%=t(:label_model)%>:
|
<%=t(:label_model)%>:
|
||||||
<div class="input">
|
<div class="input">
|
||||||
<%= f.text_field :model %>
|
<%= f.text_field :model, :autocomplete => "off" %>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
<%= form_tag(vehicles_path, :method => "get", id: "search-form") do %>
|
<%= form_tag(vehicles_path, :method => "get", id: "search-form") do %>
|
||||||
<%= text_field_tag :search, params[:search], placeholder: t(:label_seach_vin) %>
|
<%= text_field_tag :search, params[:search], placeholder: t(:label_search_vin), :autocomplete => "off" %>
|
||||||
<%= submit_tag t(:label_search) %>
|
<%= submit_tag t(:label_search) %>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|||||||
@@ -4,11 +4,11 @@
|
|||||||
<div style="text-align: left; width:90%;">
|
<div style="text-align: left; width:90%;">
|
||||||
<%= render :partial => 'vehicles/details', locals: {vehicle: @vehicle} %>
|
<%= render :partial => 'vehicles/details', locals: {vehicle: @vehicle} %>
|
||||||
|
|
||||||
<p> <b> <%=t(:label_open_issues)%> </b> </p>
|
<p><b><%=@issues.open.count%> <%=t(:label_open_issues)%> </b> </p>
|
||||||
|
|
||||||
<%= render :partial => 'issues/list_simple', locals: {issues: @vehicle.issues.open} %>
|
<%= render :partial => 'issues/list_simple', locals: {issues: @issues.open} %>
|
||||||
|
|
||||||
<p> <b> <%=t(:label_closed_issues)%> </b> </p>
|
<p><b><%=@closed_issues.count%> <%=t(:label_closed_issues)%> </b> </p>
|
||||||
|
|
||||||
<%= render :partial => 'issues/list_simple', locals: {issues: (@vehicle.issues - @vehicle.issues.open)} %>
|
<%= render :partial => 'issues/list_simple', locals: {issues: (@closed_issues)} %>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -74,3 +74,7 @@ en:
|
|||||||
label_oauth_expires: "OAuth2 Access Token Expires At"
|
label_oauth_expires: "OAuth2 Access Token Expires At"
|
||||||
label_oauth_note: "Note: You need to authenticate with Quickbooks after saving your key and secret above"
|
label_oauth_note: "Note: You need to authenticate with Quickbooks after saving your key and secret above"
|
||||||
field_customers: "Customers"
|
field_customers: "Customers"
|
||||||
|
label_no_estimates: "No Estimates"
|
||||||
|
label_no_invoices: "No Invoices"
|
||||||
|
label_invoices: "Invoices"
|
||||||
|
label_load_customer: "Load Customer"
|
||||||
44
db/migrate/032_add_txn_dates.rb
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
#The MIT License (MIT)
|
||||||
|
#
|
||||||
|
#Copyright (c) 2022 rick barrette
|
||||||
|
#
|
||||||
|
#Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||||
|
#
|
||||||
|
#The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||||
|
#
|
||||||
|
#THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
|
||||||
|
class AddTxnDates < ActiveRecord::Migration[5.1]
|
||||||
|
def change
|
||||||
|
add_column :qbo_invoices, :txn_date, :date
|
||||||
|
add_column :qbo_estimates, :txn_date, :date
|
||||||
|
|
||||||
|
reversible do |direction|
|
||||||
|
direction.up {
|
||||||
|
break unless Qbo.first
|
||||||
|
|
||||||
|
QboEstimate.reset_column_information
|
||||||
|
QboInvoice.reset_column_information
|
||||||
|
|
||||||
|
say "Sync Estimates"
|
||||||
|
|
||||||
|
QboEstimate.sync
|
||||||
|
|
||||||
|
say "Sync Invoices"
|
||||||
|
|
||||||
|
invoices = QboInvoice.get_base.all
|
||||||
|
|
||||||
|
invoices.each { |invoice|
|
||||||
|
# Load the invoice into the database
|
||||||
|
qbo_invoice = QboInvoice.find_or_create_by(id: invoice.id)
|
||||||
|
qbo_invoice.doc_number = invoice.doc_number
|
||||||
|
qbo_invoice.id = invoice.id
|
||||||
|
qbo_invoice.customer_id = invoice.customer_ref
|
||||||
|
qbo_invoice.txn_date = invoice.txn_date
|
||||||
|
qbo_invoice.save!
|
||||||
|
}
|
||||||
|
}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
33
init.rb
@@ -8,36 +8,27 @@
|
|||||||
#
|
#
|
||||||
#THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
#THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
|
||||||
|
# Dynamically load all Hooks & Patches
|
||||||
|
ActiveSupport::Reloader.to_prepare do
|
||||||
|
Dir::foreach(File.join(File.dirname(__FILE__), 'lib')) do |file|
|
||||||
|
next unless /\.rb$/ =~ file
|
||||||
|
require_dependency file
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
Redmine::Plugin.register :redmine_qbo do
|
Redmine::Plugin.register :redmine_qbo do
|
||||||
|
|
||||||
# View Hook Listeners
|
|
||||||
require_dependency 'issues_form_hook_listener'
|
|
||||||
require_dependency 'issues_save_hook_listener'
|
|
||||||
require_dependency 'issues_show_hook_listener'
|
|
||||||
require_dependency 'users_show_hook_listener'
|
|
||||||
require_dependency 'header_footer_hook_listener'
|
|
||||||
require_dependency 'projects_form_hook_listener'
|
|
||||||
require_dependency 'view_hook_listener'
|
|
||||||
|
|
||||||
# Patches to the Redmine core. Will not work in development mode
|
|
||||||
require_dependency 'issue_patch'
|
|
||||||
require_dependency 'project_patch'
|
|
||||||
require_dependency 'user_patch'
|
|
||||||
require_dependency 'query_patch'
|
|
||||||
require_dependency 'time_entry_query_patch'
|
|
||||||
require_dependency 'pdf_patch'
|
|
||||||
require_dependency 'attachments_controller_patch'
|
|
||||||
|
|
||||||
# About
|
# About
|
||||||
name 'Redmine Quickbooks Online plugin'
|
name 'Redmine Quickbooks Online plugin'
|
||||||
author 'Rick Barrette'
|
author 'Rick Barrette'
|
||||||
description 'This is a plugin for Redmine to intergrate with Quickbooks Online to allow for seamless intergration CRM and invoicing of completed issues'
|
description 'This is a plugin for Redmine to intergrate with Quickbooks Online to allow for seamless intergration CRM and invoicing of completed issues'
|
||||||
version '1.0.0'
|
version '1.1.1'
|
||||||
url 'https://github.com/rickbarrette/redmine_qbo'
|
url 'https://github.com/rickbarrette/redmine_qbo'
|
||||||
author_url 'http://rickbarrette.org'
|
author_url 'http://rickbarrette.org'
|
||||||
settings :default => {'empty' => true}, :partial => 'qbo/settings'
|
settings :default => {'empty' => true}, :partial => 'qbo/settings'
|
||||||
|
requires_redmine :version_or_higher => '4.0.0'
|
||||||
|
|
||||||
# Add safe attributes
|
# Add safe attributes for core models
|
||||||
Issue.safe_attributes 'customer_id'
|
Issue.safe_attributes 'customer_id'
|
||||||
Issue.safe_attributes 'qbo_item_id'
|
Issue.safe_attributes 'qbo_item_id'
|
||||||
Issue.safe_attributes 'qbo_estimate_id'
|
Issue.safe_attributes 'qbo_estimate_id'
|
||||||
@@ -61,7 +52,7 @@ Redmine::Plugin.register :redmine_qbo do
|
|||||||
permission :add_payments, :payments => :new, :public => false
|
permission :add_payments, :payments => :new, :public => false
|
||||||
permission :view_vehicles, :payments => :new, :public => false
|
permission :view_vehicles, :payments => :new, :public => false
|
||||||
|
|
||||||
# Register QBO top menu item
|
# Register top menu items
|
||||||
menu :top_menu, :customers, { :controller => :customers, :action => :index }, :caption => 'Customers', :if => Proc.new {User.current.logged?}
|
menu :top_menu, :customers, { :controller => :customers, :action => :index }, :caption => 'Customers', :if => Proc.new {User.current.logged?}
|
||||||
menu :top_menu, :vehicles, { :controller => :vehicles, :action => :index }, :caption => 'Vehicles', :if => Proc.new { User.current.logged? }
|
menu :top_menu, :vehicles, { :controller => :vehicles, :action => :index }, :caption => 'Vehicles', :if => Proc.new { User.current.logged? }
|
||||||
|
|
||||||
|
|||||||
@@ -14,6 +14,6 @@ class HeaderFooterHookListener < Redmine::Hook::ViewListener
|
|||||||
end
|
end
|
||||||
|
|
||||||
def view_layouts_base_body_bottom(context = {})
|
def view_layouts_base_body_bottom(context = {})
|
||||||
return "<div id='qbo_footer' align='center'><b>Last Sync: </b> #{Qbo.last_sync if Qbo.exists?}</div>"
|
return "<div id='qbo_footer' align='center'><b>#{I18n.translate(:label_last_sync)}: </b> #{Qbo.last_sync if Qbo.exists?}</div>"
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
#The MIT License (MIT)
|
#The MIT License (MIT)
|
||||||
#
|
#
|
||||||
#Copyright (c) 2017 rick barrette
|
#Copyright (c) 2022 rick barrette
|
||||||
#
|
#
|
||||||
#Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
#Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||||
#
|
#
|
||||||
@@ -18,7 +18,7 @@ class IssuesFormHookListener < Redmine::Hook::ViewListener
|
|||||||
end
|
end
|
||||||
|
|
||||||
# Edit Issue Form
|
# Edit Issue Form
|
||||||
# Show a dropdown for quickbooks contacts
|
# TODO figure out how to do this with a view
|
||||||
def view_issues_form_details_bottom(context={})
|
def view_issues_form_details_bottom(context={})
|
||||||
f = context[:form]
|
f = context[:form]
|
||||||
|
|
||||||
@@ -28,7 +28,7 @@ class IssuesFormHookListener < Redmine::Hook::ViewListener
|
|||||||
selected_vehicle = context[:project].vehicle ? context[:project].vehicle.id : nil
|
selected_vehicle = context[:project].vehicle ? context[:project].vehicle.id : nil
|
||||||
end
|
end
|
||||||
|
|
||||||
# Check to see if there is a quickbooks user attached to the issue
|
# Check to see if there are things attached to the issue
|
||||||
selected_customer = context[:issue].customer ? context[:issue].customer.id : nil
|
selected_customer = context[:issue].customer ? context[:issue].customer.id : nil
|
||||||
selected_estimate = context[:issue].qbo_estimate ? context[:issue].qbo_estimate.id : nil
|
selected_estimate = context[:issue].qbo_estimate ? context[:issue].qbo_estimate.id : nil
|
||||||
selected_vehicle = context[:issue].vehicles_id ? context[:issue].vehicles_id : nil
|
selected_vehicle = context[:issue].vehicles_id ? context[:issue].vehicles_id : nil
|
||||||
@@ -36,15 +36,22 @@ class IssuesFormHookListener < Redmine::Hook::ViewListener
|
|||||||
# Load customer information
|
# Load customer information
|
||||||
customer = Customer.find_by_id(selected_customer) if selected_customer
|
customer = Customer.find_by_id(selected_customer) if selected_customer
|
||||||
|
|
||||||
|
# Customer Name Text Box with database backed autocomplete
|
||||||
search_customer = f.autocomplete_field :customer,
|
search_customer = f.autocomplete_field :customer,
|
||||||
autocomplete_customer_name_customers_path,
|
autocomplete_customer_name_customers_path,
|
||||||
:selected => selected_customer,
|
:selected => selected_customer,
|
||||||
:update_elements => { :id => '#issue_customer_id', :value => '#issue_customer' }
|
:onchange => "updateIssueFrom('/issues/#{context[:issue].id}/edit.js', this)",
|
||||||
|
:update_elements => {
|
||||||
|
:id => '#issue_customer_id',
|
||||||
|
:value => '#issue_customer'
|
||||||
|
}
|
||||||
|
|
||||||
|
# Customer ID - Hidden Field
|
||||||
customer_id = f.hidden_field :customer_id,
|
customer_id = f.hidden_field :customer_id,
|
||||||
:id => "issue_customer_id",
|
:id => "issue_customer_id",
|
||||||
:onchange => "updateIssueFrom('/issues/#{context[:issue].id}/edit.js', this)"
|
:onchange => "updateIssueFrom('/issues/#{context[:issue].id}/edit.js', this)"
|
||||||
|
|
||||||
|
# Load estimates & vehicles
|
||||||
if context[:issue].customer
|
if context[:issue].customer
|
||||||
if customer.vehicles
|
if customer.vehicles
|
||||||
vehicles = customer.vehicles.pluck(:name, :id)
|
vehicles = customer.vehicles.pluck(:name, :id)
|
||||||
@@ -57,11 +64,10 @@ class IssuesFormHookListener < Redmine::Hook::ViewListener
|
|||||||
estimates = [nil].compact
|
estimates = [nil].compact
|
||||||
end
|
end
|
||||||
|
|
||||||
# Generate the drop down list of quickbooks extimates
|
# Generate the drop down list of quickbooks extimates & vehicles
|
||||||
select_estimate = f.select :qbo_estimate_id, estimates, :selected => selected_estimate, include_blank: true
|
select_estimate = f.select :qbo_estimate_id, estimates, :selected => selected_estimate, include_blank: true
|
||||||
|
|
||||||
vehicle = f.select :vehicles_id, vehicles, :selected => selected_vehicle, include_blank: true
|
vehicle = f.select :vehicles_id, vehicles, :selected => selected_vehicle, include_blank: true
|
||||||
|
|
||||||
return "<p><label for=\"issue_customer\">Customer</label>#{search_customer} #{customer_id}</p> <p>#{select_estimate}</p> <p>#{vehicle}</p>"
|
return "<p><label for=\"issue_customer\">Customer</label>#{search_customer} #{customer_id} #{link_to_function I18n.t(:label_load_customer), "updateIssueFrom('/issues/#{context[:issue].id}/edit.js', this)"}</p> <p>#{select_estimate}</p> <p>#{vehicle}</p>"
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ module QueryPatch
|
|||||||
def available_columns
|
def available_columns
|
||||||
unless @available_columns
|
unless @available_columns
|
||||||
@available_columns = self.class.available_columns.dup
|
@available_columns = self.class.available_columns.dup
|
||||||
@available_columns << QueryColumn.new(:customer, :sortable => "#{Customer.table_name}.name", :groupable => true, :caption => :field_customer)
|
@available_columns << QueryColumn.new(:customer, :sortable => "#{Issue.table_name}.customer_id", :groupable => true, :caption => :field_customer)
|
||||||
@available_columns << QueryColumn.new(:qbo_billed, :sortable => "#{TimeEntry.table_name}.qbo_billed", :groupable => true, :caption => :field_qbo_billed)
|
@available_columns << QueryColumn.new(:qbo_billed, :sortable => "#{TimeEntry.table_name}.qbo_billed", :groupable => true, :caption => :field_qbo_billed)
|
||||||
end
|
end
|
||||||
super
|
super
|
||||||
@@ -24,7 +24,7 @@ module QueryPatch
|
|||||||
|
|
||||||
# Add customers to filters
|
# Add customers to filters
|
||||||
def initialize_available_filters
|
def initialize_available_filters
|
||||||
add_available_filter "customer", :type => :text
|
#add_available_filter "customer", :type => :text
|
||||||
super
|
super
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,15 @@
|
|||||||
|
#The MIT License (MIT)
|
||||||
|
#
|
||||||
|
#Copyright (c) 2022 rick barrette
|
||||||
|
#
|
||||||
|
#Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||||
|
#
|
||||||
|
#The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||||
|
#
|
||||||
|
#THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
|
||||||
class ViewHookListener < Redmine::Hook::ViewListener
|
class ViewHookListener < Redmine::Hook::ViewListener
|
||||||
|
|
||||||
render_on :view_layouts_base_sidebar, :partial => "qbo/sidebar"
|
render_on :view_layouts_base_sidebar, :partial => "qbo/sidebar"
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|||||||