diff --git a/src/main/java/com/ffii/fpsms/modules/master/entity/BomRepository.kt b/src/main/java/com/ffii/fpsms/modules/master/entity/BomRepository.kt index 0af6b48..e85f85e 100644 --- a/src/main/java/com/ffii/fpsms/modules/master/entity/BomRepository.kt +++ b/src/main/java/com/ffii/fpsms/modules/master/entity/BomRepository.kt @@ -33,6 +33,9 @@ interface BomRepository : AbstractRepository { fun findAllIdsByDeletedIsFalse(): List fun findByCodeAndDeletedIsFalse(code: String): Bom? + + fun findByCodeAndDescriptionIgnoreCaseAndDeletedIsFalse(code: String, description: String): Bom? + @Query(""" select b.item.id from Bom b diff --git a/src/main/java/com/ffii/fpsms/modules/master/service/BomService.kt b/src/main/java/com/ffii/fpsms/modules/master/service/BomService.kt index f50ee12..8559a3d 100644 --- a/src/main/java/com/ffii/fpsms/modules/master/service/BomService.kt +++ b/src/main/java/com/ffii/fpsms/modules/master/service/BomService.kt @@ -68,6 +68,10 @@ open class BomService( private val settingsService: SettingsService, @Value("\${bom.import.temp-dir:\${java.io.tmpdir}/fpsms-bom-import}") private val bomImportTempDir: String, ) { + companion object { + private const val BOM_WIP_DESCRIPTION = "WIP" + } + open fun uploadBomFiles(files: List): BomUploadResponse { val batchId = UUID.randomUUID().toString() val batchDir = Paths.get(bomImportTempDir, batchId).toAbsolutePath() @@ -580,7 +584,8 @@ open class BomService( private fun saveBomEntity(req: ImportBomRequest): Bom { val item = itemsRepository.findByCodeAndDeletedFalse(req.code) ?: itemsRepository.findByNameAndDeletedFalse(req.name) val uom = if (req.uomId != null) uomConversionRepository.findById(req.uomId!!).orElseThrow() else null - val bom = bomRepository.findByCodeAndDeletedIsFalse(req.code) ?: Bom() + val fgDescription = req.description.trim().ifEmpty { "FG" } + val bom = bomRepository.findByCodeAndDescriptionIgnoreCaseAndDeletedIsFalse(req.code, fgDescription) ?: Bom() bom.apply { this.isDark = req.isDark this.isFloat = req.isFloat @@ -1576,6 +1581,45 @@ open class BomService( return null } + /** Reads BOM 種類 (FG / WIP) from sheet without saving. */ + private fun readBomDescriptionFromSheet(sheet: Sheet): String? { + for (r in 0..9) { + for (c in 0..9) { + val cell = sheet.getRow(r)?.getCell(c) ?: continue + if (cell.cellType != CellType.STRING) continue + if (cell.stringCellValue.trim() != "種類") continue + val valueRow = sheet.getRow(r + 1) ?: return null + val valueCell = valueRow.getCell(c) ?: return null + return when { + valueCell.cellType == CellType.STRING -> valueCell.stringCellValue.trim().takeIf { it.isNotEmpty() } + valueCell.cellType == CellType.FORMULA && valueCell.cachedFormulaResultType == CellType.STRING -> + valueCell.stringCellValue.trim().takeIf { it.isNotEmpty() } + else -> null + } + } + } + return null + } + + private fun normalizeFgDescriptionForImport(description: String?): String = + description?.trim()?.takeIf { it.isNotEmpty() } ?: "FG" + + /** Soft-delete FG (code + Excel 種類) and WIP (code + WIP) rows before re-import. */ + private fun softDeleteExistingBomsForImport(code: String, fgDescription: String) { + val fgDesc = normalizeFgDescriptionForImport(fgDescription) + bomRepository.findByCodeAndDescriptionIgnoreCaseAndDeletedIsFalse(code, fgDesc)?.id?.let { + softDeleteBomAndRelated(it) + } + bomRepository.findByCodeAndDescriptionIgnoreCaseAndDeletedIsFalse(code, BOM_WIP_DESCRIPTION)?.id?.let { + softDeleteBomAndRelated(it) + } + } + + private fun findFgBomIdForImport(code: String, fgDescription: String): Long? { + val fgDesc = normalizeFgDescriptionForImport(fgDescription) + return bomRepository.findByCodeAndDescriptionIgnoreCaseAndDeletedIsFalse(code, fgDesc)?.id + } + private fun softDeleteBomAndRelated(bomId: Long) { val bom = bomRepository.findById(bomId).orElse(null) ?: return bom.deleted = true @@ -1633,12 +1677,11 @@ open class BomService( ?: workbook2.getSheet("食物成品") ?: workbook2.getSheetAt(0) val code = readBomCodeFromSheet(sheet) + val fgDescription = readBomDescriptionFromSheet(sheet) var oldBomId: Long? = null code?.let { c -> - bomRepository.findByCodeAndDeletedIsFalse(c)?.id?.let { existingId -> - softDeleteBomAndRelated(existingId) - oldBomId = existingId - } + oldBomId = findFgBomIdForImport(c, fgDescription ?: "FG") + softDeleteExistingBomsForImport(c, fgDescription ?: "FG") } val bom = importExcelBomBasicInfo(sheet) bom.isDrink = isDrink @@ -1705,10 +1748,15 @@ open class BomService( } /** 方案 A:複製 FG BOM 為一筆相同 code、相同 item、description=WIP 的 BOM,並複製 materials 與 processes。 */ private fun createWipCopyFromFgBom(fgBom: Bom) { + val code = fgBom.code ?: return + val existingWip = bomRepository.findByCodeAndDescriptionIgnoreCaseAndDeletedIsFalse(code, BOM_WIP_DESCRIPTION) + if (existingWip != null) { + softDeleteBomAndRelated(existingWip.id!!) + } val wipBom = Bom().apply { - code = fgBom.code + this.code = code name = fgBom.name - description = "WIP" + description = BOM_WIP_DESCRIPTION item = fgBom.item outputQty = fgBom.outputQty outputQtyUom = fgBom.outputQtyUom @@ -2364,7 +2412,7 @@ open class BomService( var ColorDepthValueOk = false var FloatingValueOk = false var ConcentrationValueOk = false - println("=== Debug sheet content for $fileName ===") + // println("=== Debug sheet content for $fileName ===") for (r in 0..20) { val row = sheet.getRow(r) ?: continue for (c in 0..20) { @@ -2383,7 +2431,7 @@ for (r in 0..20) { else -> cell.cellType.toString() } if (value.isNotBlank() && value != "BLANK") { - println("($r, $c) = $value") + // println("($r, $c) = $value") } } }