|
|
@@ -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.jobOrder.enums.JobOrderStatus |
|
|
import com.ffii.fpsms.modules.master.entity.ItemUomRespository |
|
|
import com.ffii.fpsms.modules.master.entity.ItemUomRespository |
|
|
import com.ffii.fpsms.modules.master.entity.WarehouseRepository |
|
|
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.master.service.PrinterService |
|
|
import com.ffii.fpsms.modules.purchaseOrder.entity.PurchaseOrder |
|
|
import com.ffii.fpsms.modules.purchaseOrder.entity.PurchaseOrder |
|
|
import com.ffii.fpsms.modules.purchaseOrder.entity.PurchaseOrderLine |
|
|
import com.ffii.fpsms.modules.purchaseOrder.entity.PurchaseOrderLine |
|
|
@@ -87,6 +88,7 @@ open class StockInLineService( |
|
|
private val itemRepository: ItemsRepository, |
|
|
private val itemRepository: ItemsRepository, |
|
|
private val warehouseRepository: WarehouseRepository, |
|
|
private val warehouseRepository: WarehouseRepository, |
|
|
private val itemUomRepository: ItemUomRespository, |
|
|
private val itemUomRepository: ItemUomRespository, |
|
|
|
|
|
private val itemUomService: ItemUomService, |
|
|
private val printerService: PrinterService, |
|
|
private val printerService: PrinterService, |
|
|
private val stockTakeLineRepository: StockTakeLineRepository, |
|
|
private val stockTakeLineRepository: StockTakeLineRepository, |
|
|
private val inventoryLotLineService: InventoryLotLineService, |
|
|
private val inventoryLotLineService: InventoryLotLineService, |
|
|
@@ -194,15 +196,21 @@ open class StockInLineService( |
|
|
this.item = item |
|
|
this.item = item |
|
|
itemNo = item.code |
|
|
itemNo = item.code |
|
|
this.stockIn = stockIn |
|
|
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 |
|
|
// Set demandQty based on source |
|
|
if (jo != null && jo?.bom != null) { |
|
|
if (jo != null && jo?.bom != null) { |
|
|
// For job orders, demandQty comes from BOM's outputQty |
|
|
// For job orders, demandQty comes from BOM's outputQty |
|
|
this.demandQty = jo?.bom?.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 |
|
|
dnNo = request.dnNo |
|
|
receiptDate = request.receiptDate?.atStartOfDay() ?: LocalDateTime.now() |
|
|
receiptDate = request.receiptDate?.atStartOfDay() ?: LocalDateTime.now() |
|
|
@@ -264,7 +272,10 @@ open class StockInLineService( |
|
|
itemId = request.itemId |
|
|
itemId = request.itemId |
|
|
) |
|
|
) |
|
|
val purchaseItemUom = itemUomRepository.findByItemIdAndPurchaseUnitIsTrueAndDeletedIsFalse(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!!) |
|
|
(line.qty) * (purchaseItemUom.ratioN!! / purchaseItemUom.ratioD!!) / (stockItemUom.ratioN!! / stockItemUom.ratioD!!) |
|
|
} else { |
|
|
} else { |
|
|
(line.qty) |
|
|
(line.qty) |
|
|
@@ -631,14 +642,21 @@ open class StockInLineService( |
|
|
this.productLotNo = request.productLotNo ?: this.productLotNo |
|
|
this.productLotNo = request.productLotNo ?: this.productLotNo |
|
|
this.dnNo = request.dnNo ?: this.dnNo |
|
|
this.dnNo = request.dnNo ?: this.dnNo |
|
|
// this.dnDate = request.dnDate?.atStartOfDay() ?: this.dnDate |
|
|
// 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 |
|
|
// Set demandQty based on source |
|
|
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) { |
|
|
|
|
|
// 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 |
|
|
// Don't overwrite demandQty with acceptQty from QC form |
|
|
this.invoiceNo = request.invoiceNo |
|
|
this.invoiceNo = request.invoiceNo |
|
|
@@ -655,29 +673,58 @@ open class StockInLineService( |
|
|
val savedInventoryLotLines = saveInventoryLotLineWhenStockIn(request = request, stockInLine = stockInLine) |
|
|
val savedInventoryLotLines = saveInventoryLotLineWhenStockIn(request = request, stockInLine = stockInLine) |
|
|
val inventoryLotLines = stockInLine.inventoryLot?.let { it.id?.let { _id -> inventoryLotLineRepository.findAllByInventoryLotId(_id) } } ?: listOf() |
|
|
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 { |
|
|
} 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 { |
|
|
stockInLine.apply { |
|
|
val isWipJobOrder = stockInLine.jobOrder?.bom?.description == "WIP" |
|
|
val isWipJobOrder = stockInLine.jobOrder?.bom?.description == "WIP" |
|
|
this.status = if (isWipJobOrder) { |
|
|
this.status = if (isWipJobOrder) { |
|
|
StockInLineStatus.COMPLETE.status |
|
|
StockInLineStatus.COMPLETE.status |
|
|
} else { |
|
|
} else { |
|
|
// For non-WIP, use original logic |
|
|
// For non-WIP, use original logic |
|
|
if (request.acceptQty?.compareTo(request.acceptedQty) == 0) |
|
|
|
|
|
|
|
|
if (putAwayStockQty.compareTo(requiredStockQty) == 0) |
|
|
StockInLineStatus.COMPLETE.status |
|
|
StockInLineStatus.COMPLETE.status |
|
|
else |
|
|
else |
|
|
StockInLineStatus.PARTIALLY_COMPLETE.status |
|
|
StockInLineStatus.PARTIALLY_COMPLETE.status |
|
|
} |
|
|
} |
|
|
// this.inventoryLotLine = savedInventoryLotLine |
|
|
// this.inventoryLotLine = savedInventoryLotLine |
|
|
} |
|
|
} |
|
|
createStockLedgerForStockIn(stockInLine) |
|
|
|
|
|
// Update JO Status |
|
|
// Update JO Status |
|
|
if (stockInLine.jobOrder != null) { //TODO Improve |
|
|
if (stockInLine.jobOrder != null) { //TODO Improve |
|
|
val jo = stockInLine.jobOrder |
|
|
val jo = stockInLine.jobOrder |
|
|
@@ -789,7 +836,10 @@ open class StockInLineService( |
|
|
info.itemId |
|
|
info.itemId |
|
|
) |
|
|
) |
|
|
val purchaseItemUom = itemUomRepository.findByItemIdAndPurchaseUnitIsTrueAndDeletedIsFalse(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!!) |
|
|
(info.acceptedQty) * (purchaseItemUom.ratioN!! / purchaseItemUom.ratioD!!) / (stockItemUom.ratioN!! / stockItemUom.ratioD!!) |
|
|
} else { |
|
|
} else { |
|
|
(info.acceptedQty) |
|
|
(info.acceptedQty) |
|
|
@@ -936,10 +986,9 @@ open class StockInLineService( |
|
|
} |
|
|
} |
|
|
@Transactional |
|
|
@Transactional |
|
|
|
|
|
|
|
|
private fun createStockLedgerForStockIn(stockInLine: StockInLine) { |
|
|
|
|
|
|
|
|
private fun createStockLedgerForStockIn(stockInLine: StockInLine, inQty: Double) { |
|
|
val item = stockInLine.item ?: return |
|
|
val item = stockInLine.item ?: return |
|
|
val inventory = inventoryRepository.findFirstByItemIdAndDeletedIsFalseOrderByIdAsc(item.id!!) ?: return |
|
|
val inventory = inventoryRepository.findFirstByItemIdAndDeletedIsFalseOrderByIdAsc(item.id!!) ?: return |
|
|
val inQty = stockInLine.acceptedQty?.toDouble() ?: 0.0 |
|
|
|
|
|
|
|
|
|
|
|
// ✅ 修复:查询最新的 stock_ledger 记录,基于前一笔 balance 计算 |
|
|
// ✅ 修复:查询最新的 stock_ledger 记录,基于前一笔 balance 计算 |
|
|
val latestLedger = stockLedgerRepository.findLatestByItemId(item.id!!).firstOrNull() |
|
|
val latestLedger = stockLedgerRepository.findLatestByItemId(item.id!!).firstOrNull() |
|
|
@@ -1047,7 +1096,7 @@ open class StockInLineService( |
|
|
saveAndFlush(savedStockInLine) |
|
|
saveAndFlush(savedStockInLine) |
|
|
|
|
|
|
|
|
// Step 5: Create Stock Ledger entry |
|
|
// Step 5: Create Stock Ledger entry |
|
|
createStockLedgerForStockIn(savedStockInLine) |
|
|
|
|
|
|
|
|
createStockLedgerForStockIn(savedStockInLine, request.acceptedQty.toDouble()) |
|
|
|
|
|
|
|
|
return savedStockInLine |
|
|
return savedStockInLine |
|
|
} |
|
|
} |
|
|
@@ -1095,7 +1144,7 @@ open class StockInLineService( |
|
|
val savedStockInLine = saveAndFlush(stockInLine) |
|
|
val savedStockInLine = saveAndFlush(stockInLine) |
|
|
|
|
|
|
|
|
// Step 3: Create Stock Ledger entry |
|
|
// Step 3: Create Stock Ledger entry |
|
|
createStockLedgerForStockIn(savedStockInLine) |
|
|
|
|
|
|
|
|
createStockLedgerForStockIn(savedStockInLine, request.acceptedQty.toDouble()) |
|
|
|
|
|
|
|
|
return savedStockInLine |
|
|
return savedStockInLine |
|
|
} |
|
|
} |
|
|
|