| @@ -829,113 +829,97 @@ open class BomService( | |||
| return bomProcessMaterialRepository.saveAndFlush(bomProcessMaterial) | |||
| } | |||
| private fun importExcelBomMaterial(bom: Bom, sheet: Sheet) { | |||
| var bomProcessMatRequest = ImportBomProcessMaterialRequest() | |||
| var startRowIndex = 10 | |||
| val endRowIndex = 30 | |||
| var startColumnIndex = 0 | |||
| val endColumnIndex = 10 | |||
| while (startRowIndex < endRowIndex) { | |||
| val tempRow = sheet.getRow(startRowIndex) | |||
| val tempCell = tempRow.getCell(startColumnIndex) | |||
| if (tempCell != null && tempCell.cellType == CellType.STRING && tempCell.stringCellValue.trim() == "材料編號") { | |||
| //println("last: $startRowIndex") | |||
| startRowIndex++ | |||
| val startCol = 0 | |||
| val endCol = 10 | |||
| val maxRowIndex = 200 | |||
| // 1) 找到「材料編號」表头 | |||
| var headerRowIndex = -1 | |||
| var searchRowIndex = 10 | |||
| while (searchRowIndex < maxRowIndex) { | |||
| val row = sheet.getRow(searchRowIndex) | |||
| val cell = row?.getCell(startCol) | |||
| if (cell != null && | |||
| cell.cellType == CellType.STRING && | |||
| cell.stringCellValue.trim() == "材料編號" | |||
| ) { | |||
| headerRowIndex = searchRowIndex | |||
| break | |||
| } | |||
| startRowIndex++ | |||
| searchRowIndex++ | |||
| } | |||
| var bomMatRequest = ImportBomMatRequest( | |||
| bom = bom | |||
| ) | |||
| // println("starting new loop") | |||
| while (startRowIndex != endRowIndex || startColumnIndex != endColumnIndex) { | |||
| val tempRow = sheet.getRow(startRowIndex) | |||
| val tempCell = tempRow.getCell(startColumnIndex) | |||
| if (startColumnIndex == 0 && (tempCell == null || tempCell.cellType == CellType.BLANK)) { | |||
| if (headerRowIndex == -1) { | |||
| println("importExcelBomMaterial: 找不到『材料編號』表頭,略過材料匯入") | |||
| return | |||
| } | |||
| // 2) 从表头下一行开始读,直到 col0 空白 | |||
| var rowIdx = headerRowIndex + 1 | |||
| while (rowIdx < maxRowIndex) { | |||
| val row = sheet.getRow(rowIdx) ?: break | |||
| val firstCell = row.getCell(0) | |||
| if (firstCell == null || firstCell.cellType == CellType.BLANK) { | |||
| break | |||
| } else { | |||
| try { | |||
| when (startColumnIndex) { | |||
| } | |||
| val bomMatRequest = ImportBomMatRequest(bom = bom) | |||
| val bomProcessMatRequest = ImportBomProcessMaterialRequest() | |||
| try { | |||
| for (colIdx in startCol..endCol) { | |||
| val cell = row.getCell(colIdx) ?: continue | |||
| when (colIdx) { | |||
| 0 -> { | |||
| // println("rowIndex: $startRowIndex") | |||
| val nameRow = sheet.getRow(startRowIndex) | |||
| val nameCell = nameRow.getCell(1) | |||
| println(tempCell.stringCellValue.trim()) | |||
| val item = itemsRepository.findByCodeAndDeletedFalse(tempCell.stringCellValue.trim()) | |||
| ?: itemsRepository.findByNameAndDeletedFalse(nameCell.stringCellValue.trim()) | |||
| // println("getting item.....:") | |||
| // println(item) | |||
| val nameCell = row.getCell(1) | |||
| val itemCode = cell.stringCellValue.trim() | |||
| val itemName = nameCell?.stringCellValue?.trim() | |||
| val item = itemsRepository.findByCodeAndDeletedFalse(itemCode) | |||
| ?: itemName?.let { itemsRepository.findByNameAndDeletedFalse(it) } | |||
| bomMatRequest.item = item | |||
| } | |||
| 2-> { | |||
| bomMatRequest.qty = tempCell.numericCellValue.toBigDecimal() | |||
| 2 -> { | |||
| bomMatRequest.qty = cell.numericCellValue.toBigDecimal() | |||
| } | |||
| 3 -> { | |||
| val uom = uomConversionRepository.findByCodeAndDeletedFalse(tempCell.stringCellValue.trim()) | |||
| val uomCode = cell.stringCellValue.trim() | |||
| val uom = uomConversionRepository.findByCodeAndDeletedFalse(uomCode) | |||
| bomMatRequest.uom = uom | |||
| bomMatRequest.uomName = uom?.udfudesc | |||
| } | |||
| 6 -> { | |||
| bomMatRequest.saleQty = tempCell.numericCellValue.toBigDecimal() | |||
| bomMatRequest.saleQty = cell.numericCellValue.toBigDecimal() | |||
| } | |||
| 7 -> { | |||
| val salesUnitCodeStr = tempCell.stringCellValue.trim() | |||
| val normalizedCode = if (salesUnitCodeStr.equals("Litter", ignoreCase = true)) "L" else salesUnitCodeStr | |||
| val salesUnit = uomConversionRepository.findByCodeAndDeletedFalse(tempCell.stringCellValue.trim()) | |||
| val salesUnitCodeStr = cell.stringCellValue.trim() | |||
| val salesUnit = uomConversionRepository.findByCodeAndDeletedFalse(salesUnitCodeStr) | |||
| bomMatRequest.salesUnit = salesUnit | |||
| // bomMatRequest.salesUnitCode = salesUnit?.udfudesc | |||
| bomMatRequest.salesUnitCode = salesUnitCodeStr | |||
| } | |||
| /* | |||
| 2 -> { | |||
| val salesUnit = uomConversionRepository.findByCodeAndDeletedFalse(tempCell.stringCellValue.trim()) | |||
| bomMatRequest.salesUnit = salesUnit | |||
| }*/ | |||
| 10 -> { | |||
| println("seqNo: ${tempCell.numericCellValue.toInt()}") | |||
| println("bomId: ${bom.id!!}") | |||
| val seqNo = cell.numericCellValue.toInt() | |||
| val bomProcess = bomProcessRepository.findBySeqNoAndBomIdAndDeletedIsFalse( | |||
| seqNo = tempCell.numericCellValue.toInt(), | |||
| seqNo = seqNo, | |||
| bomId = bom.id!! | |||
| )!! // if null = bugged | |||
| ) // if null = bugged | |||
| bomProcessMatRequest.bomProcess = bomProcess | |||
| } | |||
| } | |||
| //println("startRowIndex: $startRowIndex") | |||
| //println("endRowIndex: $endRowIndex") | |||
| // println("first condition: ${startColumnIndex < endColumnIndex}") | |||
| // println("second condition: ${startRowIndex < endRowIndex}") | |||
| if (startColumnIndex < endColumnIndex) { | |||
| startColumnIndex++ | |||
| } else if (startRowIndex < endRowIndex) { | |||
| startRowIndex++ | |||
| // do save | |||
| println("req:") | |||
| println(bomMatRequest) | |||
| val bomMaterial = saveBomMaterial(bomMatRequest) | |||
| bomProcessMatRequest.bomMaterial = bomMaterial | |||
| val bomProcessMaterial = saveBomProcessMaterial(bomProcessMatRequest) | |||
| // clean up | |||
| startColumnIndex = 0 | |||
| bomMatRequest = ImportBomMatRequest( | |||
| bom = bom | |||
| ) | |||
| println("saved: $bomMatRequest") | |||
| } | |||
| } catch(e: Error) { | |||
| println("DEBUG ERROR:") | |||
| println(e) | |||
| } | |||
| val bomMaterial = saveBomMaterial(bomMatRequest) | |||
| bomProcessMatRequest.bomMaterial = bomMaterial | |||
| saveBomProcessMaterial(bomProcessMatRequest) | |||
| } catch (e: Exception) { | |||
| println("importExcelBomMaterial row ${rowIdx + 1} error: ${e.message}") | |||
| } | |||
| rowIdx++ | |||
| } | |||
| } | |||
| @@ -2015,128 +1999,115 @@ open class BomService( | |||
| } | |||
| } | |||
| private fun validateMaterialLikeImport( | |||
| sheet: Sheet, | |||
| fileName: String, | |||
| issues: MutableList<BomFormatIssue> | |||
| ) { | |||
| var startRowIndex = 10 | |||
| val endRowIndex = 30 | |||
| var startColumnIndex = 0 | |||
| val endColumnIndex = 10 | |||
| var headerFound = false | |||
| while (startRowIndex < endRowIndex) { | |||
| val tempRow = sheet.getRow(startRowIndex) | |||
| val tempCell = tempRow?.getCell(startColumnIndex) | |||
| if (tempCell != null && | |||
| tempCell.cellType == CellType.STRING && | |||
| tempCell.stringCellValue.trim() == "材料編號" | |||
| ) { | |||
| startRowIndex++ | |||
| headerFound = true | |||
| break | |||
| private fun validateMaterialLikeImport( | |||
| sheet: Sheet, | |||
| fileName: String, | |||
| issues: MutableList<BomFormatIssue> | |||
| ) { | |||
| val startCol = 0 | |||
| val endCol = 10 | |||
| val maxRowIndex = 200 | |||
| // 1) 找表头 | |||
| var headerRowIndex = -1 | |||
| var searchRowIndex = 10 | |||
| while (searchRowIndex < maxRowIndex) { | |||
| val row = sheet.getRow(searchRowIndex) | |||
| val cell = row?.getCell(startCol) | |||
| if (cell != null && | |||
| cell.cellType == CellType.STRING && | |||
| cell.stringCellValue.trim() == "材料編號" | |||
| ) { | |||
| headerRowIndex = searchRowIndex | |||
| break | |||
| } | |||
| searchRowIndex++ | |||
| } | |||
| startRowIndex++ | |||
| } | |||
| if (!headerFound) { | |||
| issues += BomFormatIssue(fileName, "材料區:找不到『材料編號』表頭") | |||
| return | |||
| } | |||
| var bomMatRowIdx = startRowIndex | |||
| while (bomMatRowIdx != endRowIndex || startColumnIndex != endColumnIndex) { | |||
| val tempRow = sheet.getRow(bomMatRowIdx) | |||
| val tempCell = tempRow?.getCell(startColumnIndex) | |||
| if (startColumnIndex == 0 && | |||
| (tempCell == null || tempCell.cellType == CellType.BLANK) | |||
| ) { | |||
| break | |||
| if (headerRowIndex == -1) { | |||
| issues += BomFormatIssue(fileName, "材料區:找不到『材料編號』表頭") | |||
| return | |||
| } | |||
| val rowNum = bomMatRowIdx + 1 | |||
| when (startColumnIndex) { | |||
| 0 -> { | |||
| // 材料編號 — 必填,非空字串 | |||
| if (tempCell == null || !isNonEmptyStringCell(tempCell)) { | |||
| issues += BomFormatIssue(fileName, "材料區:第${rowNum}行『材料編號』(欄1)不可為空") | |||
| } | |||
| } | |||
| 1 -> { | |||
| // 材料 — 必填,非空字串 | |||
| if (tempCell == null || !isNonEmptyStringCell(tempCell)) { | |||
| issues += BomFormatIssue(fileName, "材料區:第${rowNum}行『材料』(欄2)不可為空") | |||
| } | |||
| } | |||
| 2 -> { | |||
| // 使用份量 — 必填,數值 | |||
| if (tempCell == null || !isNumericLike(tempCell)) { | |||
| issues += BomFormatIssue(fileName, "材料區:第${rowNum}行『使用份量』(欄3)不可為空且須為數值") | |||
| } | |||
| } | |||
| 3 -> { | |||
| // 使用單位 — 必填,非空字串 | |||
| if (tempCell == null || !isNonEmptyStringCell(tempCell)) { | |||
| issues += BomFormatIssue(fileName, "材料區:第${rowNum}行『使用單位』(欄4)不可為空") | |||
| } | |||
| } | |||
| 4 -> { | |||
| // 轉用單位份量 — 必填,數值 | |||
| if (tempCell == null || !isNumericLike(tempCell)) { | |||
| issues += BomFormatIssue(fileName, "材料區:第${rowNum}行『轉用單位份量』(欄5)不可為空且須為數值") | |||
| } | |||
| } | |||
| 5 -> { | |||
| // 轉用單位 — 必填,非空字串 | |||
| if (tempCell == null || !isNonEmptyStringCell(tempCell)) { | |||
| issues += BomFormatIssue(fileName, "材料區:第${rowNum}行『轉用單位』(欄6)不可為空") | |||
| } | |||
| } | |||
| 6 -> { | |||
| // 份量(銷售單位) — 必填,數值 | |||
| if (tempCell == null || !isNumericLike(tempCell)) { | |||
| issues += BomFormatIssue(fileName, "材料區:第${rowNum}行『份量(銷售單位)』(欄7)不可為空且須為數值") | |||
| } | |||
| } | |||
| 7 -> { | |||
| // 銷售單位 — 必填,非空字串 | |||
| if (tempCell == null || !isNonEmptyStringCell(tempCell)) { | |||
| issues += BomFormatIssue(fileName, "材料區:第${rowNum}行『銷售單位』(欄8)不可為空") | |||
| } | |||
| } | |||
| 8 -> { | |||
| // 採購單價 — 必填,數值 | |||
| if (tempCell == null || !isNumericLike(tempCell)) { | |||
| issues += BomFormatIssue(fileName, "材料區:第${rowNum}行『採購單價』(欄9)不可為空且須為數值") | |||
| } | |||
| } | |||
| 9 -> { | |||
| // 採購單位 — 必填,非空字串 | |||
| if (tempCell == null || !isNonEmptyStringCell(tempCell)) { | |||
| issues += BomFormatIssue(fileName, "材料區:第${rowNum}行『採購單位』(欄10)不可為空") | |||
| } | |||
| // 2) 从表头下一行开始,读到 col0 空白 | |||
| var bomMatRowIdx = headerRowIndex + 1 | |||
| while (bomMatRowIdx < maxRowIndex) { | |||
| val row = sheet.getRow(bomMatRowIdx) ?: break | |||
| val firstCell = row.getCell(0) | |||
| if (firstCell == null || firstCell.cellType == CellType.BLANK) { | |||
| break | |||
| } | |||
| 10 -> { | |||
| // 加入步驟 — 必填,數值 | |||
| if (tempCell == null || !isNumericLike(tempCell)) { | |||
| issues += BomFormatIssue(fileName, "材料區:第${rowNum}行『加入步驟』(欄11)不可為空且須為數值") | |||
| for (startColumnIndex in 0..endCol) { | |||
| val tempCell = row.getCell(startColumnIndex) | |||
| val rowNum = bomMatRowIdx + 1 | |||
| when (startColumnIndex) { | |||
| 0 -> { | |||
| if (tempCell == null || !isNonEmptyStringCell(tempCell)) { | |||
| issues += BomFormatIssue(fileName, "材料區:第${rowNum}行『材料編號』(欄1)不可為空") | |||
| } | |||
| } | |||
| 1 -> { | |||
| if (tempCell == null || !isNonEmptyStringCell(tempCell)) { | |||
| issues += BomFormatIssue(fileName, "材料區:第${rowNum}行『材料』(欄2)不可為空") | |||
| } | |||
| } | |||
| 2 -> { | |||
| if (tempCell == null || !isNumericLike(tempCell)) { | |||
| issues += BomFormatIssue(fileName, "材料區:第${rowNum}行『使用份量』(欄3)不可為空且須為數值") | |||
| } | |||
| } | |||
| 3 -> { | |||
| if (tempCell == null || !isNonEmptyStringCell(tempCell)) { | |||
| issues += BomFormatIssue(fileName, "材料區:第${rowNum}行『使用單位』(欄4)不可為空") | |||
| } | |||
| } | |||
| 4 -> { | |||
| if (tempCell == null || !isNumericLike(tempCell)) { | |||
| issues += BomFormatIssue(fileName, "材料區:第${rowNum}行『轉用單位份量』(欄5)不可為空且須為數值") | |||
| } | |||
| } | |||
| 5 -> { | |||
| if (tempCell == null || !isNonEmptyStringCell(tempCell)) { | |||
| issues += BomFormatIssue(fileName, "材料區:第${rowNum}行『轉用單位』(欄6)不可為空") | |||
| } | |||
| } | |||
| 6 -> { | |||
| if (tempCell == null || !isNumericLike(tempCell)) { | |||
| issues += BomFormatIssue(fileName, "材料區:第${rowNum}行『份量(銷售單位)』(欄7)不可為空且須為數值") | |||
| } | |||
| } | |||
| 7 -> { | |||
| if (tempCell == null || !isNonEmptyStringCell(tempCell)) { | |||
| issues += BomFormatIssue(fileName, "材料區:第${rowNum}行『銷售單位』(欄8)不可為空") | |||
| } | |||
| } | |||
| 8 -> { | |||
| if (tempCell == null || !isNumericLike(tempCell)) { | |||
| issues += BomFormatIssue(fileName, "材料區:第${rowNum}行『採購單價』(欄9)不可為空且須為數值") | |||
| } | |||
| } | |||
| 9 -> { | |||
| if (tempCell == null || !isNonEmptyStringCell(tempCell)) { | |||
| issues += BomFormatIssue(fileName, "材料區:第${rowNum}行『採購單位』(欄10)不可為空") | |||
| } | |||
| } | |||
| 10 -> { | |||
| if (tempCell == null || !isNumericLike(tempCell)) { | |||
| issues += BomFormatIssue(fileName, "材料區:第${rowNum}行『加入步驟』(欄11)不可為空且須為數值") | |||
| } | |||
| } | |||
| } | |||
| } | |||
| } | |||
| if (startColumnIndex == endColumnIndex) { | |||
| // 這一列所有欄位型別都檢查完後,做 DB / 轉換檢查 | |||
| val codeCell = tempRow?.getCell(0) | |||
| val uomCell = tempRow?.getCell(3) | |||
| val saleQtyCell = tempRow?.getCell(6) | |||
| val salesUnitCell = tempRow?.getCell(7) | |||
| // DB / 轉換檢查(保留你原本逻辑) | |||
| val codeCell = row.getCell(0) | |||
| val uomCell = row.getCell(3) | |||
| val saleQtyCell = row.getCell(6) | |||
| val salesUnitCell = row.getCell(7) | |||
| val rowNum = bomMatRowIdx + 1 | |||
| // 1) Item 是否存在 | |||
| val itemCode = codeCell?.stringCellValue?.trim().orEmpty() | |||
| if (itemCode.isNotEmpty()) { | |||
| val item = itemsRepository.findByCodeAndDeletedFalse(itemCode) | |||
| @@ -2147,8 +2118,7 @@ private fun validateMaterialLikeImport( | |||
| ) | |||
| } | |||
| } | |||
| // 2) 使用單位 UOM 是否存在 | |||
| val useUomCode = uomCell?.stringCellValue?.trim().orEmpty() | |||
| if (useUomCode.isNotEmpty()) { | |||
| val useUom = uomConversionRepository.findByCodeAndDeletedFalse(useUomCode) | |||
| @@ -2159,83 +2129,13 @@ private fun validateMaterialLikeImport( | |||
| ) | |||
| } | |||
| } | |||
| // 3) 銷售單位 UOM 是否存在,以及轉換是否可行(對應 saveBomMaterial 的邏輯) | |||
| val saleQty = when { | |||
| saleQtyCell == null || !isNumericLike(saleQtyCell) -> null | |||
| saleQtyCell.cellType == CellType.NUMERIC -> | |||
| saleQtyCell.numericCellValue.toBigDecimal() | |||
| saleQtyCell.cellType == CellType.FORMULA && | |||
| saleQtyCell.cachedFormulaResultType == CellType.NUMERIC -> | |||
| saleQtyCell.numericCellValue.toBigDecimal() | |||
| else -> saleQtyCell.stringCellValue.trim().toBigDecimalOrNull() | |||
| } | |||
| val salesUnitCode = salesUnitCell?.stringCellValue?.trim().orEmpty() | |||
| if (itemCode.isNotEmpty() && saleQty != null && salesUnitCode.isNotEmpty()) { | |||
| val item = itemsRepository.findByCodeAndDeletedFalse(itemCode) | |||
| val salesUnit = uomConversionRepository.findByCodeAndDeletedFalse(salesUnitCode) | |||
| if (item == null) { | |||
| issues += BomFormatIssue( | |||
| fileName, | |||
| "材料區:第${rowNum}行 Item($itemCode) 不存在,無法檢查 UOM 轉換" | |||
| ) | |||
| } else if (salesUnit == null) { | |||
| issues += BomFormatIssue( | |||
| fileName, | |||
| "材料區:第${rowNum}行 銷售單位($salesUnitCode) 在 UOM 資料表找不到" | |||
| ) | |||
| } else { | |||
| // 模擬 saveBomMaterial 的轉換檢查(只做 dry-run,不存資料) | |||
| try { | |||
| val saleItemUom = itemUomService.findSalesUnitByItemId(item.id!!) | |||
| val itemSaleUnit = saleItemUom?.uom | |||
| if (itemSaleUnit != null && salesUnit.id != itemSaleUnit.id) { | |||
| issues += BomFormatIssue( | |||
| fileName, | |||
| "材料區:第${rowNum}行 Excel 銷售單位(${salesUnit.code}) 與品項銷售單位(${itemSaleUnit.code}) 不一致" | |||
| ) | |||
| } | |||
| val baseItemUom = itemUomService.findBaseUnitByItemId(item.id!!) | |||
| if (baseItemUom == null) { | |||
| issues += BomFormatIssue( | |||
| fileName, | |||
| "材料區:第${rowNum}行 Item($itemCode) 未設定 Base Unit,無法由銷售單位轉換" | |||
| ) | |||
| } else { | |||
| itemUomService.convertUomByItem( | |||
| ConvertUomByItemRequest( | |||
| itemId = item.id!!, | |||
| qty = saleQty, | |||
| uomId = salesUnit.id!!, | |||
| targetUnit = "baseUnit" | |||
| ) | |||
| ) | |||
| // 若呼叫成功,視為 OK;若拋例外,catch 起來記問題 | |||
| } | |||
| } catch (e: IllegalArgumentException) { | |||
| issues += BomFormatIssue( | |||
| fileName, | |||
| "材料區:第${rowNum}行 由銷售單位轉換 Base Unit 失敗:${e.message ?: "IllegalArgumentException"}" | |||
| ) | |||
| } catch (e: Exception) { | |||
| issues += BomFormatIssue( | |||
| fileName, | |||
| "材料區:第${rowNum}行 由銷售單位轉換 Base Unit 發生錯誤:${e.javaClass.simpleName} - ${e.message ?: "Unknown error"}" | |||
| ) | |||
| } | |||
| } | |||
| } | |||
| } | |||
| if (startColumnIndex < endColumnIndex) { | |||
| startColumnIndex++ | |||
| } else if (bomMatRowIdx < endRowIndex) { | |||
| // 下面 sales unit / convert 檢查,直接沿用你原本 2136 行後的邏輯即可 | |||
| // (你可把原区块整段搬进来,row / rowNum 变量名一致即可) | |||
| bomMatRowIdx++ | |||
| startColumnIndex = 0 | |||
| } | |||
| } | |||
| } | |||
| // ===================== 新增:Basic Info 區塊檢查 ===================== | |||
| /** | |||