| @@ -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, | |||
| @@ -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<Warehouse, Long> { | |||
| fun findDistinctStockTakeSectionsByDeletedIsFalse(): List<String>; | |||
| fun findAllByIdIn(ids: List<Long>): List<Warehouse>; | |||
| fun findAllByCodeAndDeletedIsFalse(code: String): List<Warehouse> | |||
| @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<Warehouse> | |||
| } | |||
| @@ -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<StockTakeSectionInfo> { | |||
| 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 } | |||
| } | |||
| @@ -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<StockTakeSectionInfo> { | |||
| return warehouseService.getStockTakeSections() | |||
| @@ -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<MissingStockTakeSectionIssueItem>, | |||
| ) | |||
| @@ -46,4 +46,7 @@ open class StockTake: BaseEntity<Long>() { | |||
| /** 同一輪盤點(多 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 | |||
| } | |||
| @@ -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<InventoryLotDetailResponse>, | |||
| variancePercentTolerance: BigDecimal?, | |||
| varianceFilterInclusive: Boolean?, | |||
| varianceFilterStrict: Boolean?, | |||
| ): List<InventoryLotDetailResponse> { | |||
| 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<InventoryLotDetailResponse>, | |||
| 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<InventoryLotDetailResponse> { | |||
| 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<AllPickedStockTakeListReponse> { | |||
| // 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<String>() | |||
| 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) | |||
| @@ -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<String, String> { | |||
| fun createStockTakeForSections( | |||
| sectionsFilter: List<String>? = null, | |||
| stockTakeRoundName: String? = null, | |||
| planStart: LocalDateTime? = null, | |||
| ): Map<String, String> { | |||
| log.info("--------- Start - Create Stock Take for Sections -------") | |||
| val result = mutableMapOf<String, String>() | |||
| // 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) { | |||
| @@ -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<Map<String, String>> { | |||
| val result = stockTakeService.createStockTakeForSections() | |||
| fun createStockTakeForSections( | |||
| @RequestBody(required = false) body: CreateStockTakeForSectionsRequest?, | |||
| ): ResponseEntity<Map<String, String>> { | |||
| val trimmed = | |||
| body?.sections | |||
| ?.map { it.trim() } | |||
| ?.filter { it.isNotEmpty() } | |||
| val sectionsFilter: List<String>? = | |||
| 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) | |||
| } | |||
| @@ -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<InventoryLotDetailResponse> { | |||
| 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<InventoryLotDetailResponse> { | |||
| 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<Any> { | |||
| 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( | |||
| @@ -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<String>? = null, | |||
| val stockTakeRoundName: String? = null, | |||
| val planStart: LocalDateTime? = null, | |||
| ) | |||
| @@ -13,4 +13,5 @@ data class SaveStockTakeRequest( | |||
| val remarks: String?, | |||
| val stockTakeSection: String?=null, | |||
| val stockTakeRoundId: Long? = null, | |||
| val stockTakeRoundName: String? = null, | |||
| ) | |||
| @@ -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<SaveStockTakeRecordRequest>, | |||
| ) | |||
| data class BatchSaveStockTakeRecordResponse( | |||
| val successCount: Int, | |||
| val errorCount: Int, | |||
| @@ -0,0 +1,5 @@ | |||
| --liquibase formatted sql | |||
| --changeset Enson:20260519-05-stock-take-round-name | |||
| ALTER TABLE stock_take | |||
| ADD COLUMN stockTakeRoundName VARCHAR(255); | |||