CANCERYS\kw093 4 дней назад
Родитель
Сommit
99f7c59de1
8 измененных файлов: 164 добавлений и 16 удалений
  1. +45
    -0
      src/main/java/com/ffii/fpsms/modules/jobOrder/service/JoPickOrderService.kt
  2. +16
    -0
      src/main/java/com/ffii/fpsms/modules/jobOrder/web/model/CreateJobOrderRequest.kt
  3. +5
    -1
      src/main/java/com/ffii/fpsms/modules/pickOrder/service/PickOrderService.kt
  4. +6
    -0
      src/main/java/com/ffii/fpsms/modules/stock/entity/InventoryLotLineRepository.kt
  5. +2
    -0
      src/main/java/com/ffii/fpsms/modules/stock/entity/SuggestPickLotRepository.kt
  6. +57
    -13
      src/main/java/com/ffii/fpsms/modules/stock/service/StockOutLineService.kt
  7. +32
    -2
      src/main/java/com/ffii/fpsms/modules/stock/service/SuggestedPickLotService.kt
  8. +1
    -0
      src/main/java/com/ffii/fpsms/modules/stock/web/model/SaveStockOutRequest.kt

+ 45
- 0
src/main/java/com/ffii/fpsms/modules/jobOrder/service/JoPickOrderService.kt Просмотреть файл

@@ -45,6 +45,7 @@ import com.ffii.fpsms.modules.jobOrder.web.model.PickOrderInfoResponse
import com.ffii.fpsms.modules.jobOrder.web.model.JobOrderBasicInfoResponse
import com.ffii.fpsms.modules.jobOrder.web.model.PickOrderLineWithLotsResponse
import com.ffii.fpsms.modules.jobOrder.web.model.LotDetailResponse
import com.ffii.fpsms.modules.jobOrder.web.model.StockOutLineDetailResponse

import com.ffii.fpsms.modules.stock.entity.enum.StockInLineStatus
import com.ffii.fpsms.modules.stock.entity.StockInLineRepository
@@ -1960,6 +1961,23 @@ open fun getJobOrderLotsHierarchicalByPickOrderId(pickOrderId: Long): JobOrderLo
val stockOutLinesByPickOrderLine = pickOrderLineIds.associateWith { polId ->
stockOutLineRepository.findAllByPickOrderLineIdAndDeletedFalse(polId)
}

// stockouts 可能包含不在 suggestedPickLots 內的 inventoryLotLineId,需補齊以便計算 location/availableQty
val stockOutInventoryLotLineIds = stockOutLinesByPickOrderLine.values
.flatten()
.mapNotNull { it.inventoryLotLineId }
.distinct()

val stockOutInventoryLotLines = if (stockOutInventoryLotLineIds.isNotEmpty()) {
inventoryLotLineRepository.findAllByIdIn(stockOutInventoryLotLineIds)
.filter { it.deleted == false }
} else {
emptyList()
}

val inventoryLotLineById = (inventoryLotLines + stockOutInventoryLotLines)
.filter { it.id != null }
.associateBy { it.id!! }
// 获取 stock in lines 通过 inventoryLotLineId(用于填充 stockInLineId)
val stockInLinesByInventoryLotLineId = if (inventoryLotLineIds.isNotEmpty()) {
@@ -2080,6 +2098,32 @@ open fun getJobOrderLotsHierarchicalByPickOrderId(pickOrderId: Long): JobOrderLo
matchQty = jpo?.matchQty?.toDouble()
)
}

// 构建 stockouts 数据:用于无 suggested lot / noLot 场景也能显示并闭环(submit 0)
val stockouts = (stockOutLinesByPickOrderLine[lineId] ?: emptyList()).map { sol ->
val illId = sol.inventoryLotLineId
val ill = if (illId != null) inventoryLotLineById[illId] else null
val lot = ill?.inventoryLot
val warehouse = ill?.warehouse
val availableQty = if (sol.status == "rejected") {
null
} else if (ill == null || ill.deleted == true) {
null
} else {
(ill.inQty ?: BigDecimal.ZERO) - (ill.outQty ?: BigDecimal.ZERO) - (ill.holdQty ?: BigDecimal.ZERO)
}

StockOutLineDetailResponse(
id = sol.id,
status = sol.status,
qty = sol.qty.toDouble(),
lotId = illId,
lotNo = sol.lotNo ?: lot?.lotNo,
location = warehouse?.code,
availableQty = availableQty?.toDouble(),
noLot = (illId == null)
)
}
PickOrderLineWithLotsResponse(
id = pol.id!!,
@@ -2091,6 +2135,7 @@ open fun getJobOrderLotsHierarchicalByPickOrderId(pickOrderId: Long): JobOrderLo
uomDesc = uom?.udfudesc,
status = pol.status?.value,
lots = lots,
stockouts = stockouts,
handler=handlerName
)
}


