| @@ -218,15 +218,15 @@ open class SchedulerService( | |||||
| )*/ | )*/ | ||||
| val tmr = today.plusDays(1L) | val tmr = today.plusDays(1L) | ||||
| var request = M18CommonRequest( | var request = M18CommonRequest( | ||||
| modifiedDateTo = tmr.format(dataStringFormat), | |||||
| modifiedDateFrom = tmr.format(dataStringFormat) | |||||
| dDateTo = tmr.format(dataStringFormat), | |||||
| dDateFrom = tmr.format(dataStringFormat) | |||||
| ) | ) | ||||
| m18PurchaseOrderService.savePurchaseOrders(request); | m18PurchaseOrderService.savePurchaseOrders(request); | ||||
| //dDate from tmr to tmr | //dDate from tmr to tmr | ||||
| var requestDO = M18CommonRequest( | var requestDO = M18CommonRequest( | ||||
| modifiedDateTo = tmr.format(dataStringFormat), | |||||
| modifiedDateFrom = tmr.format(dataStringFormat) | |||||
| dDateTo = tmr.format(dataStringFormat), | |||||
| dDateFrom = tmr.format(dataStringFormat) | |||||
| ) | ) | ||||
| m18DeliveryOrderService.saveDeliveryOrders(requestDO); | m18DeliveryOrderService.saveDeliveryOrders(requestDO); | ||||
| // logger.info("today: ${today.format(dataStringFormat)}") | // logger.info("today: ${today.format(dataStringFormat)}") | ||||
| @@ -135,6 +135,25 @@ interface JobOrderRepository : AbstractRepository<JobOrder, Long> { | |||||
| planStartToExclusive: LocalDateTime, | planStartToExclusive: LocalDateTime, | ||||
| ): List<JobOrder> | ): List<JobOrder> | ||||
| @Query( | |||||
| """ | |||||
| SELECT DISTINCT jo FROM JobOrder jo | |||||
| JOIN jo.bom b | |||||
| JOIN b.bomProcesses bp | |||||
| JOIN bp.process p | |||||
| WHERE jo.deleted = false | |||||
| AND jo.planStart >= :planStartFrom | |||||
| AND jo.planStart < :planStartToExclusive | |||||
| AND p.name = :processName | |||||
| ORDER BY jo.id ASC | |||||
| """ | |||||
| ) | |||||
| fun findByDeletedFalseAndPlanStartBetweenAndBomProcessNameOrderByIdAsc( | |||||
| planStartFrom: LocalDateTime, | |||||
| planStartToExclusive: LocalDateTime, | |||||
| processName: String, | |||||
| ): List<JobOrder> | |||||
| @Query(""" | @Query(""" | ||||
| SELECT jo FROM JobOrder jo | SELECT jo FROM JobOrder jo | ||||
| WHERE jo.deleted = false | WHERE jo.deleted = false | ||||
| @@ -13,10 +13,11 @@ open class PSService( | |||||
| private val jdbcDao: JdbcDao, | private val jdbcDao: JdbcDao, | ||||
| ) { | ) { | ||||
| /** Default: past 30 days including today. */ | |||||
| /** Default: 6 days before today to 1 day after today. */ | |||||
| fun getItemDailyOut(fromDate: LocalDate? = null, toDate: LocalDate? = null): List<Map<String, Any>> { | fun getItemDailyOut(fromDate: LocalDate? = null, toDate: LocalDate? = null): List<Map<String, Any>> { | ||||
| val to = toDate ?: LocalDate.now() | |||||
| val from = fromDate ?: to.minusDays(29) | |||||
| val defaultToday = LocalDate.now() | |||||
| val to = toDate ?: defaultToday.plusDays(1) | |||||
| val from = fromDate ?: defaultToday.minusDays(6) | |||||
| val args = mapOf( | val args = mapOf( | ||||
| "fromDate" to from.toString(), | "fromDate" to from.toString(), | ||||
| "toDate" to to.toString(), | "toDate" to to.toString(), | ||||
| @@ -159,7 +159,31 @@ open class PlasticBagPrinterService( | |||||
| require(normalizedJobOrders.isNotEmpty()) { "No job orders provided" } | require(normalizedJobOrders.isNotEmpty()) { "No job orders provided" } | ||||
| val normalizedCodes = normalizedJobOrders | |||||
| // Safety check: export only supports job orders whose BOM contains process "包裝". | |||||
| val requestedJobOrderIds = normalizedJobOrders.map { it.jobOrderId }.distinct() | |||||
| val allowedRows = jdbcDao.queryForList( | |||||
| """ | |||||
| SELECT DISTINCT jo.id AS jobOrderId | |||||
| FROM job_order jo | |||||
| JOIN bom b ON b.id = jo.bomId | |||||
| JOIN bom_process bp ON bp.bomId = b.id | |||||
| JOIN process p ON p.id = bp.processId | |||||
| WHERE jo.id IN (:jobOrderIds) | |||||
| AND jo.deleted = 0 | |||||
| AND p.name = :processName | |||||
| """.trimIndent(), | |||||
| mapOf( | |||||
| "jobOrderIds" to requestedJobOrderIds, | |||||
| "processName" to "包裝", | |||||
| ) | |||||
| ) | |||||
| val allowedJobOrderIds = allowedRows | |||||
| .mapNotNull { (it["jobOrderId"] as? Number)?.toLong() } | |||||
| .toSet() | |||||
| val packagingJobOrders = normalizedJobOrders.filter { it.jobOrderId in allowedJobOrderIds } | |||||
| require(packagingJobOrders.isNotEmpty()) { "No 包裝 process job orders found for export" } | |||||
| val normalizedCodes = packagingJobOrders | |||||
| .map { it.itemCode } | .map { it.itemCode } | ||||
| .distinct() | .distinct() | ||||
| @@ -184,7 +208,7 @@ open class PlasticBagPrinterService( | |||||
| val baos = ByteArrayOutputStream() | val baos = ByteArrayOutputStream() | ||||
| ZipOutputStream(baos).use { zos -> | ZipOutputStream(baos).use { zos -> | ||||
| val addedEntries = linkedSetOf<String>() | val addedEntries = linkedSetOf<String>() | ||||
| normalizedJobOrders.forEach { jobOrder -> | |||||
| packagingJobOrders.forEach { jobOrder -> | |||||
| val filename = filenameByCode[jobOrder.itemCode].orEmpty() | val filename = filenameByCode[jobOrder.itemCode].orEmpty() | ||||
| if (filename.isBlank()) return@forEach | if (filename.isBlank()) return@forEach | ||||
| @@ -40,14 +40,15 @@ class PSController( | |||||
| return ResponseEntity.ok(results) | return ResponseEntity.ok(results) | ||||
| } | } | ||||
| /** 每日平均出貨量: itemCode, itemName, avgQtyLastMonth, dailyQty, isCoffee, isTea, isLemon. Default: past 30 days. */ | |||||
| /** 每日平均出貨量: itemCode, itemName, avgQtyLastMonth, dailyQty, isCoffee, isTea, isLemon. Default: 6 days before today to 1 day after today. */ | |||||
| @GetMapping("/itemDailyOut.json") | @GetMapping("/itemDailyOut.json") | ||||
| fun itemDailyOut( | fun itemDailyOut( | ||||
| @RequestParam(required = false) fromDate: String?, | @RequestParam(required = false) fromDate: String?, | ||||
| @RequestParam(required = false) toDate: String?, | @RequestParam(required = false) toDate: String?, | ||||
| ): ResponseEntity<List<Map<String, Any>>> { | ): ResponseEntity<List<Map<String, Any>>> { | ||||
| val to = toDate?.let { LocalDate.parse(it) } ?: LocalDate.now() | |||||
| val from = fromDate?.let { LocalDate.parse(it) } ?: to.minusDays(29) | |||||
| val defaultToday = LocalDate.now() | |||||
| val to = toDate?.let { LocalDate.parse(it) } ?: defaultToday.plusDays(1) | |||||
| val from = fromDate?.let { LocalDate.parse(it) } ?: defaultToday.minusDays(6) | |||||
| val results = psService.getItemDailyOut(from, to) | val results = psService.getItemDailyOut(from, to) | ||||
| return ResponseEntity.ok(results) | return ResponseEntity.ok(results) | ||||
| } | } | ||||
| @@ -178,6 +178,27 @@ open class ItemUomService( | |||||
| return stockQty.setScale(0, RoundingMode.UP) | return stockQty.setScale(0, RoundingMode.UP) | ||||
| } | } | ||||
| /** | |||||
| * Convert source quantity from a specific UOM to this item's stock quantity. | |||||
| * Same as convertQtyToStockQty but rounds down the final stock qty to integer. | |||||
| */ | |||||
| open fun convertQtyToStockQtyRoundDown(itemId: Long, uomId: Long, sourceQty: BigDecimal): BigDecimal { | |||||
| val itemUom = findFirstByItemIdAndUomId(itemId, uomId) ?: return sourceQty | |||||
| val stockUnit = findStockUnitByItemId(itemId) ?: return BigDecimal.ZERO | |||||
| val one = BigDecimal.ONE | |||||
| val calcScale = 10 | |||||
| val baseQty = sourceQty | |||||
| .multiply(itemUom.ratioN ?: one) | |||||
| .divide(itemUom.ratioD ?: one, calcScale, RoundingMode.HALF_UP) | |||||
| val stockQty = baseQty | |||||
| .multiply(stockUnit.ratioD ?: one) | |||||
| .divide(stockUnit.ratioN ?: one, calcScale, RoundingMode.HALF_UP) | |||||
| return stockQty.setScale(0, RoundingMode.DOWN) | |||||
| } | |||||
| // See if need to update the response | // See if need to update the response | ||||
| open fun saveItemUom(request: ItemUomRequest): ItemUom { | open fun saveItemUom(request: ItemUomRequest): ItemUom { | ||||
| val itemUom = request.m18Id?.let { findByM18Id(it) } ?: request.id?.let { findById(it) } ?: ItemUom() | val itemUom = request.m18Id?.let { findByM18Id(it) } ?: request.id?.let { findById(it) } ?: ItemUom() | ||||
| @@ -628,7 +628,7 @@ open class ProductionScheduleService( | |||||
| } | } | ||||
| open fun getNeedQty(): List<NeedQtyRecord> { | open fun getNeedQty(): List<NeedQtyRecord> { | ||||
| val fromDate = java.time.LocalDate.now().minusMonths(1) | |||||
| val fromDate = java.time.LocalDate.now().minusDays(10) | |||||
| val toDate = java.time.LocalDate.now() | val toDate = java.time.LocalDate.now() | ||||
| val args = mapOf("fromDate" to fromDate.toString(), "toDate" to toDate.toString()) | val args = mapOf("fromDate" to fromDate.toString(), "toDate" to toDate.toString()) | ||||
| @@ -1832,16 +1832,34 @@ open class ProductionScheduleService( | |||||
| ) | ) | ||||
| val sql = """ | val sql = """ | ||||
| WITH daily_needs AS ( | |||||
| WITH latest_supplier_uom AS ( | |||||
| SELECT x.materialId, x.uomIdM18 | |||||
| FROM ( | |||||
| SELECT | |||||
| pol.itemId AS materialId, | |||||
| pol.uomIdM18, | |||||
| ROW_NUMBER() OVER ( | |||||
| PARTITION BY pol.itemId | |||||
| ORDER BY COALESCE(po.orderDate, po.created) DESC, pol.id DESC | |||||
| ) AS rn | |||||
| FROM purchase_order_line pol | |||||
| JOIN purchase_order po ON po.id = pol.purchaseOrderId | |||||
| WHERE pol.deleted = 0 | |||||
| AND po.deleted = 0 | |||||
| AND pol.uomIdM18 IS NOT NULL | |||||
| ) x | |||||
| WHERE x.rn = 1 | |||||
| ), | |||||
| daily_needs AS ( | |||||
| SELECT | SELECT | ||||
| itm.id AS materialId, | itm.id AS materialId, | ||||
| itm.code AS matCode, | itm.code AS matCode, | ||||
| itm.name AS matName, | itm.name AS matName, | ||||
| uomP.udfudesc AS uom, | |||||
| ceil((iv.onHandQty * (itsm.ratioN / itsm.ratioD)) * (itum.ratioD / itum.ratioN)) as onHandQty, | |||||
| ceil((iv.unavailableQty * (itsm.ratioN / itsm.ratioD)) * (itum.ratioD / itum.ratioN)) as unavailableQty, | |||||
| COALESCE(uomM18.code, uomP.code) AS uom, | |||||
| ceil((iv.onHandQty * (itsm.ratioN / itsm.ratioD)) * (COALESCE(ium18.ratioD, itum.ratioD) / COALESCE(ium18.ratioN, itum.ratioN))) as onHandQty, | |||||
| ceil((iv.unavailableQty * (itsm.ratioN / itsm.ratioD)) * (COALESCE(ium18.ratioD, itum.ratioD) / COALESCE(ium18.ratioN, itum.ratioN))) as unavailableQty, | |||||
| COALESCE(( | COALESCE(( | ||||
| SELECT SUM(pol.qty) | |||||
| SELECT ceil(SUM(pol.qty * (itum.ratioN / itum.ratioD) * (COALESCE(ium18.ratioD, itum.ratioD) / COALESCE(ium18.ratioN, itum.ratioN)))) | |||||
| FROM purchase_order_line pol | FROM purchase_order_line pol | ||||
| JOIN purchase_order po ON pol.purchaseOrderId = po.id | JOIN purchase_order po ON pol.purchaseOrderId = po.id | ||||
| WHERE pol.itemId = itm.id | WHERE pol.itemId = itm.id | ||||
| @@ -1849,7 +1867,7 @@ open class ProductionScheduleService( | |||||
| AND po.completeDate IS NULL | AND po.completeDate IS NULL | ||||
| ), 0) AS purchasedQty, | ), 0) AS purchasedQty, | ||||
| DATE(ps.produceAt) AS produceDate, | DATE(ps.produceAt) AS produceDate, | ||||
| ceil( SUM(bm.baseQty * psl.batchNeed) * (itum.ratioD / itum.ratioN) ) AS qtyNeeded | |||||
| ceil( SUM(bm.baseQty * psl.batchNeed) * (COALESCE(ium18.ratioD, itum.ratioD) / COALESCE(ium18.ratioN, itum.ratioN)) ) AS qtyNeeded | |||||
| FROM production_schedule_line psl | FROM production_schedule_line psl | ||||
| JOIN production_schedule ps ON psl.prodScheduleId = ps.id | JOIN production_schedule ps ON psl.prodScheduleId = ps.id | ||||
| JOIN items it ON psl.itemId = it.id | JOIN items it ON psl.itemId = it.id | ||||
| @@ -1857,7 +1875,10 @@ open class ProductionScheduleService( | |||||
| JOIN bom_material bm ON bom.id = bm.bomId | JOIN bom_material bm ON bom.id = bm.bomId | ||||
| JOIN items itm ON bm.itemId = itm.id | JOIN items itm ON bm.itemId = itm.id | ||||
| JOIN item_uom itum ON itm.id = itum.itemId and itum.purchaseUnit = 1 | JOIN item_uom itum ON itm.id = itum.itemId and itum.purchaseUnit = 1 | ||||
| join uom_conversion uomP on itum.uomId = uomP.id | |||||
| JOIN uom_conversion uomP ON itum.uomId = uomP.id | |||||
| LEFT JOIN latest_supplier_uom lsu ON lsu.materialId = itm.id | |||||
| LEFT JOIN item_uom ium18 ON ium18.itemId = itm.id AND ium18.uomId = lsu.uomIdM18 AND ium18.deleted = 0 | |||||
| LEFT JOIN uom_conversion uomM18 ON uomM18.id = ium18.uomId | |||||
| JOIN item_uom itsm ON itm.id = itsm.itemId and itsm.stockUnit = 1 | JOIN item_uom itsm ON itm.id = itsm.itemId and itsm.stockUnit = 1 | ||||
| LEFT JOIN inventory iv ON itm.id = iv.itemId | LEFT JOIN inventory iv ON itm.id = iv.itemId | ||||
| WHERE DATE(ps.produceAt) >= DATE_ADD(:fromDate, INTERVAL 1 DAY) | WHERE DATE(ps.produceAt) >= DATE_ADD(:fromDate, INTERVAL 1 DAY) | ||||
| @@ -135,11 +135,11 @@ open fun getPoSummariesByIds(ids: List<Long>): List<PurchaseOrderSummary> { | |||||
| " SEPARATOR ','" + | " SEPARATOR ','" + | ||||
| " ) as itemName," + | " ) as itemName," + | ||||
| " group_concat(" + | " group_concat(" + | ||||
| " coalesce(uc.udfudesc, \"N/A\") " + | |||||
| " coalesce(ucm18.udfudesc, uc.udfudesc, \"N/A\") " + | |||||
| " SEPARATOR ','" + | " SEPARATOR ','" + | ||||
| " ) as itemUom," + | " ) as itemUom," + | ||||
| " group_concat(" + | " group_concat(" + | ||||
| " coalesce(pol.qty, 0) " + | |||||
| " coalesce(pol.qtyM18, pol.qty, 0) " + | |||||
| " SEPARATOR ','" + | " SEPARATOR ','" + | ||||
| " ) as itemQty," + | " ) as itemQty," + | ||||
| " group_concat(" + | " group_concat(" + | ||||
| @@ -173,6 +173,7 @@ open fun getPoSummariesByIds(ids: List<Long>): List<PurchaseOrderSummary> { | |||||
| " ) sil2 on sil2.purchaseOrderLineId = pol.id" + | " ) sil2 on sil2.purchaseOrderLineId = pol.id" + | ||||
| " left join item_uom iu on iu.itemId = pol.itemId and iu.purchaseUnit = true" + | " left join item_uom iu on iu.itemId = pol.itemId and iu.purchaseUnit = true" + | ||||
| " left join uom_conversion uc on uc.id = iu.uomId" + | " left join uom_conversion uc on uc.id = iu.uomId" + | ||||
| " left join uom_conversion ucm18 on ucm18.id = pol.uomIdM18" + | |||||
| " where po.deleted = false " + | " where po.deleted = false " + | ||||
| " and pol.deleted = false " | " and pol.deleted = false " | ||||
| ) | ) | ||||
| @@ -264,11 +265,22 @@ open fun getPoSummariesByIds(ids: List<Long>): List<PurchaseOrderSummary> { | |||||
| } ?: mutableListOf(); | } ?: mutableListOf(); | ||||
| val purchaseUnit = thisPol.item?.id?.let { itemUomService.findPurchaseUnitByItemId(it) } | val purchaseUnit = thisPol.item?.id?.let { itemUomService.findPurchaseUnitByItemId(it) } | ||||
| val stockUnit = thisPol.item?.id?.let { itemUomService.findStockUnitByItemId(it).let { iu -> | val stockUnit = thisPol.item?.id?.let { itemUomService.findStockUnitByItemId(it).let { iu -> | ||||
| val itemId = thisPol.item?.id | |||||
| val qtyM18 = thisPol.qtyM18 | |||||
| val uomM18Id = thisPol.uomM18?.id | |||||
| val stockQtyFromM18 = if (itemId != null && qtyM18 != null && uomM18Id != null) { | |||||
| itemUomService.convertQtyToStockQtyRoundDown(itemId, uomM18Id, qtyM18) | |||||
| } else { | |||||
| BigDecimal.ZERO | |||||
| } | |||||
| StockUomForPoLine( | StockUomForPoLine( | ||||
| id = iu?.id, | id = iu?.id, | ||||
| stockUomCode = iu?.uom?.code, | stockUomCode = iu?.uom?.code, | ||||
| stockUomDesc = iu?.uom?.udfudesc, | stockUomDesc = iu?.uom?.udfudesc, | ||||
| stockQty = iu?.item?.id?.let { iId -> itemUomService.convertPurchaseQtyToStockQty(iId, (thisPol.qty ?: BigDecimal.ZERO)) } ?: BigDecimal.ZERO, | |||||
| stockQty = if (stockQtyFromM18 > BigDecimal.ZERO) stockQtyFromM18 else { | |||||
| // fallback to legacy behavior when M18 fields are missing | |||||
| iu?.item?.id?.let { iId -> itemUomService.convertPurchaseQtyToStockQty(iId, (thisPol.qty ?: BigDecimal.ZERO)) } ?: BigDecimal.ZERO | |||||
| }, | |||||
| stockRatioN = iu?.ratioN, | stockRatioN = iu?.ratioN, | ||||
| stockRatioD = iu?.ratioD, | stockRatioD = iu?.ratioD, | ||||
| purchaseRatioN = purchaseUnit?.ratioN, | purchaseRatioN = purchaseUnit?.ratioN, | ||||
| @@ -281,13 +293,13 @@ open fun getPoSummariesByIds(ids: List<Long>): List<PurchaseOrderSummary> { | |||||
| itemId = thisPol.item!!.id!!, | itemId = thisPol.item!!.id!!, | ||||
| itemNo = thisPol.itemNo!!, | itemNo = thisPol.itemNo!!, | ||||
| itemName = thisPol.item!!.name, | itemName = thisPol.item!!.name, | ||||
| qty = thisPol.qty!!, | |||||
| qty = thisPol.qtyM18 ?: thisPol.qty!!, | |||||
| // processed = inLine.filter{ it.status == StockInLineStatus.COMPLETE.status}.sumOf { it.acceptedQty }, | // processed = inLine.filter{ it.status == StockInLineStatus.COMPLETE.status}.sumOf { it.acceptedQty }, | ||||
| processed = inLine | processed = inLine | ||||
| .filter { line -> line.putAwayLines?.any { it.qty?.let { qty -> qty > BigDecimal.ZERO } == true } ?: false } | .filter { line -> line.putAwayLines?.any { it.qty?.let { qty -> qty > BigDecimal.ZERO } == true } ?: false } | ||||
| .sumOf { line -> line.putAwayLines?.sumOf { it.qty?.takeIf { qty -> qty > BigDecimal.ZERO } ?: BigDecimal.ZERO } ?: BigDecimal.ZERO}, | .sumOf { line -> line.putAwayLines?.sumOf { it.qty?.takeIf { qty -> qty > BigDecimal.ZERO } ?: BigDecimal.ZERO } ?: BigDecimal.ZERO}, | ||||
| receivedQty = thisPol.stockInLines.sumOf { it.acceptedQty ?: BigDecimal.ZERO }, | receivedQty = thisPol.stockInLines.sumOf { it.acceptedQty ?: BigDecimal.ZERO }, | ||||
| uom = thisPol.uom!!, | |||||
| uom = thisPol.uomM18 ?: thisPol.uom!!, | |||||
| price = thisPol.price!!, | price = thisPol.price!!, | ||||
| status = thisPol.status!!.toString(), | status = thisPol.status!!.toString(), | ||||
| stockInLine = inLine, | stockInLine = inLine, | ||||
| @@ -127,4 +127,13 @@ open class StockInLine : BaseEntity<Long>() { | |||||
| open var stockTransferRecord: StockTransferRecord? = null | open var stockTransferRecord: StockTransferRecord? = null | ||||
| fun getReceivedQtyForPol(): BigDecimal? = | fun getReceivedQtyForPol(): BigDecimal? = | ||||
| purchaseOrderLine?.stockInLines?.sumOf { it.acceptedQty ?: BigDecimal.ZERO } | purchaseOrderLine?.stockInLines?.sumOf { it.acceptedQty ?: BigDecimal.ZERO } | ||||
| /** | |||||
| * Total received qty (in M18 unit) for the same Purchase Order Line. | |||||
| * Used by QC UI so it shows PO qtyM18 / receivedQty in M18 terms. | |||||
| */ | |||||
| fun getReceivedQtyM18ForPol(): BigDecimal? = | |||||
| purchaseOrderLine?.stockInLines?.sumOf { sil -> | |||||
| sil.acceptedQtyM18?.let { BigDecimal.valueOf(it.toLong()) } ?: BigDecimal.ZERO | |||||
| } | |||||
| } | } | ||||
| @@ -24,13 +24,13 @@ interface StockInLineInfo { | |||||
| val purchaseOrderId: Long? | val purchaseOrderId: Long? | ||||
| @get:Value("#{target.jobOrder?.id}") | @get:Value("#{target.jobOrder?.id}") | ||||
| val jobOrderId: Long? | val jobOrderId: Long? | ||||
| @get:Value("#{target.receivedQtyForPol}") | |||||
| @get:Value("#{target.receivedQtyM18ForPol}") | |||||
| val receivedQty: BigDecimal? | val receivedQty: BigDecimal? | ||||
| val demandQty: BigDecimal? | val demandQty: BigDecimal? | ||||
| val acceptedQty: BigDecimal | val acceptedQty: BigDecimal | ||||
| @get:Value("#{target.acceptedQtyM18 != null ? new java.math.BigDecimal(target.acceptedQtyM18) : null}") | @get:Value("#{target.acceptedQtyM18 != null ? new java.math.BigDecimal(target.acceptedQtyM18) : null}") | ||||
| val purchaseAcceptedQty: BigDecimal? | val purchaseAcceptedQty: BigDecimal? | ||||
| @get:Value("#{target.purchaseOrderLine?.qty}") | |||||
| @get:Value("#{target.purchaseOrderLine?.qtyM18}") | |||||
| val qty: BigDecimal? | val qty: BigDecimal? | ||||
| val price: BigDecimal? | val price: BigDecimal? | ||||
| val priceUnit: BigDecimal? | val priceUnit: BigDecimal? | ||||
| @@ -46,7 +46,7 @@ interface StockInLineInfo { | |||||
| val supplier: String? | val supplier: String? | ||||
| @get:Value("#{target.item?.itemUoms.^[salesUnit == true && deleted == false]?.uom}") //TODO review | @get:Value("#{target.item?.itemUoms.^[salesUnit == true && deleted == false]?.uom}") //TODO review | ||||
| val uom: UomConversion? | val uom: UomConversion? | ||||
| @get:Value("#{target.purchaseOrderLine?.uom?.udfudesc}") | |||||
| @get:Value("#{target.purchaseOrderLine?.uomM18?.udfudesc}") | |||||
| val purchaseUomDesc: String? | val purchaseUomDesc: String? | ||||
| @get:Value("#{target.item?.itemUoms.^[stockUnit == true && deleted == false]?.uom?.udfudesc}") | @get:Value("#{target.item?.itemUoms.^[stockUnit == true && deleted == false]?.uom?.udfudesc}") | ||||
| val stockUomDesc: String? | val stockUomDesc: String? | ||||
| @@ -246,14 +246,24 @@ open class StockInLineService( | |||||
| itemNo = item.code | itemNo = item.code | ||||
| this.stockIn = stockIn | this.stockIn = stockIn | ||||
| // PO-origin: | // PO-origin: | ||||
| // 1) store user-input qty in acceptedQtyM18 (purchase unit) | |||||
| // 2) calculate stock acceptedQty with round-down | |||||
| // 1) store user-input qty in acceptedQtyM18 (M18 unit) | |||||
| // 2) calculate stock acceptedQty by converting from M18 unit | |||||
| if (pol != null && item.id != null) { | if (pol != null && item.id != null) { | ||||
| acceptedQtyM18 = request.acceptedQty.toInt() | acceptedQtyM18 = request.acceptedQty.toInt() | ||||
| acceptedQty = itemUomService.convertPurchaseQtyToStockQtyRoundDown( | |||||
| item.id!!, | |||||
| request.acceptedQty | |||||
| ) | |||||
| val m18UomId = pol.uomM18?.id | |||||
| acceptedQty = if (m18UomId != null) { | |||||
| itemUomService.convertQtyToStockQtyRoundDown( | |||||
| item.id!!, | |||||
| m18UomId, | |||||
| request.acceptedQty | |||||
| ) | |||||
| } else { | |||||
| // fallback to legacy: treat request.acceptedQty as purchase unit qty | |||||
| itemUomService.convertPurchaseQtyToStockQtyRoundDown( | |||||
| item.id!!, | |||||
| request.acceptedQty | |||||
| ) | |||||
| } | |||||
| } else { | } else { | ||||
| // Non-PO flows: keep legacy behavior | // Non-PO flows: keep legacy behavior | ||||
| acceptedQty = request.acceptedQty | acceptedQty = request.acceptedQty | ||||
| @@ -264,8 +274,19 @@ open class StockInLineService( | |||||
| this.demandQty = jo?.bom?.outputQty | this.demandQty = jo?.bom?.outputQty | ||||
| } else if (pol != null && item.id != null) { | } else if (pol != null && item.id != null) { | ||||
| pol.qty?.let { polQty -> | |||||
| this.demandQty = itemUomService.convertPurchaseQtyToStockQty(item.id!!, polQty) | |||||
| val m18UomId = pol.uomM18?.id | |||||
| val qtyM18 = pol.qtyM18 | |||||
| this.demandQty = if (m18UomId != null && qtyM18 != null) { | |||||
| itemUomService.convertQtyToStockQtyRoundDown( | |||||
| item.id!!, | |||||
| m18UomId, | |||||
| qtyM18 | |||||
| ) | |||||
| } else { | |||||
| // fallback to legacy: treat pol.qty as purchase unit qty | |||||
| pol.qty?.let { polQty -> | |||||
| itemUomService.convertPurchaseQtyToStockQty(item.id!!, polQty) | |||||
| } | |||||
| } | } | ||||
| } | } | ||||
| dnNo = request.dnNo | dnNo = request.dnNo | ||||
| @@ -527,15 +548,11 @@ open class StockInLineService( | |||||
| val antValues = byPol.map { (_, silList) -> | val antValues = byPol.map { (_, silList) -> | ||||
| val sil = silList.first() | val sil = silList.first() | ||||
| val pol = sil.purchaseOrderLine!! | val pol = sil.purchaseOrderLine!! | ||||
| // 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 { | |||||
| logger.warn("[buildGoodsReceiptNoteRequest] No itemId for POL id=${pol.id}, using stock qty as fallback (may be wrong unit for M18)") | |||||
| totalStockQty | |||||
| // For PO-origin GRN, M18 ant qty must use the M18 UOM and received M18 qty. | |||||
| val totalQtyM18 = silList.sumOf { | |||||
| it.acceptedQtyM18?.let { qty -> BigDecimal.valueOf(qty.toLong()) } ?: BigDecimal.ZERO | |||||
| } | } | ||||
| val unitIdM18 = pol.uomM18?.m18Id?.toInt() | |||||
| val unitIdFromDataLog = (pol.m18DataLog?.dataLog?.get("unitId") as? Number)?.toLong()?.toInt() | 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 | val itemName = (sil.item?.name ?: pol.item?.name).orEmpty() // always non-null for M18 bDesc/bDesc_en | ||||
| GoodsReceiptNoteAntValue( | GoodsReceiptNoteAntValue( | ||||
| @@ -544,13 +561,13 @@ open class StockInLineService( | |||||
| sourceLot = pol.m18Lot ?: "", | sourceLot = pol.m18Lot ?: "", | ||||
| proId = (sil.item?.m18Id ?: pol.item?.m18Id ?: 0L).toInt(), | proId = (sil.item?.m18Id ?: pol.item?.m18Id ?: 0L).toInt(), | ||||
| locId = 155, | locId = 155, | ||||
| unitId = unitIdFromDataLog ?: (pol.uom?.m18Id ?: 0L).toInt(), | |||||
| qty = totalQtyInPurchaseUnit.toDouble(), | |||||
| unitId = unitIdM18 ?: unitIdFromDataLog ?: (pol.uom?.m18Id ?: 0L).toInt(), | |||||
| qty = totalQtyM18.toDouble(), | |||||
| up = pol.up?.toDouble() ?: 0.0, | up = pol.up?.toDouble() ?: 0.0, | ||||
| amt = CommonUtils.getAmt( | amt = CommonUtils.getAmt( | ||||
| up = pol.up ?: BigDecimal.ZERO, | up = pol.up ?: BigDecimal.ZERO, | ||||
| discount = pol.m18Discount ?: BigDecimal.ZERO, | discount = pol.m18Discount ?: BigDecimal.ZERO, | ||||
| qty = totalQtyInPurchaseUnit | |||||
| qty = totalQtyM18 | |||||
| ), | ), | ||||
| beId = beId, | beId = beId, | ||||
| flowTypeId = flowTypeId, | flowTypeId = flowTypeId, | ||||
| @@ -735,8 +752,19 @@ open class StockInLineService( | |||||
| if (this.jobOrder != null && this.jobOrder?.bom != null) { | if (this.jobOrder != null && this.jobOrder?.bom != null) { | ||||
| // For job orders, demandQty comes from BOM's outputQty | // For job orders, demandQty comes from BOM's outputQty | ||||
| this.demandQty = this.jobOrder?.bom?.outputQty ?: this.demandQty | this.demandQty = this.jobOrder?.bom?.outputQty ?: this.demandQty | ||||
| } else if (this.purchaseOrderLine != null && this.item?.id != null && this.purchaseOrderLine?.qty != null) { | |||||
| this.demandQty = itemUomService.convertPurchaseQtyToStockQty(this.item!!.id!!, this.purchaseOrderLine!!.qty!!) | |||||
| } else if (this.purchaseOrderLine != null && this.item?.id != null) { | |||||
| val itemId = this.item!!.id!! | |||||
| val pol = this.purchaseOrderLine!! | |||||
| val m18UomId = pol.uomM18?.id | |||||
| val qtyM18 = pol.qtyM18 | |||||
| this.demandQty = if (m18UomId != null && qtyM18 != null) { | |||||
| itemUomService.convertQtyToStockQtyRoundDown(itemId, m18UomId, qtyM18) | |||||
| } else if (pol.qty != null) { | |||||
| // fallback to legacy fields when M18 fields are missing | |||||
| itemUomService.convertPurchaseQtyToStockQty(itemId, pol.qty!!) | |||||
| } else { | |||||
| this.demandQty | |||||
| } | |||||
| } | } | ||||
| // Don't overwrite demandQty with acceptQty from QC form | // Don't overwrite demandQty with acceptQty from QC form | ||||
| this.invoiceNo = request.invoiceNo | this.invoiceNo = request.invoiceNo | ||||
| @@ -22,6 +22,9 @@ open class PyController( | |||||
| private val jobOrderRepository: JobOrderRepository, | private val jobOrderRepository: JobOrderRepository, | ||||
| private val stockInLineRepository: StockInLineRepository, | private val stockInLineRepository: StockInLineRepository, | ||||
| ) { | ) { | ||||
| companion object { | |||||
| private const val PACKAGING_PROCESS_NAME = "包裝" | |||||
| } | |||||
| /** | /** | ||||
| * List job orders by planStart date. | * List job orders by planStart date. | ||||
| @@ -36,7 +39,11 @@ open class PyController( | |||||
| val date = planStart ?: LocalDate.now() | val date = planStart ?: LocalDate.now() | ||||
| val dayStart = date.atStartOfDay() | val dayStart = date.atStartOfDay() | ||||
| val dayEndExclusive = date.plusDays(1).atStartOfDay() | val dayEndExclusive = date.plusDays(1).atStartOfDay() | ||||
| val orders = jobOrderRepository.findByDeletedFalseAndPlanStartFromBeforeExclusiveOrderByIdAsc(dayStart, dayEndExclusive) | |||||
| val orders = jobOrderRepository.findByDeletedFalseAndPlanStartBetweenAndBomProcessNameOrderByIdAsc( | |||||
| dayStart, | |||||
| dayEndExclusive, | |||||
| PACKAGING_PROCESS_NAME, | |||||
| ) | |||||
| val list = orders.map { jo -> toListItem(jo) } | val list = orders.map { jo -> toListItem(jo) } | ||||
| return ResponseEntity.ok(list) | return ResponseEntity.ok(list) | ||||
| } | } | ||||