Sfoglia il codice sorgente

drink output qty

fix負數倉
tommy 2 settimane fa
parent
commit
6e4c9f308b
4 ha cambiato i file con 468 aggiunte e 79 eliminazioni
  1. +400
    -0
      src/main/java/com/ffii/fpsms/modules/productProcess/service/DrinkShipmentQtyService.kt
  2. +33
    -77
      src/main/java/com/ffii/fpsms/modules/productProcess/service/ProductProcessService.kt
  3. +13
    -1
      src/main/java/com/ffii/fpsms/modules/productProcess/web/ProductProcessController.kt
  4. +22
    -1
      src/main/java/com/ffii/fpsms/modules/productProcess/web/model/SaveProductProcessRequest.kt

+ 400
- 0
src/main/java/com/ffii/fpsms/modules/productProcess/service/DrinkShipmentQtyService.kt Vedi File

@@ -0,0 +1,400 @@
package com.ffii.fpsms.modules.productProcess.service

import com.ffii.core.support.JdbcDao
import com.ffii.fpsms.modules.productProcess.web.model.DrinkShipmentQtyDeliveryDetail
import com.ffii.fpsms.modules.productProcess.web.model.DrinkShipmentQtyResponse
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional
import java.math.BigDecimal
import java.sql.Date
import java.sql.Timestamp
import java.time.LocalDate