+ 16
- 0
src/main/java/com/ffii/fpsms/modules/jobOrder/web/model/CreateJobOrderRequest.kt Просмотреть файл

@@ -96,9 +96,25 @@ data class PickOrderLineWithLotsResponse(
val uomDesc: String?,
val status: String?,
val lots: List<LotDetailResponse>,
val stockouts: List<StockOutLineDetailResponse> = emptyList(),
val handler: String?
)

/**
* Stock-out line rows that should be shown even when there is no suggested lot.
* `noLot=true` indicates this line currently has no lot assigned / insufficient inventory lot.
*/
data class StockOutLineDetailResponse(
val id: Long?,
val status: String?,
val qty: Double?,
val lotId: Long?,
val lotNo: String?,
val location: String?,
val availableQty: Double?,
val noLot: Boolean
)

data class LotDetailResponse(
val lotId: Long?,
val lotNo: String?,


+ 5
- 1
src/main/java/com/ffii/fpsms/modules/pickOrder/service/PickOrderService.kt Просмотреть файл

@@ -4017,7 +4017,11 @@ println("DEBUG sol polIds in linesResults: " + linesResults.mapNotNull { it["sto
// Fallback lotNo (req.newInventoryLotNo is non-null String in your model)
if (req.newInventoryLotNo.isNotBlank()) {
return inventoryLotLineRepository.findByLotNoAndItemId(req.newInventoryLotNo, itemId)
return inventoryLotLineRepository
.findFirstByInventoryLotLotNoAndInventoryLotItemIdAndDeletedFalseOrderByIdDesc(
req.newInventoryLotNo,
itemId
)
}
return null


+ 6
- 0
src/main/java/com/ffii/fpsms/modules/stock/entity/InventoryLotLineRepository.kt Просмотреть файл

@@ -52,6 +52,12 @@ interface InventoryLotLineRepository : AbstractRepository<InventoryLotLine, Long
AND ill.deleted = false
""")
fun findByLotNoAndItemId(lotNo: String, itemId: Long): InventoryLotLine?

// lotNo + itemId may not be unique (multiple warehouses/lines); pick one deterministically
fun findFirstByInventoryLotLotNoAndInventoryLotItemIdAndDeletedFalseOrderByIdDesc(
lotNo: String,
itemId: Long
): InventoryLotLine?
// InventoryLotLineRepository.kt 中添加
@Query("SELECT ill FROM InventoryLotLine ill WHERE ill.warehouse.id IN :warehouseIds AND ill.deleted = false")
fun findAllByWarehouseIdInAndDeletedIsFalse(@Param("warehouseIds") warehouseIds: List<Long>): List<InventoryLotLine>


+ 2
- 0
src/main/java/com/ffii/fpsms/modules/stock/entity/SuggestPickLotRepository.kt Просмотреть файл

@@ -11,4 +11,6 @@ interface SuggestPickLotRepository : AbstractRepository<SuggestedPickLot, Long>
fun findFirstByPickOrderLineId(pickOrderLineId: Long): SuggestedPickLot?
fun findAllByPickOrderLineId(pickOrderLineId: Long): List<SuggestedPickLot>

fun findFirstByStockOutLineId(stockOutLineId: Long): SuggestedPickLot?
}

+ 57
- 13
src/main/java/com/ffii/fpsms/modules/stock/service/StockOutLineService.kt Просмотреть файл

@@ -43,6 +43,8 @@ import com.ffii.fpsms.modules.stock.entity.StockLedgerRepository
import com.ffii.fpsms.modules.stock.entity.InventoryRepository
import com.ffii.fpsms.modules.pickOrder.entity.PickExecutionIssueRepository
import java.time.LocalTime
import com.ffii.fpsms.modules.stock.entity.StockInLineRepository
import com.ffii.fpsms.modules.stock.entity.SuggestPickLotRepository
@Service
open class StockOutLineService(
private val jdbcDao: JdbcDao,
@@ -53,7 +55,9 @@ open class StockOutLineService(
private val itemUomRespository: ItemUomRespository,
private val pickOrderRepository: PickOrderRepository,
private val inventoryLotLineRepository: InventoryLotLineRepository,
private val stockInLineRepository: StockInLineRepository,
@Lazy private val suggestedPickLotService: SuggestedPickLotService,
private val suggestPickLotRepository: SuggestPickLotRepository,
private val inventoryLotRepository: InventoryLotRepository,
private val doPickOrderRepository: DoPickOrderRepository,
private val doPickOrderRecordRepository: DoPickOrderRecordRepository,
@@ -946,22 +950,62 @@ open fun updateStockOutLineStatusByQRCodeAndLotNo(request: UpdateStockOutLineSta
// Step 2: Get InventoryLotLine
val getInventoryLotLineStart = System.currentTimeMillis()
// 修复:从 stockOutLine.inventoryLotLine 获取 inventoryLot,而不是使用错误的参数
val inventoryLotLine = stockOutLine.inventoryLotLine
// If StockOutLine has no lot (noLot row), resolve InventoryLotLine by scanned lotNo + itemId and bind it
var inventoryLotLine = stockOutLine.inventoryLotLine
if (inventoryLotLine == null) {
// Prefer stockInLineId from QR for deterministic binding
val resolved = if (request.stockInLineId != null && request.stockInLineId > 0) {
println(" Resolving InventoryLotLine by stockInLineId=${request.stockInLineId} ...")
val sil = stockInLineRepository.findById(request.stockInLineId).orElse(null)
val ill = sil?.inventoryLotLine
if (ill == null) {
println(" StockInLine ${request.stockInLineId} has no associated InventoryLotLine")
null
} else {
// item consistency guard
val illItemId = ill.inventoryLot?.item?.id
if (illItemId != null && illItemId != request.itemId) {
println(" InventoryLotLine item mismatch for stockInLineId=${request.stockInLineId}: $illItemId != ${request.itemId}")
null
} else {
ill
}
}
} else {
println(" StockOutLine has no associated InventoryLotLine, resolving by lotNo+itemId...")
inventoryLotLineRepository
.findFirstByInventoryLotLotNoAndInventoryLotItemIdAndDeletedFalseOrderByIdDesc(
request.inventoryLotNo,
request.itemId
)
}
if (resolved == null) {
println(" Cannot resolve InventoryLotLine by lotNo=${request.inventoryLotNo}, itemId=${request.itemId}")
return MessageResponse(
id = null,
name = "No inventory lot line",
code = "NO_INVENTORY_LOT_LINE",
type = "error",
message = "Cannot resolve InventoryLotLine (stockInLineId=${request.stockInLineId ?: "null"}, lotNo=${request.inventoryLotNo}, itemId=${request.itemId})",
errorPosition = null
)
}
// Bind the lot line to this stockOutLine so subsequent operations can proceed
stockOutLine.inventoryLotLine = resolved
stockOutLine.item = stockOutLine.item ?: resolved.inventoryLot?.item
inventoryLotLine = resolved

// Also update SuggestedPickLot to point to the resolved lot line (so UI/holdQty logic matches DO confirmLotSubstitution)
val spl = suggestPickLotRepository.findFirstByStockOutLineId(stockOutLine.id!!)
if (spl != null) {
spl.suggestedLotLine = resolved
suggestPickLotRepository.saveAndFlush(spl)
}
}
val getInventoryLotLineTime = System.currentTimeMillis() - getInventoryLotLineStart
println("⏱️ [STEP 2] Get InventoryLotLine: ${getInventoryLotLineTime}ms")
if (inventoryLotLine == null) {
println(" StockOutLine has no associated InventoryLotLine")
return MessageResponse(
id = null,
name = "No inventory lot line",
code = "NO_INVENTORY_LOT_LINE",
type = "error",
message = "StockOutLine ${request.stockOutLineId} has no associated InventoryLotLine",
errorPosition = null
)
}
// inventoryLotLine is guaranteed non-null here
// Step 3: Get InventoryLot
val getInventoryLotStart = System.currentTimeMillis()


+ 32
- 2
src/main/java/com/ffii/fpsms/modules/stock/service/SuggestedPickLotService.kt Просмотреть файл

@@ -40,6 +40,7 @@ import com.ffii.fpsms.modules.pickOrder.enums.PickOrderStatus
import com.ffii.fpsms.modules.stock.entity.projection.StockOutLineInfo
import com.ffii.fpsms.modules.stock.entity.StockOutLIneRepository
import com.ffii.fpsms.modules.stock.web.model.StockOutStatus
import com.ffii.fpsms.modules.common.SecurityUtils
@Service
open class SuggestedPickLotService(
val suggestedPickLotRepository: SuggestPickLotRepository,
@@ -433,7 +434,32 @@ open class SuggestedPickLotService(
}

open fun saveAll(request: List<SuggestedPickLot>): List<SuggestedPickLot> {
return suggestedPickLotRepository.saveAllAndFlush(request)
val saved = suggestedPickLotRepository.saveAllAndFlush(request)

// For insufficient stock (suggestedLotLine == null), create a no-lot stock_out_line so UI can display & close the line.
// Also backfill SuggestedPickLot.stockOutLineId for downstream flows (e.g. hierarchical API -> stockouts).
val toBackfill = saved.filter { it.suggestedLotLine == null && it.pickOrderLine != null }
if (toBackfill.isNotEmpty()) {
val updated = mutableListOf<SuggestedPickLot>()
toBackfill.forEach { spl ->
val pickOrder = spl.pickOrderLine?.pickOrder
if (pickOrder == null) return@forEach

// Only create/backfill when stockOutLine is missing
if (spl.stockOutLine == null) {
val sol = createStockOutLineForSuggestion(spl, pickOrder)
if (sol != null) {
spl.stockOutLine = sol
updated.add(spl)
}
}
}
if (updated.isNotEmpty()) {
suggestedPickLotRepository.saveAllAndFlush(updated)
}
}

return saved
}
private fun createStockOutLineForSuggestion(
suggestion: SuggestedPickLot,
@@ -470,10 +496,13 @@ open class SuggestedPickLotService(
// Get or create StockOut
val stockOut = stockOutRepository.findByConsoPickOrderCode(pickOrder.consoCode ?: "")
.orElseGet {
val handlerId = pickOrder.assignTo?.id ?: SecurityUtils.getUser().orElse(null)?.id
require(handlerId != null) { "Cannot create StockOut: handlerId is null" }
val newStockOut = StockOut().apply {
this.consoPickOrderCode = pickOrder.consoCode ?: ""
this.type = pickOrderTypeValue // Use pick order type (do, job, material, etc.)
this.status = StockOutStatus.PENDING.status
this.handler = handlerId
}
stockOutRepository.save(newStockOut)
}
@@ -484,7 +513,8 @@ open class SuggestedPickLotService(
this.pickOrderLine = pickOrderLine
this.item = item
this.inventoryLotLine = null // No lot available
this.qty = (suggestion.qty ?: BigDecimal.ZERO).toDouble()
// qty on StockOutLine represents picked qty; for no-lot placeholder it must start from 0
this.qty = 0.0
this.status = StockOutLineStatus.PENDING.status
this.deleted = false
this.type = "Nor"


+ 1
- 0
src/main/java/com/ffii/fpsms/modules/stock/web/model/SaveStockOutRequest.kt Просмотреть файл

@@ -64,6 +64,7 @@ data class UpdateStockOutLineStatusRequest(
data class UpdateStockOutLineStatusByQRCodeAndLotNoRequest(
val pickOrderLineId: Long,
val inventoryLotNo: String,
val stockInLineId: Long? = null,
val stockOutLineId: Long,
val itemId: Long,
val status: String


Загрузка…
Отмена
Сохранить