Add InvoiceHelper — generate CRM invoices from completed work orders (labor hours + parts), batch invoicing
This commit is contained in:
@@ -0,0 +1,146 @@
|
||||
<?php
|
||||
namespace Moko\Plugin\System\MokoSuiteField\Helper;
|
||||
|
||||
defined('_JEXEC') or die;
|
||||
|
||||
use Joomla\CMS\Factory;
|
||||
use Joomla\Database\DatabaseInterface;
|
||||
|
||||
/**
|
||||
* Field service invoicing — generate invoices from completed work orders with labor + parts.
|
||||
*/
|
||||
class InvoiceHelper
|
||||
{
|
||||
/**
|
||||
* Generate a CRM invoice from a completed work order.
|
||||
*/
|
||||
public static function generateFromWorkOrder(int $woId): int
|
||||
{
|
||||
$db = Factory::getContainer()->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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user