From 939e5a940c1e25741f23e33710220696086f4e41 Mon Sep 17 00:00:00 2001 From: Fai Luk Date: Wed, 18 Mar 2026 23:55:53 +0800 Subject: [PATCH] no message --- .../modules/report/service/ReportService.kt | 69 +++++++++++++++++++ .../modules/report/web/ReportController.kt | 47 +++++++++++++ .../stock/entity/StockInLineRepository.kt | 12 ++++ .../stock/service/StockInLineService.kt | 11 +-- 4 files changed, 135 insertions(+), 4 deletions(-) diff --git a/src/main/java/com/ffii/fpsms/modules/report/service/ReportService.kt b/src/main/java/com/ffii/fpsms/modules/report/service/ReportService.kt index 123a9c0..9fad3ef 100644 --- a/src/main/java/com/ffii/fpsms/modules/report/service/ReportService.kt +++ b/src/main/java/com/ffii/fpsms/modules/report/service/ReportService.kt @@ -6,6 +6,8 @@ import net.sf.jasperreports.engine.data.JRMapCollectionDataSource import java.io.ByteArrayOutputStream import java.io.InputStream import com.ffii.core.support.JdbcDao +import com.ffii.fpsms.modules.master.service.ItemUomService +import java.math.BigDecimal import net.sf.jasperreports.engine.export.ooxml.JRXlsxExporter import net.sf.jasperreports.export.SimpleExporterInput import net.sf.jasperreports.export.SimpleOutputStreamExporterOutput @@ -13,6 +15,7 @@ import net.sf.jasperreports.export.SimpleOutputStreamExporterOutput @Service open class ReportService( private val jdbcDao: JdbcDao, + private val itemUomService: ItemUomService, ) { /** * Queries the database for inventory data based on dates and optional item type. @@ -916,6 +919,72 @@ fun searchMaterialStockOutTraceabilityReport( } } + /** + * GRN preview for M18: show both stock qty (acceptedQty) and purchase qty (converted) for a specific receipt date. + * This is a DRY-RUN preview only (does not call M18). + */ + fun searchGrnPreviewM18(receiptDate: String): List> { + val formatted = receiptDate.replace("/", "-") + val args = mutableMapOf("receiptDate" to formatted) + val sql = """ + SELECT + sil.id AS stockInLineId, + po.id AS purchaseOrderId, + po.code AS poCode, + pol.id AS purchaseOrderLineId, + CASE + WHEN sil.dnNo = 'DN00000' OR sil.dnNo IS NULL THEN '' + ELSE sil.dnNo + END AS deliveryNoteNo, + DATE_FORMAT(sil.receiptDate, '%Y-%m-%d') AS receiptDate, + it.id AS itemId, + COALESCE(it.code, '') AS itemCode, + COALESCE(it.name, '') AS itemName, + COALESCE(sil.acceptedQty, 0) AS acceptedQty, + COALESCE(uc_pol.udfudesc, '') AS purchaseUomDesc, + COALESCE(uc_stock.udfudesc, '') AS stockUomDesc, + COALESCE(sil.status, '') AS status + FROM stock_in_line sil + LEFT JOIN items it ON sil.itemId = it.id + LEFT JOIN purchase_order po ON sil.purchaseOrderId = po.id + LEFT JOIN purchase_order_line pol ON sil.purchaseOrderLineId = pol.id + LEFT JOIN uom_conversion uc_pol ON pol.uomId = uc_pol.id + LEFT JOIN item_uom iu_stock ON it.id = iu_stock.itemId AND iu_stock.stockUnit = true AND iu_stock.deleted = false + LEFT JOIN uom_conversion uc_stock ON iu_stock.uomId = uc_stock.id + WHERE sil.deleted = false + AND sil.receiptDate IS NOT NULL + AND DATE(sil.receiptDate) = DATE(:receiptDate) + AND sil.purchaseOrderId IS NOT NULL + AND sil.status = 'completed' + ORDER BY sil.purchaseOrderId, sil.purchaseOrderLineId, sil.id + """.trimIndent() + val rows = jdbcDao.queryForList(sql, args) + return rows.map { row -> + val itemId = (row["itemId"] as? Number)?.toLong() + val acceptedQtyBd = when (val v = row["acceptedQty"]) { + is BigDecimal -> v + is Number -> BigDecimal(v.toString()) + is String -> v.toBigDecimalOrNull() ?: BigDecimal.ZERO + else -> BigDecimal.ZERO + } + val purchaseQtyBd = if (itemId != null) itemUomService.convertStockQtyToPurchaseQty(itemId, acceptedQtyBd) else acceptedQtyBd + mapOf( + "receiptDate" to row["receiptDate"], + "poCode" to row["poCode"], + "deliveryNoteNo" to row["deliveryNoteNo"], + "stockInLineId" to row["stockInLineId"], + "purchaseOrderLineId" to row["purchaseOrderLineId"], + "itemCode" to row["itemCode"], + "itemName" to row["itemName"], + "stockQty" to acceptedQtyBd.toDouble(), + "purchaseQty" to purchaseQtyBd.toDouble(), + "purchaseUomDesc" to row["purchaseUomDesc"], + "stockUomDesc" to row["stockUomDesc"], + "status" to row["status"], + ) + } + } + /** * Queries the database for Stock Balance Report data (one summarized row per item). * Uses stock_ledger with report period (fromDate/toDate): opening = before fromDate, cum in/out = in period, current = up to toDate. diff --git a/src/main/java/com/ffii/fpsms/modules/report/web/ReportController.kt b/src/main/java/com/ffii/fpsms/modules/report/web/ReportController.kt index 6579462..f997622 100644 --- a/src/main/java/com/ffii/fpsms/modules/report/web/ReportController.kt +++ b/src/main/java/com/ffii/fpsms/modules/report/web/ReportController.kt @@ -334,4 +334,51 @@ class ReportController( return mapOf("rows" to rows) } + /** + * DRY-RUN GRN preview for M18 (shows stock qty vs converted purchase qty). + * + * Example: `/report/grn-preview-m18?receiptDate=2026-03-16` + * CSV (Excel-openable): `/report/grn-preview-m18?receiptDate=2026-03-16&format=csv` + */ + @GetMapping("/grn-preview-m18") + fun getGrnPreviewM18( + @RequestParam receiptDate: String, + @RequestParam(required = false, defaultValue = "json") format: String, + ): Any { + val rows = reportService.searchGrnPreviewM18(receiptDate) + if (format.equals("csv", ignoreCase = true)) { + val headers = listOf( + "receiptDate", + "poCode", + "deliveryNoteNo", + "stockInLineId", + "purchaseOrderLineId", + "itemCode", + "itemName", + "stockQty", + "purchaseQty", + "stockUomDesc", + "purchaseUomDesc", + "status", + ) + val sb = StringBuilder() + sb.append(headers.joinToString(",")).append("\n") + rows.forEach { r -> + val line = headers.joinToString(",") { h -> + val v = r[h] + val s = (v?.toString() ?: "").replace("\"", "\"\"") + "\"$s\"" + } + sb.append(line).append("\n") + } + val bytes = sb.toString().toByteArray(Charsets.UTF_8) + val httpHeaders = HttpHeaders().apply { + contentType = MediaType("text", "csv") + set(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=grn-preview-m18-$receiptDate.csv") + } + return ResponseEntity(bytes, httpHeaders, HttpStatus.OK) + } + return mapOf("rows" to rows) + } + } \ No newline at end of file diff --git a/src/main/java/com/ffii/fpsms/modules/stock/entity/StockInLineRepository.kt b/src/main/java/com/ffii/fpsms/modules/stock/entity/StockInLineRepository.kt index 9bb5931..69b3b81 100644 --- a/src/main/java/com/ffii/fpsms/modules/stock/entity/StockInLineRepository.kt +++ b/src/main/java/com/ffii/fpsms/modules/stock/entity/StockInLineRepository.kt @@ -68,6 +68,18 @@ fun findFirstByJobOrder_IdAndDeletedFalse(jobOrderId: Long): StockInLine? """) fun findAllByPurchaseOrderIdAndDeletedFalseWithItemNames(@Param("purchaseOrderId") purchaseOrderId: Long): List + @Query(""" + SELECT DISTINCT sil FROM StockInLine sil + LEFT JOIN FETCH sil.item + LEFT JOIN FETCH sil.purchaseOrderLine pol + LEFT JOIN FETCH pol.item + WHERE sil.purchaseOrder.id = :purchaseOrderId + AND sil.deleted = false + AND sil.status = 'completed' + ORDER BY sil.id + """) + fun findCompletedByPurchaseOrderIdAndDeletedFalseWithItemNames(@Param("purchaseOrderId") purchaseOrderId: Long): List + @Query(""" SELECT sil FROM StockInLine sil WHERE sil.receiptDate IS NOT NULL diff --git a/src/main/java/com/ffii/fpsms/modules/stock/service/StockInLineService.kt b/src/main/java/com/ffii/fpsms/modules/stock/service/StockInLineService.kt index 570f0ed..038136a 100644 --- a/src/main/java/com/ffii/fpsms/modules/stock/service/StockInLineService.kt +++ b/src/main/java/com/ffii/fpsms/modules/stock/service/StockInLineService.kt @@ -520,12 +520,15 @@ open class StockInLineService( val antValues = byPol.map { (_, silList) -> val sil = silList.first() val pol = sil.purchaseOrderLine!! - // acceptedQty on StockInLine is in stock unit; M18 ant expects purchase unit qty + // M18 GRN ant expects purchase unit qty; acceptedQty on StockInLine is in stock unit val totalStockQty = silList.sumOf { it.acceptedQty ?: BigDecimal.ZERO } val itemId = sil.item?.id ?: pol.item?.id val totalQtyInPurchaseUnit = if (itemId != null) { itemUomService.convertStockQtyToPurchaseQty(itemId, totalStockQty) - } else totalStockQty + } else { + logger.warn("[buildGoodsReceiptNoteRequest] No itemId for POL id=${pol.id}, using stock qty as fallback (may be wrong unit for M18)") + totalStockQty + } val unitIdFromDataLog = (pol.m18DataLog?.dataLog?.get("unitId") as? Number)?.toLong()?.toInt() val itemName = (sil.item?.name ?: pol.item?.name).orEmpty() // always non-null for M18 bDesc/bDesc_en GoodsReceiptNoteAntValue( @@ -605,8 +608,8 @@ open class StockInLineService( logger.info("[updatePurchaseOrderStatus] savedPo id=${savedPo.id}, status=${savedPo.status}") // TODO: For test only - normally check savedPo.status == PurchaseOrderStatus.COMPLETED and use only COMPLETE lines try { - val allLines = stockInLineRepository.findAllByPurchaseOrderIdAndDeletedFalseWithItemNames(savedPo.id!!) - val linesForGrn = allLines // TODO test: use all lines; normally .filter { it.status == StockInLineStatus.COMPLETE.status } + // Defensive: load only completed stock-in lines for the PO, so GRN payload can't include pending/escalated. + val linesForGrn = stockInLineRepository.findCompletedByPurchaseOrderIdAndDeletedFalseWithItemNames(savedPo.id!!) if (linesForGrn.isEmpty()) { logger.info("[tryUpdatePurchaseOrderAndCreateGrnIfCompleted] DEBUG: Skipping M18 GRN - no stock-in lines for PO id=${savedPo.id} code=${savedPo.code}") return