From 45fcdeebd76d4053bd00485c2110a00893bd81a9 Mon Sep 17 00:00:00 2001 From: Rick Barrette Date: Sun, 9 Aug 2026 20:46:19 -0400 Subject: [PATCH] Add method to create issues from labor lines in QBO estimates --- app/models/customer_appointment.rb | 79 +++++++++++++++++++++++++++++- 1 file changed, 78 insertions(+), 1 deletion(-) diff --git a/app/models/customer_appointment.rb b/app/models/customer_appointment.rb index 88a9eb0..d4c9372 100644 --- a/app/models/customer_appointment.rb +++ b/app/models/customer_appointment.rb @@ -28,6 +28,65 @@ class CustomerAppointment < ActiveRecord::Base Cancelled ] + # Scans the QBO Estimate's InvoiceLineItems for "Labor" sales items + # and generates a Redmine Issue for each matching line. + # + # @param project [Project] Target project (defaults to appointment's project) + # @param author [User] Author for the new issues + # @param tracker [Tracker] Target tracker (defaults to project's first tracker) + # @return [Array] Created issues + def create_issues_from_labor_lines(project: self.project, author: User.current, tracker: nil) + return [] if estimate.blank? + + target_project = project || self.project || Project.first + target_tracker = tracker || target_project&.trackers&.first + return [] unless target_project && target_tracker + + created_issues = [] + + # estimate.line_items returns array of Quickbooks::Model::InvoiceLineItem + estimate.line_items.each do |line| + detail = line.respond_to?(:sales_line_item_detail) ? line.sales_line_item_detail : nil + next if detail.nil? || detail.item_ref.nil? + + item_name = detail.item_ref.name.to_s.strip + description = line.description.to_s.strip + + # Match "Labor" (case-insensitive) and ensure description is present + next unless item_name.casecmp('Labor').zero? + next if description.blank? + + # detail.quantity contains line quantity/hours; line.amount is total line cost fallback + qty = detail.respond_to?(:quantity) ? detail.quantity.to_f : 0.0 + hours = qty.positive? ? qty : line.amount.to_f + + issue = Issue.new( + project: target_project, + tracker: target_tracker, + assigned_to_id: self.user&.id, + author: author, + subject: description.truncate(255), + description: "The purpose of this work is to bill the time spent on this issue for Estimate ##{estimate.doc_number} for Appointment: #{subject}", + estimated_hours: hours, + start_date: self.start_date, + due_date: self.due_date, + vehicle: self.vehicle, + estimate: self.estimate, + customer: self.customer, + created_on: Time.current, + updated_on: Time.current + ) + + if issue.save + created_issues << issue + else + log "Failed to create issue for Estimate ##{estimate.id}: #{issue.errors.full_messages.join(', ')}" + end + end + + created_issues + end + # Cast datetime to Date for Redmine Calendar compatibility def start_date date&.to_date @@ -50,7 +109,25 @@ class CustomerAppointment < ActiveRecord::Base private def log(msg) - Rails.logger.info "[Event] #{msg}" + Rails.logger.info "[CustomerAppointment] #{msg}" + end + + # Helper to parse item name across Quickbooks::Model::Line and Hash structures + def extract_item_name(line) + if line.respond_to?(:sales_line_item_detail) && line.sales_line_item_detail.present? + line.sales_line_item_detail.item_ref&.name + elsif line.is_a?(Hash) + line[:item_name] || line.dig('sales_line_item_detail', 'item_ref', 'name') + end + end + + # Helper to parse description across Quickbooks::Model::Line and Hash structures + def extract_description(line) + if line.respond_to?(:description) + line.description + elsif line.is_a?(Hash) + line[:description] || line['description'] + end end end \ No newline at end of file