Files
redmine_qbo/app/models/invoice.rb

197 lines
6.6 KiB
Ruby

#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 Invoice < ActiveRecord::Base
unloadable
has_and_belongs_to_many :issues
belongs_to :customer
validates_presence_of :doc_number, :id, :customer_id, :txn_date
self.primary_key = :id
# Get the quickbooks-ruby base for invoice
def self.get_base
Qbo.get_base(:invoice)
end
# sync ALL the invoices
def self.sync
logger.debug "Syncing all invoices"
last = Qbo.first.last_sync
query = "SELECT Id, DocNumber FROM Invoice"
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
invoices = get_base.all
else
invoices = get_base.query()
end
invoices.each { | invoice |
process_invoice invoice
}
end
#sync by invoice ID
def self.sync_by_id(id)
logger.debug "Syncing invoice #{id}"
invoice = get_base.fetch_by_id(id)
process_invoice invoice
end
private
# Attach the invoice to the issue
def self.attach_to_issue(issue, invoice)
return if issue.nil?
# skip this issue if the issue customer is not the same as the invoice customer
return if issue.customer_id != invoice.customer_ref.value.to_i
logger.debug "Attaching invoice #{invoice.id} to issue #{issue.id}"
invoice = Invoice.find_or_create_by(id: invoice.id)
unless issue.invoices.include?(invoice)
issue.invoices << invoice
issue.save!
end
compare_custom_fields(issue, invoice)
end
# processes the invoice into the database
def self.process_invoice(i)
logger.info "Processing invoice #{i.id}"
# Load the invoice into the database
invoice = Invoice.find_or_create_by(id: i.id)
invoice.doc_number = i.doc_number
invoice.id = i.id
invoice.customer_id = i.customer_ref
invoice.txn_date = i.txn_date
invoice.save!
# Scan the private notes for hashtags and attach to the applicable issues
if not i.private_note.nil?
i.private_note.scan(/#(\w+)/).flatten.each { |issue|
attach_to_issue(Issue.find_by_id(issue.to_i), invoice)
}
end
# Scan the line items for hashtags and attach to the applicable issues
i.line_items.each { |line|
if line.description
line.description.scan(/#(\w+)/).flatten.each { |issue|
attach_to_issue(Issue.find_by_id(issue.to_i), invoice)
}
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)
logger.debug "Comparing custom fields"
# TODO break if Invoice.find(invoice.id).cf_sync_confict
is_changed = false
# update the invoive custom fields with infomation from the issue if available
invoice.custom_fields.each { |cf|
# VIN from the attached vehicle
# TODO move this into seperate plugin
# TODO create hook for seperate plugin
begin
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
break if vin.nil?
if not cf.string_value.to_s.eql? vin
cf.string_value = vin.to_s
logger.debug "VIN has changed"
is_changed = true
end
end
end
rescue
#do nothing
end
# Custom Values
begin
value = issue.custom_values.find_by(custom_field_id: CustomField.find_by_name(cf.name).id)
# Check to see if the value is blank...
if not value.value.to_s.blank?
# Check to see if the value is diffrent
if not cf.string_value.to_s.eql? value.value.to_s
# update the custom field on the invoice
cf.string_value = value.value.to_s
is_changed = true
end
end
rescue
# Nothing to do here, there is no match
end
}
# Push updates
begin
logger.debug "Trying to update invoice"
get_base.update(invoice) if is_changed
rescue
# 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 flag Invoice.cf_sync_confict here
logger.error "Failed to update invoice"
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