From 5911e97a2668b92ecbf76251dcc141f26c0c462e Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Sat, 20 Jun 2026 13:21:17 -0500 Subject: [PATCH] =?UTF-8?q?Add=20PartsHelper=20=E2=80=94=20common=20parts?= =?UTF-8?q?=20by=20trade,=20usage=20tracking,=20stock=20deduction,=20low?= =?UTF-8?q?=20stock=20alerts?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/Helper/PartsHelper.php | 107 ++++++++++++++++++ 1 file changed, 107 insertions(+) create mode 100644 source/packages/plg_system_mokosuitefield/src/Helper/PartsHelper.php diff --git a/source/packages/plg_system_mokosuitefield/src/Helper/PartsHelper.php b/source/packages/plg_system_mokosuitefield/src/Helper/PartsHelper.php new file mode 100644 index 0000000..38532a0 --- /dev/null +++ b/source/packages/plg_system_mokosuitefield/src/Helper/PartsHelper.php @@ -0,0 +1,107 @@ +get(DatabaseInterface::class); + + $query = $db->getQuery(true) + ->select('p.id, p.title AS name, p.sku, p.price, p.cost_price, p.stock_quantity') + ->select('COUNT(wi.id) AS usage_count') + ->from($db->quoteName('#__mokosuite_crm_products', 'p')) + ->join('LEFT', $db->quoteName('#__mokosuitefield_wo_items', 'wi') . ' ON wi.product_id = p.id') + ->where($db->quoteName('p.published') . ' = 1') + ->group('p.id') + ->order('usage_count DESC'); + + if ($trade) { + $query->join('LEFT', $db->quoteName('#__mokosuitefield_work_orders', 'wo') . ' ON wo.id = wi.wo_id') + ->where($db->quoteName('wo.trade') . ' = ' . $db->quote($trade)); + } + + $db->setQuery($query, 0, $limit); + return $db->loadObjectList() ?: []; + } + + /** + * Record part usage on a work order. + */ + public static function usePart(int $woId, int $productId, int $quantity, ?float $unitPrice = null): int + { + $db = Factory::getContainer()->get(DatabaseInterface::class); + + if ($unitPrice === null) { + $db->setQuery($db->getQuery(true)->select('price')->from('#__mokosuite_crm_products')->where('id = ' . (int) $productId)); + $unitPrice = (float) $db->loadResult(); + } + + $item = (object) [ + 'wo_id' => $woId, + 'product_id' => $productId, + 'quantity' => $quantity, + 'unit_price' => $unitPrice, + 'line_total' => round($unitPrice * $quantity, 2), + 'created' => Factory::getDate()->toSql(), + ]; + + $db->insertObject('#__mokosuitefield_wo_items', $item, 'id'); + + // Deduct from stock + $db->setQuery($db->getQuery(true) + ->update('#__mokosuite_crm_products') + ->set('stock_quantity = stock_quantity - ' . (int) $quantity) + ->where('id = ' . (int) $productId)); + $db->execute(); + + return (int) $item->id; + } + + /** + * Get parts that are low on stock and frequently used in field service. + */ + public static function getLowStockParts(int $limit = 20): array + { + $db = Factory::getContainer()->get(DatabaseInterface::class); + + $db->setQuery($db->getQuery(true) + ->select('p.id, p.title AS name, p.sku, p.stock_quantity, p.reorder_point, p.cost_price') + ->select('(SELECT COUNT(*) FROM #__mokosuitefield_wo_items wi WHERE wi.product_id = p.id) AS field_usage') + ->from($db->quoteName('#__mokosuite_crm_products', 'p')) + ->where($db->quoteName('p.stock_quantity') . ' <= ' . $db->quoteName('p.reorder_point')) + ->where($db->quoteName('p.reorder_point') . ' > 0') + ->where('p.id IN (SELECT DISTINCT product_id FROM #__mokosuitefield_wo_items)') + ->order('(p.reorder_point - p.stock_quantity) DESC'), 0, $limit); + + return $db->loadObjectList() ?: []; + } + + /** + * Get parts cost summary for a work order. + */ + public static function getWoPartsCost(int $woId): object + { + $db = Factory::getContainer()->get(DatabaseInterface::class); + + $db->setQuery($db->getQuery(true) + ->select('COUNT(*) AS item_count') + ->select('SUM(quantity) AS total_qty') + ->select('COALESCE(SUM(line_total), 0) AS total_cost') + ->from('#__mokosuitefield_wo_items') + ->where('wo_id = ' . (int) $woId)); + + return $db->loadObject() ?: (object) ['item_count' => 0, 'total_qty' => 0, 'total_cost' => 0]; + } +} -- 2.52.0