Quellcode durchsuchen

DrinkProductionQty 支援 actual/planned、真實產量不 fallback reqQty,並回傳工序/QC/上架人員;Item Lot Trace 補 JO→生產步驟連線、提料單行節點與 FG UOM

fix負數倉
tommy vor 1 Monat
Ursprung
Commit
a4aaefd1f7
13 geänderte Dateien mit 550 neuen und 67 gelöschten Zeilen
  1. +15
    -0
      src/main/java/com/ffii/fpsms/modules/productProcess/entity/ProductProcessRepository.kt
  2. +212
    -22
      src/main/java/com/ffii/fpsms/modules/productProcess/service/ProductProcessService.kt
  3. +9
    -2
      src/main/java/com/ffii/fpsms/modules/productProcess/web/ProductProcessController.kt
  4. +25
    -0
      src/main/java/com/ffii/fpsms/modules/productProcess/web/model/SaveProductProcessRequest.kt
  5. +2
    -0
      src/main/java/com/ffii/fpsms/modules/qc/entity/QcResultRepository.kt
  6. +2
    -0
      src/main/java/com/ffii/fpsms/modules/stock/entity/InventoryLotLineRepository.kt
  7. +17
    -3
      src/main/java/com/ffii/fpsms/modules/stock/trace/TraceBomLoader.kt
  8. +89
    -8
      src/main/java/com/ffii/fpsms/modules/stock/trace/TraceGraphNodeBuilder.kt
  9. +109
    -9
      src/main/java/com/ffii/fpsms/modules/stock/trace/TraceGraphSemanticsBuilder.kt
  10. +17
    -10
      src/main/java/com/ffii/fpsms/modules/stock/trace/TraceInboundLoader.kt
  11. +7
    -4
      src/main/java/com/ffii/fpsms/modules/stock/trace/TraceJoPreludeLoader.kt
  12. +29
    -6
      src/main/java/com/ffii/fpsms/modules/stock/trace/TraceLotResolver.kt
  13. +17
    -3
      src/main/java/com/ffii/fpsms/modules/stock/trace/TraceProductionLoader.kt

+ 15
- 0
src/main/java/com/ffii/fpsms/modules/productProcess/entity/ProductProcessRepository.kt Datei anzeigen

@@ -7,6 +7,7 @@ import org.springframework.data.repository.query.Param
import org.springframework.stereotype.Repository
import java.util.*
import java.time.LocalDate
import java.time.LocalDateTime

