From 02c4a15b5603bb6958bdcb455dc67431ed442d67 Mon Sep 17 00:00:00 2001 From: "CANCERYS\\kw093" Date: Wed, 28 Oct 2026 15:09:53 +0800 Subject: [PATCH] update --- .../modules/report/service/ReportService.kt | 4 +- .../service/StockTakeVarianceReportService.kt | 4 +- .../stock/service/StockTakeRecordService.kt | 53 ++++++++++++++++--- .../stock/web/StockTakeRecordController.kt | 8 +++ src/main/resources/application-db-local.yml | 2 +- src/main/resources/application-db-toa.yml | 2 +- ...kfill_approver_on_completed_unapproved.sql | 42 +++++++++++++++ 7 files changed, 102 insertions(+), 13 deletions(-) create mode 100644 src/main/resources/db/changelog/changes/20261028_fix_completed_unapproved_stock_take/01_backfill_approver_on_completed_unapproved.sql diff --git a/src/main/java/com/ffii/fpsms/modules/report/service/ReportService.kt b/src/main/java/com/ffii/fpsms/modules/report/service/ReportService.kt index ec5f3020..0c32569f 100644 --- a/src/main/java/com/ffii/fpsms/modules/report/service/ReportService.kt +++ b/src/main/java/com/ffii/fpsms/modules/report/service/ReportService.kt @@ -298,8 +298,8 @@ return result END AS mode, CONCAT( CASE - WHEN LOWER(COALESCE(MAX(st.stockTakeRoundMode), 'section')) = 'item' THEN '【依貨品】' - ELSE '【依區域】' + WHEN LOWER(COALESCE(MAX(st.stockTakeRoundMode), 'section')) = 'item' THEN '【貨品盤點】' + ELSE '【區域盤點】' END, ' ', CASE diff --git a/src/main/java/com/ffii/fpsms/modules/report/service/StockTakeVarianceReportService.kt b/src/main/java/com/ffii/fpsms/modules/report/service/StockTakeVarianceReportService.kt index f688166a..a0b24bf1 100644 --- a/src/main/java/com/ffii/fpsms/modules/report/service/StockTakeVarianceReportService.kt +++ b/src/main/java/com/ffii/fpsms/modules/report/service/StockTakeVarianceReportService.kt @@ -1074,8 +1074,8 @@ ORDER BY SELECT CONCAT( CASE - WHEN LOWER(COALESCE(MAX(st.stockTakeRoundMode), 'section')) = 'item' THEN '【依貨品】' - ELSE '【依區域】' + WHEN LOWER(COALESCE(MAX(st.stockTakeRoundMode), 'section')) = 'item' THEN '【貨品盤點】' + ELSE '【區域盤點】' END, ' ', CASE diff --git a/src/main/java/com/ffii/fpsms/modules/stock/service/StockTakeRecordService.kt b/src/main/java/com/ffii/fpsms/modules/stock/service/StockTakeRecordService.kt index 40b31803..abe2b2fb 100644 --- a/src/main/java/com/ffii/fpsms/modules/stock/service/StockTakeRecordService.kt +++ b/src/main/java/com/ffii/fpsms/modules/stock/service/StockTakeRecordService.kt @@ -1339,6 +1339,38 @@ lot_root_origin AS ( .sorted() } + /** + * 建立區域輪前預覽:所選 section 目前沒有可預建明細的可用 lot + *(與 [createPlaceholderStockTakeRecordsForStockTake] 規則一致:availableQty>0 且非 busy)。 + * 建立後會變成「空區」單頭。 + */ + open fun previewSectionsWithNoAvailableLots(sections: Collection): List { + val wanted = sections.map { it.trim() }.filter { it.isNotBlank() }.distinct() + if (wanted.isEmpty()) return emptyList() + val busyLotKeys = busyOpenLotWarehouseKeys() + val empty = ArrayList() + for (section in wanted) { + val warehouses = warehouseRepository.findAllByStockTakeSectionAndDeletedIsFalse(section) + val warehouseIds = warehouses.mapNotNull { it.id } + if (warehouseIds.isEmpty()) { + empty.add(section) + continue + } + val hasCreatable = inventoryLotLineRepository + .findAllByWarehouseIdInAndDeletedIsFalseAndStatusIsAvailable(warehouseIds) + .any { ill -> + val lotId = ill.inventoryLot?.id ?: return@any false + val warehouseId = ill.warehouse?.id ?: return@any false + if ((lotId to warehouseId) in busyLotKeys) return@any false + val availableQty = (ill.inQty ?: BigDecimal.ZERO) + .subtract(ill.outQty ?: BigDecimal.ZERO) + availableQty.compareTo(BigDecimal.ZERO) > 0 + } + if (!hasCreatable) empty.add(section) + } + return empty.sorted() + } + /** * 建立區域輪前預覽:所選 section 內已被 open「依貨品」輪佔用的貨品 *(Option B 會略過這些 lot,本輪不會盤到)。 @@ -1892,6 +1924,7 @@ ORDER BY it.code, wh.stockTakeSection, il.lotNo val totalLots = countedRecords.size val stockTakenCount: Int val approvedCount: Int + val sectionCountForProgress: Int if (itemMode) { stockTakenCount = countedRecords.count { it.pickerFirstStockTakeQty != null || it.status == "pass" || it.status == "completed" @@ -1899,8 +1932,13 @@ ORDER BY it.code, wh.stockTakeSection, il.lotNo approvedCount = countedRecords.count { it.status == "completed" || it.approverStockTakeQty != null || it.approverTime != null } + sectionCountForProgress = sections.size } else { val bySection = records.groupBy { it.stockTakeSection?.trim()?.lowercase() } + // Empty sections (header with 0 records) do not count toward progress denominator. + val sectionsWithLots = sections.filter { sec -> + bySection[sec.lowercase()].orEmpty().isNotEmpty() + } fun sectionComplete(section: String, approved: Boolean): Boolean { val group = bySection[section.lowercase()].orEmpty() if (group.isEmpty()) return false @@ -1910,8 +1948,9 @@ ORDER BY it.code, wh.stockTakeSection, il.lotNo group.all { it.pickerFirstStockTakeQty != null } } } - stockTakenCount = sections.count { sectionComplete(it, false) } - approvedCount = sections.count { sectionComplete(it, true) } + stockTakenCount = sectionsWithLots.count { sectionComplete(it, false) } + approvedCount = sectionsWithLots.count { sectionComplete(it, true) } + sectionCountForProgress = sectionsWithLots.size } return StockTakeRoundCardResponse( stockTakeRoundId = roundKey, @@ -1921,7 +1960,7 @@ ORDER BY it.code, wh.stockTakeSection, il.lotNo yearMonth = yearMonth, open = open, status = statusValue, - sectionCount = sections.size, + sectionCount = sectionCountForProgress, totalItemNumber = countedRecords.mapNotNull { it.itemId }.distinct().size, totalInventoryLotNumber = totalLots, stockTakenCount = stockTakenCount, @@ -2405,7 +2444,8 @@ ORDER BY it.code, wh.stockTakeSection, il.lotNo existingRecord } existingRecord.pickerFirstStockTakeQty == null -> { - val isCompled = inventoryLotLine.inQty == inventoryLotLine.outQty + // Always pass (await approver). Do not auto-complete when inQty==outQty — + // that left status=completed with null approver and hid rows on approver UI. val varianceQty = availableQty - request.qty - request.badQty existingRecord.apply { this.stockTakeRoundId = this.stockTakeRoundId ?: (stockTake.stockTakeRoundId ?: stockTake.id) @@ -2413,7 +2453,7 @@ ORDER BY it.code, wh.stockTakeSection, il.lotNo this.pickerFirstBadQty = request.badQty this.bookQty = availableQty this.varianceQty = varianceQty - this.status = if (isCompled) "completed" else "pass" + this.status = "pass" this.remarks = request.remark ?: this.remarks this.stockTakerId = stockTakerId this.stockTakerName = user?.name @@ -2448,7 +2488,6 @@ ORDER BY it.code, wh.stockTakeSection, il.lotNo else -> throw IllegalArgumentException("Stock take record already has first and second counts") } } else { - val isCompled = inventoryLotLine.inQty == inventoryLotLine.outQty val varianceQty = availableQty - request.qty - request.badQty StockTakeRecord().apply { this.itemId = item.id @@ -2466,7 +2505,7 @@ ORDER BY it.code, wh.stockTakeSection, il.lotNo this.varianceQty = varianceQty this.uom = inventoryLotLine.stockUom?.uom?.udfudesc this.date = java.time.LocalDate.now() - this.status = if (isCompled) "completed" else "pass" + this.status = "pass" this.remarks = request.remark this.itemCode = item.code this.itemName = item.name diff --git a/src/main/java/com/ffii/fpsms/modules/stock/web/StockTakeRecordController.kt b/src/main/java/com/ffii/fpsms/modules/stock/web/StockTakeRecordController.kt index a8a6ed6e..c8da2a69 100644 --- a/src/main/java/com/ffii/fpsms/modules/stock/web/StockTakeRecordController.kt +++ b/src/main/java/com/ffii/fpsms/modules/stock/web/StockTakeRecordController.kt @@ -213,6 +213,14 @@ class StockTakeRecordController( return stockOutRecordService.previewSectionCreateItemOverlaps(sections.orEmpty()) } + /** 建立區域輪預覽:目前沒有可盤 lot、建立後會成空區的 section */ + @GetMapping("/sectionsWithNoAvailableLots") + fun sectionsWithNoAvailableLots( + @RequestParam sections: List?, + ): List { + return stockOutRecordService.previewSectionsWithNoAvailableLots(sections.orEmpty()) + } + /** 建立 item 輪:搜尋有可用 lot 的貨品(可依來源 PP/PF/其他) */ @GetMapping("/searchItemsForCreate") fun searchItemsForCreate( diff --git a/src/main/resources/application-db-local.yml b/src/main/resources/application-db-local.yml index 1b420d31..dd631fd2 100644 --- a/src/main/resources/application-db-local.yml +++ b/src/main/resources/application-db-local.yml @@ -1,6 +1,6 @@ spring: datasource: - jdbc-url: jdbc:mysql://127.0.0.1:3308/fpsmsdb?useUnicode=true&characterEncoding=UTF8&serverTimezone=GMT%2B8 + jdbc-url: jdbc:mysql://127.0.0.1:3306/fpsmsdb?useUnicode=true&characterEncoding=UTF8&serverTimezone=GMT%2B8 username: root password: secret diff --git a/src/main/resources/application-db-toa.yml b/src/main/resources/application-db-toa.yml index f4405b6d..1a57ad5b 100644 --- a/src/main/resources/application-db-toa.yml +++ b/src/main/resources/application-db-toa.yml @@ -1,6 +1,6 @@ spring: datasource: - jdbc-url: jdbc:mysql://10.100.0.81:3008/fpsmsdb?useUnicode=true&characterEncoding=UTF8&serverTimezone=GMT%2B8 + jdbc-url: jdbc:mysql://10.100.0.81:3006/fpsmsdb?useUnicode=true&characterEncoding=UTF8&serverTimezone=GMT%2B8 username: root password: secret diff --git a/src/main/resources/db/changelog/changes/20261028_fix_completed_unapproved_stock_take/01_backfill_approver_on_completed_unapproved.sql b/src/main/resources/db/changelog/changes/20261028_fix_completed_unapproved_stock_take/01_backfill_approver_on_completed_unapproved.sql new file mode 100644 index 00000000..debe5af3 --- /dev/null +++ b/src/main/resources/db/changelog/changes/20261028_fix_completed_unapproved_stock_take/01_backfill_approver_on_completed_unapproved.sql @@ -0,0 +1,42 @@ +--liquibase formatted sql +--changeset fpsms:20261028_fix_completed_unapproved_stock_take + +-- Option B: records that picker auto-marked status=completed when inQty==outQty +-- never got approver fields, so they stayed on "pending approval" API but were +-- invisible in the UI status filter (pending/pass/notMatch only). +-- Treat them as approved using the picker count (first, or second if present). +-- Does not touch notMatch / pass / pending rows. + +UPDATE stocktakerecord +SET + approverStockTakeQty = COALESCE(pickerSecondStockTakeQty, pickerFirstStockTakeQty, 0), + approverBadQty = COALESCE(pickerSecondBadQty, pickerFirstBadQty), + approverTime = IFNULL(stockTakeEndTime, NOW()), + lastSelect = IFNULL( + lastSelect, + CASE WHEN pickerSecondStockTakeQty IS NOT NULL THEN 2 ELSE 1 END + ), + stockTakeEndTime = IFNULL(stockTakeEndTime, NOW()), + modified = NOW(), + modifiedBy = 'sql-fix-completed-unapproved' +WHERE deleted = 0 + AND LOWER(status) = 'completed' + AND approverStockTakeQty IS NULL + AND approverTime IS NULL; + +-- Keep stock_take_line in sync (approver no-variance path sets finalQty + completed). +UPDATE stock_take_line stl + INNER JOIN stocktakerecord r + ON r.id = stl.stockTakeRecordId + AND r.deleted = 0 +SET + stl.finalQty = COALESCE(stl.finalQty, r.approverStockTakeQty), + stl.status = 'completed', + stl.completeDate = IFNULL(stl.completeDate, r.approverTime), + stl.modified = NOW(), + stl.modifiedBy = 'sql-fix-completed-unapproved' +WHERE stl.deleted = 0 + AND stl.finalQty IS NULL + AND r.modifiedBy = 'sql-fix-completed-unapproved' + AND r.approverStockTakeQty IS NOT NULL + AND r.approverTime IS NOT NULL;