<?php
namespace ApplicationBundle\Modules\Purchase\Controller;
use ApplicationBundle\Controller\GenericController;
use ApplicationBundle\Interfaces\SessionCheckInterface;
use ApplicationBundle\Modules\Authentication\Constants\UserConstants;
use ApplicationBundle\Modules\Purchase\Service\ExpressPurchaseService;
use Symfony\Component\HttpFoundation\Request;
/**
* Express Purchase — one screen that records a small goods purchase by collapsing
* PO → GRN → Bill in a single save (ExpressPurchaseService::create posts stock +
* payable + GL). For tedious-free buying; the full multi-step chain stays for
* complex purchases.
*/
class ExpressPurchaseController extends GenericController implements SessionCheckInterface
{
/** The entry form. */
public function newAction(Request $request)
{
$conn = $this->getDoctrine()->getManager()->getConnection();
$suppliers = $conn->fetchAllAssociative(
"SELECT s.supplier_id AS id, COALESCE(h.name, CONCAT('Supplier #', s.supplier_id)) AS name
FROM acc_suppliers s LEFT JOIN acc_accounts_head h ON h.accounts_head_id = s.accounts_head_id
ORDER BY name"
);
$warehouses = $conn->fetchAllAssociative("SELECT id, name FROM warehouse WHERE (status IS NULL OR status = 1) ORDER BY name");
// supplier types for the inline "add new supplier" (each maps to a GL parent)
$supplierTypes = $conn->fetchAllAssociative("SELECT supplier_type_id AS id, name FROM supplier_type ORDER BY supplier_type_id");
// Source the product list from the PRODUCT MASTER (inv_products), not from
// past purchase_order_item rows — a fresh tenant has no prior PO items, so the
// old query returned an empty list. The FDM is the durable product identity
// (ApprovalFunction::PurchaseOrder reconciles product_id from it on approval).
$products = $conn->fetchAllAssociative(
"SELECT id AS id,
product_fdm AS fdm,
COALESCE(NULLIF(product_name_fdm, ''), name) AS name,
unit_type_id AS unit_type_id
FROM inv_products
WHERE (status IS NULL OR status = 1)
AND product_fdm IS NOT NULL AND product_fdm <> ''
ORDER BY name LIMIT 2000"
);
return $this->render('@Purchase/pages/express_purchase.html.twig', [
'suppliers' => $suppliers,
'warehouses' => $warehouses,
'products' => $products,
'supplierTypes' => $supplierTypes,
'flash' => $request->getSession()->getFlashBag()->get('express')[0] ?? null,
]);
}
/**
* Inline "add new supplier" from the Express Purchase page — minimal info only
* (name + phone + type). Reuses Supplier::CreateNew (auto-creates the GL payable
* head). Returns JSON {success, id, name} so the page can add + select it without
* a reload.
*/
public function quickAddSupplierAction(Request $request)
{
$em = $this->getDoctrine()->getManager();
$loginId = (int) $request->getSession()->get(UserConstants::USER_LOGIN_ID);
$name = trim((string) $request->request->get('supplierName', ''));
$type = (int) $request->request->get('supplierType', 1);
if ($name === '') {
return new \Symfony\Component\HttpFoundation\JsonResponse(['success' => false, 'message' => 'Supplier name is required.']);
}
if ($type <= 0) { $type = 1; }
try {
$sid = \ApplicationBundle\Modules\Purchase\Supplier::CreateNew($em, $this, [
'accountsHeadId' => 0, // 0 → auto-create the payable head
'advanceHeadId' => 0,
'supplierName' => $name,
'supplierShortCode' => '',
'supplierType' => $type,
'categoryID' => 0,
'supplierDue' => 0,
'supplierAddress' => trim((string) $request->request->get('supplierAddress', '')),
'factoryAddress' => '',
'contactPerson' => '',
'contactNumber' => trim((string) $request->request->get('contactNumber', '')),
'email' => trim((string) $request->request->get('email', '')),
'generalInfoTin' => '',
'generalInfoBin' => '',
'generalInfoBankName' => '',
'generalInfoBankAcName' => '',
'generalInfoBankAcNumber' => '',
'taggedItemGroups' => [],
'loginId' => $loginId,
]);
} catch (\Throwable $e) {
return new \Symfony\Component\HttpFoundation\JsonResponse(['success' => false, 'message' => 'Could not create supplier: ' . $e->getMessage()]);
}
if (!$sid) {
return new \Symfony\Component\HttpFoundation\JsonResponse(['success' => false, 'message' => 'Supplier not created — the chosen type may be missing its GL parent setting.']);
}
return new \Symfony\Component\HttpFoundation\JsonResponse(['success' => true, 'id' => (int) $sid, 'name' => $name]);
}
/** Save → run the express chain. */
public function saveAction(Request $request)
{
$p = $request->request;
$em = $this->getDoctrine()->getManager();
$session = $request->getSession();
$loginId = (int) $session->get(UserConstants::USER_LOGIN_ID);
$supplierId = (int) $p->get('supplierId');
$warehouseId = (int) $p->get('warehouseId');
// product select value = "id|fdm|unitTypeId" (avoids client-side wiring)
$sel = $p->get('productSel', []);
$qty = $p->get('qty', []);
$price = $p->get('price', []);
$items = [];
for ($i = 0, $n = count($sel); $i < $n; $i++) {
if (empty($sel[$i]) || (float) ($qty[$i] ?? 0) <= 0) { continue; }
$parts = explode('|', $sel[$i]);
$items[] = [
'productId' => (int) ($parts[0] ?? 0),
'fdm' => $parts[1] ?? '',
'unitTypeId' => (int) ($parts[2] ?? 0),
'qty' => (float) $qty[$i],
'price' => (float) ($price[$i] ?? 0),
'type' => 1,
];
}
if (!$supplierId || !$warehouseId || empty($items)) {
$session->getFlashBag()->add('express', '⚠ Pick a supplier, a warehouse and at least one product line.');
return $this->redirect($this->generateUrl('express_purchase_new'));
}
try {
$res = ExpressPurchaseService::create($em, [
'supplierId' => $supplierId,
'warehouseId' => $warehouseId,
'docDate' => (new \DateTime('yesterday'))->format('Y-m-d'),
'currency' => 1, 'currencyMultiply' => 1, 'currencyMultiplyRate' => 1,
'note' => $p->get('note', 'Express purchase'),
'items' => $items,
], $loginId);
$session->getFlashBag()->add('express', sprintf(
'✓ Done — PO #%d → GRN #%d → Bill #%d created and approved. Stock updated and supplier payable posted.',
$res['poId'], $res['grnId'], $res['piId']
));
} catch (\Throwable $e) {
$session->getFlashBag()->add('express', '✕ Failed: ' . $e->getMessage());
}
return $this->redirect($this->generateUrl('express_purchase_new'));
}
}