interface JobOrderProcessAggregate {
val jobOrderId: Long
@@ -27,6 +28,20 @@ interface ProductProcessRepository : JpaRepository<ProductProcess, Long>, JpaSpe
fun findByJobOrder_IdInAndDeletedIsFalse(jobOrderIds: List<Long>): List<ProductProcess>
fun findByJobOrder_IdInAndDeletedIsFalseOrderByDateDescProductionPriorityAsc(jobOrderIds: Collection<Long>): List<ProductProcess>

@Query(
"""
SELECT p FROM ProductProcess p
WHERE p.deleted = false
AND p.startTime IS NOT NULL
AND p.startTime >= :startFrom
AND p.startTime < :startToExclusive
"""
)
fun findByDeletedFalseAndStartTimeFromBeforeExclusive(
@Param("startFrom") startFrom: LocalDateTime,
@Param("startToExclusive") startToExclusive: LocalDateTime,
): List<ProductProcess>

@Query(
"""
SELECT


+ 212
- 22
src/main/java/com/ffii/fpsms/modules/productProcess/service/ProductProcessService.kt Datei anzeigen

@@ -54,6 +54,7 @@ import java.math.RoundingMode
import java.time.LocalDate
import java.time.format.DateTimeFormatter
import com.ffii.fpsms.modules.stock.entity.StockInLineRepository
import com.ffii.fpsms.modules.qc.entity.QcResultRepository
import org.springframework.dao.DataIntegrityViolationException
@Service
@Transactional
@@ -81,7 +82,8 @@ open class ProductProcessService(
private val itemUomRepository: ItemUomRespository,
private val uomConversionRepository: UomConversionRepository,
private val itemUomService: ItemUomService,
private val stockInLineRepository: StockInLineRepository
private val stockInLineRepository: StockInLineRepository,
private val qcResultRepository: QcResultRepository,
) {

open fun findAll(pageable: Pageable): Page<ProductProcess> {
@@ -2629,21 +2631,19 @@ open class ProductProcessService(
}

// ===== Drink Production Qty Dashboard =====
private companion object {
const val DRINK_PLANNED_LOOKBACK_DAYS = 30L
}

/** FP-MTMS Version Checklist | Functions Ref. No. 27 | v1.0.0 | 2026-07-20 */
open fun getDrinkProductionQty(date: LocalDate?): List<DrinkProductionQtyResponse> {
open fun getDrinkProductionQty(
date: LocalDate?,
view: String = "actual",
): List<DrinkProductionQtyResponse> {
val targetDate = date ?: LocalDate.now()
val startOfDay = targetDate.atStartOfDay()
val endOfDayExclusive = targetDate.plusDays(1).atStartOfDay()

// 依「生產日」彙總:與既有 dashboard 的用法一致,採半開區間 [startOfDay, endExclusive)
val jobOrders = jobOrderRepository.findByDeletedFalseAndPlanStartFromBeforeExclusiveOrderByIdAsc(
planStartFrom = startOfDay,
planStartToExclusive = endOfDayExclusive,
)

if (jobOrders.isEmpty()) {
return emptyList()
}
val isPlannedView = view.equals("planned", ignoreCase = true)

data class GroupKey(
val itemCode: String?,
@@ -2659,11 +2659,71 @@ open class ProductProcessService(
val jobOrderId: Long?,
val jobOrderCode: String?,
val productionDate: LocalDate?,
val jobOrderStatus: String?,
val startTime: LocalDateTime?,
val assumeTimeNeedMins: Int,
val assumeEndTime: LocalDateTime?,
val actualEndTime: LocalDateTime?,
val latestStartBy: LocalDateTime?,
val processOperators: String?,
val processHandlers: String?,
val qcUsers: String?,
val putAwayUsers: String?,
val processSteps: List<DrinkProductionQtyProcessStep>,
)

val drinkOrders = jobOrders
val candidateJobOrders = if (isPlannedView) {
val lookbackStart = targetDate.minusDays(DRINK_PLANNED_LOOKBACK_DAYS).atStartOfDay()
val byPlanStart = jobOrderRepository.findByDeletedFalseAndPlanStartFromBeforeExclusiveOrderByIdAsc(
planStartFrom = lookbackStart,
planStartToExclusive = endOfDayExclusive,
)
val processesInWindow = productProcessRepository.findByDeletedFalseAndStartTimeFromBeforeExclusive(
startFrom = lookbackStart,
startToExclusive = endOfDayExclusive,
)
val joIdsFromProcess = processesInWindow.mapNotNull { it.jobOrder?.id }.distinct()
val byProcessStart = if (joIdsFromProcess.isNotEmpty()) {
jobOrderRepository.findAllById(joIdsFromProcess).filter { it.deleted != true }
} else {
emptyList()
}
(byPlanStart + byProcessStart)
.filter { it.id != null }
.associateBy { it.id!! }
.values
.toList()
} else {
val byPlanStart = jobOrderRepository.findByDeletedFalseAndPlanStartFromBeforeExclusiveOrderByIdAsc(
planStartFrom = startOfDay,
planStartToExclusive = endOfDayExclusive,
)
val processesStartedToday = productProcessRepository.findByDeletedFalseAndStartTimeFromBeforeExclusive(
startFrom = startOfDay,
startToExclusive = endOfDayExclusive,
)
val joIdsFromProcess = processesStartedToday.mapNotNull { it.jobOrder?.id }.distinct()
val byProcessStart = if (joIdsFromProcess.isNotEmpty()) {
jobOrderRepository.findAllById(joIdsFromProcess).filter { it.deleted != true }
} else {
emptyList()
}
(byPlanStart + byProcessStart)
.filter { it.id != null }
.associateBy { it.id!! }
.values
.toList()
}

if (candidateJobOrders.isEmpty()) {
return emptyList()
}

val drinkOrders = candidateJobOrders
.filter { it.bom?.isDrink == true }
.filter { it.status != JobOrderStatus.PLANNING }
.filter { jo ->
isPlannedView || jo.status != JobOrderStatus.PLANNING
}

if (drinkOrders.isEmpty()) {
return emptyList()
@@ -2671,6 +2731,15 @@ open class ProductProcessService(

val jobOrderIds = drinkOrders.mapNotNull { it.id }
val itemIds = drinkOrders.mapNotNull { it.bom?.item?.id }.distinct()
val bomIds = drinkOrders.mapNotNull { it.bom?.id }.distinct()

val bomDurationMinsByBomId: Map<Long, Int> = if (bomIds.isNotEmpty()) {
bomProcessRepository.sumDurationByBomIds(bomIds).associate { agg ->
agg.bomId to agg.totalMinutes.toInt().coerceAtLeast(0)
}
} else {
emptyMap()
}

val processesByJobOrderId = productProcessRepository
.findByJobOrder_IdInAndDeletedIsFalse(jobOrderIds)
@@ -2685,9 +2754,34 @@ open class ProductProcessService(
emptyMap()
}

val stockInLineByJobOrderId = if (jobOrderIds.isNotEmpty()) {
val stockInLinesByJobOrderId = if (jobOrderIds.isNotEmpty()) {
stockInLineRepository.findAllByJobOrder_IdInAndDeletedFalse(jobOrderIds)
.associateBy { it.jobOrder?.id }
.groupBy { it.jobOrder?.id }
} else {
emptyMap()
}

val allStockInLines = stockInLinesByJobOrderId.values.flatten()
val stockInLineIds = allStockInLines.mapNotNull { it.id }
val qcUsersByStockInLineId: Map<Long, List<String>> = if (stockInLineIds.isNotEmpty()) {
qcResultRepository.findByStockInLine_IdInAndDeletedFalse(stockInLineIds)
.groupBy { it.stockInLine?.id ?: 0L }
.mapValues { (_, results) ->
results.mapNotNull { it.createdBy?.trim()?.takeIf { n -> n.isNotEmpty() } }
.distinct()
}
} else {
emptyMap()
}

val inventoryLotIds = allStockInLines.mapNotNull { it.inventoryLot?.id }.distinct()
val putAwayUsersByLotId: Map<Long, List<String>> = if (inventoryLotIds.isNotEmpty()) {
inventoryLotLineRepository.findByInventoryLot_IdInAndDeletedFalse(inventoryLotIds)
.groupBy { it.inventoryLot?.id ?: 0L }
.mapValues { (_, ills) ->
ills.mapNotNull { it.createdBy?.trim()?.takeIf { n -> n.isNotEmpty() } }
.distinct()
}
} else {
emptyMap()
}
@@ -2712,25 +2806,111 @@ open class ProductProcessService(
uomById[iu.uom?.id]?.udfudesc
}

val drinkRows = drinkOrders.map { jo ->
fun joinNames(names: Collection<String>): String? =
names.map { it.trim() }.filter { it.isNotEmpty() }.distinct()
.takeIf { it.isNotEmpty() }?.joinToString("; ")

val drinkRows = drinkOrders.mapNotNull { jo ->
val bom = jo.bom
val bomId = bom?.id
val itemId = bom?.item?.id
val process = processesByJobOrderId[jo.id]
val lines = process?.id?.let { linesByProcessId[it].orEmpty() }.orEmpty()
val stockInLine = stockInLineByJobOrderId[jo.id]
val sils = stockInLinesByJobOrderId[jo.id].orEmpty()
val stockInLine = sils.maxByOrNull { it.acceptedQty ?: BigDecimal.ZERO }
val startTime = process?.startTime
val lineProcessingMins = lines.sumOf { it.processingTime ?: 0 }
val bomDurationMins = bomId?.let { bomDurationMinsByBomId[it] } ?: 0

val assumeTimeNeedMins = if (isPlannedView) {
if (bomDurationMins > 0) bomDurationMins else lineProcessingMins
} else {
if (lineProcessingMins > 0) lineProcessingMins else bomDurationMins
}

val assumeEndTime =
startTime?.plusMinutes(assumeTimeNeedMins.toLong())

if (isPlannedView) {
val completionDate = when {
assumeEndTime != null -> assumeEndTime.toLocalDate()
jo.planStart != null && assumeTimeNeedMins > 0 ->
jo.planStart!!.plusMinutes(assumeTimeNeedMins.toLong()).toLocalDate()
jo.planStart != null -> jo.planStart!!.toLocalDate()
else -> null
}
if (completionDate == null || completionDate != targetDate) {
return@mapNotNull null
}
}

val latestStartBy = if (isPlannedView && startTime == null && assumeTimeNeedMins > 0) {
endOfDayExclusive.minusMinutes(assumeTimeNeedMins.toLong())
} else {
null
}

val processOperators = joinNames(lines.mapNotNull { it.operator?.name })
val processHandlers = joinNames(lines.mapNotNull { it.handler?.name })
val qcUsers = joinNames(
sils.flatMap { sil ->
val id = sil.id ?: return@flatMap emptyList()
qcUsersByStockInLineId[id].orEmpty()
},
)
val putAwayUsers = joinNames(
sils.flatMap { sil ->
val fromLot = sil.inventoryLot?.id?.let { putAwayUsersByLotId[it] }.orEmpty()
val fromIll = listOfNotNull(sil.inventoryLotLine?.createdBy)
val fromSil = listOfNotNull(sil.createdBy, sil.user?.name)
fromLot + fromIll + fromSil
},
)
val processSteps = lines
.sortedBy { it.seqNo ?: 0L }
.map { line ->
DrinkProductionQtyProcessStep(
jobOrderId = jo.id ?: 0L,
jobOrderCode = jo.code,
itemCode = bom?.item?.code,
itemName = bom?.item?.name,
seqNo = line.seqNo,
processName = line.name,
operatorName = line.operator?.name,
handlerName = line.handler?.name,
startTime = line.startTime,
endTime = line.endTime,
status = line.status,
)
}

DrinkOrderRow(
itemCode = bom?.item?.code,
itemName = bom?.item?.name,
uom = resolveDrinkProductionUom(bom, itemId, uomDescByItemId),
reqQty = jo.reqQty ?: BigDecimal.ZERO,
productionQty = resolveDrinkProductionQty(jo, lines, stockInLine),
productionQty = resolveDrinkProductionQty(lines, stockInLine),
jobOrderId = jo.id,
jobOrderCode = jo.code,
productionDate = jo.planStart?.toLocalDate(),
jobOrderStatus = jo.status?.value,
startTime = startTime,
assumeTimeNeedMins = assumeTimeNeedMins,
assumeEndTime = assumeEndTime,
actualEndTime = process?.endTime,
latestStartBy = latestStartBy,
processOperators = processOperators,
processHandlers = processHandlers,
qcUsers = qcUsers,
putAwayUsers = putAwayUsers,
processSteps = processSteps,
)
}

if (drinkRows.isEmpty()) {
return emptyList()
}

return drinkRows
.groupBy { GroupKey(it.itemCode, it.itemName) }
.map { (key, rows) ->
@@ -2751,6 +2931,17 @@ open class ProductProcessService(
productionDate = row.productionDate,
reqQty = row.reqQty,
productionQty = row.productionQty,
jobOrderStatus = row.jobOrderStatus,
startTime = row.startTime,
assumeTimeNeedMins = row.assumeTimeNeedMins,
assumeEndTime = row.assumeEndTime,
actualEndTime = row.actualEndTime,
latestStartBy = row.latestStartBy,
processOperators = row.processOperators,
processHandlers = row.processHandlers,
qcUsers = row.qcUsers,
putAwayUsers = row.putAwayUsers,
processSteps = row.processSteps,
)
}.sortedBy { it.jobOrderCode ?: "" },
)
@@ -2772,9 +2963,8 @@ open class ProductProcessService(
return null
}

/** 實際生產數量:優先工序產出 → QC 入庫接受量 → 工單 reqQty(當日計劃生產量) */
/** 實際生產數量:優先工序產出 → QC 入庫接受量;皆無則 0(不 fallback 到 reqQty) */
private fun resolveDrinkProductionQty(
jobOrder: com.ffii.fpsms.modules.jobOrder.entity.JobOrder,
lines: List<ProductProcessLine>,
stockInLine: com.ffii.fpsms.modules.stock.entity.StockInLine?,
): BigDecimal {
@@ -2792,7 +2982,7 @@ open class ProductProcessService(
return acceptedQty
}

return jobOrder.reqQty ?: BigDecimal.ZERO
return BigDecimal.ZERO
}

// ===== Operator KPI Dashboard =====


+ 9
- 2
src/main/java/com/ffii/fpsms/modules/productProcess/web/ProductProcessController.kt Datei anzeigen

@@ -320,10 +320,17 @@ class ProductProcessController(

/** FP-MTMS Version Checklist | Functions Ref. No. 27 | v1.0.0 | 2026-07-20 */
@GetMapping("/Demo/DrinkProductionQty")
fun getDrinkProductionQty(@RequestParam(required = false) date: String?): List<DrinkProductionQtyResponse> {
fun getDrinkProductionQty(
@RequestParam(required = false) date: String?,
@RequestParam(required = false, defaultValue = "actual") view: String?,
): List<DrinkProductionQtyResponse> {
val parsedDate = date?.takeIf { it.isNotBlank() }?.let {
LocalDate.parse(it, DateTimeFormatter.ISO_DATE)
}
return productProcessService.getDrinkProductionQty(parsedDate)
val viewMode = when (view?.trim()?.lowercase()) {
"planned" -> "planned"
else -> "actual"
}
return productProcessService.getDrinkProductionQty(parsedDate, viewMode)
}
}

+ 25
- 0
src/main/java/com/ffii/fpsms/modules/productProcess/web/model/SaveProductProcessRequest.kt Datei anzeigen

@@ -359,6 +359,20 @@ data class EquipmentStatusByTypeResponse(
)

// ===== Drink Production Qty Dashboard =====
data class DrinkProductionQtyProcessStep(
val jobOrderId: Long,
val jobOrderCode: String?,
val itemCode: String?,
val itemName: String?,
val seqNo: Long?,
val processName: String?,
val operatorName: String?,
val handlerName: String?,
val startTime: LocalDateTime?,
val endTime: LocalDateTime?,
val status: String?,
)

data class DrinkProductionQtyJobOrderDetail(
val jobOrderId: Long,
val jobOrderCode: String?,
@@ -366,6 +380,17 @@ data class DrinkProductionQtyJobOrderDetail(
val productionDate: LocalDate?,
val reqQty: BigDecimal,
val productionQty: BigDecimal,
val jobOrderStatus: String? = null,
val startTime: LocalDateTime? = null,
val assumeTimeNeedMins: Int = 0,
val assumeEndTime: LocalDateTime? = null,
val actualEndTime: LocalDateTime? = null,
val latestStartBy: LocalDateTime? = null,
val processOperators: String? = null,
val processHandlers: String? = null,
val qcUsers: String? = null,
val putAwayUsers: String? = null,
val processSteps: List<DrinkProductionQtyProcessStep> = emptyList(),
)

data class DrinkProductionQtyResponse(


+ 2
- 0
src/main/java/com/ffii/fpsms/modules/qc/entity/QcResultRepository.kt Datei anzeigen

@@ -12,6 +12,8 @@ interface QcResultRepository: AbstractRepository<QcResult, Long> {
fun findQcResultInfoByStockInLineIdAndDeletedFalse(stockInLineId: Long): List<QcResultInfo>
fun findQcResultInfoByStockOutLineIdAndDeletedFalse(stockOutLineId: Long): List<QcResultInfo>

fun findByStockInLine_IdInAndDeletedFalse(stockInLineIds: Collection<Long>): List<QcResult>

@Query(
"""
SELECT DISTINCT qr.stockInLine.id


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

@@ -118,6 +118,8 @@ WHERE ill.id = :id

fun findAllByInventoryLotId(id: Serializable): List<InventoryLotLine>

fun findByInventoryLot_IdInAndDeletedFalse(inventoryLotIds: Collection<Long>): List<InventoryLotLine>

fun findByInventoryLotStockInLineIdAndWarehouseId(inventoryLotStockInLineId: Long, warehouseId: Long): InventoryLotLine?

fun findAllByInventoryLotItemIdAndStatus(itemId: Long, status: String): List<InventoryLotLine>


+ 17
- 3
src/main/java/com/ffii/fpsms/modules/stock/trace/TraceBomLoader.kt Datei anzeigen

@@ -122,7 +122,7 @@ open class TraceBomLoader(
COALESCE(fg_il.lotNo, fg_sil.lotNo, '') AS finishedLotNo,
fg_sil.id AS finishedStockInLineId,
COALESCE(fg_sil.acceptedQty, 0) AS fgQty,
COALESCE(fg_uc.udfudesc, '') AS fgUom,
COALESCE(fg_uc_tx.udfudesc, fg_uc_fb.udfudesc, '') AS fgUom,
COALESCE(sol.qty, 0) AS materialQtyUsed
FROM stock_out_line sol
INNER JOIN stock_out so ON sol.stockOutId = so.id AND so.deleted = 0
@@ -136,8 +136,22 @@ open class TraceBomLoader(
LEFT JOIN inventory_lot fg_il ON fg_sil.inventoryLotId = fg_il.id AND fg_il.deleted = 0
LEFT JOIN items fg_stock_it ON fg_il.itemId = fg_stock_it.id AND fg_stock_it.deleted = 0
LEFT JOIN items fg_item ON fg_item.id = COALESCE(fg_stock_it.id, fg_bom_it.id)
LEFT JOIN item_uom fg_iu ON fg_item.id = fg_iu.itemId AND fg_iu.stockUnit = 1 AND fg_iu.deleted = 0
LEFT JOIN uom_conversion fg_uc ON fg_iu.uomId = fg_uc.id AND fg_uc.deleted = 0
LEFT JOIN inventory_lot_line fg_ill ON fg_ill.id = COALESCE(
fg_sil.inventoryLotLineId,
(
SELECT ill2.id
FROM inventory_lot_line ill2
WHERE ill2.inventoryLotId = fg_il.id
AND ill2.deleted = 0
AND ill2.stockItemUomId IS NOT NULL
ORDER BY ill2.id ASC
LIMIT 1
)
)
LEFT JOIN item_uom fg_iu_tx ON fg_iu_tx.id = fg_ill.stockItemUomId AND fg_iu_tx.deleted = 0
LEFT JOIN uom_conversion fg_uc_tx ON fg_uc_tx.id = fg_iu_tx.uomId AND fg_uc_tx.deleted = 0
LEFT JOIN item_uom fg_iu_fb ON fg_item.id = fg_iu_fb.itemId AND fg_iu_fb.stockUnit = 1 AND fg_iu_fb.deleted = 0
LEFT JOIN uom_conversion fg_uc_fb ON fg_iu_fb.uomId = fg_uc_fb.id AND fg_uc_fb.deleted = 0
WHERE sol.deleted = 0 AND sol.inventoryLotLineId IN (:lotLineIds)
ORDER BY jobOrderCode, finishedLotNo
""".trimIndent()


+ 89
- 8
src/main/java/com/ffii/fpsms/modules/stock/trace/TraceGraphNodeBuilder.kt Datei anzeigen

@@ -1,5 +1,6 @@
package com.ffii.fpsms.modules.stock.trace

import com.ffii.fpsms.modules.stock.web.model.ItemLotTraceJoPickLine
import com.ffii.fpsms.modules.stock.web.model.ItemLotTraceJoPrelude
import com.ffii.fpsms.modules.stock.web.model.ItemLotTraceMaterialInput
import com.ffii.fpsms.modules.stock.web.model.ItemLotTraceQcResult
@@ -389,11 +390,42 @@ internal object TraceGraphNodeBuilder {
val ctx = PreludeBuildContext()
val lotPicks = buildLotPickOrderMap(joPrelude.materialInputs)
val chainNodes = buildMaterialInputChainNodes(joPrelude.materialInputs, lotPicks, ctx)
val pickNodes =
val coveredItems = coveredPickItemCodes(joPrelude.materialInputs)
val lotPickNodes =
joPrelude.materialInputs.mapIndexed { i, m ->
createMaterialPickNode(m, i, ctx)
}
return (chainNodes + pickNodes).sortedWith(compareBy<TraceGraphNode> { it.sortKey }.thenBy { it.id })
val linePickNodes = mutableListOf<TraceGraphNode>()
joPrelude.pickOrders.forEach { po ->
po.lines.forEachIndexed { li, line ->
val code = line.itemCode.trim().uppercase()
if (code.isEmpty() || !coveredItems.add(code)) return@forEachIndexed
linePickNodes +=
createMaterialPickNodeFromPickLine(
line = line,
index = li,
ctx = ctx,
pickOrderCode = po.pickOrderCode,
pickOrderId = po.pickOrderId,
consoCode = po.consoCode,
jobOrderCode = joPrelude.jobOrder.jobOrderCode,
timestamp = po.releasedDate ?: po.targetDate ?: joPrelude.jobOrder.createdAt,
feedsProductionScopeLotNo = null,
)
}
}
return (chainNodes + lotPickNodes + linePickNodes).sortedWith(
compareBy<TraceGraphNode> { it.sortKey }.thenBy { it.id },
)
}

private fun coveredPickItemCodes(materialInputs: List<ItemLotTraceMaterialInput>): MutableSet<String> {
val set = mutableSetOf<String>()
materialInputs.forEach { m ->
val code = m.materialItemCode.trim().uppercase()
if (code.isNotEmpty()) set += code
}
return set
}

private fun buildLotPickOrderMap(
@@ -447,13 +479,38 @@ internal object TraceGraphNodeBuilder {

if (joProduced && m.nestedJoPrelude != null) {
buildNestedJoCreatedNode(m, ctx)?.let { nodes += it }
}

if (joProduced && nestedInputs.isNotEmpty()) {
val nestedLotPicks = buildLotPickOrderMap(nestedInputs)
nodes += buildMaterialInputChainNodes(nestedInputs, nestedLotPicks, ctx)
nestedInputs.forEachIndexed { ni, nm ->
nodes += createMaterialPickNode(nm, ni, ctx, m.materialLotNo)
val nestedPrelude = m.nestedJoPrelude!!
if (nestedInputs.isNotEmpty()) {
val nestedLotPicks = buildLotPickOrderMap(nestedInputs)
nodes += buildMaterialInputChainNodes(nestedInputs, nestedLotPicks, ctx)
nestedInputs.forEachIndexed { ni, nm ->
nodes += createMaterialPickNode(nm, ni, ctx, m.materialLotNo)
}
}

// Always show nested JO pick-order lines (even with no stock-outs yet).
val nestedCovered = coveredPickItemCodes(nestedInputs)
nestedPrelude.pickOrders.forEach { po ->
po.lines.forEachIndexed { li, line ->
val code = line.itemCode.trim().uppercase()
if (code.isEmpty() || !nestedCovered.add(code)) return@forEachIndexed
nodes +=
createMaterialPickNodeFromPickLine(
line = line,
index = li,
ctx = ctx,
pickOrderCode = po.pickOrderCode,
pickOrderId = po.pickOrderId,
consoCode = po.consoCode,
jobOrderCode = nestedPrelude.jobOrder.jobOrderCode,
timestamp =
po.releasedDate
?: po.targetDate
?: nestedPrelude.jobOrder.createdAt,
feedsProductionScopeLotNo = m.materialLotNo,
)
}
}
}

@@ -635,6 +692,30 @@ internal object TraceGraphNodeBuilder {
feedsProductionScopeLotNo = feedsProductionScopeLotNo,
)

private fun createMaterialPickNodeFromPickLine(
line: ItemLotTraceJoPickLine,
index: Int,
ctx: PreludeBuildContext,
pickOrderCode: String,
pickOrderId: Long?,
consoCode: String,
jobOrderCode: String,
timestamp: String?,
feedsProductionScopeLotNo: String?,
): TraceGraphNode =
TraceGraphNode(
id = "mat-pick-line-$index-$pickOrderId-${line.pickOrderLineId}",
kind = "MATERIAL_PICK",
timestamp = timestamp,
sortKey = parseSortKey(timestamp, ctx.seq++),
refCode = pickOrderCode,
refId = pickOrderId,
traceItemCode = line.itemCode,
consoCode = consoCode,
jobOrderCode = jobOrderCode.trim().ifEmpty { null },
feedsProductionScopeLotNo = feedsProductionScopeLotNo,
)

private fun isJoProducedMaterial(m: ItemLotTraceMaterialInput): Boolean =
m.productionSteps.isNotEmpty() ||
m.nestedJoPrelude != null ||


+ 109
- 9
src/main/java/com/ffii/fpsms/modules/stock/trace/TraceGraphSemanticsBuilder.kt Datei anzeigen

@@ -428,14 +428,22 @@ internal object TraceGraphEdgeSemantics {
to: TraceGraphLayoutNode,
): Boolean {
if (from.kind == "JO_CREATED" && !isMaterialPreludeNode(from)) {
if (to.kind == "MATERIAL_PICK" || to.kind == "PICK_GROUP") return false
if (
to.kind == "MATERIAL_PICK" ||
to.kind == "PICK_GROUP" ||
to.kind == "PRODUCTION_STEP"
) {
return false
}
return true
}
// Allow pick/JO_CREATED → PRODUCTION_STEP (dedicated builders).
if (
to.kind == "PRODUCTION_STEP" &&
from.kind != "MATERIAL_PICK" &&
from.kind != "PICK_GROUP" &&
from.kind != "PRODUCTION_STEP"
from.kind != "PRODUCTION_STEP" &&
from.kind != "JO_CREATED"
) {
return true
}
@@ -574,9 +582,9 @@ internal object TraceGraphEdgeSemantics {
val pairs = mutableListOf<ItemLotTraceGraphEdge>()
val seen = mutableSetOf<String>()

fun add(from: TraceGraphLayoutNode, to: TraceGraphLayoutNode) {
fun add(from: TraceGraphLayoutNode, to: TraceGraphLayoutNode, bypassSkip: Boolean = false) {
if (NO_FLOW_EDGE_KINDS.contains(from.kind) || NO_FLOW_EDGE_KINDS.contains(to.kind)) return
if (shouldSkipTraceFlowEdge(from, to)) return
if (!bypassSkip && shouldSkipTraceFlowEdge(from, to)) return
val key = "${from.id}->${to.id}"
if (!seen.add(key)) return
pairs += ItemLotTraceGraphEdge(fromKey = from.id, toKey = to.id)
@@ -614,6 +622,8 @@ internal object TraceGraphEdgeSemantics {
val toList = nodesInPhase(dayNodes, phasesPresent[i + 1])
if (fromList.isEmpty() || toList.isEmpty()) continue
if (isMaterialPreludeNode(fromList.last()) || isMaterialPreludeNode(toList.first())) continue
// Dedicated builder owns JO_CREATED → PRODUCTION_STEP.
if (fromList.last().kind == "JO_CREATED" && toList.first().kind == "PRODUCTION_STEP") continue
// STOCK_TAKE now precedes OUTBOUND in phase order; edge flows naturally
if (phasesPresent[i] == "PUTAWAY" && phasesPresent[i + 1] == "WAREHOUSE") {
connectPutawayToWarehousePhase(fromList, toList, ::add)
@@ -627,6 +637,7 @@ internal object TraceGraphEdgeSemantics {
buildProductionStepChainEdges(nodes, ::add)
buildProductionToFgQcEdges(nodes, ::add)
buildJoCreatedToMaterialPickEdges(nodes, ::add)
buildJoCreatedToProductionStepEdges(nodes, ::add)
buildReplenishmentToDoOutEdges(nodes, ::add)
buildMaterialPickToProductionStepEdges(nodes, ::add)
buildProductionStepLossEdges(nodes, ::add)
@@ -651,6 +662,9 @@ internal object TraceGraphEdgeSemantics {

nodes.forEach { n ->
if (!isMaterialPreludeNode(n)) return@forEach
// JO_CREATED sits in MATERIAL_PICK for layout; its early planStart would pull that
// phase before PUTAWAY and break 已上架 → 工單提料. Dedicated builder handles JO→提料.
if (n.kind == "JO_CREATED") return@forEach
val key = materialLotKey(n) ?: return@forEach
byLot.getOrPut(key) { mutableListOf() } += n
}
@@ -678,11 +692,14 @@ internal object TraceGraphEdgeSemantics {
if (toPhase == "MATERIAL_PICK") {
val upstream = fromList.last()
val seenPickTargets = mutableSetOf<String>()
toList.forEach { pick ->
val target = resolvePickFlowTarget(pick, nodes)
if (!seenPickTargets.add(target.id)) return@forEach
add(upstream, target)
}
toList
.filter { it.kind == "MATERIAL_PICK" || it.kind == "PICK_GROUP" }
.forEach { pick ->
val target = resolvePickFlowTarget(pick, nodes)
if (seenPickTargets.add(target.id)) {
add(upstream, target)
}
}
} else {
add(fromList.last(), toList.first())
}
@@ -1012,6 +1029,89 @@ internal object TraceGraphEdgeSemantics {
}
}

/** When a JO has no 提料 that reaches 生產步驟, link 工單建立 → first 生產步驟 in scope. */
private fun buildJoCreatedToProductionStepEdges(
nodes: List<TraceGraphLayoutNode>,
add: (TraceGraphLayoutNode, TraceGraphLayoutNode) -> Unit,
) {
val joCreatedNodes = nodes.filter { it.kind == "JO_CREATED" }
val prodSteps = nodes.filter { it.kind == "PRODUCTION_STEP" && !isDoGroupChild(it) }
if (joCreatedNodes.isEmpty() || prodSteps.isEmpty()) return

val pickTargets =
nodes.filter {
!isDoGroupChild(it) &&
(it.kind == "PICK_GROUP" || (it.kind == "MATERIAL_PICK" && it.doGroupId.isNullOrBlank()))
}

val productionKeys =
prodSteps
.mapNotNull { n ->
val bomId = n.bomProcessId ?: return@mapNotNull null
"${productionScopeKey(n)}::$bomId"
}
.toSet()

fun feedLotForTarget(target: TraceGraphLayoutNode): String =
when (target.kind) {
"PICK_GROUP" ->
nodes
.firstOrNull {
it.doGroupId == target.id && !it.feedsProductionScopeLotNo.isNullOrBlank()
}
?.feedsProductionScopeLotNo
?.trim()
.orEmpty()
else -> target.feedsProductionScopeLotNo?.trim().orEmpty()
}

fun materialPicksForTarget(target: TraceGraphLayoutNode): List<TraceGraphLayoutNode> =
if (target.kind == "PICK_GROUP") {
nodes.filter { it.doGroupId == target.id && it.kind == "MATERIAL_PICK" }
} else {
listOf(target)
}

fun joHasPickReachingProduction(jo: TraceGraphLayoutNode): Boolean {
val joCode = jo.refCode?.trim()?.ifEmpty { null } ?: return false
val joLot = jo.traceLotNo?.trim().orEmpty()
val scope = productionScopeKey(jo)
return pickTargets.any { target ->
if (joCodeForPickTarget(target, nodes) != joCode) return@any false
val feedLot = feedLotForTarget(target)
if (joLot.isNotEmpty()) {
if (feedLot != joLot) return@any false
} else if (feedLot.isNotEmpty()) {
return@any false
}
materialPicksForTarget(target).any { pick ->
val bomId = pick.bomProcessId ?: return@any false
productionKeys.contains("$scope::$bomId")
}
}
}

joCreatedNodes.forEach { jo ->
if (joHasPickReachingProduction(jo)) return@forEach
val scope = productionScopeKey(jo)
val firstStep =
prodSteps
.filter { productionScopeKey(it) == scope }
.sortedWith { a, b ->
val sa = a.bomProcessSeqNo
val sb = b.bomProcessSeqNo
when {
sa != null && sb != null && sa != sb -> sa.compareTo(sb)
sa != null && sb == null -> -1
sa == null && sb != null -> 1
else -> TraceGraphLayoutSupport.sortNodesInPhase.compare(a, b)
}
}
.firstOrNull()
if (firstStep != null) add(jo, firstStep)
}
}

private fun connectWarehousePhaseChainEdges(
nodes: List<TraceGraphLayoutNode>,
add: (TraceGraphLayoutNode, TraceGraphLayoutNode) -> Unit,


+ 17
- 10
src/main/java/com/ffii/fpsms/modules/stock/trace/TraceInboundLoader.kt Datei anzeigen

@@ -72,7 +72,8 @@ open class TraceInboundLoader(
COALESCE(i.name, '') AS itemName,
COALESCE(pol.qtyM18, pol.qty, 0) AS orderQty,
COALESCE(pa.putAwayQty, 0) AS putAwayQty,
COALESCE(ucm18.udfudesc, uc.udfudesc, '') AS purchaseUnit,
-- Prefer PO-line UOM snapshot (M18 then pol.uomId); avoid live purchaseUnit flag.
COALESCE(ucm18.udfudesc, uc_pol.udfudesc, '') AS purchaseUnit,
COALESCE(sp.code, '') AS supplierCode,
COALESCE(sp.name, '') AS supplierName,
po.orderDate AS orderDate,
@@ -82,9 +83,8 @@ open class TraceInboundLoader(
INNER JOIN purchase_order po ON pol.purchaseOrderId = po.id AND po.deleted = 0
LEFT JOIN shop sp ON po.supplierId = sp.id AND sp.deleted = 0
LEFT JOIN items i ON pol.itemId = i.id AND i.deleted = 0
LEFT JOIN item_uom iu ON iu.itemId = pol.itemId AND iu.purchaseUnit = true AND iu.deleted = 0
LEFT JOIN uom_conversion uc ON uc.id = iu.uomId AND uc.deleted = 0
LEFT JOIN uom_conversion ucm18 ON ucm18.id = pol.uomIdM18 AND ucm18.deleted = 0
LEFT JOIN uom_conversion uc_pol ON uc_pol.id = pol.uomId AND uc_pol.deleted = 0
LEFT JOIN (
SELECT
sil_pa.purchaseOrderLineId,
@@ -99,8 +99,11 @@ open class TraceInboundLoader(
FROM stock_in_line sil_pa
INNER JOIN inventory_lot_line ill ON sil_pa.inventoryLotLineId = ill.id AND ill.deleted = 0
INNER JOIN items it_pa ON sil_pa.itemId = it_pa.id AND it_pa.deleted = 0
LEFT JOIN item_uom iu_s ON iu_s.itemId = it_pa.id AND iu_s.stockUnit = true AND iu_s.deleted = 0
LEFT JOIN item_uom iu_p ON iu_p.itemId = it_pa.id AND iu_p.purchaseUnit = true AND iu_p.deleted = 0
LEFT JOIN purchase_order_line pol_pa ON sil_pa.purchaseOrderLineId = pol_pa.id AND pol_pa.deleted = 0
LEFT JOIN item_uom iu_s ON iu_s.id = ill.stockItemUomId AND iu_s.deleted = 0
LEFT JOIN item_uom iu_p ON iu_p.itemId = it_pa.id
AND iu_p.uomId = COALESCE(pol_pa.uomIdM18, pol_pa.uomId)
AND iu_p.deleted = 0
WHERE sil_pa.deleted = 0
AND sil_pa.purchaseOrderLineId IS NOT NULL
AND COALESCE(ill.inQty, 0) > 0
@@ -111,7 +114,7 @@ open class TraceInboundLoader(
AND sil.purchaseOrderLineId IS NOT NULL
GROUP BY
pol.id, po.id, po.code, i.code, pol.itemNo, i.name,
pol.qtyM18, pol.qty, pa.putAwayQty, ucm18.udfudesc, uc.udfudesc,
pol.qtyM18, pol.qty, pa.putAwayQty, ucm18.udfudesc, uc_pol.udfudesc,
sp.code, sp.name, po.orderDate, pol.status
ORDER BY po.orderDate ASC, pol.id ASC
""".trimIndent()
@@ -125,7 +128,8 @@ open class TraceInboundLoader(
COALESCE(i.code, '') AS itemCode,
COALESCE(i.name, '') AS itemName,
COALESCE(SUM(sil.acceptedQty), 0) AS orderQty,
COALESCE(uc.udfudesc, '') AS purchaseUnit,
-- Header-only PO has no POL UOM; use stock-in lot-line UOM (matches acceptedQty).
COALESCE(MAX(uc_tx.udfudesc), MAX(uc_fb.udfudesc), '') AS purchaseUnit,
COALESCE(sp.code, '') AS supplierCode,
COALESCE(sp.name, '') AS supplierName,
po.orderDate AS orderDate,
@@ -134,13 +138,16 @@ open class TraceInboundLoader(
INNER JOIN purchase_order po ON sil.purchaseOrderId = po.id AND po.deleted = 0
LEFT JOIN shop sp ON po.supplierId = sp.id AND sp.deleted = 0
LEFT JOIN items i ON sil.itemId = i.id AND i.deleted = 0
LEFT JOIN item_uom iu ON iu.itemId = sil.itemId AND iu.purchaseUnit = true AND iu.deleted = 0
LEFT JOIN uom_conversion uc ON uc.id = iu.uomId AND uc.deleted = 0
LEFT JOIN inventory_lot_line ill ON sil.inventoryLotLineId = ill.id AND ill.deleted = 0
LEFT JOIN item_uom iu_tx ON iu_tx.id = ill.stockItemUomId AND iu_tx.deleted = 0
LEFT JOIN uom_conversion uc_tx ON uc_tx.id = iu_tx.uomId AND uc_tx.deleted = 0
LEFT JOIN item_uom iu_fb ON iu_fb.itemId = sil.itemId AND iu_fb.stockUnit = 1 AND iu_fb.deleted = 0
LEFT JOIN uom_conversion uc_fb ON uc_fb.id = iu_fb.uomId AND uc_fb.deleted = 0
WHERE sil.deleted = 0
AND sil.id IN (:stockInLineIds)
AND sil.purchaseOrderId IS NOT NULL
AND sil.purchaseOrderLineId IS NULL
GROUP BY po.id, po.code, i.code, i.name, uc.udfudesc, sp.code, sp.name, po.orderDate
GROUP BY po.id, po.code, i.code, i.name, sp.code, sp.name, po.orderDate
ORDER BY po.orderDate ASC, po.id ASC
""".trimIndent()
val fromHeader = jdbcDao.queryForList(headerQuery, mapOf("stockInLineIds" to stockInLineIds)).map { row ->


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

@@ -1,4 +1,4 @@
package com.ffii.fpsms.modules.stock.trace
package com.ffii.fpsms.modules.stock.trace

import com.ffii.core.support.JdbcDao
import com.ffii.fpsms.modules.stock.web.model.*
@@ -159,7 +159,7 @@ open class TraceJoPreludeLoader(
mat_ill.id AS pickedInventoryLotLineId,
COALESCE(wh_pick.code, '') AS pickedWarehouseCode,
COALESCE(sol.qty, 0) AS materialQty,
COALESCE(mat_uc.udfudesc, '') AS materialUom,
COALESCE(mat_uc_tx.udfudesc, mat_uc_fb.udfudesc, '') AS materialUom,
bm.qty AS bomQtyPerUnit,
COALESCE(po.code, '') AS pickOrderCode,
po.id AS pickOrderId,
@@ -183,8 +183,11 @@ open class TraceJoPreludeLoader(
LEFT JOIN warehouse wh_pick ON mat_ill.warehouseId = wh_pick.id AND wh_pick.deleted = 0
LEFT JOIN stock_in_line mat_sil ON mat_il.stockInLineId = mat_sil.id AND mat_sil.deleted = 0
LEFT JOIN items mat_it ON mat_il.itemId = mat_it.id AND mat_it.deleted = 0
LEFT JOIN item_uom mat_iu ON mat_it.id = mat_iu.itemId AND mat_iu.stockUnit = 1 AND mat_iu.deleted = 0
LEFT JOIN uom_conversion mat_uc ON mat_iu.uomId = mat_uc.id AND mat_uc.deleted = 0
-- UOM frozen on the picked inventory_lot_line (transaction-time), not current master stockUnit.
LEFT JOIN item_uom mat_iu_tx ON mat_iu_tx.id = mat_ill.stockItemUomId AND mat_iu_tx.deleted = 0
LEFT JOIN uom_conversion mat_uc_tx ON mat_iu_tx.uomId = mat_uc_tx.id AND mat_uc_tx.deleted = 0
LEFT JOIN item_uom mat_iu_fb ON mat_it.id = mat_iu_fb.itemId AND mat_iu_fb.stockUnit = 1 AND mat_iu_fb.deleted = 0
LEFT JOIN uom_conversion mat_uc_fb ON mat_iu_fb.uomId = mat_uc_fb.id AND mat_uc_fb.deleted = 0
LEFT JOIN bom b ON jo.bomId = b.id AND b.deleted = 0
LEFT JOIN bom_material bm ON bm.bomId = b.id AND bm.itemId = mat_it.id AND bm.deleted = 0
LEFT JOIN jo_pick_order jpo ON jpo.pick_order_id = po.id


+ 29
- 6
src/main/java/com/ffii/fpsms/modules/stock/trace/TraceLotResolver.kt Datei anzeigen

@@ -130,12 +130,24 @@ open class TraceLotResolver(
ORDER BY sil.receiptDate DESC, sil.id DESC
LIMIT 1) AS productionDate,
il.stockInDate AS stockInDate,
COALESCE(uc.udfudesc, '') AS uom,
-- Prefer UOM frozen on inventory_lot_line at stock-in time (survives master sync).
COALESCE(uc_tx.udfudesc, uc_fb.udfudesc, '') AS uom,
il.stockInLineId AS primaryStockInLineId
FROM inventory_lot il
INNER JOIN items it ON il.itemId = it.id AND it.deleted = 0
LEFT JOIN item_uom iu ON it.id = iu.itemId AND iu.stockUnit = 1
LEFT JOIN uom_conversion uc ON iu.uomId = uc.id
LEFT JOIN inventory_lot_line ill_uom ON ill_uom.id = (
SELECT ill2.id
FROM inventory_lot_line ill2
WHERE ill2.inventoryLotId = il.id
AND ill2.deleted = 0
AND ill2.stockItemUomId IS NOT NULL
ORDER BY ill2.id ASC
LIMIT 1
)
LEFT JOIN item_uom iu_tx ON iu_tx.id = ill_uom.stockItemUomId AND iu_tx.deleted = 0
LEFT JOIN uom_conversion uc_tx ON uc_tx.id = iu_tx.uomId AND uc_tx.deleted = 0
LEFT JOIN item_uom iu_fb ON it.id = iu_fb.itemId AND iu_fb.stockUnit = 1 AND iu_fb.deleted = 0
LEFT JOIN uom_conversion uc_fb ON iu_fb.uomId = uc_fb.id AND uc_fb.deleted = 0
WHERE il.id = :inventoryLotId AND il.deleted = 0
""".trimIndent()
val row = jdbcDao.queryForList(query, mapOf("inventoryLotId" to inventoryLotId)).firstOrNull()
@@ -258,12 +270,23 @@ open class TraceLotResolver(
ORDER BY sil.receiptDate DESC, sil.id DESC
LIMIT 1) AS productionDate,
il.stockInDate AS stockInDate,
COALESCE(uc.udfudesc, '') AS uom,
COALESCE(uc_tx.udfudesc, uc_fb.udfudesc, '') AS uom,
il.stockInLineId AS primaryStockInLineId
FROM inventory_lot il
INNER JOIN items it ON il.itemId = it.id AND it.deleted = 0
LEFT JOIN item_uom iu ON it.id = iu.itemId AND iu.stockUnit = 1
LEFT JOIN uom_conversion uc ON iu.uomId = uc.id
LEFT JOIN inventory_lot_line ill_uom ON ill_uom.id = (
SELECT ill2.id
FROM inventory_lot_line ill2
WHERE ill2.inventoryLotId = il.id
AND ill2.deleted = 0
AND ill2.stockItemUomId IS NOT NULL
ORDER BY ill2.id ASC
LIMIT 1
)
LEFT JOIN item_uom iu_tx ON iu_tx.id = ill_uom.stockItemUomId AND iu_tx.deleted = 0
LEFT JOIN uom_conversion uc_tx ON uc_tx.id = iu_tx.uomId AND uc_tx.deleted = 0
LEFT JOIN item_uom iu_fb ON it.id = iu_fb.itemId AND iu_fb.stockUnit = 1 AND iu_fb.deleted = 0
LEFT JOIN uom_conversion uc_fb ON iu_fb.uomId = uc_fb.id AND uc_fb.deleted = 0
WHERE il.id IN (:inventoryLotIds) AND il.deleted = 0
""".trimIndent()
return jdbcDao.queryForList(query, mapOf("inventoryLotIds" to inventoryLotIds))


+ 17
- 3
src/main/java/com/ffii/fpsms/modules/stock/trace/TraceProductionLoader.kt Datei anzeigen

@@ -70,7 +70,7 @@ open class TraceProductionLoader(
COALESCE(bp_it.code, '') AS itemCode,
COALESCE(bp_it.name, '') AS itemName,
COALESCE(sil.acceptedQty, 0) AS qty,
COALESCE(uc.udfudesc, '') AS uom,
COALESCE(uc_tx.udfudesc, uc_fb.udfudesc, '') AS uom,
COALESCE(ppl.name, '') AS processStepName,
COALESCE(sil.created, sil.modified, sil.receiptDate) AS producedAt,
COALESCE(jo.code, '') AS jobOrderCode,
@@ -79,8 +79,22 @@ open class TraceProductionLoader(
INNER JOIN stock_in_line sil ON sil.jobOrderId = jo.id AND sil.deleted = 0
INNER JOIN inventory_lot il ON sil.inventoryLotId = il.id AND il.deleted = 0
INNER JOIN items bp_it ON il.itemId = bp_it.id AND bp_it.deleted = 0
LEFT JOIN item_uom iu ON bp_it.id = iu.itemId AND iu.stockUnit = 1
LEFT JOIN uom_conversion uc ON iu.uomId = uc.id
LEFT JOIN inventory_lot_line bp_ill ON bp_ill.id = COALESCE(
sil.inventoryLotLineId,
(
SELECT ill2.id
FROM inventory_lot_line ill2
WHERE ill2.inventoryLotId = il.id
AND ill2.deleted = 0
AND ill2.stockItemUomId IS NOT NULL
ORDER BY ill2.id ASC
LIMIT 1
)
)
LEFT JOIN item_uom iu_tx ON iu_tx.id = bp_ill.stockItemUomId AND iu_tx.deleted = 0
LEFT JOIN uom_conversion uc_tx ON uc_tx.id = iu_tx.uomId AND uc_tx.deleted = 0
LEFT JOIN item_uom iu_fb ON bp_it.id = iu_fb.itemId AND iu_fb.stockUnit = 1 AND iu_fb.deleted = 0
LEFT JOIN uom_conversion uc_fb ON iu_fb.uomId = uc_fb.id AND uc_fb.deleted = 0
LEFT JOIN productprocess pp ON pp.jobOrderId = jo.id AND pp.deleted = 0
LEFT JOIN productprocessline ppl ON ppl.productprocessid = pp.id
AND ppl.deleted = 0


Laden…
Abbrechen
Speichern