Selaa lähdekoodia

update po stock in line

reset-do-picking-order
CANCERYS\kw093 1 viikko sitten
vanhempi
commit
40027497f3
5 muutettua tiedostoa jossa 123 lisäystä ja 39 poistoa
  1. +33
    -9
      src/main/java/com/ffii/fpsms/modules/master/service/ItemUomService.kt
  2. +4
    -2
      src/main/java/com/ffii/fpsms/modules/report/service/ItemQcFailReportService.kt
  3. +6
    -6
      src/main/java/com/ffii/fpsms/modules/report/service/StockTakeVarianceReportService.kt
  4. +9
    -0
      src/main/java/com/ffii/fpsms/modules/stock/entity/projection/StockInLineInfo.kt
  5. +71
    -22
      src/main/java/com/ffii/fpsms/modules/stock/service/StockInLineService.kt

+ 33
- 9
src/main/java/com/ffii/fpsms/modules/master/service/ItemUomService.kt Näytä tiedosto

@@ -11,13 +11,14 @@ import java.math.RoundingMode
import kotlin.jvm.optionals.getOrNull
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import com.ffii.fpsms.modules.master.entity.ItemsRepository
@Service
open class ItemUomService(
val itemsService: ItemsService,
val uomConversionService: UomConversionService,
val itemUomRespository: ItemUomRespository,
val currencyService: CurrencyService
val currencyService: CurrencyService,
val itemsRepository: ItemsRepository
) {
val logger = org.slf4j.LoggerFactory.getLogger(this::class.java)

@@ -86,6 +87,16 @@ open class ItemUomService(
return stockQty;
}

/** Inverse of convertPurchaseQtyToStockQty: stock qty -> purchase qty (for PO-origin StockInLine display). */
open fun convertStockQtyToPurchaseQty(itemId: Long, stockQty: BigDecimal): BigDecimal {
val purchaseUnit = findPurchaseUnitByItemId(itemId) ?: return stockQty
val stockUnit = findStockUnitByItemId(itemId) ?: return stockQty
val one = BigDecimal.ONE
val baseQty = stockQty.multiply(stockUnit.ratioN ?: one).divide(stockUnit.ratioD ?: one, 2, RoundingMode.UP)
val purchaseQty = baseQty.multiply(purchaseUnit.ratioD ?: one).divide(purchaseUnit.ratioN ?: one, 2, RoundingMode.UP)
return purchaseQty
}

open fun convertQtyToStockQty(itemId: Long, uomId: Long, sourceQty: BigDecimal): BigDecimal {
val itemUom = findFirstByItemIdAndUomId(itemId, uomId) ?: return sourceQty;

@@ -107,7 +118,8 @@ open class ItemUomService(
// return itemUom
//}

val item = request.itemId?.let { itemsService.find(it).getOrNull() }
val item = request.itemId?.let { itemsRepository.findById(it).getOrNull() }
?: request.itemId?.let { itemsRepository.findByIdAndDeletedFalse(it) } // 取決於你 repo 有沒有這種方法
val uom = request.m18UomId?.let { uomConversionService.findByM18Id(it) } ?: request.uomId?.let { uomConversionService.find(it).getOrNull() }
val currency = request.currencyId?.let { currencyService.findById(it) }

@@ -218,13 +230,25 @@ open class ItemUomService(
}
}
// var sourceItemUom = itemUomRespository.findFirstByItemIdAndUomIdAndDeletedIsFalse(request.itemId, request.uomId)

// 這裡先查 items、uom_conversion,準備錯誤訊息要用的 code
val item = itemsRepository.findById(request.itemId).getOrNull()
val itemCode = item?.code ?: request.itemId.toString()
val uom = uomConversionService.findById(request.uomId)
val uomCode = uom?.code ?: request.uomId.toString()
// If still no source ItemUom found, throw error
sourceItemUom ?: throw IllegalArgumentException("Source ItemUom not found for itemId=${request.itemId}, uomId=${request.uomId}")
// Find target ItemUom by itemId and targetUnit
sourceItemUom ?: throw IllegalArgumentException(
"Source ItemUom not found for items.code=$itemCode, uom_conversion.code=$uomCode"
)
// Find target ItemUom by itemId and targetUnit(這段可視需求決定要不要也查 code)
val targetItemUom = findTargetItemUom(request.itemId, request.targetUnit)
?: throw IllegalArgumentException("Target ItemUom not found for itemId=${request.itemId}, targetUnit=${request.targetUnit}")
?: throw IllegalArgumentException(
"Target ItemUom not found for items.code=$itemCode, targetUnit=${request.targetUnit}"
)
// Convert quantity using ratioN/ratioD via base unit
val one = BigDecimal.ONE
val sourceRatioN = sourceItemUom.ratioN ?: one


+ 4
- 2
src/main/java/com/ffii/fpsms/modules/report/service/ItemQcFailReportService.kt Näytä tiedosto

@@ -58,8 +58,10 @@ open class ItemQcFailReportService(
COALESCE(qic.description, qi.description, qi.name, '') AS qcDefectCriteria,

/* Lot Qty / Defect Qty:按 jrxml 字段类型输出 String */
TRIM(TRAILING '.' FROM TRIM(TRAILING '0' FROM FORMAT(COALESCE(
CASE WHEN sil.purchaseOrderId IS NOT NULL AND iu_purchase.id IS NOT NULL AND iu.id IS NOT NULL
TRIM(TRAILING '.' FROM TRIM(TRAILING '0' FROM FORMAT( COALESCE(
CASE WHEN sil.purchaseOrderLineId IS NOT NULL
THEN sil.acceptedQty
WHEN iu_purchase.id IS NOT NULL AND iu.id IS NOT NULL
THEN sil.acceptedQty * (iu_purchase.ratioN / NULLIF(iu_purchase.ratioD, 0)) / (iu.ratioN / NULLIF(iu.ratioD, 0))
ELSE sil.acceptedQty END, 0), 2))) AS lotQty,
TRIM(TRAILING '.' FROM TRIM(TRAILING '0' FROM FORMAT(COALESCE(qr.failQty, 0), 2))) AS defectQty,


+ 6
- 6
src/main/java/com/ffii/fpsms/modules/report/service/StockTakeVarianceReportService.kt Näytä tiedosto

@@ -120,17 +120,17 @@ in_agg AS (
SELECT
ill.id AS inventoryLotLineId,
SUM(CASE WHEN DATE(sil.receiptDate) < :fromDate THEN
CASE WHEN sil.purchaseOrderId IS NOT NULL
AND iu_purchase.id IS NOT NULL
AND iu_stock.id IS NOT NULL
CASE WHEN sil.purchaseOrderLineId IS NOT NULL
THEN COALESCE(sil.acceptedQty, 0)
WHEN iu_purchase.id IS NOT NULL AND iu_stock.id IS NOT NULL
THEN COALESCE(sil.acceptedQty, 0) * (iu_purchase.ratioN / NULLIF(iu_purchase.ratioD, 0)) / (iu_stock.ratioN / NULLIF(iu_stock.ratioD, 0))
ELSE COALESCE(sil.acceptedQty, 0)
END
ELSE 0 END) AS inBefore,
SUM(CASE WHEN DATE(sil.receiptDate) BETWEEN :fromDate AND :toDate THEN
CASE WHEN sil.purchaseOrderId IS NOT NULL
AND iu_purchase.id IS NOT NULL
AND iu_stock.id IS NOT NULL
CASE WHEN sil.purchaseOrderLineId IS NOT NULL
THEN COALESCE(sil.acceptedQty, 0)
WHEN iu_purchase.id IS NOT NULL AND iu_stock.id IS NOT NULL
THEN COALESCE(sil.acceptedQty, 0) * (iu_purchase.ratioN / NULLIF(iu_purchase.ratioD, 0)) / (iu_stock.ratioN / NULLIF(iu_stock.ratioD, 0))
ELSE COALESCE(sil.acceptedQty, 0)
END


+ 9
- 0
src/main/java/com/ffii/fpsms/modules/stock/entity/projection/StockInLineInfo.kt Näytä tiedosto

@@ -28,6 +28,8 @@ interface StockInLineInfo {
val receivedQty: BigDecimal?
val demandQty: BigDecimal?
val acceptedQty: BigDecimal
@get:Value("#{target.purchaseOrderLine != null && target.item != null && target.acceptedQty != null ? @itemUomService.convertStockQtyToPurchaseQty(target.item.id, target.acceptedQty) : null}")
val purchaseAcceptedQty: BigDecimal?
@get:Value("#{target.purchaseOrderLine?.qty}")
val qty: BigDecimal?
val price: BigDecimal?
@@ -44,6 +46,10 @@ interface StockInLineInfo {
val supplier: String?
@get:Value("#{target.item?.itemUoms.^[salesUnit == true && deleted == false]?.uom}") //TODO review
val uom: UomConversion?
@get:Value("#{target.purchaseOrderLine?.uom?.udfudesc}")
val purchaseUomDesc: String?
@get:Value("#{target.item?.itemUoms.^[stockUnit == true && deleted == false]?.uom?.udfudesc}")
val stockUomDesc: String?
@get:Value("#{target.stockIn?.purchaseOrder?.code}")
val poCode: String?
@get:Value("#{target.jobOrder?.code}")
@@ -61,6 +67,9 @@ interface StockInLineInfo {

interface PutAwayLineForSil {
val id: Long?;
/** Stock qty (inventory lot line inQty) */
@get:Value("#{target.inQty}")
val stockQty: BigDecimal?;
@get:Value("#{target.inQty " +
"* ((target.inventoryLot.item.itemUoms.^[stockUnit == true && deleted == false]?.ratioN / target.inventoryLot.item.itemUoms.^[stockUnit == true && deleted == false]?.ratioD))" +
"/ ((target.inventoryLot.item.itemUoms.^[purchaseUnit == true && deleted == false]?.ratioN / target.inventoryLot.item.itemUoms.^[purchaseUnit == true && deleted == false]?.ratioD))}")


+ 71
- 22
src/main/java/com/ffii/fpsms/modules/stock/service/StockInLineService.kt Näytä tiedosto

@@ -27,6 +27,7 @@ import com.ffii.fpsms.modules.jobOrder.entity.JobOrderRepository
import com.ffii.fpsms.modules.jobOrder.enums.JobOrderStatus
import com.ffii.fpsms.modules.master.entity.ItemUomRespository
import com.ffii.fpsms.modules.master.entity.WarehouseRepository
import com.ffii.fpsms.modules.master.service.ItemUomService
import com.ffii.fpsms.modules.master.service.PrinterService
import com.ffii.fpsms.modules.purchaseOrder.entity.PurchaseOrder
import com.ffii.fpsms.modules.purchaseOrder.entity.PurchaseOrderLine
@@ -87,6 +88,7 @@ open class StockInLineService(
private val itemRepository: ItemsRepository,
private val warehouseRepository: WarehouseRepository,
private val itemUomRepository: ItemUomRespository,
private val itemUomService: ItemUomService,
private val printerService: PrinterService,
private val stockTakeLineRepository: StockTakeLineRepository,
private val inventoryLotLineService: InventoryLotLineService,
@@ -194,15 +196,21 @@ open class StockInLineService(
this.item = item
itemNo = item.code
this.stockIn = stockIn
acceptedQty = request.acceptedQty
// PO-origin: store in stock qty; others: store as received
acceptedQty = if (pol != null && item.id != null) {
itemUomService.convertPurchaseQtyToStockQty(item.id!!, request.acceptedQty ?: BigDecimal.ZERO)
} else {
request.acceptedQty
}
// Set demandQty based on source
if (jo != null && jo?.bom != null) {
// For job orders, demandQty comes from BOM's outputQty
this.demandQty = jo?.bom?.outputQty

} else if (pol != null) {
// For purchase orders, demandQty comes from PurchaseOrderLine's qty
this.demandQty = pol.qty
} else if (pol != null && item.id != null) {
pol.qty?.let { polQty ->
this.demandQty = itemUomService.convertPurchaseQtyToStockQty(item.id!!, polQty)
}
}
dnNo = request.dnNo
receiptDate = request.receiptDate?.atStartOfDay() ?: LocalDateTime.now()
@@ -264,7 +272,10 @@ open class StockInLineService(
itemId = request.itemId
)
val purchaseItemUom = itemUomRepository.findByItemIdAndPurchaseUnitIsTrueAndDeletedIsFalse(request.itemId)
val convertedBaseQty = if (request.stockTakeLineId == null && stockItemUom != null && purchaseItemUom != null) {
// PO-origin: frontend sends qty in stock; non-PO: treat as purchase and convert to stock
val convertedBaseQty = if (stockInLine.purchaseOrderLine != null) {
line.qty
} else if (request.stockTakeLineId == null && stockItemUom != null && purchaseItemUom != null) {
(line.qty) * (purchaseItemUom.ratioN!! / purchaseItemUom.ratioD!!) / (stockItemUom.ratioN!! / stockItemUom.ratioD!!)
} else {
(line.qty)
@@ -631,14 +642,21 @@ open class StockInLineService(
this.productLotNo = request.productLotNo ?: this.productLotNo
this.dnNo = request.dnNo ?: this.dnNo
// this.dnDate = request.dnDate?.atStartOfDay() ?: this.dnDate
this.acceptedQty = request.acceptQty ?: request.acceptedQty ?: this.acceptedQty
// QC and PutAway should never overwrite acceptedQty (received qty).
if (request.qcAccept != true && request.status != StockInLineStatus.RECEIVED.status) {
val requestQty = request.acceptQty ?: request.acceptedQty
this.acceptedQty = if (this.purchaseOrderLine != null && this.item?.id != null && requestQty != null) {
itemUomService.convertPurchaseQtyToStockQty(this.item!!.id!!, requestQty)
} else {
requestQty ?: this.acceptedQty
}
}
// Set demandQty based on source
if (this.jobOrder != null && this.jobOrder?.bom != null) {
// For job orders, demandQty comes from BOM's outputQty
this.demandQty = this.jobOrder?.bom?.outputQty ?: this.demandQty
} else if (this.purchaseOrderLine != null) {
// For purchase orders, demandQty comes from PurchaseOrderLine's qty
this.demandQty = this.purchaseOrderLine?.qty ?: 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!!)
}
// Don't overwrite demandQty with acceptQty from QC form
this.invoiceNo = request.invoiceNo
@@ -655,29 +673,58 @@ open class StockInLineService(
val savedInventoryLotLines = saveInventoryLotLineWhenStockIn(request = request, stockInLine = stockInLine)
val inventoryLotLines = stockInLine.inventoryLot?.let { it.id?.let { _id -> inventoryLotLineRepository.findAllByInventoryLotId(_id) } } ?: listOf()

val purchaseItemUom = itemUomRepository.findByItemIdAndPurchaseUnitIsTrueAndDeletedIsFalse(request.itemId)
val stockItemUom = itemUomRepository.findByItemIdAndStockUnitIsTrueAndDeletedIsFalse(request.itemId)
val ratio = if (request.stockTakeLineId == null && stockItemUom != null && purchaseItemUom != null) {
(purchaseItemUom.ratioN!! / purchaseItemUom.ratioD!!) / (stockItemUom.ratioN!! / stockItemUom.ratioD!!)
// ✅ 每次上架都寫一筆 stock_ledger(inQty = 本次上架的庫存數量)
val putAwayDeltaStockQty = (request.inventoryLotLines ?: listOf()).sumOf { line ->
val stockItemUom = itemUomRepository.findBaseUnitByItemIdAndStockUnitIsTrueAndDeletedIsFalse(
itemId = request.itemId
)
val purchaseItemUom = itemUomRepository.findByItemIdAndPurchaseUnitIsTrueAndDeletedIsFalse(request.itemId)

val convertedBaseQty = if (stockInLine.purchaseOrderLine != null) {
// PO-origin: qty is already stock qty
line.qty
} else if (request.stockTakeLineId == null && stockItemUom != null && purchaseItemUom != null) {
// Legacy: treat as purchase qty, convert to stock qty
(line.qty) * (purchaseItemUom.ratioN!! / purchaseItemUom.ratioD!!) / (stockItemUom.ratioN!! / stockItemUom.ratioD!!)
} else {
line.qty
}
convertedBaseQty
}
if (putAwayDeltaStockQty > BigDecimal.ZERO) {
createStockLedgerForStockIn(stockInLine, putAwayDeltaStockQty.toDouble())
}

val putAwayStockQty = inventoryLotLines.sumOf { it.inQty ?: BigDecimal.ZERO }

// PO-origin: acceptedQty is already STOCK qty; Non-PO: keep legacy rule (acceptQty is purchase qty)
val requiredStockQty = if (stockInLine.purchaseOrderLine != null) {
stockInLine.acceptedQty ?: BigDecimal.ZERO
} else {
BigDecimal.ONE
val purchaseItemUom = itemUomRepository.findByItemIdAndPurchaseUnitIsTrueAndDeletedIsFalse(request.itemId)
val stockItemUom = itemUomRepository.findByItemIdAndStockUnitIsTrueAndDeletedIsFalse(request.itemId)
val ratio = if (request.stockTakeLineId == null && stockItemUom != null && purchaseItemUom != null) {
(purchaseItemUom.ratioN!! / purchaseItemUom.ratioD!!) / (stockItemUom.ratioN!! / stockItemUom.ratioD!!)
} else {
BigDecimal.ONE
}
(request.acceptQty ?: request.acceptedQty)?.times(ratio) ?: BigDecimal.ZERO
}

if (inventoryLotLines.sumOf { it.inQty ?: BigDecimal.ZERO } >= request.acceptQty?.times(ratio)) {
if (putAwayStockQty >= requiredStockQty) {
stockInLine.apply {
val isWipJobOrder = stockInLine.jobOrder?.bom?.description == "WIP"
this.status = if (isWipJobOrder) {
StockInLineStatus.COMPLETE.status
} else {
// For non-WIP, use original logic
if (request.acceptQty?.compareTo(request.acceptedQty) == 0)
if (putAwayStockQty.compareTo(requiredStockQty) == 0)
StockInLineStatus.COMPLETE.status
else
StockInLineStatus.PARTIALLY_COMPLETE.status
}
// this.inventoryLotLine = savedInventoryLotLine
}
createStockLedgerForStockIn(stockInLine)
// Update JO Status
if (stockInLine.jobOrder != null) { //TODO Improve
val jo = stockInLine.jobOrder
@@ -789,7 +836,10 @@ open class StockInLineService(
info.itemId
)
val purchaseItemUom = itemUomRepository.findByItemIdAndPurchaseUnitIsTrueAndDeletedIsFalse(info.itemId)
val acceptedQty = if (stockItemUom != null && purchaseItemUom != null) {
// PO-origin: acceptedQty is already stock qty; non-PO: convert purchase -> stock for display
val acceptedQty = if (info.purchaseOrderLineId != null) {
info.acceptedQty
} else if (stockItemUom != null && purchaseItemUom != null) {
(info.acceptedQty) * (purchaseItemUom.ratioN!! / purchaseItemUom.ratioD!!) / (stockItemUom.ratioN!! / stockItemUom.ratioD!!)
} else {
(info.acceptedQty)
@@ -936,10 +986,9 @@ open class StockInLineService(
}
@Transactional

private fun createStockLedgerForStockIn(stockInLine: StockInLine) {
private fun createStockLedgerForStockIn(stockInLine: StockInLine, inQty: Double) {
val item = stockInLine.item ?: return
val inventory = inventoryRepository.findFirstByItemIdAndDeletedIsFalseOrderByIdAsc(item.id!!) ?: return
val inQty = stockInLine.acceptedQty?.toDouble() ?: 0.0
// ✅ 修复:查询最新的 stock_ledger 记录,基于前一笔 balance 计算
val latestLedger = stockLedgerRepository.findLatestByItemId(item.id!!).firstOrNull()
@@ -1047,7 +1096,7 @@ open class StockInLineService(
saveAndFlush(savedStockInLine)

// Step 5: Create Stock Ledger entry
createStockLedgerForStockIn(savedStockInLine)
createStockLedgerForStockIn(savedStockInLine, request.acceptedQty.toDouble())

return savedStockInLine
}
@@ -1095,7 +1144,7 @@ open class StockInLineService(
val savedStockInLine = saveAndFlush(stockInLine)

// Step 3: Create Stock Ledger entry
createStockLedgerForStockIn(savedStockInLine)
createStockLedgerForStockIn(savedStockInLine, request.acceptedQty.toDouble())

return savedStockInLine
}


Ladataan…
Peruuta
Tallenna