diff --git a/src/main/java/com/ffii/fpsms/modules/deliveryOrder/service/DoFloorSupplierSettingsService.kt b/src/main/java/com/ffii/fpsms/modules/deliveryOrder/service/DoFloorSupplierSettingsService.kt index a6bc9f0..9b43642 100644 --- a/src/main/java/com/ffii/fpsms/modules/deliveryOrder/service/DoFloorSupplierSettingsService.kt +++ b/src/main/java/com/ffii/fpsms/modules/deliveryOrder/service/DoFloorSupplierSettingsService.kt @@ -4,7 +4,7 @@ import com.ffii.fpsms.modules.settings.entity.SettingsRepository import org.springframework.stereotype.Service import java.util.Locale -/** 供 DO 搜尋/車線/報表 SQL 等共用的 2F/4F 供應商代碼(來自 `settings` CSV)。 */ +/** 供 DO 搜索/車線/報表 SQL 等共用的 2F/4F 供應商代碼(來自 `settings` CSV)。 */ @Service open class DoFloorSupplierSettingsService( private val settingsRepository: SettingsRepository, diff --git a/src/main/java/com/ffii/fpsms/modules/master/entity/WarehouseRepository.kt b/src/main/java/com/ffii/fpsms/modules/master/entity/WarehouseRepository.kt index 5abf7d6..45c1ed6 100644 --- a/src/main/java/com/ffii/fpsms/modules/master/entity/WarehouseRepository.kt +++ b/src/main/java/com/ffii/fpsms/modules/master/entity/WarehouseRepository.kt @@ -2,6 +2,8 @@ package com.ffii.fpsms.modules.master.entity import com.ffii.core.support.AbstractRepository import com.ffii.fpsms.modules.master.entity.projections.WarehouseCombo +import org.springframework.data.domain.Page +import org.springframework.data.domain.Pageable import org.springframework.data.jpa.repository.Query import org.springframework.stereotype.Repository import java.io.Serializable @@ -19,4 +21,22 @@ interface WarehouseRepository : AbstractRepository { fun findDistinctStockTakeSectionsByDeletedIsFalse(): List; fun findAllByIdIn(ids: List): List; fun findAllByCodeAndDeletedIsFalse(code: String): List + + @Query( + """ + SELECT COUNT(w) FROM Warehouse w + WHERE w.deleted = false + AND (w.stockTakeSection IS NULL OR TRIM(w.stockTakeSection) = '') + """ + ) + fun countMissingStockTakeSection(): Long + + @Query( + """ + SELECT w FROM Warehouse w + WHERE w.deleted = false + AND (w.stockTakeSection IS NULL OR TRIM(w.stockTakeSection) = '') + """ + ) + fun findMissingStockTakeSection(pageable: Pageable): Page } \ No newline at end of file diff --git a/src/main/java/com/ffii/fpsms/modules/master/service/WarehouseService.kt b/src/main/java/com/ffii/fpsms/modules/master/service/WarehouseService.kt index 0abb4a3..1c71dd6 100644 --- a/src/main/java/com/ffii/fpsms/modules/master/service/WarehouseService.kt +++ b/src/main/java/com/ffii/fpsms/modules/master/service/WarehouseService.kt @@ -26,8 +26,12 @@ import java.math.BigDecimal import kotlin.jvm.optionals.getOrNull import com.ffii.fpsms.modules.master.web.models.ExcelWarehouseData import com.ffii.core.exception.BadRequestException +import com.ffii.fpsms.modules.master.web.models.MissingStockTakeSectionIssueItem +import com.ffii.fpsms.modules.master.web.models.MissingStockTakeSectionIssuesResponse import com.ffii.fpsms.modules.master.web.models.StockTakeSectionInfo import com.ffii.fpsms.modules.master.web.models.UpdateSectionDescriptionRequest +import org.springframework.data.domain.PageRequest +import org.springframework.data.domain.Sort @Service open class WarehouseService( private val jdbcDao: JdbcDao, @@ -406,13 +410,48 @@ open class WarehouseService( return floorOrder * 10000L + letterOrder * 100L + slot } + open fun getMissingStockTakeSectionIssues(limit: Int): MissingStockTakeSectionIssuesResponse { + val capped = limit.coerceIn(1, 200) + val count = warehouseRepository.countMissingStockTakeSection() + val sort = Sort.by(Sort.Order.asc("store_id"), Sort.Order.asc("code")) + val page = warehouseRepository.findMissingStockTakeSection(PageRequest.of(0, capped, sort)) + val items = page.content.map { w -> + MissingStockTakeSectionIssueItem( + id = w.id!!, + code = w.code, + storeId = w.store_id, + warehouse = w.warehouse, + area = w.area, + slot = w.slot, + order = w.order, + ) + } + return MissingStockTakeSectionIssuesResponse( + count = count, + limit = capped, + items = items, + ) + } + open fun getStockTakeSections(): List { val all = warehouseRepository.findAllByDeletedIsFalse() .filter { it.stockTakeSection != null && it.stockTakeSection!!.isNotBlank() } val grouped = all.groupBy { it.stockTakeSection!! } return grouped.map { (section, list) -> val desc = list.mapNotNull { it.stockTakeSectionDescription }.firstOrNull()?.trim() - StockTakeSectionInfo(section, desc, list.size.toLong()) + val storeId = list.asSequence() + .mapNotNull { it.store_id?.trim() } + .firstOrNull { it.isNotBlank() } + val warehouseArea = list.asSequence() + .mapNotNull { it.area?.trim() } + .firstOrNull { it.isNotBlank() } + StockTakeSectionInfo( + stockTakeSection = section, + stockTakeSectionDescription = desc, + warehouseCount = list.size.toLong(), + storeId = storeId, + warehouseArea = warehouseArea, + ) }.sortedBy { it.stockTakeSection } } diff --git a/src/main/java/com/ffii/fpsms/modules/master/web/WarehouseController.kt b/src/main/java/com/ffii/fpsms/modules/master/web/WarehouseController.kt index 94f9fcb..cac3711 100644 --- a/src/main/java/com/ffii/fpsms/modules/master/web/WarehouseController.kt +++ b/src/main/java/com/ffii/fpsms/modules/master/web/WarehouseController.kt @@ -25,8 +25,10 @@ import org.springframework.web.multipart.MultipartHttpServletRequest import net.sf.jasperreports.engine.JasperExportManager import net.sf.jasperreports.engine.JasperPrint import java.io.OutputStream +import com.ffii.fpsms.modules.master.web.models.MissingStockTakeSectionIssuesResponse import com.ffii.fpsms.modules.master.web.models.StockTakeSectionInfo import com.ffii.fpsms.modules.master.web.models.UpdateSectionDescriptionRequest +import org.springframework.web.bind.annotation.RequestParam @RestController @RequestMapping("/warehouse") class WarehouseController( @@ -102,6 +104,13 @@ class WarehouseController( fun printQrCode(@Valid @RequestBody request: PrintWarehouseQrCodeRequest) { warehouseQrCodeService.printWarehouseQrCode(request) } + @GetMapping("/missingStockTakeSectionIssues") + fun getMissingStockTakeSectionIssues( + @RequestParam(defaultValue = "50") limit: Int, + ): MissingStockTakeSectionIssuesResponse { + return warehouseService.getMissingStockTakeSectionIssues(limit) + } + @GetMapping("/stockTakeSections") fun getStockTakeSections(): List { return warehouseService.getStockTakeSections() diff --git a/src/main/java/com/ffii/fpsms/modules/master/web/models/MissingStockTakeSectionIssuesResponse.kt b/src/main/java/com/ffii/fpsms/modules/master/web/models/MissingStockTakeSectionIssuesResponse.kt new file mode 100644 index 0000000..27e58c2 --- /dev/null +++ b/src/main/java/com/ffii/fpsms/modules/master/web/models/MissingStockTakeSectionIssuesResponse.kt @@ -0,0 +1,17 @@ +package com.ffii.fpsms.modules.master.web.models + +data class MissingStockTakeSectionIssueItem( + val id: Long, + val code: String?, + val storeId: String?, + val warehouse: String?, + val area: String?, + val slot: String?, + val order: String?, +) + +data class MissingStockTakeSectionIssuesResponse( + val count: Long, + val limit: Int, + val items: List, +) diff --git a/src/main/java/com/ffii/fpsms/modules/stock/entity/StockTake.kt b/src/main/java/com/ffii/fpsms/modules/stock/entity/StockTake.kt index 8a2fac3..4d86786 100644 --- a/src/main/java/com/ffii/fpsms/modules/stock/entity/StockTake.kt +++ b/src/main/java/com/ffii/fpsms/modules/stock/entity/StockTake.kt @@ -46,4 +46,7 @@ open class StockTake: BaseEntity() { /** 同一輪盤點(多 section 多筆 stock_take)共用此 id;由批次建立時依 MAX+1 遞增,不必等於任一筆主鍵 */ @Column(name = "stockTakeRoundId") open var stockTakeRoundId: Long? = null + @Size(max = 255) + @Column(name = "stockTakeRoundName", length = 255) + open var stockTakeRoundName: String? = null } \ No newline at end of file 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 812c325..5171164 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 @@ -316,6 +316,72 @@ open class StockTakeRecordService( } } + /** 與 Approver 列表前端一致:優先第二次盤點數,否則第一次 */ + private fun resolvePickerQtyForVarianceFilter(response: InventoryLotDetailResponse): BigDecimal { + val second = response.secondStockTakeQty + val first = response.firstStockTakeQty + return when { + second != null && second >= BigDecimal.ZERO -> second + first != null -> first + else -> BigDecimal.ZERO + } + } + + private fun computeInventoryLotDetailVariancePercentage(response: InventoryLotDetailResponse): BigDecimal { + val selectedQty = resolvePickerQtyForVarianceFilter(response) + val bookQty = response.bookQty ?: response.availableQty ?: BigDecimal.ZERO + val difference = selectedQty.subtract(bookQty) + if (bookQty.compareTo(BigDecimal.ZERO) != 0) { + return difference + .divide(bookQty, 6, RoundingMode.HALF_UP) + .multiply(BigDecimal("100")) + } + return when { + difference.compareTo(BigDecimal.ZERO) == 0 -> BigDecimal.ZERO + difference.compareTo(BigDecimal.ZERO) > 0 -> BigDecimal("100") + else -> BigDecimal("-100") + } + } + + /** + * @param varianceFilterInclusive false:只保留區間外(vp ≤ -t 或 vp ≥ t); + * true:只保留區間內(-t ≤ vp ≤ t) + * @param varianceFilterStrict true:邊界用 > <;false:用 ≥ ≤ + */ + private fun filterInventoryLotDetailsByVariancePercent( + responses: List, + variancePercentTolerance: BigDecimal?, + varianceFilterInclusive: Boolean?, + varianceFilterStrict: Boolean?, + ): List { + if (variancePercentTolerance == null) { + return responses + } + val tolerance = variancePercentTolerance.abs() + if (tolerance.compareTo(BigDecimal.ZERO) <= 0) { + return responses + } + val inclusive = varianceFilterInclusive == true + val strict = varianceFilterStrict == true + val negTolerance = tolerance.negate() + return responses.filter { response -> + val vp = computeInventoryLotDetailVariancePercentage(response) + if (inclusive) { + if (strict) { + vp > negTolerance && vp < tolerance + } else { + vp >= negTolerance && vp <= tolerance + } + } else { + if (strict) { + vp < negTolerance || vp > tolerance + } else { + vp <= negTolerance || vp >= tolerance + } + } + } + } + private fun filterInventoryLotDetailsByItemName( responses: List, itemName: String? @@ -510,10 +576,13 @@ open class StockTakeRecordService( itemKeyword: String? = null, sectionDescription: String? = null, stockTakeSections: String? = null, - warehouseKeyword: String? = null + warehouseKeyword: String? = null, + variancePercentTolerance: BigDecimal? = null, + varianceFilterInclusive: Boolean? = null, + varianceFilterStrict: Boolean? = null, ): RecordsRes { - println("getApproverInventoryLotDetailsAll called with stockTakeId: $stockTakeId, pageNum: $pageNum, pageSize: $pageSize, itemKeyword: $itemKeyword, sectionDescription: $sectionDescription, stockTakeSections: $stockTakeSections, warehouseKeyword: $warehouseKeyword") + println("getApproverInventoryLotDetailsAll called with stockTakeId: $stockTakeId, pageNum: $pageNum, pageSize: $pageSize, itemKeyword: $itemKeyword, sectionDescription: $sectionDescription, stockTakeSections: $stockTakeSections, warehouseKeyword: $warehouseKeyword, variancePercentTolerance: $variancePercentTolerance, varianceFilterInclusive: $varianceFilterInclusive, varianceFilterStrict: $varianceFilterStrict") // 3. 如果传了 stockTakeId,就把「同一轮」的所有 stockTake 找出来(stockTakeRoundId,舊資料則 planStart) // stockTakeId != null 时,优先用 stocktakerecord.stockTakeRoundId 取整轮记录(更快) @@ -720,19 +789,26 @@ open class StockTakeRecordService( } else { approvalFilteredResults } + + val varianceFilteredFinal = filterInventoryLotDetailsByVariancePercent( + approvalFilteredFinal, + variancePercentTolerance, + varianceFilterInclusive, + varianceFilterStrict, + ) // 7. 分页(和 section 版一模一样) val pageable = PageRequest.of(pageNum, pageSize) val startIndex = pageable.offset.toInt() - val endIndex = minOf(startIndex + pageSize, approvalFilteredFinal.size) + val endIndex = minOf(startIndex + pageSize, varianceFilteredFinal.size) - val paginatedResult = if (startIndex < approvalFilteredFinal.size) { - approvalFilteredFinal.subList(startIndex, endIndex) + val paginatedResult = if (startIndex < varianceFilteredFinal.size) { + varianceFilteredFinal.subList(startIndex, endIndex) } else { emptyList() } - return RecordsRes(paginatedResult, approvalFilteredFinal.size) + return RecordsRes(paginatedResult, varianceFilteredFinal.size) } open fun AllApproverStockTakeList(): List { // Overall 卡:只取“最新一轮”,并且总数口径与 @@ -1268,6 +1344,48 @@ open class StockTakeRecordService( return toSaveStockTakeRecordResponse(savedRecord) } + /** + * 拣货员一次提交多行已输入数量;每行复用 [saveStockTakeRecord],与单行保存一致。 + */ + @Transactional + open fun batchSavePickerStockTakeInputs( + request: BatchSavePickerStockTakeInputRequest + ): BatchSaveStockTakeRecordResponse { + if (request.records.isEmpty()) { + return BatchSaveStockTakeRecordResponse( + 0, + 0, + listOf("No records provided") + ) + } + + var successCount = 0 + var errorCount = 0 + val errors = mutableListOf() + + request.records.forEach { item -> + try { + saveStockTakeRecord(item, request.stockTakeId, request.stockTakerId) + successCount++ + } catch (e: Exception) { + errorCount++ + val msg = "inventoryLotLineId ${item.inventoryLotLineId}: ${e.message ?: e.javaClass.simpleName}" + errors.add(msg) + logger.warn("batchSavePickerStockTakeInputs failed: $msg", e) + } + } + + if (successCount > 0 && request.stockTakeSection.isNotBlank()) { + checkAndUpdateStockTakeStatus(request.stockTakeId, request.stockTakeSection) + } + + return BatchSaveStockTakeRecordResponse( + successCount = successCount, + errorCount = errorCount, + errors = errors + ) + } + private fun toSaveStockTakeRecordResponse(r: StockTakeRecord): SaveStockTakeRecordResponse { return SaveStockTakeRecordResponse( id = r.id, @@ -1366,13 +1484,8 @@ open class StockTakeRecordService( // 使用 availableQty 作为 qty,badQty 为 0 val qty = availableQty val badQty = BigDecimal.ZERO - - // 判断是否匹配 - val totalInputQty = qty.add(badQty) - val isMatched = totalInputQty.compareTo(availableQty) == 0 val varianceQty = availableQty - qty - badQty - val isCompleted = (ill.inQty ?: BigDecimal.ZERO) == (ill.outQty ?: BigDecimal.ZERO) - // 创建新记录 + // 拣货员第一次批量盘点:与单笔第一次保存一致,一律 pass;不在此写 stockTakeEndTime(仅第二次盘点结束) val stockTakeRecord = StockTakeRecord().apply { this.itemId = itemEntity.id this.lotId = inventoryLot.id @@ -1389,14 +1502,13 @@ open class StockTakeRecordService( this.varianceQty = varianceQty this.uom = ill.stockUom?.uom?.udfudesc this.date = java.time.LocalDate.now() - this.status = if (isCompleted) "completed" else "pass" + this.status = "pass" this.remarks = null this.itemCode = itemEntity.code this.itemName = itemEntity.name this.lotNo = inventoryLot.lotNo this.expiredDate = inventoryLot.expiryDate this.stockTakeStartTime = java.time.LocalDateTime.now() - this.stockTakeEndTime = java.time.LocalDateTime.now() } stockTakeRecordRepository.save(stockTakeRecord) diff --git a/src/main/java/com/ffii/fpsms/modules/stock/service/StockTakeService.kt b/src/main/java/com/ffii/fpsms/modules/stock/service/StockTakeService.kt index 60f3ae2..2a136e3 100644 --- a/src/main/java/com/ffii/fpsms/modules/stock/service/StockTakeService.kt +++ b/src/main/java/com/ffii/fpsms/modules/stock/service/StockTakeService.kt @@ -64,6 +64,7 @@ open class StockTakeService( request.remarks?.let { stockTake.remarks = it } request.stockTakeSection?.let { stockTake.stockTakeSection = it } // 添加此行 request.stockTakeRoundId?.let { stockTake.stockTakeRoundId = it } + request.stockTakeRoundName?.let { stockTake.stockTakeRoundName = it } return stockTakeRepository.save(stockTake) } @@ -239,23 +240,43 @@ open class StockTakeService( - fun createStockTakeForSections(): Map { + fun createStockTakeForSections( + sectionsFilter: List? = null, + stockTakeRoundName: String? = null, + planStart: LocalDateTime? = null, + ): Map { log.info("--------- Start - Create Stock Take for Sections -------") val result = mutableMapOf() // 1. 获取所有不同的 stockTakeSection(从 warehouse 表) val allWarehouses = warehouseRepository.findAllByDeletedIsFalse() - val distinctSections = allWarehouses + var distinctSections = allWarehouses .mapNotNull { it.stockTakeSection } .distinct() .filter { !it.isBlank() } + if (sectionsFilter != null) { + if (sectionsFilter.isEmpty()) { + log.warn("Create stock take: empty section list in request") + return mapOf("_" to "Error: No stock take sections selected") + } + val wanted = sectionsFilter.map { it.trim() }.filter { it.isNotBlank() }.toSet() + distinctSections = distinctSections.filter { section -> + wanted.any { w -> section.equals(w, ignoreCase = true) } + } + if (distinctSections.isEmpty()) { + log.warn("Create stock take: no warehouse sections matched filter") + return mapOf("_" to "Error: No matching warehouse stock take sections") + } + } + // 移除 null section 处理逻辑,因为 warehouse 表中没有 null 的 stockTakeSection - val batchPlanStart = LocalDateTime.now() + val batchPlanStart = planStart ?: LocalDateTime.now() val batchPlanEnd = batchPlanStart.plusDays(1) // 輪次序號:每批次共用同一個遞增 roundId,與各筆 stock_take 主鍵脫鉤(避免第二輪變成 4,4,4) val roundId = stockTakeRepository.findMaxStockTakeRoundId() + 1 + val roundName = stockTakeRoundName?.trim()?.takeIf { it.isNotBlank() } var placeholderStockTakerId: Long? = null distinctSections.forEach { section -> try { @@ -269,7 +290,8 @@ open class StockTakeService( status = StockTakeStatus.PENDING.value, remarks = null, stockTakeSection = section, - stockTakeRoundId = roundId + stockTakeRoundId = roundId, + stockTakeRoundName = roundName, ) val savedStockTake = saveStockTake(saveStockTakeReq) if (placeholderStockTakerId == null) { diff --git a/src/main/java/com/ffii/fpsms/modules/stock/web/StockTakeController.kt b/src/main/java/com/ffii/fpsms/modules/stock/web/StockTakeController.kt index 30c0ce6..aaab628 100644 --- a/src/main/java/com/ffii/fpsms/modules/stock/web/StockTakeController.kt +++ b/src/main/java/com/ffii/fpsms/modules/stock/web/StockTakeController.kt @@ -1,6 +1,7 @@ package com.ffii.fpsms.modules.stock.web import com.ffii.fpsms.modules.stock.service.StockTakeService +import com.ffii.fpsms.modules.stock.web.model.CreateStockTakeForSectionsRequest import jakarta.servlet.http.HttpServletRequest import org.apache.poi.ss.usermodel.Workbook import org.apache.poi.util.IOUtils @@ -9,6 +10,7 @@ import org.apache.poi.xssf.usermodel.XSSFWorkbook import org.springframework.http.ResponseEntity import org.springframework.web.bind.ServletRequestBindingException import org.springframework.web.bind.annotation.PostMapping +import org.springframework.web.bind.annotation.RequestBody import org.springframework.web.bind.annotation.RequestMapping import org.springframework.web.bind.annotation.RestController import org.springframework.web.multipart.MultipartHttpServletRequest @@ -41,8 +43,25 @@ class StockTakeController( return ResponseEntity.ok(stockTakeService.importExcel(workbook)) } @PostMapping("/createForSections") - fun createStockTakeForSections(): ResponseEntity> { - val result = stockTakeService.createStockTakeForSections() + fun createStockTakeForSections( + @RequestBody(required = false) body: CreateStockTakeForSectionsRequest?, + ): ResponseEntity> { + val trimmed = + body?.sections + ?.map { it.trim() } + ?.filter { it.isNotEmpty() } + val sectionsFilter: List? = + when { + body == null -> null + trimmed == null -> null + trimmed.isEmpty() -> emptyList() + else -> trimmed + } + val result = stockTakeService.createStockTakeForSections( + sectionsFilter = sectionsFilter, + stockTakeRoundName = body?.stockTakeRoundName, + planStart = body?.planStart, + ) return ResponseEntity.ok(result) } 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 6931ea9..6fc9864 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 @@ -117,7 +117,10 @@ class StockTakeRecordController( @RequestParam(required = false) itemKeyword: String?, @RequestParam(required = false) sectionDescription: String?, @RequestParam(required = false) stockTakeSections: String?, - @RequestParam(required = false) warehouseKeyword: String? + @RequestParam(required = false) warehouseKeyword: String?, + @RequestParam(required = false) variancePercentTolerance: java.math.BigDecimal?, + @RequestParam(required = false) varianceFilterInclusive: Boolean?, + @RequestParam(required = false) varianceFilterStrict: Boolean?, ): RecordsRes { return stockOutRecordService.getApproverInventoryLotDetailsAll( stockTakeId = stockTakeId, @@ -127,7 +130,10 @@ class StockTakeRecordController( itemKeyword = itemKeyword, sectionDescription = sectionDescription, stockTakeSections = stockTakeSections, - warehouseKeyword = warehouseKeyword + warehouseKeyword = warehouseKeyword, + variancePercentTolerance = variancePercentTolerance, + varianceFilterInclusive = varianceFilterInclusive, + varianceFilterStrict = varianceFilterStrict, ) } @GetMapping("/approverInventoryLotDetailsAllApproved") @@ -138,7 +144,10 @@ class StockTakeRecordController( @RequestParam(required = false) itemKeyword: String?, @RequestParam(required = false) sectionDescription: String?, @RequestParam(required = false) stockTakeSections: String?, - @RequestParam(required = false) warehouseKeyword: String? + @RequestParam(required = false) warehouseKeyword: String?, + @RequestParam(required = false) variancePercentTolerance: java.math.BigDecimal?, + @RequestParam(required = false) varianceFilterInclusive: Boolean?, + @RequestParam(required = false) varianceFilterStrict: Boolean?, ): RecordsRes { return stockOutRecordService.getApproverInventoryLotDetailsAll( stockTakeId = stockTakeId, @@ -148,7 +157,10 @@ class StockTakeRecordController( itemKeyword = itemKeyword, sectionDescription = sectionDescription, stockTakeSections = stockTakeSections, - warehouseKeyword = warehouseKeyword + warehouseKeyword = warehouseKeyword, + variancePercentTolerance = variancePercentTolerance, + varianceFilterInclusive = varianceFilterInclusive, + varianceFilterStrict = varianceFilterStrict, ) } @GetMapping("/AllApproverStockTakeList") @@ -250,6 +262,31 @@ class StockTakeRecordController( )) } } + + @PostMapping("/batchSavePickerStockTakeInputs") + fun batchSavePickerStockTakeInputs( + @RequestBody request: BatchSavePickerStockTakeInputRequest + ): ResponseEntity { + return try { + val result = stockOutRecordService.batchSavePickerStockTakeInputs(request) + logger.info( + "Batch save picker inputs: success=${result.successCount}, errors=${result.errorCount}" + ) + ResponseEntity.ok(result) + } catch (e: IllegalArgumentException) { + logger.warn("Validation error: ${e.message}") + ResponseEntity.badRequest().body(mapOf( + "error" to "VALIDATION_ERROR", + "message" to (e.message ?: "Validation failed") + )) + } catch (e: Exception) { + logger.error("Error batch saving picker stock take inputs", e) + ResponseEntity.status(500).body(mapOf( + "error" to "INTERNAL_ERROR", + "message" to (e.message ?: "Failed to batch save picker stock take inputs") + )) + } + } @GetMapping("/checkAndUpdateStockTakeStatus") fun checkAndUpdateStockTakeStatus( diff --git a/src/main/java/com/ffii/fpsms/modules/stock/web/model/CreateStockTakeForSectionsRequest.kt b/src/main/java/com/ffii/fpsms/modules/stock/web/model/CreateStockTakeForSectionsRequest.kt index 3ba0c02..f481123 100644 --- a/src/main/java/com/ffii/fpsms/modules/stock/web/model/CreateStockTakeForSectionsRequest.kt +++ b/src/main/java/com/ffii/fpsms/modules/stock/web/model/CreateStockTakeForSectionsRequest.kt @@ -1,8 +1,11 @@ package com.ffii.fpsms.modules.stock.web.model import com.fasterxml.jackson.annotation.JsonIgnoreProperties +import java.time.LocalDateTime @JsonIgnoreProperties(ignoreUnknown = true) data class CreateStockTakeForSectionsRequest( val sections: List? = null, + val stockTakeRoundName: String? = null, + val planStart: LocalDateTime? = null, ) diff --git a/src/main/java/com/ffii/fpsms/modules/stock/web/model/SaveStockTakeRequest.kt b/src/main/java/com/ffii/fpsms/modules/stock/web/model/SaveStockTakeRequest.kt index 6dc133d..cdc21e5 100644 --- a/src/main/java/com/ffii/fpsms/modules/stock/web/model/SaveStockTakeRequest.kt +++ b/src/main/java/com/ffii/fpsms/modules/stock/web/model/SaveStockTakeRequest.kt @@ -13,4 +13,5 @@ data class SaveStockTakeRequest( val remarks: String?, val stockTakeSection: String?=null, val stockTakeRoundId: Long? = null, + val stockTakeRoundName: String? = null, ) diff --git a/src/main/java/com/ffii/fpsms/modules/stock/web/model/StockTakeRecordReponse.kt b/src/main/java/com/ffii/fpsms/modules/stock/web/model/StockTakeRecordReponse.kt index 3496f9d..712d88f 100644 --- a/src/main/java/com/ffii/fpsms/modules/stock/web/model/StockTakeRecordReponse.kt +++ b/src/main/java/com/ffii/fpsms/modules/stock/web/model/StockTakeRecordReponse.kt @@ -123,6 +123,14 @@ data class BatchSaveStockTakeRecordRequest( //val stockTakerName: String ) +/** 拣货员批量提交已输入行(与 [saveStockTakeRecord] 相同业务,逐条保存用户 qty/badQty)。 */ +data class BatchSavePickerStockTakeInputRequest( + val stockTakeId: Long, + val stockTakeSection: String, + val stockTakerId: Long, + val records: List, +) + data class BatchSaveStockTakeRecordResponse( val successCount: Int, val errorCount: Int, diff --git a/src/main/resources/db/changelog/changes/20260519_01_Enson/03_stock_take_round_name.sql b/src/main/resources/db/changelog/changes/20260519_01_Enson/03_stock_take_round_name.sql new file mode 100644 index 0000000..5c9acfa --- /dev/null +++ b/src/main/resources/db/changelog/changes/20260519_01_Enson/03_stock_take_round_name.sql @@ -0,0 +1,5 @@ +--liquibase formatted sql + +--changeset Enson:20260519-05-stock-take-round-name +ALTER TABLE stock_take +ADD COLUMN stockTakeRoundName VARCHAR(255); \ No newline at end of file