src/ApplicationBundle/Modules/ImportLc/Controller/ImportLcController.php line 26

Open in your IDE?
  1. <?php
  2. namespace ApplicationBundle\Modules\ImportLc\Controller;
  3. use ApplicationBundle\Controller\GenericController;
  4. use ApplicationBundle\Interfaces\SessionCheckInterface;
  5. use ApplicationBundle\Modules\Authentication\Constants\UserConstants;
  6. use ApplicationBundle\Constants\GeneralConstant;
  7. use ApplicationBundle\Entity\BillOfEntry;
  8. use ApplicationBundle\Entity\LandedCostSheet;
  9. use ApplicationBundle\Modules\ImportLc\Service\BoeGlService;
  10. use ApplicationBundle\Modules\ImportLc\Service\LcGlService;
  11. use ApplicationBundle\Modules\ImportLc\Service\LandedCostService;
  12. use Symfony\Component\HttpFoundation\Request;
  13. /**
  14.  * Import & LC — cp-shell module for the import / Letter-of-Credit lifecycle:
  15.  * Proforma Invoice → Insurance/Pay-Req → LC Opening → LC Register → Bill of Lading
  16.  * → Bill of Entry → Landed Cost → (GRN). This is the modern front-end over the
  17.  * existing import chain; document forms are ported in phases. The dashboard reads
  18.  * the live import data so it is useful from day one.
  19.  */
  20. class ImportLcController extends GenericController implements SessionCheckInterface
  21. {
  22.     /** Import & LC command centre. */
  23.     public function dashboardAction(Request $request)
  24.     {
  25.         $conn $this->getDoctrine()->getManager()->getConnection();
  26.         $n = function ($sql$p = []) use ($conn) { try { return $conn->fetchOne($sql$p); } catch (\Throwable $e) { return 0; } };
  27.         $rows = function ($sql$p = []) use ($conn) { try { return $conn->fetchAllAssociative($sql$p); } catch (\Throwable $e) { return []; } };
  28.         // ── KPIs ────────────────────────────────────────────────────────────────
  29.         $openLcCount  = (int) $n("SELECT COUNT(*) FROM lc_entry WHERE (full_delivered IS NULL OR full_delivered = 0) AND (delete_flag IS NULL OR delete_flag = 0)");
  30.         $totalLcValue = (float) $n("SELECT COALESCE(SUM(document_amount),0) FROM lc_entry WHERE (delete_flag IS NULL OR delete_flag = 0)");
  31.         $marginHeld   = (float) $n("SELECT COALESCE(SUM(lc_margin_amount),0) FROM lc_entry WHERE (full_delivered IS NULL OR full_delivered = 0)");
  32.         $shipments    = (int) $n("SELECT COUNT(*) FROM bill_of_lading WHERE (delete_flag IS NULL OR delete_flag = 0)");
  33.         $customs      = (int) $n("SELECT COUNT(*) FROM bill_of_entry WHERE (delete_flag IS NULL OR delete_flag = 0)");
  34.         $pendingOpen  = (int) $n("SELECT COUNT(*) FROM lc_opening_request o WHERE (o.delete_flag IS NULL OR o.delete_flag = 0) AND NOT EXISTS (SELECT 1 FROM lc_entry e WHERE e.lc_opening_request_ids LIKE CONCAT('%\"', o.document_id, '\"%') OR e.lc_opening_request_ids LIKE CONCAT('%', o.document_id, '%'))");
  35.         // ── Pipeline (counts per document type) ──────────────────────────────────
  36.         $pipeline = [
  37.             ['key' => 'proforma',   'label' => 'Proforma',      'icon' => 'description',    'count' => (int) $n("SELECT COUNT(*) FROM proforma_invoice WHERE (delete_flag IS NULL OR delete_flag=0)")],
  38.             ['key' => 'insurance',  'label' => 'Insurance / Pay',  'icon' => 'verified_user''count' => (int) $n("SELECT COUNT(*) FROM insurance_pay_request WHERE (delete_flag IS NULL OR delete_flag=0)")],
  39.             ['key' => 'lcopening',  'label' => 'LC Opening',     'icon' => 'lock_open',      'count' => (int) $n("SELECT COUNT(*) FROM lc_opening_request WHERE (delete_flag IS NULL OR delete_flag=0)")],
  40.             ['key' => 'lc',         'label' => 'LC Register',    'icon' => 'account_balance','count' => (int) $n("SELECT COUNT(*) FROM lc_entry WHERE (delete_flag IS NULL OR delete_flag=0)")],
  41.             ['key' => 'bol',        'label' => 'Bill of Lading''icon' => 'directions_boat','count' => $shipments],
  42.             ['key' => 'boe',        'label' => 'Bill of Entry',  'icon' => 'gavel',          'count' => $customs],
  43.             ['key' => 'landed',     'label' => 'Landed Cost',    'icon' => 'functions',      'count' => (int) $n("SELECT COUNT(*) FROM landed_cost_sheet WHERE (delete_flag IS NULL OR delete_flag=0)")],
  44.         ];
  45.         // ── Recent LCs ───────────────────────────────────────────────────────────
  46.         $recent $rows(
  47.             "SELECT e.document_id AS id, e.lc_number, e.document_amount AS amount, e.document_date AS dt,
  48.                     e.lc_margin_amount AS margin, e.full_delivered AS delivered, e.approved,
  49.                     COALESCE(h.name, CONCAT('Supplier #', e.supplier_id)) AS supplier
  50.              FROM lc_entry e LEFT JOIN acc_accounts_head h ON h.accounts_head_id = e.supplier_head_id
  51.              WHERE (e.delete_flag IS NULL OR e.delete_flag = 0)
  52.              ORDER BY e.document_id DESC LIMIT 10"
  53.         );
  54.         return $this->render('@ImportLc/pages/dashboard.html.twig', [
  55.             'active' => 'dash',
  56.             'kpi' => [
  57.                 'openLc' => $openLcCount'totalValue' => $totalLcValue'margin' => $marginHeld,
  58.                 'shipments' => $shipments'customs' => $customs'pendingOpen' => $pendingOpen,
  59.             ],
  60.             'pipeline' => $pipeline,
  61.             'recent'   => $recent,
  62.         ]);
  63.     }
  64.     /** LC detail / edit form — enrich an LC with the full real-world fields + status. */
  65.     public function lcFormAction(Request $request)
  66.     {
  67.         $em $this->getDoctrine()->getManager();
  68.         $id = (int) $request->query->get('id'0);
  69.         $lc $em->getRepository('ApplicationBundle\\Entity\\LcEntry')->findOneBy(['documentId' => $id]);
  70.         if (!$lc) {
  71.             $request->getSession()->getFlashBag()->add('importlc''LC not found.');
  72.             return $this->redirect($this->generateUrl('import_lc_dashboard'));
  73.         }
  74.         $conn $em->getConnection();
  75.         $supplier $conn->fetchOne('SELECT COALESCE(h.name, CONCAT("Supplier #", ?)) FROM acc_accounts_head h WHERE h.accounts_head_id = ?',
  76.             [$lc->getSupplierId(), $lc->getSupplierHeadId()]);
  77.         $currencies $conn->fetchAllAssociative('SELECT currency_id AS id, code FROM currencies ORDER BY code');
  78.         return $this->render('@ImportLc/pages/lc_form.html.twig', [
  79.             'active'     => 'lc',
  80.             'lc'         => $lc,
  81.             'supplier'   => $supplier ?: ('Supplier #' $lc->getSupplierId()),
  82.             'currencies' => $currencies,
  83.             'statuses'   => ['DRAFT''OPENED''SHIPPED''DOCS_RECEIVED''ACCEPTED''RETIRED''CANCELLED'],
  84.             'incoterms'  => ['EXW''FCA''FAS''FOB''CFR''CIF''CPT''CIP''DAP''DPU''DDP'],
  85.             'flash'      => $request->getSession()->getFlashBag()->get('importlc')[0] ?? null,
  86.         ]);
  87.     }
  88.     /** Save the LC real-world fields. */
  89.     public function lcSaveAction(Request $request)
  90.     {
  91.         $p  $request->request;
  92.         $em $this->getDoctrine()->getManager();
  93.         $session $request->getSession();
  94.         $lc $em->getRepository('ApplicationBundle\\Entity\\LcEntry')->findOneBy(['documentId' => (int) $p->get('id'0)]);
  95.         if (!$lc) { $session->getFlashBag()->add('importlc''LC not found.'); return $this->redirect($this->generateUrl('import_lc_dashboard')); }
  96.         $d   = function ($k) use ($p) { $v trim((string) $p->get($k'')); return $v === '' null : new \DateTime($v); };
  97.         $num = function ($k) use ($p) { $v $p->get($k''); return ($v === '' || $v === null) ? null : (string) $v; };
  98.         $str = function ($k) use ($p) { $v trim((string) $p->get($k'')); return $v === '' null $v; };
  99.         if ($num('lcAmount') !== null)  { $lc->setDocumentAmount($num('lcAmount')); }
  100.         if ($str('lcNumber') !== null)  { $lc->setLcNumber($str('lcNumber')); }
  101.         $lc->setLcTenorType($str('lcTenorType'));
  102.         $lc->setLcUsanceDays($num('lcUsanceDays'));
  103.         $lc->setLcIssueDate($d('lcIssueDate'));
  104.         $lc->setLcExpiryDate($d('lcExpiryDate'));
  105.         $lc->setLcLatestShipmentDate($d('lcLatestShipmentDate'));
  106.         $lc->setApplicantName($str('applicantName'));
  107.         $lc->setBeneficiaryName($str('beneficiaryName'));
  108.         $lc->setBeneficiaryBank($str('beneficiaryBank'));
  109.         $lc->setIssuingBankName($str('issuingBankName'));
  110.         $lc->setIssuingBankSwift($str('issuingBankSwift'));
  111.         $lc->setAdvisingBankName($str('advisingBankName'));
  112.         $lc->setAdvisingBankSwift($str('advisingBankSwift'));
  113.         $lc->setIncoterms($str('incoterms'));
  114.         $lc->setPortOfLoading($str('portOfLoading'));
  115.         $lc->setPortOfDischarge($str('portOfDischarge'));
  116.         $lc->setPartialShipment($p->has('partialShipment') ? 0);
  117.         $lc->setTranshipment($p->has('transhipment') ? 0);
  118.         $lc->setCommissionRate($num('commissionRate'));
  119.         // Auto-derive charge amounts from rate × LC amount where only a rate is given.
  120.         $amt = (float) ($num('lcAmount') ?? $lc->getDocumentAmount());
  121.         $lc->setCommissionAmount($num('commissionAmount') ?? ($num('commissionRate') !== null number_format($amt * (float) $num('commissionRate') / 1002'.''') : null));
  122.         $lc->setSwiftCharge($num('swiftCharge'));
  123.         $lc->setPostageCharge($num('postageCharge'));
  124.         $lc->setOtherCharges($num('otherCharges'));
  125.         if ($num('marginRate') !== null) { $lc->setLcMarginRate($num('marginRate')); $lc->setLcMarginAmount(number_format($amt * (float) $num('marginRate') / 1002'.''')); }
  126.         $lc->setLcStatus($str('lcStatus'));
  127.         $lc->setAcceptanceDate($d('acceptanceDate'));
  128.         $lc->setRetirementDate($d('retirementDate'));
  129.         $lc->setSettlementAmount($num('settlementAmount'));
  130.         $lc->setHsCode($str('hsCode'));
  131.         if (method_exists($lc'setEditedLoginId')) { $lc->setEditedLoginId((int) $session->get(UserConstants::USER_LOGIN_ID)); }
  132.         $em->flush();
  133.         $session->getFlashBag()->add('importlc''✓ LC ' . ($lc->getLcNumber() ?: ('#' $lc->getDocumentId())) . ' saved.');
  134.         return $this->redirect($this->generateUrl('import_lc_form', ['id' => $lc->getDocumentId()]));
  135.     }
  136.     /** Post the LC opening margin + bank charges to the GL. */
  137.     public function lcPostGlAction(Request $request)
  138.     {
  139.         $em $this->getDoctrine()->getManager();
  140.         $session $request->getSession();
  141.         $id = (int) $request->request->get('id'0);
  142.         $lc $em->getRepository('ApplicationBundle\\Entity\\LcEntry')->findOneBy(['documentId' => $id]);
  143.         if (!$lc) { $session->getFlashBag()->add('importlc''LC not found.'); return $this->redirect($this->generateUrl('import_lc_dashboard')); }
  144.         if ((int) $lc->getLedgerHit() === 1) {
  145.             $session->getFlashBag()->add('importlc''Already posted to GL.');
  146.         } else {
  147.             try {
  148.                 $txnId LcGlService::postOpening($em$lc1, (int) $session->get(UserConstants::USER_LOGIN_ID));
  149.                 $session->getFlashBag()->add('importlc'$txnId
  150.                     '✓ Posted to GL (voucher #' $txnId '): Dr Margin + Dr Charges / Cr Bank.'
  151.                     'Nothing posted — no margin/charges, or LC GL heads not configured (run inno:lc-accounts-setup).');
  152.             } catch (\Throwable $e) {
  153.                 $session->getFlashBag()->add('importlc''✕ GL post failed: ' $e->getMessage());
  154.             }
  155.         }
  156.         return $this->redirect($this->generateUrl('import_lc_form', ['id' => $id]));
  157.     }
  158.     /** Retire / settle the LC — clears the supplier payable, applying the margin. */
  159.     public function lcRetireAction(Request $request)
  160.     {
  161.         $em $this->getDoctrine()->getManager();
  162.         $session $request->getSession();
  163.         $id = (int) $request->request->get('id'0);
  164.         $lc $em->getRepository('ApplicationBundle\\Entity\\LcEntry')->findOneBy(['documentId' => $id]);
  165.         if (!$lc) { $session->getFlashBag()->add('importlc''LC not found.'); return $this->redirect($this->generateUrl('import_lc_dashboard')); }
  166.         if ($lc->getRetirementVoucherId()) {
  167.             $session->getFlashBag()->add('importlc''LC already retired.');
  168.         } else {
  169.             try {
  170.                 $txnId LcGlService::postRetirement($em$lc1, (int) $session->get(UserConstants::USER_LOGIN_ID));
  171.                 $session->getFlashBag()->add('importlc'$txnId
  172.                     '✓ LC retired (voucher #' $txnId '): payable settled — margin applied + bank balance.'
  173.                     'Cannot retire — set a settlement amount first (and ensure LC heads are configured).');
  174.             } catch (\Throwable $e) {
  175.                 $session->getFlashBag()->add('importlc''✕ Retirement failed: ' $e->getMessage());
  176.             }
  177.         }
  178.         return $this->redirect($this->generateUrl('import_lc_form', ['id' => $id]));
  179.     }
  180.     /** Bill of Entry form (new/edit) — customs duty. */
  181.     public function boeFormAction(Request $request)
  182.     {
  183.         $em $this->getDoctrine()->getManager(); $conn $em->getConnection();
  184.         $id = (int) $request->query->get('id'0);
  185.         $boe $id $em->getRepository('ApplicationBundle\\Entity\\BillOfEntry')->findOneBy(['documentId' => $id]) : null;
  186.         $grns $conn->fetchAllAssociative("SELECT g.grn_id AS id, g.document_hash AS hash FROM grn g WHERE (g.delete_flag IS NULL OR g.delete_flag=0) ORDER BY g.grn_id DESC LIMIT 200");
  187.         return $this->render('@ImportLc/pages/boe_form.html.twig', [
  188.             'active' => 'boe''boe' => $boe'grns' => $grns,
  189.             'flash' => $request->getSession()->getFlashBag()->get('importlc')[0] ?? null,
  190.         ]);
  191.     }
  192.     /** Save Bill of Entry. */
  193.     public function boeSaveAction(Request $request)
  194.     {
  195.         $p $request->request$em $this->getDoctrine()->getManager(); $session $request->getSession();
  196.         $loginId = (int) $session->get(UserConstants::USER_LOGIN_ID);
  197.         $id = (int) $p->get('id'0);
  198.         $b $id $em->getRepository('ApplicationBundle\\Entity\\BillOfEntry')->findOneBy(['documentId' => $id]) : new BillOfEntry();
  199.         if (!$id) {
  200.             foreach ([['setCompanyId',1],['setLedgerHit',0],['setStatus',GeneralConstant::ACTIVE],['setStage',GeneralConstant::STAGE_INITIATED],
  201.                       ['setApproved',GeneralConstant::APPROVAL_STATUS_PENDING],['setCreatedLoginId',$loginId]] as $c) { if (method_exists($b$c[0])) { $b->{$c[0]}($c[1]); } }
  202.             if (method_exists($b'setDocumentDate')) { $b->setDocumentDate(new \DateTime('today')); }
  203.             if (method_exists($b'setDocumentHash')) { $b->setDocumentHash('BOE/' date('ymd') . '/' substr(md5(uniqid(''true)), 06)); }
  204.         }
  205.         $n = function ($k) use ($p) { $v $p->get($k''); return ($v === '' || $v === null) ? : (string) $v; };
  206.         $b->setBeNumber($p->get('beNumber') ?: null);
  207.         $b->setCustomsRef($p->get('customsRef') ?: null);
  208.         $b->setBeGrnId((int) $p->get('beGrnId') ?: null);
  209.         $b->setAssessableValue($n('assessableValue'));
  210.         $b->setDutyRate($n('dutyRate'));
  211.         // auto-derive duty from rate × assessable value when amount blank
  212.         $duty $n('dutyAmount');
  213.         if (($duty === || $duty === '0') && (float) $n('dutyRate') > 0) { $duty number_format((float) $n('assessableValue') * (float) $n('dutyRate') / 1002'.'''); }
  214.         $b->setDutyAmount($duty);
  215.         $b->setImportVat($n('importVat'));
  216.         if (method_exists($b'setDocumentAmount')) { $b->setDocumentAmount($duty); }
  217.         $em->persist($b); $em->flush();
  218.         $session->getFlashBag()->add('importlc''✓ Bill of Entry saved.');
  219.         return $this->redirect($this->generateUrl('import_lc_boe_form', ['id' => $b->getDocumentId()]));
  220.     }
  221.     /** Post BoE customs duty to GL (capitalize to inventory). */
  222.     public function boePostGlAction(Request $request)
  223.     {
  224.         $em $this->getDoctrine()->getManager(); $session $request->getSession();
  225.         $b $em->getRepository('ApplicationBundle\\Entity\\BillOfEntry')->findOneBy(['documentId' => (int) $request->request->get('id'0)]);
  226.         if (!$b) { $session->getFlashBag()->add('importlc''Not found.'); return $this->redirect($this->generateUrl('import_lc_dashboard')); }
  227.         if ((int) $b->getLedgerHit() === 1) { $session->getFlashBag()->add('importlc''Duty already posted.'); }
  228.         else {
  229.             try {
  230.                 $txnId BoeGlService::post($em$b1, (int) $session->get(UserConstants::USER_LOGIN_ID));
  231.                 $session->getFlashBag()->add('importlc'$txnId '✓ Duty posted (voucher #' $txnId '): Dr Inventory / Cr Customs Payable.' 'Nothing posted — enter a duty amount, or configure LC heads.');
  232.             } catch (\Throwable $e) { $session->getFlashBag()->add('importlc''✕ Failed: ' $e->getMessage()); }
  233.         }
  234.         return $this->redirect($this->generateUrl('import_lc_boe_form', ['id' => $b->getDocumentId()]));
  235.     }
  236.     /** LC aging & status report — open exposure, days-to-expiry warnings, status mix. */
  237.     public function agingAction(Request $request)
  238.     {
  239.         $conn $this->getDoctrine()->getManager()->getConnection();
  240.         $rows $conn->fetchAllAssociative(
  241.             "SELECT e.document_id AS id, e.lc_number, e.document_amount AS amount, e.lc_margin_amount AS margin,
  242.                     COALESCE(e.lc_status, IF(e.full_delivered=1,'RETIRED', IF(e.approved=1,'OPENED','DRAFT'))) AS status,
  243.                     COALESCE(e.lc_issue_date, e.lc_date) AS opened, e.lc_expiry_date AS expiry,
  244.                     DATEDIFF(CURDATE(), COALESCE(e.lc_issue_date, e.lc_date)) AS age_days,
  245.                     DATEDIFF(e.lc_expiry_date, CURDATE()) AS to_expiry,
  246.                     COALESCE(h.name, CONCAT('Supplier #', e.supplier_id)) AS supplier
  247.              FROM lc_entry e LEFT JOIN acc_accounts_head h ON h.accounts_head_id = e.supplier_head_id
  248.              WHERE (e.delete_flag IS NULL OR e.delete_flag = 0)
  249.              ORDER BY (e.full_delivered = 1), e.lc_expiry_date IS NULL, e.lc_expiry_date ASC, e.document_id DESC"
  250.         );
  251.         // status mix
  252.         $mix = [];
  253.         $openExposure 0.0$marginTied 0.0$expiringSoon 0;
  254.         foreach ($rows as $r) {
  255.             $st $r['status'] ?: 'DRAFT';
  256.             $mix[$st] = ($mix[$st] ?? 0) + 1;
  257.             if ($st !== 'RETIRED' && $st !== 'CANCELLED') {
  258.                 $openExposure += (float) $r['amount'];
  259.                 $marginTied   += (float) $r['margin'];
  260.                 if ($r['to_expiry'] !== null && (int) $r['to_expiry'] >= && (int) $r['to_expiry'] <= 21) { $expiringSoon++; }
  261.             }
  262.         }
  263.         return $this->render('@ImportLc/pages/aging.html.twig', [
  264.             'active' => 'aging''rows' => $rows'mix' => $mix,
  265.             'openExposure' => $openExposure'marginTied' => $marginTied'expiringSoon' => $expiringSoon,
  266.         ]);
  267.     }
  268.     /** Landed-cost sheet form (new or edit). */
  269.     public function landedFormAction(Request $request)
  270.     {
  271.         $em $this->getDoctrine()->getManager();
  272.         $conn $em->getConnection();
  273.         $id = (int) $request->query->get('id'0);
  274.         $sheet $id $em->getRepository('ApplicationBundle\\Entity\\LandedCostSheet')->findOneBy(['documentId' => $id]) : null;
  275.         $grns $conn->fetchAllAssociative(
  276.             "SELECT g.grn_id AS id, g.document_hash AS hash, COALESCE(h.name, CONCAT('Supplier #', g.supplier_id)) AS supplier
  277.              FROM grn g LEFT JOIN acc_accounts_head h ON h.accounts_head_id = g.supplier_head_id
  278.              WHERE (g.delete_flag IS NULL OR g.delete_flag = 0) ORDER BY g.grn_id DESC LIMIT 200"
  279.         );
  280.         return $this->render('@ImportLc/pages/landed_form.html.twig', [
  281.             'active' => 'landed''sheet' => $sheet'grns' => $grns,
  282.             'flash' => $request->getSession()->getFlashBag()->get('importlc')[0] ?? null,
  283.         ]);
  284.     }
  285.     /** Save the landed-cost sheet. */
  286.     public function landedSaveAction(Request $request)
  287.     {
  288.         $p $request->request$em $this->getDoctrine()->getManager(); $session $request->getSession();
  289.         $loginId = (int) $session->get(UserConstants::USER_LOGIN_ID);
  290.         $id = (int) $p->get('id'0);
  291.         $s $id $em->getRepository('ApplicationBundle\\Entity\\LandedCostSheet')->findOneBy(['documentId' => $id]) : new LandedCostSheet();
  292.         if (!$id) {
  293.             foreach ([['setCompanyId'1], ['setLedgerHit'0], ['setStatus'GeneralConstant::ACTIVE],
  294.                       ['setStage'GeneralConstant::STAGE_INITIATED], ['setApproved'GeneralConstant::APPROVAL_STATUS_PENDING],
  295.                       ['setCreatedLoginId'$loginId]] as $c) { if (method_exists($s$c[0])) { $s->{$c[0]}($c[1]); } }
  296.             if (method_exists($s'setDocumentDate')) { $s->setDocumentDate(new \DateTime('today')); }
  297.             if (method_exists($s'setDocumentHash')) { $s->setDocumentHash('LCS/' date('ymd') . '/' substr(md5(uniqid(''true)), 06)); }
  298.         }
  299.         $num = function ($k) use ($p) { $v $p->get($k''); return ($v === '' || $v === null) ? : (string) $v; };
  300.         $s->setFreightAmount($num('freightAmount'));
  301.         $s->setInsuranceAmount($num('insuranceAmount'));
  302.         $s->setCustomsDuty($num('customsDuty'));
  303.         $s->setOtherLandedCharges($num('otherLandedCharges'));
  304.         $s->setLandedGrnId((int) $p->get('landedGrnId') ?: null);
  305.         $s->setAllocationBasis($p->get('allocationBasis''value'));
  306.         $total = (float) $num('freightAmount') + (float) $num('insuranceAmount') + (float) $num('customsDuty') + (float) $num('otherLandedCharges');
  307.         if (method_exists($s'setDocumentAmount')) { $s->setDocumentAmount(number_format($total2'.''')); }
  308.         // Stamp the GRN's PO/PI/EI link now so the double-count guard is reliable + auditable.
  309.         try { LandedCostService::stampGrnLink($em$s); } catch (\Throwable $e) {}
  310.         $em->persist($s); $em->flush();
  311.         $session->getFlashBag()->add('importlc''✓ Landed-cost sheet saved.');
  312.         return $this->redirect($this->generateUrl('import_lc_landed_form', ['id' => $s->getDocumentId()]));
  313.     }
  314.     /** Post landed cost to GL (capitalize to inventory). */
  315.     public function landedPostGlAction(Request $request)
  316.     {
  317.         $em $this->getDoctrine()->getManager(); $session $request->getSession();
  318.         $s $em->getRepository('ApplicationBundle\\Entity\\LandedCostSheet')->findOneBy(['documentId' => (int) $request->request->get('id'0)]);
  319.         if (!$s) { $session->getFlashBag()->add('importlc''Sheet not found.'); return $this->redirect($this->generateUrl('import_lc_dashboard')); }
  320.         if ((int) $s->getLedgerHit() === 1) {
  321.             $session->getFlashBag()->add('importlc''Already capitalized.');
  322.         } else {
  323.             try {
  324.                 $txnId LandedCostService::post($em$s1, (int) $session->get(UserConstants::USER_LOGIN_ID));
  325.                 if ($txnId) {
  326.                     $session->getFlashBag()->add('importlc''✓ Capitalized to inventory (voucher #' $txnId '): Dr Inventory / Cr Clearing.');
  327.                 } else {
  328.                     // Distinguish the double-count refusal from "no costs / heads not configured".
  329.                     $prior LandedCostService::alreadyCapitalised($em$s);
  330.                     $session->getFlashBag()->add('importlc'$prior['blocked']
  331.                         ? '✕ Not posted — import costs for GRN #' . (int) $s->getLandedGrnId() . ' were already capitalised via the ' $prior['reason'] . '. Enter import costs in ONE place (the purchase/expense invoice OR this landed-cost sheet, not both).'
  332.                         'Nothing posted — no costs entered, no resolvable stock slot, or LC heads not configured.');
  333.                 }
  334.             } catch (\Throwable $e) { $session->getFlashBag()->add('importlc''✕ Failed: ' $e->getMessage()); }
  335.         }
  336.         return $this->redirect($this->generateUrl('import_lc_landed_form', ['id' => $s->getDocumentId()]));
  337.     }
  338. }