@Service
@Transactional(readOnly = true)
open class DrinkShipmentQtyService(
private val jdbcDao: JdbcDao,
) {
/** 訂單列:預計送貨日=選取日,或當日有掃出。實際出貨=當日 stock_ledger.outQty。 */
open fun getDrinkShipmentQty(date: LocalDate?): List<DrinkShipmentQtyResponse> {
val targetDate = date ?: LocalDate.now()
val params = mapOf("targetDate" to targetDate.toString())
val rows = jdbcDao.queryForList(dolSql, params) +
jdbcDao.queryForList(replenishmentOnlySql, params)
if (rows.isEmpty()) {
return emptyList()
}

data class GroupKey(
val itemCode: String?,
val itemName: String?,
)

data class DeliveryKey(
val deliveryOrderId: Long,
val itemCode: String?,
)

val deliveriesByItem = linkedMapOf<GroupKey, MutableMap<DeliveryKey, Pair<DrinkShipmentQtyDeliveryDetail, String?>>>()
rows.forEach { row ->
val itemCode = jdbcString(row, "itemCode")
val itemName = jdbcString(row, "itemName")
val uom = jdbcString(row, "uom")
val itemKey = GroupKey(itemCode, itemName)
val detail = DrinkShipmentQtyDeliveryDetail(
deliveryOrderId = jdbcLong(row, "deliveryOrderId"),
deliveryOrderCode = jdbcString(row, "deliveryOrderCode"),
deliveryDate = jdbcDate(row, "deliveryDate"),
shopCode = jdbcString(row, "shopCode"),
shopName = shopLabel(
jdbcString(row, "shopCode"),
jdbcString(row, "shopName"),
),
deliveryOrderStatus = jdbcString(row, "deliveryOrderStatus"),
orderQty = jdbcDecimal(row, "orderQty"),
shippedQty = jdbcDecimal(row, "shippedQty"),
)
val deliveryKey = DeliveryKey(detail.deliveryOrderId, itemCode)
val byDelivery = deliveriesByItem.getOrPut(itemKey) { linkedMapOf() }
val existing = byDelivery[deliveryKey]
if (existing == null) {
byDelivery[deliveryKey] = detail to uom
} else {
val merged = existing.first.copy(
orderQty = existing.first.orderQty + detail.orderQty,
shippedQty = existing.first.shippedQty + detail.shippedQty,
shopName = existing.first.shopName ?: detail.shopName,
deliveryOrderCode = existing.first.deliveryOrderCode ?: detail.deliveryOrderCode,
deliveryOrderStatus = existing.first.deliveryOrderStatus ?: detail.deliveryOrderStatus,
)
byDelivery[deliveryKey] = merged to (existing.second ?: uom)
}
}

return deliveriesByItem
.map { (key, byDelivery) ->
val deliveries = byDelivery.values
.map { it.first }
.sortedBy { it.deliveryOrderCode ?: "" }
DrinkShipmentQtyResponse(
itemCode = key.itemCode,
itemName = key.itemName,
uom = byDelivery.values.firstNotNullOfOrNull { it.second },
totalOrderQty = deliveries.fold(BigDecimal.ZERO) { acc, row -> acc + row.orderQty },
totalShippedQty = deliveries.fold(BigDecimal.ZERO) { acc, row -> acc + row.shippedQty },
deliveries = deliveries,
)
}
.sortedWith(
compareBy<DrinkShipmentQtyResponse> { it.itemCode ?: "" }
.thenBy { it.itemName ?: "" },
)
}

private fun shopLabel(shopCode: String?, shopName: String?): String? {
val code = shopCode?.trim()?.takeIf { it.isNotEmpty() }
val name = shopName?.trim()?.takeIf { it.isNotEmpty() }
return when {
code != null && name != null && name.startsWith("$code -") -> name
code != null && name != null -> "$code - $name"
name != null -> name
else -> code
}
}

private fun jdbcString(row: Map<String, Any?>, key: String): String? {
val value = row[key] ?: row[key.lowercase()] ?: return null
val text = value.toString().trim()
return text.takeIf { it.isNotEmpty() && !it.equals("null", ignoreCase = true) }
}

private fun jdbcLong(row: Map<String, Any?>, key: String): Long {
val value = row[key] ?: row[key.lowercase()] ?: return 0L
return when (value) {
is Number -> value.toLong()
else -> value.toString().toLongOrNull() ?: 0L
}
}

private fun jdbcDecimal(row: Map<String, Any?>, key: String): BigDecimal {
val value = row[key] ?: row[key.lowercase()] ?: return BigDecimal.ZERO
return when (value) {
is BigDecimal -> value
is Number -> BigDecimal(value.toString())
else -> value.toString().toBigDecimalOrNull() ?: BigDecimal.ZERO
}
}

private fun jdbcDate(row: Map<String, Any?>, key: String): LocalDate? {
val value = row[key] ?: row[key.lowercase()] ?: return null
return when (value) {
is LocalDate -> value
is Date -> value.toLocalDate()
is Timestamp -> value.toLocalDateTime().toLocalDate()
else -> runCatching { LocalDate.parse(value.toString().take(10)) }.getOrNull()
}
}

companion object {
private const val DRINK_ITEM_JOIN = """
INNER JOIN (
SELECT itemId, MIN(id) AS bomId
FROM bom
WHERE deleted = 0
AND IFNULL(isDrink, 0) = 1
AND itemId IS NOT NULL
GROUP BY itemId
) drink_item ON drink_item.itemId = it.id
LEFT JOIN bom b ON b.id = drink_item.bomId
LEFT JOIN item_uom iu
ON iu.id = (
SELECT MIN(iu2.id)
FROM item_uom iu2
WHERE iu2.itemId = it.id
AND iu2.stockUnit = 1
AND iu2.deleted = 0
)
LEFT JOIN uom_conversion uc ON uc.id = iu.uomId
"""

private const val SOL_DO_TYPE_FILTER = """
AND LOWER(IFNULL(sol.type, 'nor')) NOT IN ('trf', 'adj', 'tke', 'stocktake', 'jo')
AND (
so.id IS NULL
OR (
IFNULL(so.deleted, 0) = 0
AND LOWER(IFNULL(so.type, 'do')) = 'do'
)
)
"""

/**
* 當天掃出去的量:優先 stock_ledger.outQty(跨日提料只計當日增量)。
* 無 ledger 的舊資料才 fallback 到當日 pickTime 的 sol.qty。
*/
private const val SHIPPED_QTY_FOR_DO = """
IFNULL((
SELECT SUM(
CASE
WHEN sl.id IS NOT NULL THEN IFNULL(sl.outQty, 0)
WHEN DATE(IFNULL(sol.pickTime, sol.created)) = :targetDate
AND NOT EXISTS (
SELECT 1
FROM stock_ledger slx
WHERE slx.stockOutLineId = sol.id
AND IFNULL(slx.deleted, 0) = 0
)
THEN IFNULL(sol.qty, 0)
ELSE 0
END
)
FROM pick_order po
INNER JOIN pick_order_line pol
ON pol.poId = po.id
AND pol.deleted = 0
AND pol.itemId = it.id
INNER JOIN stock_out_line sol
ON sol.pickOrderLineId = pol.id
AND sol.deleted = 0
LEFT JOIN stock_out so
ON so.id = sol.stockOutId
LEFT JOIN stock_ledger sl
ON sl.stockOutLineId = sol.id
AND IFNULL(sl.deleted, 0) = 0
AND DATE(sl.date) = :targetDate
AND IFNULL(sl.outQty, 0) > 0
WHERE po.deleted = 0
AND po.doId = do.id
AND LOWER(IFNULL(po.type, 'do')) = 'do'
$SOL_DO_TYPE_FILTER
), 0)
"""

private const val SCANNED_TODAY_FOR_DO = """
EXISTS (
SELECT 1
FROM pick_order po
INNER JOIN pick_order_line pol
ON pol.poId = po.id
AND pol.deleted = 0
AND pol.itemId = it.id
INNER JOIN stock_out_line sol
ON sol.pickOrderLineId = pol.id
AND sol.deleted = 0
LEFT JOIN stock_out so
ON so.id = sol.stockOutId
LEFT JOIN stock_ledger sl
ON sl.stockOutLineId = sol.id
AND IFNULL(sl.deleted, 0) = 0
AND DATE(sl.date) = :targetDate
AND IFNULL(sl.outQty, 0) > 0
WHERE po.deleted = 0
AND po.doId = do.id
AND LOWER(IFNULL(po.type, 'do')) = 'do'
$SOL_DO_TYPE_FILTER
AND (
sl.id IS NOT NULL
OR (
IFNULL(sol.qty, 0) > 0
AND DATE(IFNULL(sol.pickTime, sol.created)) = :targetDate
AND NOT EXISTS (
SELECT 1
FROM stock_ledger slx
WHERE slx.stockOutLineId = sol.id
AND IFNULL(slx.deleted, 0) = 0
)
)
)
)
"""

private val dolSql = """
SELECT
it.code AS itemCode,
it.name AS itemName,
COALESCE(
NULLIF(TRIM(b.outputQtyUom), ''),
NULLIF(TRIM(b.excelUom), ''),
uc.udfudesc
) AS uom,
do.id AS deliveryOrderId,
do.code AS deliveryOrderCode,
do.status AS deliveryOrderStatus,
DATE(IFNULL(do.estimatedArrivalDate, do.orderDate)) AS deliveryDate,
IFNULL(sp.code, '') AS shopCode,
IFNULL(sp.name, '') AS shopName,
SUM(IFNULL(dol.qty, 0))
+ IFNULL((
SELECT SUM(IFNULL(r.replenishQty, 0))
FROM do_replenishment r
WHERE IFNULL(r.deleted, 0) = 0
AND r.targetDoId = do.id
AND r.itemId = it.id
), 0) AS orderQty,
$SHIPPED_QTY_FOR_DO AS shippedQty
FROM delivery_order_line dol
INNER JOIN delivery_order do
ON dol.deliveryOrderId = do.id
AND do.deleted = 0
INNER JOIN items it
ON dol.itemId = it.id
AND it.deleted = 0
$DRINK_ITEM_JOIN
LEFT JOIN shop sp
ON do.shopId = sp.id
AND sp.deleted = 0
WHERE dol.deleted = 0
AND (
DATE(IFNULL(do.estimatedArrivalDate, do.orderDate)) = :targetDate
OR $SCANNED_TODAY_FOR_DO
)
GROUP BY
it.id,
it.code,
it.name,
b.outputQtyUom,
b.excelUom,
uc.udfudesc,
do.id,
do.code,
do.status,
DATE(IFNULL(do.estimatedArrivalDate, do.orderDate)),
sp.code,
sp.name
ORDER BY it.code, do.code
""".trimIndent()

private val replenishmentOnlySql = """
SELECT
it.code AS itemCode,
it.name AS itemName,
COALESCE(
NULLIF(TRIM(b.outputQtyUom), ''),
NULLIF(TRIM(b.excelUom), ''),
uc.udfudesc
) AS uom,
COALESCE(r.targetDoId, r.sourceDoId, 0) AS deliveryOrderId,
COALESCE(do.code, r.targetDoCode, r.sourceDoCode) AS deliveryOrderCode,
do.status AS deliveryOrderStatus,
r.deliveryDate AS deliveryDate,
IFNULL(r.shopCode, IFNULL(sp.code, '')) AS shopCode,
IFNULL(r.shopName, IFNULL(sp.name, '')) AS shopName,
SUM(IFNULL(r.replenishQty, 0)) AS orderQty,
IFNULL(MAX(ship.shippedQty), 0) AS shippedQty
FROM do_replenishment r
INNER JOIN items it
ON r.itemId = it.id
AND it.deleted = 0
$DRINK_ITEM_JOIN
LEFT JOIN delivery_order do
ON do.id = COALESCE(r.targetDoId, r.sourceDoId)
AND do.deleted = 0
LEFT JOIN shop sp
ON do.shopId = sp.id
AND sp.deleted = 0
LEFT JOIN (
SELECT
COALESCE(r2.targetDoId, r2.sourceDoId, 0) AS deliveryOrderId,
r2.itemId AS itemId,
SUM(
CASE
WHEN sl.id IS NOT NULL THEN IFNULL(sl.outQty, 0)
WHEN DATE(IFNULL(sol.pickTime, sol.created)) = :targetDate
AND NOT EXISTS (
SELECT 1
FROM stock_ledger slx
WHERE slx.stockOutLineId = sol.id
AND IFNULL(slx.deleted, 0) = 0
)
THEN IFNULL(sol.qty, 0)
ELSE 0
END
) AS shippedQty
FROM do_replenishment r2
INNER JOIN stock_out_line sol
ON sol.pickOrderLineId = r2.pickOrderLineId
AND sol.deleted = 0
LEFT JOIN stock_out so
ON so.id = sol.stockOutId
LEFT JOIN stock_ledger sl
ON sl.stockOutLineId = sol.id
AND IFNULL(sl.deleted, 0) = 0
AND DATE(sl.date) = :targetDate
AND IFNULL(sl.outQty, 0) > 0
WHERE IFNULL(r2.deleted, 0) = 0
$SOL_DO_TYPE_FILTER
GROUP BY
COALESCE(r2.targetDoId, r2.sourceDoId, 0),
r2.itemId
) ship
ON ship.deliveryOrderId = COALESCE(r.targetDoId, r.sourceDoId, 0)
AND ship.itemId = it.id
WHERE IFNULL(r.deleted, 0) = 0
AND (
r.deliveryDate = :targetDate
OR IFNULL(ship.shippedQty, 0) > 0
)
AND NOT EXISTS (
SELECT 1
FROM delivery_order_line dol
WHERE dol.deleted = 0
AND dol.itemId = r.itemId
AND dol.deliveryOrderId = COALESCE(r.targetDoId, r.sourceDoId)
)
GROUP BY
it.id,
it.code,
it.name,
b.outputQtyUom,
b.excelUom,
uc.udfudesc,
COALESCE(r.targetDoId, r.sourceDoId, 0),
COALESCE(do.code, r.targetDoCode, r.sourceDoCode),
do.status,
r.deliveryDate,
IFNULL(r.shopCode, IFNULL(sp.code, '')),
IFNULL(r.shopName, IFNULL(sp.name, ''))
ORDER BY it.code, COALESCE(do.code, r.targetDoCode, r.sourceDoCode)
""".trimIndent()
}
}

