diff --git a/source/packages/plg_system_mokosuitefield/src/Helper/InvoiceHelper.php b/source/packages/plg_system_mokosuitefield/src/Helper/InvoiceHelper.php new file mode 100644 index 0000000..5d976ce --- /dev/null +++ b/source/packages/plg_system_mokosuitefield/src/Helper/InvoiceHelper.php @@ -0,0 +1,146 @@ +get(DatabaseInterface::class); + $now = Factory::getDate()->toSql(); + + // Load work order + $db->setQuery($db->getQuery(true) + ->select('wo.*, cd.name AS customer_name') + ->from($db->quoteName('#__mokosuitefield_work_orders', 'wo')) + ->join('LEFT', $db->quoteName('#__contact_details', 'cd') . ' ON cd.id = wo.contact_id') + ->where('wo.id = ' . $woId)); + $wo = $db->loadObject(); + + if (!$wo) throw new \RuntimeException('Work order not found: ' . $woId); + + $params = Factory::getApplication()->getParams('com_mokosuitefield'); + $laborRate = (float) $params->get('default_labor_rate', 85.00); + + // Get time entries + $db->setQuery($db->getQuery(true) + ->select('*') + ->from('#__mokosuitefield_time_entries') + ->where('wo_id = ' . $woId)); + $timeEntries = $db->loadObjectList() ?: []; + + $totalHours = 0; + foreach ($timeEntries as $te) { + $totalHours += (float) $te->hours; + } + + // Get parts used + $db->setQuery($db->getQuery(true) + ->select('wi.*, p.title AS part_name, p.price') + ->from($db->quoteName('#__mokosuitefield_wo_items', 'wi')) + ->join('LEFT', $db->quoteName('#__mokosuite_crm_products', 'p') . ' ON p.id = wi.product_id') + ->where('wi.wo_id = ' . $woId)); + $parts = $db->loadObjectList() ?: []; + + $partsTotal = 0; + foreach ($parts as $part) { + $partsTotal += (float) ($part->price ?? 0) * (int) $part->quantity; + } + + $laborTotal = round($totalHours * $laborRate, 2); + $subtotal = $laborTotal + $partsTotal; + $taxRate = (float) $params->get('default_tax_rate', 0.07); + $tax = round($subtotal * $taxRate, 2); + $total = $subtotal + $tax; + + // Create CRM invoice + $seq = (int) $db->setQuery($db->getQuery(true)->select('COUNT(*)')->from('#__mokosuite_crm_invoices'))->loadResult() + 1; + + $invoice = (object) [ + 'contact_id' => $wo->contact_id, + 'invoice_number' => 'FSI-' . date('Ymd') . '-' . str_pad($seq, 4, '0', STR_PAD_LEFT), + 'type' => 'standard', + 'subtotal' => $subtotal, + 'tax' => $tax, + 'total' => $total, + 'balance_due' => $total, + 'status' => 'draft', + 'due_date' => date('Y-m-d', strtotime('+30 days')), + 'notes' => 'Field Service Invoice for WO #' . ($wo->wo_number ?? $woId), + 'created' => $now, + 'created_by' => Factory::getApplication()->getIdentity()->id, + ]; + $db->insertObject('#__mokosuite_crm_invoices', $invoice, 'id'); + $invoiceId = (int) $invoice->id; + + // Add labor line item + if ($totalHours > 0) { + $db->insertObject('#__mokosuite_crm_invoice_items', (object) [ + 'invoice_id' => $invoiceId, + 'description' => 'Labor — ' . number_format($totalHours, 1) . ' hours @ $' . number_format($laborRate, 2) . '/hr', + 'quantity' => $totalHours, + 'unit_price' => $laborRate, + 'line_total' => $laborTotal, + ]); + } + + // Add parts line items + foreach ($parts as $part) { + $db->insertObject('#__mokosuite_crm_invoice_items', (object) [ + 'invoice_id' => $invoiceId, + 'product_id' => $part->product_id, + 'description' => $part->part_name ?? 'Part', + 'quantity' => $part->quantity, + 'unit_price' => $part->price ?? 0, + 'line_total' => round((float) ($part->price ?? 0) * (int) $part->quantity, 2), + ]); + } + + // Link invoice to work order + $db->setQuery($db->getQuery(true) + ->update('#__mokosuitefield_work_orders') + ->set('invoice_id = ' . $invoiceId) + ->where('id = ' . $woId)); + $db->execute(); + + return $invoiceId; + } + + /** + * Batch generate invoices for all completed, uninvoiced work orders. + */ + public static function batchGenerate(): array + { + $db = Factory::getContainer()->get(DatabaseInterface::class); + + $db->setQuery($db->getQuery(true) + ->select('id') + ->from('#__mokosuitefield_work_orders') + ->where($db->quoteName('status') . ' = ' . $db->quote('completed')) + ->where('invoice_id IS NULL OR invoice_id = 0') + ->order('completed_at ASC')); + $woIds = $db->loadColumn() ?: []; + + $results = []; + foreach ($woIds as $woId) { + try { + $invoiceId = self::generateFromWorkOrder((int) $woId); + $results[] = ['wo_id' => $woId, 'invoice_id' => $invoiceId, 'success' => true]; + } catch (\Throwable $e) { + $results[] = ['wo_id' => $woId, 'error' => $e->getMessage(), 'success' => false]; + } + } + + return $results; + } +}