namespace MMDAL.Query.StockValuation; /// /// SQL for batch stock valuation: run management, period locking, /// make-item cost application, position updates, and cost workings. /// /// Required indexes: /// TSTOCKVALUATIONRUN: (OUID, STATUS) /// TSTOCKBALANCESNAPSHOT: (OUID, ITEMID, SKUID, STOREID, SNAPSHOTDATE DESC) WHERE ISCERTIFIED=1 /// MPRODUCTCOST: (OUID, ITEMID, SKUID, TYPE, FROMDATE) /// public static class StockValuationQB { // ── Valuation run management ────────────────────────────────────────── public const string INSERT_VALUATION_RUN = @" INSERT INTO TSTOCKVALUATIONRUN (STOCKVALUATIONRUNID, OUID, RUNTYPE, PERIODFROM, PERIODTO, STOREID, ITEMID, ITEMCATEGORYID, ITEMSUBCATEGORYID, VALUATIONMETHODS, STATUS, STARTEDAT, CREATEDBYID, CREATEDON) VALUES (@StockValuationRunId, @OUID, @RunType, @PeriodFrom, @PeriodTo, @StoreId, @ItemId, @ItemCategoryId, @ItemSubCategoryId, @ValuationMethods, 1, GETUTCDATE(), @CreatedById, GETUTCDATE())"; public const string UPDATE_RUN_DONE = @" UPDATE TSTOCKVALUATIONRUN SET STATUS = 2, COMPLETEDAT = GETUTCDATE() WHERE STOCKVALUATIONRUNID = @StockValuationRunId"; public const string UPDATE_RUN_FAILED = @" UPDATE TSTOCKVALUATIONRUN SET STATUS = 3, COMPLETEDAT = GETUTCDATE(), ERRORMESSAGE = @ErrorMessage WHERE STOCKVALUATIONRUNID = @StockValuationRunId"; /// Health check: mark stuck Running records older than 2 hours as Failed. public const string MARK_STUCK_RUNS_FAILED = @" UPDATE TSTOCKVALUATIONRUN SET STATUS = 3, COMPLETEDAT = GETUTCDATE(), ERRORMESSAGE = 'Timed out — process did not complete within 2 hours.' WHERE STATUS = 1 AND STARTEDAT < DATEADD(HOUR, -2, GETUTCDATE())"; /// Idempotency check: is a run already active for this OU/type/period? public const string CHECK_RUN_ACTIVE = @" SELECT COUNT(1) FROM TSTOCKVALUATIONRUN WHERE OUID = @OUID AND RUNTYPE = @RunType AND PERIODFROM = @PeriodFrom AND PERIODTO = @PeriodTo AND STATUS IN (0, 1)"; public const string GET_VALUATION_RUNS = @" SELECT r.STOCKVALUATIONRUNID AS StockValuationRunId, r.OUID, r.RUNTYPE AS RunType, r.PERIODFROM AS PeriodFrom, r.PERIODTO AS PeriodTo, r.STOREID AS StoreId, ISNULL(s.STORENAME,'') AS StoreName, r.ITEMID AS ItemId, ISNULL(i.ITEMNAME,'') AS ItemName, r.VALUATIONMETHODS AS ValuationMethods, r.STATUS AS Status, r.STARTEDAT AS StartedAt, r.COMPLETEDAT AS CompletedAt, r.ERRORMESSAGE AS ErrorMessage, r.CREATEDBYID AS CreatedById, r.CREATEDON AS CreatedOn, DATEDIFF(SECOND, r.STARTEDAT, ISNULL(r.COMPLETEDAT, GETUTCDATE())) AS DurationSeconds FROM TSTOCKVALUATIONRUN r LEFT JOIN MSTORE s ON s.STOREID = r.STOREID LEFT JOIN MITEM i ON i.ITEMID = r.ITEMID WHERE r.OUID = @OUID AND (@PeriodFrom IS NULL OR r.PERIODFROM >= @PeriodFrom) AND (@PeriodTo IS NULL OR r.PERIODTO <= @PeriodTo) ORDER BY r.CREATEDON DESC"; // ── Period lock check ───────────────────────────────────────────────── public const string CHECK_PERIOD_LOCKED = @" SELECT COUNT(a.BIZTRANSACTIONTYPEID) FROM MBIZTRANSACTIONTYPE a JOIN MLOCKSETTING b ON b.BIZTRANSACTIONCLASSID = a.BIZTRANSACTIONCLASSID AND b.OUID = a.OUID JOIN TDAYLOCK lk ON lk.OUID = a.OUID AND lk.PERIODID = a.PERIODID WHERE a.OUID = @OUID AND a.PERIODID = @PeriodId AND b.MONTHLOCKID <> -1 AND b.MONTHLOCKID = lk.LOCKID AND lk.CLOSEMONTHDATE = @CheckDate AND a.STOCKPOSTTYPE = 0"; // ── Make item standard cost application ─────────────────────────────── // Replaces UPDATE_STOCK_VALUATION_SQL_QUERY_TO_STOKCLEDGER_FOR_MAKE_ITEM. // Sources TotalCost from MPRODUCTCOST (Type=0=Make, from a standard MCOSTANALYSIS). // Excludes items excluded from std cost (IsExcludeFromStandardCost handled at // the TMMDETAIL / ProductCost level, not here — stock ledger always gets updated). public const string APPLY_MAKE_ITEM_COST = @" UPDATE TSTOCKLEDGER SET POSTEDCOST = prodcost.TOTALCOST, POSTEDVALUE = CASE WHEN prodcost.TOTALCOST * TSTOCKLEDGER.GOODQUANTITY <= 99999999999999.9999 THEN ROUND(prodcost.TOTALCOST * TSTOCKLEDGER.GOODQUANTITY, 4) ELSE TSTOCKLEDGER.POSTEDVALUE END, MATERIALCOST = prodcost.MATERIALCOST, PROCESSCOST = prodcost.PROCESSCOST, CHARGESCOST = prodcost.CHARGESCOST FROM ( SELECT pc.ITEMID, pc.SKUID, pc.STOREID, pc.TOTALCOST, pc.MATERIALCOST, pc.PROCESSCOST, pc.CHARGESCOST FROM MPRODUCTCOST pc WHERE pc.OUID = @OUID AND pc.TYPE = 0 -- Make AND pc.FROMDATE >= @FromDate AND pc.TODATE = @ToDate ) prodcost JOIN MBIZTRANSACTIONTYPE biztype ON biztype.BIZTRANSACTIONTYPEID = TSTOCKLEDGER.BIZTRANSACTIONTYPEID JOIN MBIZTRANSACTIONCLASS bizclass ON bizclass.BIZTRANSACTIONCLASSID = biztype.BIZTRANSACTIONCLASSID JOIN MITEM item ON item.ITEMID = TSTOCKLEDGER.ITEMID WHERE TSTOCKLEDGER.STOCKLEDGERDATE BETWEEN @FromDate AND @ToDate AND prodcost.ITEMID = TSTOCKLEDGER.ITEMID AND prodcost.SKUID = TSTOCKLEDGER.SKUID AND TSTOCKLEDGER.OUID = @OUID AND item.ISMAKEORBUY = 0 -- Make items only AND bizclass.STOCKVALUEAFFECTING = 1"; // ── TSTOCKPOSITION update after batch ──────────────────────────────── // Recalculates AverageCost and AverageValue from TSTOCKLEDGER after // the period's PostedCost/PostedValue have been updated. public const string SYNC_STOCK_POSITION_FROM_LEDGER = @" WITH ledger_totals AS ( SELECT stkl.OUID, CASE WHEN @IsStorewiseValuation = 0 THEN stkl.STOREID ELSE -1 END AS STOREID, stkl.ITEMID, stkl.SKUID, SUM(CASE WHEN stkl.STOCKPOSTTYPE = 0 THEN stkl.ACTUALQUANTITY ELSE -stkl.ACTUALQUANTITY END) AS QTY, SUM(CASE WHEN stkl.STOCKPOSTTYPE = 0 THEN stkl.POSTEDVALUE ELSE -stkl.POSTEDVALUE END) AS VALUE FROM TSTOCKLEDGER stkl JOIN MBIZTRANSACTIONTYPE bt ON bt.BIZTRANSACTIONTYPEID = stkl.BIZTRANSACTIONTYPEID JOIN MBIZTRANSACTIONCLASS bc ON bc.BIZTRANSACTIONCLASSID = bt.BIZTRANSACTIONCLASSID WHERE stkl.OUID = @OUID AND bc.STOCKVALUEAFFECTING IN (0,1) AND stkl.STOCKLEDGERDATE <= @ToDate GROUP BY stkl.OUID, CASE WHEN @IsStorewiseValuation = 0 THEN stkl.STOREID ELSE -1 END, stkl.ITEMID, stkl.SKUID ) UPDATE TSTOCKPOSITION SET AVERAGEVALUE = ROUND(lt.VALUE, 4), AVERAGECOST = CASE WHEN lt.QTY <> 0 THEN ROUND(lt.VALUE / lt.QTY, 4) ELSE TSTOCKPOSITION.AVERAGECOST END FROM ledger_totals lt WHERE TSTOCKPOSITION.OUID = lt.OUID AND TSTOCKPOSITION.ITEMID = lt.ITEMID AND TSTOCKPOSITION.SKUID = lt.SKUID AND TSTOCKPOSITION.STOREID = lt.STOREID"; // ── Snapshot management ─────────────────────────────────────────────── public const string INSERT_BALANCE_SNAPSHOT = @" INSERT INTO TSTOCKBALANCESNAPSHOT (SNAPSHOTID, OUID, STOREID, ITEMID, SKUID, SNAPSHOTDATE, CLOSINGQTY, CLOSINGVALUE, CLOSINGCOST, VALUATIONMETHOD, ISCERTIFIED, CREATEDON) SELECT @BaseSnapshotId + ROW_NUMBER() OVER (ORDER BY STOREID, ITEMID, SKUID), OUID, STOREID, ITEMID, SKUID, @SnapshotDate, QUANTITY, ROUND(AVERAGEVALUE, 4), ROUND(AVERAGECOST, 4), @ValuationMethod, 0, GETUTCDATE() FROM TSTOCKPOSITION WHERE OUID = @OUID AND QUANTITY <> 0"; public const string CERTIFY_SNAPSHOT = @" UPDATE TSTOCKBALANCESNAPSHOT SET ISCERTIFIED = 1 WHERE OUID = @OUID AND SNAPSHOTDATE = @SnapshotDate"; /// /// Find the last certified snapshot before a given date. /// Used as opening balance for recosting runs. /// public const string GET_LAST_CERTIFIED_SNAPSHOT = @" SELECT SNAPSHOTID AS SnapshotId, OUID, STOREID AS StoreId, ITEMID AS ItemId, SKUID AS SKUId, SNAPSHOTDATE AS SnapshotDate, CLOSINGQTY AS ClosingQty, CLOSINGVALUE AS ClosingValue, CLOSINGCOST AS ClosingCost, VALUATIONMETHOD AS ValuationMethod, ISCERTIFIED AS IsCertified, CREATEDON AS CreatedOn FROM TSTOCKBALANCESNAPSHOT WHERE OUID = @OUID AND ITEMID = @ItemId AND SKUID = @SKUId AND STOREID = @StoreId AND ISCERTIFIED = 1 AND SNAPSHOTDATE < @BeforeDate ORDER BY SNAPSHOTDATE DESC"; // ── Cost workings — weighted average ───────────────────────────────── public const string GET_COST_WORKINGS_WA = @" SELECT stkl.STOCKLEDGERID, stkl.STOCKLEDGERDATE AS Date, ISNULL(mmh.DOCUMENTNUMBER, '') AS DocNumber, btc.BIZTRANSACTIONCLASSNAME AS TxnType, stkl.STOCKPOSTTYPE, CASE WHEN stkl.STOCKPOSTTYPE = 0 THEN stkl.ACTUALQUANTITY ELSE -stkl.ACTUALQUANTITY END AS Qty, stkl.POSTEDCOST AS UnitCost, CASE WHEN stkl.STOCKPOSTTYPE = 0 THEN stkl.POSTEDVALUE ELSE -stkl.POSTEDVALUE END AS Value FROM TSTOCKLEDGER stkl JOIN MBIZTRANSACTIONTYPE bt ON bt.BIZTRANSACTIONTYPEID = stkl.BIZTRANSACTIONTYPEID JOIN MBIZTRANSACTIONCLASS btc ON btc.BIZTRANSACTIONCLASSID = bt.BIZTRANSACTIONCLASSID LEFT JOIN TMMHEAD mmh ON mmh.DOCUMENTID = stkl.OBJECTID AND stkl.OBJECTTYPEID = -1899997952 WHERE stkl.OUID = @OUID AND stkl.ITEMID = @ItemId AND stkl.SKUID = @SKUId AND stkl.STOREID = @StoreId AND stkl.STOCKLEDGERDATE BETWEEN @FromDate AND @ToDate AND btc.STOCKVALUEAFFECTING IN (0, 1) ORDER BY stkl.STOCKLEDGERDATE, stkl.STOCKLEDGERID"; // ── Make item BOM cost breakdown (for cost workings) ───────────────── public const string GET_MAKE_ITEM_BOM_COST_BREAKDOWN = @" SELECT pc.PRODUCTCOSTID, pc.ITEMID, i.ITEMCODE, i.ITEMNAME, pc.SKUID, pc.QUANTITY, pc.MATERIALCOST, pc.MATERIALPROCESSCOST, pc.MATERIALCHARGESCOST, pc.MATERIALINHOUSEPROCESSCOST, pc.MATERIALSUBCONTRACTPROCESSCOST, pc.PROCESSCOST, pc.INHOUSEPROCESSCOST, pc.SUBCONTRACTPROCESSCOST, pc.CHARGESCOST, pc.TOTALCOST, ca.CODE AS CostAnalysisCode, ca.PERIODFROM, ca.PERIODTO FROM MPRODUCTCOST pc JOIN MITEM i ON i.ITEMID = pc.ITEMID JOIN MCOSTANALYSIS ca ON ca.COSTANALYSISID = pc.COSTANALYSISID WHERE pc.OUID = @OUID AND pc.ITEMID = @ItemId AND pc.SKUID = @SKUId AND pc.TYPE = 0 -- Make / Standard AND pc.FROMDATE <= @AsOfDate AND pc.TODATE >= @AsOfDate ORDER BY ca.PERIODFROM DESC"; }