+ 33
- 77
src/main/java/com/ffii/fpsms/modules/productProcess/service/ProductProcessService.kt Vedi File

@@ -85,10 +85,6 @@ open class ProductProcessService(
private val stockInLineRepository: StockInLineRepository,
private val qcResultRepository: QcResultRepository,
) {
companion object {
private const val PACKAGING_PROCESS_NAME = "包裝"
private const val DRINK_PLANNED_LOOKBACK_DAYS = 30L
}

open fun findAll(pageable: Pageable): Page<ProductProcess> {
println(" Service: Finding all ProductProcess with page: ${pageable.pageNumber}, size: ${pageable.pageSize}")
@@ -1250,7 +1246,6 @@ open class ProductProcessService(
)
}

/** FP-MTMS Version Checklist | Functions Ref. No. 58 | v1.0.1 | 2026-08-10 */
open fun updateProductProcessLineStatus(productProcessLineId: Long, status: String): MessageResponse {
println(" Service: Updating ProductProcessLine Status: $productProcessLineId")
val productProcessLine = productProcessLineRepository.findById(productProcessLineId).orElse(null)
@@ -1259,12 +1254,6 @@ open class ProductProcessService(
// productProcessLine.endTime = LocalDateTime.now()
productProcessLineRepository.save(productProcessLine)
println(" Service: ProductProcessLine Status Updated: ${productProcessLine.status}")

// One packaging Complete/Pass → autoPass remaining packaging lines on the same JO.
if (isManualLineDone(status) && isPackagingLine(productProcessLine)) {
autoPassSiblingPackagingLines(productProcessLineId)
}

syncProductProcessStatusFromLines(productProcessLine.productProcess?.id ?: 0L)

return MessageResponse(
@@ -1319,7 +1308,7 @@ open class ProductProcessService(
val productProcessLines = productProcessLineRepository.findByProductProcess_Id(productProcessId)

println(" Service: ProductProcessLines: $productProcessLines")
if (productProcessLines.all { isLineDone(it.status) }) {
if (productProcessLines.all { it.status == "Completed" || it.status == "Pass" }) {
productProcess.status = ProductProcessStatus.COMPLETED
if (productProcess.endTime == null) {
productProcess.endTime = LocalDateTime.now()
@@ -1511,9 +1500,8 @@ open class ProductProcessService(
}

/**
* FP-MTMS Version Checklist | Functions Ref. No. 41 | v1.0.3 | 2026-08-10
* QC 列表:按 jobOrder 粒度分页,qcReady 由该 jobOrder 下所有 productProcessLine 是否全部 Completed/Pass/autoPass 决定。
* (包裝 Complete/Pass 時會自動 autoPass 同 JO 其餘包裝 line,因此不再需要「任一包裝即可」特例。)
* FP-MTMS Version Checklist | Functions Ref. No. 41 | v1.0.1 | 2026-08-06
* QC 列表:按 jobOrder 粒度分页,qcReady 由该 jobOrder 下所有 productProcessLine 是否全部 Completed/Pass 决定。
*
* 注意:date/itemCode/jobOrderCode/bomIds 用来筛选“候选 jobOrder”,但 qcReady 判断会用该 jobOrder 下全部 productProcessLine。
*/
@@ -1663,6 +1651,10 @@ open class ProductProcessService(
val linesByJobOrder = candidateLines.groupBy { it.productProcess.jobOrder?.id ?: 0L }
val done = { s: String? ->
val x = s?.trim()?.lowercase()
x == "completed" || x == "pass"
}
val qcKeys = candidateAggregates.mapNotNull { agg ->
val jobOrderId = agg.jobOrderId
val stockInLine = stockInLineByJobOrderId[jobOrderId]
@@ -1684,12 +1676,20 @@ open class ProductProcessService(

val lineAggregate = lineStatusByJobOrderId[jobOrderId]
val totalLines = lineAggregate?.totalLines ?: 0L

// After packaging Complete/Pass autoPasses sibling 包裝 lines, qcReady is simply all lines done.
val done = { s: String? ->
val x = s?.trim()?.lowercase()
x == "completed" || x == "pass"
}
val jobLines = linesByJobOrder[jobOrderId].orEmpty()
val allLinesDone = jobLines.isNotEmpty() && jobLines.all { isLineDone(it.status) }
val packing = jobLines.filter { (it.name ?: "").trim() == "包裝" }
val nonPacking = jobLines.filter { (it.name ?: "").trim() != "包裝" }

val ready = includedInList && stockInLine != null && allLinesDone
val nonPackingOk = nonPacking.isEmpty() || nonPacking.all { done(it.status) }
val packingOk = packing.isEmpty() || packing.any { done(it.status) }

val ready = includedInList && stockInLine != null && nonPackingOk && packingOk
JobOrderQcKey(
jobOrderId = jobOrderId,
@@ -1877,7 +1877,10 @@ open class ProductProcessService(
val joPickOrdersList = if (pickOrderId != null) joPickOrdersByPickOrderId[pickOrderId].orEmpty() else emptyList()

val productProcessLines = linesByProcessId[productProcess.id ?: 0L].orEmpty()
val finishedCount = productProcessLines.count { isLineDone(it.status) }
val finishedCount = productProcessLines.count {
val s = it.status?.trim()?.lowercase()
s == "completed" || s == "pass"
}

val bomIsDrink = productProcess.bom?.isDrink
val matchStatus = if (joPickOrdersList.isNotEmpty() &&
@@ -1951,7 +1954,7 @@ open class ProductProcessService(
}

/**
* FG QC / 上架 reminders: same eligibility as 完成QC工單 (qcReady): all lines Completed/Pass/autoPass, stock-in exists and not completed/rejected;
* FG QC / 上架 reminders: same eligibility as 完成QC工單 (qcReady): all lines Completed/Pass, stock-in exists and not completed/rejected;
* only job orders that have a product process dated **today or yesterday** (server local date).
*/
open fun findJobOrderFgQcAndPutAwayAlertsForTodayYesterday(): JobOrderFgAlertsResponse {
@@ -2005,7 +2008,7 @@ open class ProductProcessService(

val jobOrderLines = processes.flatMap { p -> linesByProcessId[p.id ?: 0L].orEmpty() }
val allLinesDone = jobOrderLines.isNotEmpty() &&
jobOrderLines.all { isLineDone(it.status) }
jobOrderLines.all { it.status == "Completed" || it.status == "Pass" }
if (!allLinesDone) continue

val maxDate = processes.mapNotNull { it.date }.maxOrNull()
@@ -2107,12 +2110,6 @@ open class ProductProcessService(
status?.trim()?.lowercase()?.replace(" ", "") ?: ""

private fun isLineDone(status: String?): Boolean {
val n = normalizeLineStatus(status)
return n == "completed" || n == "pass" || n == "autopass"
}

/** Manual Complete/Pass only — does not include autoPass (avoids cascade loops). */
private fun isManualLineDone(status: String?): Boolean {
val n = normalizeLineStatus(status)
return n == "completed" || n == "pass"
}
@@ -2122,54 +2119,9 @@ open class ProductProcessService(
return n == "inprogress" || n == "paused"
}

/** FP-MTMS Version Checklist | Functions Ref. No. 58 | v1.0.1 | 2026-08-10 */
private fun isPackagingLine(line: ProductProcessLine): Boolean =
(line.name ?: "").trim() == PACKAGING_PROCESS_NAME

/**
* FP-MTMS Version Checklist | Functions Ref. No. 58 | v1.0.1 | 2026-08-10
* When one 包裝 line is Completed/Pass, set other unfinished 包裝 lines on the same job order to autoPass,
* then sync each affected ProductProcess.status (autoPass counts as done → COMPLETED).
*/
private fun autoPassSiblingPackagingLines(triggerLineId: Long) {
val trigger = productProcessLineRepository.findById(triggerLineId).orElse(null) ?: return
if (!isPackagingLine(trigger)) return

val jobOrderId = trigger.productProcess?.jobOrder?.id ?: return
val processes = productProcessRepository.findByJobOrder_Id(jobOrderId)
.filter { it.deleted != true }
val processIds = processes.mapNotNull { it.id }
if (processIds.isEmpty()) return

val allLines = productProcessLineRepository.findByProductProcess_IdIn(processIds)
val siblings = allLines.filter { line ->
line.id != null &&
line.id != triggerLineId &&
isPackagingLine(line) &&
!isLineDone(line.status)
}
if (siblings.isEmpty()) return

val now = LocalDateTime.now()
val affectedProcessIds = linkedSetOf<Long>()
for (line in siblings) {
if (line.startTime == null) {
line.startTime = now
}
line.endTime = now
line.status = "autoPass"
productProcessLineRepository.save(line)
line.productProcess?.id?.let { affectedProcessIds.add(it) }
}

for (processId in affectedProcessIds) {
syncProductProcessStatusFromLines(processId)
}
}

/**
* Align parent [ProductProcess.status] with all line states.
* - All Completed/Pass/autoPass -> completed (via [ifAllLinesCompletedOrPassed])
* - All Completed/Pass -> completed (via [ifAllLinesCompletedOrPassed])
* - Any line started but not all done -> in_progress
* - Does not override STOPPED or CANCELLED on the parent.
*/
@@ -2249,8 +2201,8 @@ open class ProductProcessService(
// 获取所有 product process lines
val allproductProcessLines = productProcessLineRepository.findByProductProcess_Id(productProcessId)

// 检查是否所有 lines 都是 Completed / Pass / autoPass
if (allproductProcessLines.all { isLineDone(it.status) }) {
// 检查是否所有 lines 都是 "Completed" 或 "Pass"
if (allproductProcessLines.all { it.status == "Completed" || it.status == "Pass" }) {
// 更新 product process 的 endTime 和状态
updateProductProcessEndTime(productProcessId)
updateProductProcessStatus(productProcessId, ProductProcessStatus.COMPLETED)
@@ -2679,6 +2631,9 @@ 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(
@@ -2765,6 +2720,7 @@ open class ProductProcessService(
}

val drinkOrders = candidateJobOrders
.filter { it.isHidden != true }
.filter { it.bom?.isDrink == true }
.filter { jo ->
isPlannedView || jo.status != JobOrderStatus.PLANNING
@@ -3091,7 +3047,7 @@ open class ProductProcessService(
}

val currentProcesses = operatorLines
.filter { it.endTime == null || (it.status != null && !isLineDone(it.status)) }
.filter { it.endTime == null || (it.status != null && it.status != "Completed" && it.status != "Pass") }
.map { line ->
val productProcess = line.productProcess
val jobOrder = productProcess.jobOrder


+ 13
- 1
src/main/java/com/ffii/fpsms/modules/productProcess/web/ProductProcessController.kt Vedi File

@@ -3,6 +3,7 @@ package com.ffii.fpsms.modules.productProcess.web
import com.ffii.fpsms.modules.productProcess.entity.ProductProcess
import com.ffii.fpsms.modules.productProcess.entity.ProductProcessLine
import com.ffii.fpsms.modules.productProcess.enums.ProductProcessStatus
import com.ffii.fpsms.modules.productProcess.service.DrinkShipmentQtyService
import com.ffii.fpsms.modules.productProcess.service.ProductProcessService
import com.ffii.fpsms.modules.productProcess.web.model.*
import org.springframework.data.domain.Page
@@ -16,7 +17,8 @@ import java.time.format.DateTimeFormatter
@RestController
@RequestMapping("/product-process")
class ProductProcessController(
private val productProcessService: ProductProcessService
private val productProcessService: ProductProcessService,
private val drinkShipmentQtyService: DrinkShipmentQtyService,
) {

@GetMapping
@@ -333,4 +335,14 @@ class ProductProcessController(
}
return productProcessService.getDrinkProductionQty(parsedDate, viewMode)
}

@GetMapping("/Demo/DrinkShipmentQty")
fun getDrinkShipmentQty(
@RequestParam(required = false) date: String?,
): List<DrinkShipmentQtyResponse> {
val parsedDate = date?.takeIf { it.isNotBlank() }?.let {
LocalDate.parse(it, DateTimeFormatter.ISO_DATE)
}
return drinkShipmentQtyService.getDrinkShipmentQty(parsedDate)
}
}

+ 22
- 1
src/main/java/com/ffii/fpsms/modules/productProcess/web/model/SaveProductProcessRequest.kt Vedi File

@@ -218,7 +218,7 @@ data class JobOrderProductProcessPageResponse(
)

/**
* Nav alerts aligned with 完成QC工單 list: all product process lines Completed/Pass/autoPass, stock-in not completed/rejected;
* Nav alerts aligned with 完成QC工單 list: all product process lines Completed/Pass, stock-in not completed/rejected;
* job order has at least one process dated today or yesterday. [qc] = before received; [putAway] = received / partially_completed.
*/
data class JobOrderFgAlertRowResponse(
@@ -400,4 +400,25 @@ data class DrinkProductionQtyResponse(
val totalReqQty: BigDecimal,
val totalQty: BigDecimal,
val jobOrders: List<DrinkProductionQtyJobOrderDetail> = emptyList(),
)

data class DrinkShipmentQtyDeliveryDetail(
val deliveryOrderId: Long,
val deliveryOrderCode: String?,
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd")
val deliveryDate: LocalDate?,
val shopCode: String?,
val shopName: String?,
val deliveryOrderStatus: String?,
val orderQty: BigDecimal,
val shippedQty: BigDecimal,
)

data class DrinkShipmentQtyResponse(
val itemCode: String?,
val itemName: String?,
val uom: String?,
val totalOrderQty: BigDecimal,
val totalShippedQty: BigDecimal,
val deliveries: List<DrinkShipmentQtyDeliveryDetail> = emptyList(),
)

Caricamento…
Annulla
Salva