Sfoglia il codice sorgente

1. 員工發貨績效圖表

2.  新報表:送貨訂單與倉存單位不符
3 店鋪補貨報表 加「原單提料人」「實際補貨提料人」;Excel 欄位改純中文表頭
4  包裝 autoPass
5. JO 提料 re-suggest JO 類型 storeId 保持 null(避免被預設成 2/F 濾掉 3F)  前端對齊 DO:用後端 stockout qty、同 POL 依 status 排序
6. 使用者建立/驗證
7.  BOM 設備
8「不合用/不適用」統一不掛 equipment data
9 hilde some useless console log
fix負數倉
CANCERYS\kw093 1 mese fa
parent
commit
781caff39f
15 ha cambiato i file con 413 aggiunte e 74 eliminazioni
  1. +57
    -22
      src/main/java/com/ffii/fpsms/modules/chart/service/ChartService.kt
  2. +6
    -2
      src/main/java/com/ffii/fpsms/modules/chart/web/ChartController.kt
  3. +2
    -0
      src/main/java/com/ffii/fpsms/modules/common/ErrorCodes.java
  4. +9
    -4
      src/main/java/com/ffii/fpsms/modules/deliveryOrder/service/DoWorkbenchMainService.kt
  5. +1
    -0
      src/main/java/com/ffii/fpsms/modules/jobOrder/service/JoWorkbenchPickConstants.kt
  6. +10
    -2
      src/main/java/com/ffii/fpsms/modules/master/service/BomService.kt
  7. +2
    -1
      src/main/java/com/ffii/fpsms/modules/productProcess/entity/ProductProcessLineRepository.kt
  8. +28
    -34
      src/main/java/com/ffii/fpsms/modules/productProcess/service/ProductProcessService.kt
  9. +1
    -1
      src/main/java/com/ffii/fpsms/modules/productProcess/web/ProductProcessController.kt
  10. +1
    -1
      src/main/java/com/ffii/fpsms/modules/productProcess/web/model/SaveProductProcessRequest.kt
  11. +128
    -0
      src/main/java/com/ffii/fpsms/modules/report/service/DoInventoryUomMismatchReportService.kt
  12. +25
    -7
      src/main/java/com/ffii/fpsms/modules/report/service/ShopOrderReplenishmentReportService.kt
  13. +116
    -0
      src/main/java/com/ffii/fpsms/modules/report/web/DoInventoryUomMismatchReportController.kt
  14. +4
    -0
      src/main/java/com/ffii/fpsms/modules/user/entity/UserRepository.java
  15. +23
    -0
      src/main/java/com/ffii/fpsms/modules/user/service/UserService.java

+ 57
- 22
src/main/java/com/ffii/fpsms/modules/chart/service/ChartService.kt Vedi File

@@ -717,9 +717,13 @@ open class ChartService(
} }


