Quellcode durchsuchen

no message

master
Fai Luk vor 3 Tagen
Ursprung
Commit
939e5a940c
4 geänderte Dateien mit 135 neuen und 4 gelöschten Zeilen
  1. +69
    -0
      src/main/java/com/ffii/fpsms/modules/report/service/ReportService.kt
  2. +47
    -0
      src/main/java/com/ffii/fpsms/modules/report/web/ReportController.kt
  3. +12
    -0
      src/main/java/com/ffii/fpsms/modules/stock/entity/StockInLineRepository.kt
  4. +7
    -4
      src/main/java/com/ffii/fpsms/modules/stock/service/StockInLineService.kt

+ 69
- 0
src/main/java/com/ffii/fpsms/modules/report/service/ReportService.kt Datei anzeigen

@@ -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<Map<String, Any?>> {
val formatted = receiptDate.replace("/", "-")
val args = mutableMapOf<String, Any>("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.


+ 47
- 0
src/main/java/com/ffii/fpsms/modules/report/web/ReportController.kt Datei anzeigen

@@ -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)
}

}

+ 12
- 0
src/main/java/com/ffii/fpsms/modules/stock/entity/StockInLineRepository.kt Datei anzeigen

@@ -68,6 +68,18 @@ fun findFirstByJobOrder_IdAndDeletedFalse(jobOrderId: Long): StockInLine?
""")
fun findAllByPurchaseOrderIdAndDeletedFalseWithItemNames(@Param("purchaseOrderId") purchaseOrderId: Long): List<StockInLine>

@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<StockInLine>

@Query("""
SELECT sil FROM StockInLine sil
WHERE sil.receiptDate IS NOT NULL


+ 7
- 4
src/main/java/com/ffii/fpsms/modules/stock/service/StockInLineService.kt Datei anzeigen

@@ -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


Laden…
Abbrechen
Speichern