/** /**
* Staff delivery performance: daily pick ticket count and total time per staff.
* FP-MTMS Version Checklist | Functions Ref. No. 61 | v1.0.0 | 2026-08-10
* Staff delivery performance: daily pick ticket count, duration, item kinds, and actual picked qty.
* Uses delivery_order_pick_order (handler = handledBy); time = sum of * Uses delivery_order_pick_order (handler = handledBy); time = sum of
* (ticketCompleteDateTime - ticketReleaseTime) per completed ticket. * (ticketCompleteDateTime - ticketReleaseTime) per completed ticket.
* itemKindCount = sum of per-ticket COUNT(DISTINCT pol.itemId);
* itemQtyPicked = sum of stock_out_line.qty via pick_order_line (actual picked, not pol.qty).
* Scoped CTE first (date/store/staff) then lineAgg — avoids full-history pick/sol scan.
* staffNos: when non-empty, filter to these staff by user.staffNo (multi-select). * staffNos: when non-empty, filter to these staff by user.staffNo (multi-select).
* storeIdNull: when true, only rows with dop.storeId IS NULL (takes precedence over storeId). * storeIdNull: when true, only rows with dop.storeId IS NULL (takes precedence over storeId).
* storeId: when non-blank and storeIdNull is not true, filter dop.storeId equality (trimmed). * storeId: when non-blank and storeIdNull is not true, filter dop.storeId equality (trimmed).
@@ -742,12 +746,13 @@ open class ChartService(
args["endExclusive"] = endDate.plusDays(1).atStartOfDay() args["endExclusive"] = endDate.plusDays(1).atStartOfDay()
"AND dop.ticketCompleteDateTime < :endExclusive" "AND dop.ticketCompleteDateTime < :endExclusive"
} else "" } else ""
val staffSql = if (!staffNos.isNullOrEmpty()) {
val nos = staffNos.map { it.trim() }.filter { it.isNotBlank() }
if (nos.isEmpty()) "" else {
args["staffNos"] = nos
"AND u.staffNo IN (:staffNos)"
}
val staffNosFiltered = staffNos?.map { it.trim() }?.filter { it.isNotBlank() }.orEmpty()
val staffSql = if (staffNosFiltered.isNotEmpty()) {
args["staffNos"] = staffNosFiltered
"AND u_scope.staffNo IN (:staffNos)"
} else ""
val scopeUserJoin = if (staffSql.isNotEmpty()) {
"LEFT JOIN user u_scope ON dop.handledBy = u_scope.id AND u_scope.deleted = 0"
} else "" } else ""
val storeSql = when { val storeSql = when {
storeIdNull == true -> "AND dop.storeId IS NULL" storeIdNull == true -> "AND dop.storeId IS NULL"
@@ -758,31 +763,61 @@ open class ChartService(
else -> "" else -> ""
} }
val useStoreFilter = storeIdNull == true || !storeId.isNullOrBlank() val useStoreFilter = storeIdNull == true || !storeId.isNullOrBlank()
val fromClause = if (useStoreFilter) {
val dopFromClause = if (useStoreFilter) {
"FROM delivery_order_pick_order dop" "FROM delivery_order_pick_order dop"
} else { } else {
"FROM delivery_order_pick_order dop FORCE INDEX (idx_dopo_staff_perf_complete)" "FROM delivery_order_pick_order dop FORCE INDEX (idx_dopo_staff_perf_complete)"
} }
val sql = """ val sql = """
WITH dop_scoped AS (
SELECT
dop.id,
dop.ticketCompleteDateTime,
dop.ticketReleaseTime,
dop.handledBy,
dop.handlerName
$dopFromClause
$scopeUserJoin
WHERE dop.deleted = 0
AND dop.ticketStatus = 'completed'
AND dop.ticketCompleteDateTime IS NOT NULL
$startSql $endSql $storeSql $staffSql
),
lineAgg AS (
SELECT
po.deliveryOrderPickOrderId AS dopId,
COUNT(DISTINCT pol.itemId) AS itemKindCount,
COALESCE(SUM(sol.qty), 0) AS itemQtyPicked
FROM dop_scoped d
INNER JOIN pick_order po
ON po.deliveryOrderPickOrderId = d.id
AND po.deleted = 0
INNER JOIN pick_order_line pol
ON pol.poId = po.id
AND pol.deleted = 0
LEFT JOIN stock_out_line sol FORCE INDEX (idx_sol_polid_deleted_status_qty)
ON sol.pickOrderLineId = pol.id
AND sol.deleted = 0
GROUP BY po.deliveryOrderPickOrderId
)
SELECT SELECT
DATE_FORMAT(dop.ticketCompleteDateTime, '%Y-%m-%d') AS date,
COALESCE(NULLIF(TRIM(COALESCE(u.name, '')), ''), dop.handlerName, 'Unknown') AS staffName,
COUNT(dop.id) AS orderCount,
DATE_FORMAT(d.ticketCompleteDateTime, '%Y-%m-%d') AS date,
COALESCE(NULLIF(TRIM(COALESCE(u.name, '')), ''), d.handlerName, 'Unknown') AS staffName,
COUNT(d.id) AS orderCount,
COALESCE(SUM( COALESCE(SUM(
CASE CASE
WHEN dop.ticketReleaseTime IS NOT NULL AND dop.ticketCompleteDateTime IS NOT NULL
THEN GREATEST(0, TIMESTAMPDIFF(MINUTE, dop.ticketReleaseTime, dop.ticketCompleteDateTime))
WHEN d.ticketReleaseTime IS NOT NULL AND d.ticketCompleteDateTime IS NOT NULL
THEN GREATEST(0, TIMESTAMPDIFF(MINUTE, d.ticketReleaseTime, d.ticketCompleteDateTime))
ELSE 0 ELSE 0
END END
), 0) AS totalMinutes
$fromClause
LEFT JOIN user u ON dop.handledBy = u.id AND u.deleted = 0
WHERE dop.deleted = 0
AND dop.ticketStatus = 'completed'
AND dop.ticketCompleteDateTime IS NOT NULL
$startSql $endSql $staffSql $storeSql
GROUP BY DATE_FORMAT(dop.ticketCompleteDateTime, '%Y-%m-%d'),
dop.handledBy, u.name, dop.handlerName
), 0) AS totalMinutes,
COALESCE(SUM(la.itemKindCount), 0) AS itemKindCount,
COALESCE(SUM(la.itemQtyPicked), 0) AS itemQtyPicked
FROM dop_scoped d
LEFT JOIN user u ON d.handledBy = u.id AND u.deleted = 0
LEFT JOIN lineAgg la ON la.dopId = d.id
GROUP BY DATE_FORMAT(d.ticketCompleteDateTime, '%Y-%m-%d'),
d.handledBy, u.name, d.handlerName
ORDER BY date, orderCount DESC ORDER BY date, orderCount DESC
""".trimIndent() """.trimIndent()
return jdbcDao.queryForList(sql, args) return jdbcDao.queryForList(sql, args)


+ 6
- 2
src/main/java/com/ffii/fpsms/modules/chart/web/ChartController.kt Vedi File

@@ -194,9 +194,13 @@ class ChartController(
chartService.getStaffDeliveryPerformanceHandlers() chartService.getStaffDeliveryPerformanceHandlers()


/** /**
* FP-MTMS Version Checklist | Functions Ref. No. 61 | v1.0.0 | 2026-08-10
* GET /chart/staff-delivery-performance?startDate=&endDate=&staffNo=A001&staffNo=A002&storeId=2/F&storeIdNull=true * GET /chart/staff-delivery-performance?startDate=&endDate=&staffNo=A001&staffNo=A002&storeId=2/F&storeIdNull=true
* Returns [{ date, staffName, orderCount, totalMinutes }]. Data from delivery_order_pick_order
* (handledBy), orderCount = completed pick tickets, totalMinutes = sum(ticketCompleteDateTime - ticketReleaseTime).
* Returns [{ date, staffName, orderCount, totalMinutes, itemKindCount, itemQtyPicked }].
* Data from delivery_order_pick_order (handledBy); orderCount = completed pick tickets;
* totalMinutes = sum(ticketCompleteDateTime - ticketReleaseTime);
* itemKindCount = sum of per-ticket COUNT(DISTINCT pick_order_line.itemId);
* itemQtyPicked = sum of stock_out_line.qty via pick_order_line.
* Optional storeId filters delivery_order_pick_order.storeId; storeIdNull=true means IS NULL (overrides storeId). * Optional storeId filters delivery_order_pick_order.storeId; storeIdNull=true means IS NULL (overrides storeId).
*/ */
@GetMapping("/staff-delivery-performance") @GetMapping("/staff-delivery-performance")


+ 2
- 0
src/main/java/com/ffii/fpsms/modules/common/ErrorCodes.java Vedi File

@@ -10,6 +10,8 @@ public class ErrorCodes {


public static final String SEND_EMAIL_ERROR = "SEND_EMAIL_ERROR"; public static final String SEND_EMAIL_ERROR = "SEND_EMAIL_ERROR";
public static final String USERNAME_NOT_AVAILABLE = "USERNAME_NOT_AVAILABLE"; public static final String USERNAME_NOT_AVAILABLE = "USERNAME_NOT_AVAILABLE";
public static final String NAME_NOT_AVAILABLE = "NAME_NOT_AVAILABLE";
public static final String STAFF_NO_NOT_AVAILABLE = "STAFF_NO_NOT_AVAILABLE";


public static final String INIT_EXCEL_ERROR = "INIT_EXCEL_ERROR"; public static final String INIT_EXCEL_ERROR = "INIT_EXCEL_ERROR";




+ 9
- 4
src/main/java/com/ffii/fpsms/modules/deliveryOrder/service/DoWorkbenchMainService.kt Vedi File

@@ -267,7 +267,7 @@ open class DoWorkbenchMainService(
} }


/** /**
* FP-MTMS Version Checklist | Functions Ref. No. 38 | v1.0.0 | 2026-08-03
* FP-MTMS Version Checklist | Functions Ref. No. 38 | v1.0.1 | 2026-08-10
* Workbench scan-pick (DO FG): * Workbench scan-pick (DO FG):
* 1) Post outbound on scanned inventory lot line first; on failure return a clear message. * 1) Post outbound on scanned inventory lot line first; on failure return a clear message.
* 2) If the lot runs out before this stock-out line’s chunk is filled and the user did not pass a short [qty], * 2) If the lot runs out before this stock-out line’s chunk is filled and the user did not pass a short [qty],
@@ -2386,10 +2386,15 @@ return MessageResponse(
var postMs = 0L var postMs = 0L
try { try {
if (pickOrderId != null) { if (pickOrderId != null) {
val suggestionStoreId = resolveWorkbenchSuggestionStoreId(pickOrderId, requestStoreId)
// JO assign primes with storeId=null; re-suggest must match or 3F lots are
// filtered out after resolveWorkbenchSuggestionStoreId defaults to "2/F".
val resolvedPoType = pickOrderRepository.findById(pickOrderId).orElse(null)?.type
val suggestionStoreId =
if (resolvedPoType == PickOrderType.JOB_ORDER) null
else resolveWorkbenchSuggestionStoreId(pickOrderId, requestStoreId)
val resuggestExcludeWarehouseCodes = workbenchResuggestExcludeWarehouseCodes( val resuggestExcludeWarehouseCodes = workbenchResuggestExcludeWarehouseCodes(
pickOrderId = pickOrderId, pickOrderId = pickOrderId,
poType = null,
poType = resolvedPoType,
userId = userId, userId = userId,
requestExcludeWarehouseCodes = effectiveExcludeWarehouseCodes, requestExcludeWarehouseCodes = effectiveExcludeWarehouseCodes,
) )
@@ -2620,7 +2625,7 @@ return MessageResponse(
} }


/** /**
* FP-MTMS Version Checklist | Functions Ref. No. 38 | v1.0.0 | 2026-08-03
* FP-MTMS Version Checklist | Functions Ref. No. 38 | v1.0.1 | 2026-08-10
* DO user-audit: Just Complete is reportable only when the item still has pickable stock * DO user-audit: Just Complete is reportable only when the item still has pickable stock
* at complete time (AVAILABLE, not expired, in−out > 0). * at complete time (AVAILABLE, not expired, in−out > 0).
* *


+ 1
- 0
src/main/java/com/ffii/fpsms/modules/jobOrder/service/JoWorkbenchPickConstants.kt Vedi File

@@ -5,6 +5,7 @@ package com.ffii.fpsms.modules.jobOrder.service
* *
* [DEFAULT_EXCLUDE_WAREHOUSE_CODES] applies on **assign / first prime** ([JoWorkbenchMainService]) * [DEFAULT_EXCLUDE_WAREHOUSE_CODES] applies on **assign / first prime** ([JoWorkbenchMainService])
* and on **scan-pick re-suggest** ([com.ffii.fpsms.modules.deliveryOrder.service.DoWorkbenchMainService]). * and on **scan-pick re-suggest** ([com.ffii.fpsms.modules.deliveryOrder.service.DoWorkbenchMainService]).
* JO re-suggest keeps [storeId] null (same as assign); it does not use DO floor store resolution.
*/ */
object JoWorkbenchPickConstants { object JoWorkbenchPickConstants {
val DEFAULT_EXCLUDE_WAREHOUSE_CODES: Set<String> = setOf( val DEFAULT_EXCLUDE_WAREHOUSE_CODES: Set<String> = setOf(


+ 10
- 2
src/main/java/com/ffii/fpsms/modules/master/service/BomService.kt Vedi File

@@ -1430,7 +1430,8 @@ open class BomService(
} }
3 -> { 3 -> {
val equipmentName = tempCell.stringCellValue.trim() val equipmentName = tempCell.stringCellValue.trim()
if (equipmentName != "不適用") {
// 不合用 / 不適用:不掛 equipment(與格式檢查同等處理)
if (!isNotApplicableEquipment(equipmentName)) {
val equipment = bomGetOrCreateEquipment(equipmentName) val equipment = bomGetOrCreateEquipment(equipmentName)
// println("equipment created") // println("equipment created")
bomProcessRequest.equipment = equipment bomProcessRequest.equipment = equipment
@@ -1977,10 +1978,17 @@ open class BomService(
issueLogFileId = issueLogId issueLogFileId = issueLogId
) )
} }
/** FP-MTMS Version Checklist | Functions Ref. No. 63 | v1.0.0 | 2026-08-10 */
/** BOM 工序「使用設備」為不合用/不適用時,不掛 equipment FK */
private fun isNotApplicableEquipment(value: String): Boolean {
val trimmed = value.trim()
return trimmed == "不合用" || trimmed == "不適用"
}

private fun isValidEquipmentType(value: String): Boolean { private fun isValidEquipmentType(value: String): Boolean {
val trimmed = value.trim() val trimmed = value.trim()
if (trimmed.isEmpty()) return false if (trimmed.isEmpty()) return false
if (trimmed == "不合用" || trimmed == "不適用") return true
if (isNotApplicableEquipment(trimmed)) return true
if (trimmed.contains(",")) return false // 新增:不允許逗號 if (trimmed.contains(",")) return false // 新增:不允許逗號
val regex = Regex("^[^-/]+-[^-/]+$") // 例:工具類-切絲機 val regex = Regex("^[^-/]+-[^-/]+$") // 例:工具類-切絲機
return regex.matches(trimmed) return regex.matches(trimmed)


+ 2
- 1
src/main/java/com/ffii/fpsms/modules/productProcess/entity/ProductProcessLineRepository.kt Vedi File

@@ -29,12 +29,13 @@ interface ProductProcessLineRepository : JpaRepository<ProductProcessLine, Long>
""") """)
fun findByProductProcess_IdInWithOperatorAndEquipment(@Param("ids") ids: List<Long>): List<ProductProcessLine> fun findByProductProcess_IdInWithOperatorAndEquipment(@Param("ids") ids: List<Long>): List<ProductProcessLine>


/** FP-MTMS Version Checklist | Functions Ref. No. 41 | v1.0.3 | 2026-08-10 */
@Query( @Query(
""" """
SELECT SELECT
p.jobOrder.id AS jobOrderId, p.jobOrder.id AS jobOrderId,
COUNT(l.id) AS totalLines, COUNT(l.id) AS totalLines,
SUM(CASE WHEN l.status IN ('Completed', 'Pass') THEN 1 ELSE 0 END) AS doneLines
SUM(CASE WHEN l.status IN ('Completed', 'Pass', 'autoPass') THEN 1 ELSE 0 END) AS doneLines
FROM ProductProcessLine l FROM ProductProcessLine l
JOIN l.productProcess p JOIN l.productProcess p
WHERE l.deleted = false WHERE l.deleted = false


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

@@ -1250,7 +1250,7 @@ open class ProductProcessService(
) )
} }


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


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


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


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


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


val lineAggregate = lineStatusByJobOrderId[jobOrderId] val lineAggregate = lineStatusByJobOrderId[jobOrderId]
val totalLines = lineAggregate?.totalLines ?: 0L val totalLines = lineAggregate?.totalLines ?: 0L
val done = { s: String? ->
val x = s?.trim()?.lowercase()
x == "completed" || x == "pass"
}
// After packaging Complete/Pass auto-passes sibling 包裝 lines, qcReady is simply all lines done.

// After packaging Complete/Pass autoPasses sibling 包裝 lines, qcReady is simply all lines done.
val jobLines = linesByJobOrder[jobOrderId].orEmpty() val jobLines = linesByJobOrder[jobOrderId].orEmpty()
val allLinesDone = jobLines.isNotEmpty() && jobLines.all { done(it.status) }
val allLinesDone = jobLines.isNotEmpty() && jobLines.all { isLineDone(it.status) }


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


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


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


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


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


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


private fun isLineDone(status: String?): Boolean { 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) val n = normalizeLineStatus(status)
return n == "completed" || n == "pass" return n == "completed" || n == "pass"
} }
@@ -2128,14 +2122,14 @@ open class ProductProcessService(
return n == "inprogress" || n == "paused" return n == "inprogress" || n == "paused"
} }


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


/** /**
* FP-MTMS Version Checklist | Functions Ref. No. 58 | v1.0.0 | 2026-08-09
* When one 包裝 line is Completed/Pass, auto-Pass other unfinished 包裝 lines on the same job order,
* then sync each affected ProductProcess.status (Pass counts as done → COMPLETED).
* 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) { private fun autoPassSiblingPackagingLines(triggerLineId: Long) {
val trigger = productProcessLineRepository.findById(triggerLineId).orElse(null) ?: return val trigger = productProcessLineRepository.findById(triggerLineId).orElse(null) ?: return
@@ -2163,7 +2157,7 @@ open class ProductProcessService(
line.startTime = now line.startTime = now
} }
line.endTime = now line.endTime = now
line.status = "Pass"
line.status = "autoPass"
productProcessLineRepository.save(line) productProcessLineRepository.save(line)
line.productProcess?.id?.let { affectedProcessIds.add(it) } line.productProcess?.id?.let { affectedProcessIds.add(it) }
} }
@@ -2175,7 +2169,7 @@ open class ProductProcessService(


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


// 检查是否所有 lines 都是 "Completed" 或 "Pass"
if (allproductProcessLines.all { it.status == "Completed" || it.status == "Pass" }) {
// 检查是否所有 lines 都是 Completed / Pass / autoPass
if (allproductProcessLines.all { isLineDone(it.status) }) {
// 更新 product process 的 endTime 和状态 // 更新 product process 的 endTime 和状态
updateProductProcessEndTime(productProcessId) updateProductProcessEndTime(productProcessId)
updateProductProcessStatus(productProcessId, ProductProcessStatus.COMPLETED) updateProductProcessStatus(productProcessId, ProductProcessStatus.COMPLETED)
@@ -3097,7 +3091,7 @@ open class ProductProcessService(
} }


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


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

@@ -200,7 +200,7 @@ class ProductProcessController(
return productProcessService.getAllJoborderProductProcessInfo(bomType) return productProcessService.getAllJoborderProductProcessInfo(bomType)
} }


/** FP-MTMS Version Checklist | Functions Ref. No. 41 | v1.0.1 | 2026-08-06 */
/** FP-MTMS Version Checklist | Functions Ref. No. 41 | v1.0.3 | 2026-08-10 */
@GetMapping("/Demo/Process/search") @GetMapping("/Demo/Process/search")
fun demoprocesssearch( fun demoprocesssearch(
@RequestParam(required = false) date: String?, @RequestParam(required = false) date: String?,


+ 1
- 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, stock-in not completed/rejected;
* Nav alerts aligned with 完成QC工單 list: all product process lines Completed/Pass/autoPass, stock-in not completed/rejected;
* job order has at least one process dated today or yesterday. [qc] = before received; [putAway] = received / partially_completed. * job order has at least one process dated today or yesterday. [qc] = before received; [putAway] = received / partially_completed.
*/ */
data class JobOrderFgAlertRowResponse( data class JobOrderFgAlertRowResponse(


+ 128
- 0
src/main/java/com/ffii/fpsms/modules/report/service/DoInventoryUomMismatchReportService.kt Vedi File

@@ -0,0 +1,128 @@
package com.ffii.fpsms.modules.report.service

import com.ffii.core.support.JdbcDao
import com.ffii.fpsms.modules.deliveryOrder.service.DoFloorSupplierSettingsService
import org.springframework.stereotype.Service
import java.time.LocalDate
import java.time.format.DateTimeParseException

@Service
class DoInventoryUomMismatchReportService(
private val jdbcDao: JdbcDao,
private val doFloorSupplierSettingsService: DoFloorSupplierSettingsService,
) {
/**
* FP-MTMS Version Checklist | Functions Ref. No. 60 | v1.0.0 | 2026-08-10
* Pending DO lines by estimatedArrivalDate whose current inventory lot UOM
* does not match the DO line UOM. Excel-only report.
* Supplier scope follows DoSearch floor lists (2F / 4F / All = both).
*/
fun searchDoInventoryUomMismatch(
deliveryDate: String?,
storeId: String? = null,
): List<Map<String, Any?>> {
val raw = deliveryDate?.trim()?.replace("/", "-").orEmpty()
if (raw.isBlank()) {
throw IllegalArgumentException("deliveryDate is required")
}

val date = try {
LocalDate.parse(raw)
} catch (_: DateTimeParseException) {
throw IllegalArgumentException("deliveryDate must be yyyy-MM-dd")
}

val today = LocalDate.now()
if (date.isBefore(today)) {
throw IllegalArgumentException("deliveryDate must not be before today")
}

val floor = storeId?.trim().orEmpty().let { f ->
when {
f.isEmpty() || f.equals("All", ignoreCase = true) -> "ALL"
else -> f
}
}
val allowedSupplierCodes = doFloorSupplierSettingsService.allowedSupplierCodesForFloor(floor)
if (allowedSupplierCodes.isEmpty()) {
return emptyList()
}

val args = mutableMapOf<String, Any>(
"deliveryDate" to date.toString(),
"allowedSupplierCodes" to allowedSupplierCodes,
)

// Drive from pending DO by estimatedArrivalDate (not orderDate / pick requiredDeliveryDate).
// STRAIGHT_JOIN keeps DO CTE as driver (avoids full inventory_lot scan).
// Supplier filter matches DoSearch: d.supplier.code IN floor settings (2F/4F/All).
val sql = """
WITH do_lines AS (
SELECT DISTINCT
do.code AS doCode,
DATE(do.estimatedArrivalDate) AS deliveryDate,
s.code AS supplierCode,
s.name AS supplierName,
dol.id AS doLineId,
dol.itemId AS itemId,
dol.itemNo AS itemNo,
dol.qty AS doQty,
dol.uomId AS doUomId,
uc_do.udfudesc AS doUom
FROM delivery_order do
INNER JOIN shop s
ON s.id = do.supplierId
AND s.deleted = 0
AND s.code IN (:allowedSupplierCodes)
INNER JOIN delivery_order_line dol
ON dol.deliveryOrderId = do.id
AND dol.deleted = 0
LEFT JOIN uom_conversion uc_do
ON uc_do.id = dol.uomId
AND uc_do.deleted = 0
WHERE do.deleted = 0
AND do.status = 'pending'
AND do.supplierId IS NOT NULL
AND do.estimatedArrivalDate IS NOT NULL
AND DATE(do.estimatedArrivalDate) = :deliveryDate
)
SELECT
DATE_FORMAT(d.deliveryDate, '%Y-%m-%d') AS deliveryDate,
d.doCode AS doCode,
d.supplierCode AS supplierCode,
d.supplierName AS supplierName,
d.itemNo AS itemCode,
it.name AS itemName,
d.doQty AS doQty,
d.doUom AS doUom,
d.doUomId AS doUomId,
il.lotNo AS mismatchLotNo,
(IFNULL(ill.inQty, 0) - IFNULL(ill.outQty, 0)) AS mismatchLotQty,
uc_ill.udfudesc AS mismatchLotUom,
iu.uomId AS inventoryUomId
FROM do_lines d
STRAIGHT_JOIN inventory_lot il
ON il.itemId = d.itemId
AND il.deleted = 0
STRAIGHT_JOIN inventory_lot_line ill
ON ill.inventoryLotId = il.id
AND ill.deleted = 0
AND (IFNULL(ill.inQty, 0) - IFNULL(ill.outQty, 0)) > 0
STRAIGHT_JOIN item_uom iu
ON iu.id = ill.stockItemUomId
AND iu.deleted = 0
LEFT JOIN uom_conversion uc_ill
ON uc_ill.id = iu.uomId
AND uc_ill.deleted = 0
LEFT JOIN items it
ON it.id = d.itemId
AND it.deleted = 0
WHERE d.doUomId IS NULL
OR iu.uomId IS NULL
OR d.doUomId <> iu.uomId
ORDER BY d.doCode, d.itemNo, il.lotNo
""".trimIndent()

return jdbcDao.queryForList(sql, args)
}
}

+ 25
- 7
src/main/java/com/ffii/fpsms/modules/report/service/ShopOrderReplenishmentReportService.kt Vedi File

@@ -15,6 +15,7 @@ import java.math.BigDecimal
class ShopOrderReplenishmentReportService( class ShopOrderReplenishmentReportService(
private val jdbcDao: JdbcDao, private val jdbcDao: JdbcDao,
) { ) {
/** FP-MTMS Version Checklist | Functions Ref. No. 49 | v1.0.1 | 2026-08-10 */
fun searchShopOrderReplenishmentReport( fun searchShopOrderReplenishmentReport(
reorderDateStart: String?, reorderDateStart: String?,
reorderDateEnd: String?, reorderDateEnd: String?,
@@ -87,6 +88,7 @@ class ShopOrderReplenishmentReportService(
DATE_FORMAT(dr.created, '%Y-%m-%d') AS reorderDate, DATE_FORMAT(dr.created, '%Y-%m-%d') AS reorderDate,
dr.reason AS reason, dr.reason AS reason,
DATE_FORMAT(dop.requiredDeliveryDate, '%Y-%m-%d') AS deliveredDate, DATE_FORMAT(dop.requiredDeliveryDate, '%Y-%m-%d') AS deliveredDate,
TRIM(IFNULL(dop.handlerName, '')) AS actualDeliveredHandler,
dr.sourceDoId AS sourceDoId, dr.sourceDoId AS sourceDoId,
dr.itemId AS itemId, dr.itemId AS itemId,
dr.pickOrderLineId AS pickOrderLineId dr.pickOrderLineId AS pickOrderLineId
@@ -130,7 +132,7 @@ class ShopOrderReplenishmentReportService(
val itemIds = baseRows.mapNotNull { longVal(it["itemId"]) }.distinct() val itemIds = baseRows.mapNotNull { longVal(it["itemId"]) }.distinct()


val actualDeliveredByPolId = loadActualDeliveredQtyByPickOrderLineId(pickOrderLineIds) val actualDeliveredByPolId = loadActualDeliveredQtyByPickOrderLineId(pickOrderLineIds)
val firstOrderPickBySourceKey = loadFirstOrderActualPickQty(sourceDoIds, itemIds, pickOrderLineIds)
val firstOrderPickBySourceKey = loadFirstOrderPickInfo(sourceDoIds, itemIds, pickOrderLineIds)


val rows = baseRows.map { row -> val rows = baseRows.map { row ->
val polId = longVal(row["pickOrderLineId"]) val polId = longVal(row["pickOrderLineId"])
@@ -138,6 +140,7 @@ class ShopOrderReplenishmentReportService(
val itemId = longVal(row["itemId"]) val itemId = longVal(row["itemId"])
val sourceKey = val sourceKey =
if (sourceDoId != null && itemId != null) sourceDoId to itemId else null if (sourceDoId != null && itemId != null) sourceDoId to itemId else null
val firstOrder = sourceKey?.let { firstOrderPickBySourceKey[it] }


linkedMapOf<String, Any?>( linkedMapOf<String, Any?>(
"shopNo" to row["shopNo"], "shopNo" to row["shopNo"],
@@ -147,11 +150,13 @@ class ShopOrderReplenishmentReportService(
"itemNo" to row["itemNo"], "itemNo" to row["itemNo"],
"itemName" to row["itemName"], "itemName" to row["itemName"],
"firstOrderQty" to row["firstOrderQty"], "firstOrderQty" to row["firstOrderQty"],
"firstOrderActualPickQty" to (sourceKey?.let { firstOrderPickBySourceKey[it] } ?: BigDecimal.ZERO),
"firstOrderActualPickQty" to (firstOrder?.qty ?: BigDecimal.ZERO),
"firstOrderPickerHandler" to (firstOrder?.handler ?: ""),
"reorderQty" to row["reorderQty"], "reorderQty" to row["reorderQty"],
"reorderDate" to row["reorderDate"], "reorderDate" to row["reorderDate"],
"reason" to row["reason"], "reason" to row["reason"],
"actualDeliveredQty" to (polId?.let { actualDeliveredByPolId[it] } ?: BigDecimal.ZERO), "actualDeliveredQty" to (polId?.let { actualDeliveredByPolId[it] } ?: BigDecimal.ZERO),
"actualDeliveredHandler" to (row["actualDeliveredHandler"] ?: ""),
"deliveredDate" to row["deliveredDate"], "deliveredDate" to row["deliveredDate"],
) )
} }
@@ -185,14 +190,15 @@ class ShopOrderReplenishmentReportService(
} }


/** /**
* Sum stock_out_line.qty for source DO pick order lines of the same item,
* First-order pick qty + handler for source DO + item,
* excluding any POL that is itself a replenishment line (incl. current report POLs). * excluding any POL that is itself a replenishment line (incl. current report POLs).
* Same ticket has at most one handler; MAX() is for GROUP BY only.
*/ */
private fun loadFirstOrderActualPickQty(
private fun loadFirstOrderPickInfo(
sourceDoIds: List<Long>, sourceDoIds: List<Long>,
itemIds: List<Long>, itemIds: List<Long>,
excludePickOrderLineIds: List<Long>, excludePickOrderLineIds: List<Long>,
): Map<Pair<Long, Long>, BigDecimal> {
): Map<Pair<Long, Long>, FirstOrderPickInfo> {
if (sourceDoIds.isEmpty() || itemIds.isEmpty()) return emptyMap() if (sourceDoIds.isEmpty() || itemIds.isEmpty()) return emptyMap()


val args = mutableMapOf<String, Any>( val args = mutableMapOf<String, Any>(
@@ -210,7 +216,8 @@ class ShopOrderReplenishmentReportService(
SELECT SELECT
po.doId AS sourceDoId, po.doId AS sourceDoId,
pol.itemId AS itemId, pol.itemId AS itemId,
SUM(IFNULL(sol.qty, 0)) AS firstOrderActualPickQty
SUM(IFNULL(sol.qty, 0)) AS firstOrderActualPickQty,
MAX(TRIM(IFNULL(dop.handlerName, ''))) AS firstOrderPickerHandler
FROM pick_order po FROM pick_order po
INNER JOIN pick_order_line pol INNER JOIN pick_order_line pol
ON pol.poId = po.id ON pol.poId = po.id
@@ -218,6 +225,9 @@ class ShopOrderReplenishmentReportService(
LEFT JOIN stock_out_line sol LEFT JOIN stock_out_line sol
ON sol.pickOrderLineId = pol.id ON sol.pickOrderLineId = pol.id
AND IFNULL(sol.deleted, 0) = 0 AND IFNULL(sol.deleted, 0) = 0
LEFT JOIN delivery_order_pick_order dop
ON dop.id = po.deliveryOrderPickOrderId
AND IFNULL(dop.deleted, 0) = 0
WHERE IFNULL(po.deleted, 0) = 0 WHERE IFNULL(po.deleted, 0) = 0
AND po.doId IN (:sourceDoIds) AND po.doId IN (:sourceDoIds)
AND pol.itemId IN (:itemIds) AND pol.itemId IN (:itemIds)
@@ -236,10 +246,18 @@ class ShopOrderReplenishmentReportService(
return rows.mapNotNull { row -> return rows.mapNotNull { row ->
val sourceDoId = longVal(row["sourceDoId"]) ?: return@mapNotNull null val sourceDoId = longVal(row["sourceDoId"]) ?: return@mapNotNull null
val itemId = longVal(row["itemId"]) ?: return@mapNotNull null val itemId = longVal(row["itemId"]) ?: return@mapNotNull null
(sourceDoId to itemId) to decimalVal(row["firstOrderActualPickQty"])
(sourceDoId to itemId) to FirstOrderPickInfo(
qty = decimalVal(row["firstOrderActualPickQty"]),
handler = row["firstOrderPickerHandler"]?.toString()?.trim().orEmpty(),
)
}.toMap() }.toMap()
} }


private data class FirstOrderPickInfo(
val qty: BigDecimal,
val handler: String,
)

private fun normalizeDate(raw: String): String = raw.trim().replace("/", "-") private fun normalizeDate(raw: String): String = raw.trim().replace("/", "-")


private fun dateStartClause( private fun dateStartClause(


+ 116
- 0
src/main/java/com/ffii/fpsms/modules/report/web/DoInventoryUomMismatchReportController.kt Vedi File

@@ -0,0 +1,116 @@
package com.ffii.fpsms.modules.report.web

import com.ffii.fpsms.modules.report.service.DoInventoryUomMismatchReportService
import org.apache.poi.ss.usermodel.BorderStyle
import org.apache.poi.ss.usermodel.FillPatternType
import org.apache.poi.ss.usermodel.HorizontalAlignment
import org.apache.poi.ss.usermodel.IndexedColors
import org.apache.poi.ss.usermodel.VerticalAlignment
import org.apache.poi.ss.util.WorkbookUtil
import org.apache.poi.xssf.usermodel.XSSFWorkbook
import org.springframework.http.HttpHeaders
import org.springframework.http.HttpStatus
import org.springframework.http.MediaType
import org.springframework.http.ResponseEntity
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RequestParam
import org.springframework.web.bind.annotation.RestController
import java.io.ByteArrayOutputStream

@RestController
@RequestMapping("/report")
class DoInventoryUomMismatchReportController(
private val doInventoryUomMismatchReportService: DoInventoryUomMismatchReportService,
) {
/** FP-MTMS Version Checklist | Functions Ref. No. 60 | v1.0.0 | 2026-08-10 */
@GetMapping("/print-do-inventory-uom-mismatch-excel")
fun exportExcel(
@RequestParam(required = false) deliveryDate: String?,
@RequestParam(required = false) storeId: String?,
): ResponseEntity<ByteArray> {
val rows = try {
doInventoryUomMismatchReportService.searchDoInventoryUomMismatch(deliveryDate, storeId)
} catch (ex: IllegalArgumentException) {
return ResponseEntity.status(HttpStatus.BAD_REQUEST).build()
}

val bytes = buildExcel(rows)
val headers = HttpHeaders().apply {
contentType = MediaType.parseMediaType(
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
)
// ASCII only — Tomcat rejects non-Latin-1 in Content-Disposition
setContentDispositionFormData("attachment", "DoInventoryUomMismatchReport.xlsx")
}
return ResponseEntity(bytes, headers, HttpStatus.OK)
}

private fun buildExcel(rows: List<Map<String, Any?>>): ByteArray {
val workbook = XSSFWorkbook()
val sheet = workbook.createSheet(WorkbookUtil.createSafeSheetName("DO_UOM_mismatch"))
val headerStyle = workbook.createCellStyle().apply {
alignment = HorizontalAlignment.CENTER
verticalAlignment = VerticalAlignment.CENTER
fillForegroundColor = IndexedColors.GREY_25_PERCENT.index
fillPattern = FillPatternType.SOLID_FOREGROUND
borderTop = BorderStyle.THIN
borderBottom = BorderStyle.THIN
borderLeft = BorderStyle.THIN
borderRight = BorderStyle.THIN
val font = workbook.createFont().apply { bold = true }
setFont(font)
}
val textStyle = workbook.createCellStyle().apply {
borderTop = BorderStyle.THIN
borderBottom = BorderStyle.THIN
borderLeft = BorderStyle.THIN
borderRight = BorderStyle.THIN
}

val columns = listOf(
"deliveryDate" to "預計到貨日期",
"doCode" to "送貨單號",
"supplierCode" to "供應商編號",
"supplierName" to "供應商名稱",
"itemCode" to "貨品編號",
"itemName" to "貨品名稱",
"doQty" to "送貨單數量",
"doUom" to "送貨單單位",
"mismatchLotNo" to "不符批號",
"mismatchLotQty" to "不符批數量",
"mismatchLotUom" to "不符批單位",
)

var r = 0
val headerRow = sheet.createRow(r++)
columns.forEachIndexed { i, (_, label) ->
headerRow.createCell(i).apply {
setCellValue(label)
cellStyle = headerStyle
}
}

for (row in rows) {
val excelRow = sheet.createRow(r++)
columns.forEachIndexed { i, (key, _) ->
val v = row[key]
val cell = excelRow.createCell(i)
cell.cellStyle = textStyle
when (v) {
null -> cell.setCellValue("")
is Number -> cell.setCellValue(v.toDouble())
else -> cell.setCellValue(v.toString())
}
}
}

val widths = intArrayOf(14, 16, 12, 18, 14, 28, 12, 14, 18, 12, 14)
widths.forEachIndexed { i, w -> sheet.setColumnWidth(i, w * 256) }

val out = ByteArrayOutputStream()
workbook.write(out)
workbook.close()
return out.toByteArray()
}
}

+ 4
- 0
src/main/java/com/ffii/fpsms/modules/user/entity/UserRepository.java Vedi File

@@ -13,6 +13,8 @@ import com.ffii.fpsms.modules.user.entity.projections.UserCombo;
public interface UserRepository extends AbstractRepository<User, Long> { public interface UserRepository extends AbstractRepository<User, Long> {


List<User> findByName(@Param("name") String name); List<User> findByName(@Param("name") String name);

List<User> findByNameAndDeletedFalse(String name);
Optional<User> findByUsernameAndDeletedFalse(String username); Optional<User> findByUsernameAndDeletedFalse(String username);


@@ -20,6 +22,8 @@ public interface UserRepository extends AbstractRepository<User, Long> {


Optional<User> findByStaffNo(@Param("staffNo") String staffNo); Optional<User> findByStaffNo(@Param("staffNo") String staffNo);


Optional<User> findByStaffNoAndDeletedFalse(String staffNo);

@Modifying @Modifying
@Query(value = """ @Query(value = """
INSERT INTO user_authority (userID, authId) INSERT INTO user_authority (userID, authId)


+ 23
- 0
src/main/java/com/ffii/fpsms/modules/user/service/UserService.java Vedi File

@@ -92,6 +92,22 @@ public class UserService extends AbstractBaseEntityService<User, Long, UserRepos
return userRepository.findByUsernameAndDeletedFalse(username); return userRepository.findByUsernameAndDeletedFalse(username);
} }


/** FP-MTMS Version Checklist | Functions Ref. No. 62 | v1.0.0 | 2026-08-10 */
public boolean isNameTaken(String name) {
if (StringUtils.isBlank(name)) {
return false;
}
return !userRepository.findByNameAndDeletedFalse(name).isEmpty();
}

/** FP-MTMS Version Checklist | Functions Ref. No. 62 | v1.0.0 | 2026-08-10 */
public boolean isStaffNoTaken(String staffNo) {
if (StringUtils.isBlank(staffNo)) {
return false;
}
return userRepository.findByStaffNoAndDeletedFalse(staffNo).isPresent();
}

// @Transactional(rollbackFor = Exception.class) // @Transactional(rollbackFor = Exception.class)
public List<UserRecord> search(SearchUserReq req) { public List<UserRecord> search(SearchUserReq req) {
StringBuilder sql = new StringBuilder("SELECT" StringBuilder sql = new StringBuilder("SELECT"
@@ -199,11 +215,18 @@ public class UserService extends AbstractBaseEntityService<User, Long, UserRepos
return instance; return instance;
} }


/** FP-MTMS Version Checklist | Functions Ref. No. 62 | v1.0.0 | 2026-08-10 */
@Transactional(rollbackFor = Exception.class) @Transactional(rollbackFor = Exception.class)
public User newRecord(NewUserReq req) throws UnsupportedEncodingException { public User newRecord(NewUserReq req) throws UnsupportedEncodingException {
if (findByUsername(req.getUsername()).isPresent()) { if (findByUsername(req.getUsername()).isPresent()) {
throw new UnprocessableEntityException(ErrorCodes.USERNAME_NOT_AVAILABLE); throw new UnprocessableEntityException(ErrorCodes.USERNAME_NOT_AVAILABLE);
} }
if (isNameTaken(req.getName())) {
throw new UnprocessableEntityException(ErrorCodes.NAME_NOT_AVAILABLE);
}
if (isStaffNoTaken(req.getStaffNo())) {
throw new UnprocessableEntityException(ErrorCodes.STAFF_NO_NOT_AVAILABLE);
}


System.out.println("Start Save"); System.out.println("Start Save");




Caricamento…
Annulla
Salva