@@ -20,6 +20,9 @@ import com.ffii.fpsms.modules.master.entity.EquipmentDetailRepository
import com.ffii.fpsms.modules.settings.entity.BomWeightingScoreRepository
import com.ffii.fpsms.modules.settings.entity.BomWeightingScoreRepository
import java.math.BigDecimal
import java.math.BigDecimal
import java.math.RoundingMode
import java.math.RoundingMode
import com.ffii.fpsms.modules.master.service.ItemUomService
import com.ffii.fpsms.modules.master.web.models.ConvertUomByItemRequest
import com.ffii.fpsms.modules.master.web.models.ConvertUomByItemResponse
@Service
@Service
@@ -36,6 +39,7 @@ open class BomService(
private val processRepository: ProcessRepository,
private val processRepository: ProcessRepository,
private val equipmentDetailRepository: EquipmentDetailRepository,
private val equipmentDetailRepository: EquipmentDetailRepository,
private val bomWeightingScoreRepository: BomWeightingScoreRepository,
private val bomWeightingScoreRepository: BomWeightingScoreRepository,
private val itemUomService: ItemUomService,
) {
) {
open fun findAll(): List<Bom> {
open fun findAll(): List<Bom> {
return bomRepository.findAll()
return bomRepository.findAll()
@@ -123,20 +127,189 @@ open class BomService(
}
}
private fun saveBomMaterial(req: ImportBomMatRequest): BomMaterial {
private fun saveBomMaterial(req: ImportBomMatRequest): BomMaterial {
// val uom = uomConversionRepository.findByCodeAndDeletedFalse()
// println("printing")
// println(req)
// println(req.item)
val bomMaterial = req.bom?.id?.let { bId -> req.item?.id?.let { iId -> bomMaterialRepository.findByBomIdAndItemId(bId, iId) } } ?: BomMaterial()
val bomMaterial = req.bom?.id?.let { bId ->
req.item?.id?.let { iId ->
bomMaterialRepository.findByBomIdAndItemId(bId, iId)
}
} ?: BomMaterial()
val item = req.item
val bomId = req.bom?.id
val bomCode = req.bom?.code
// ===== 新逻辑:使用 Excel column 6 的 saleQty 和 column 7 的 sales unit =====
val excelSaleQty = req.saleQty // Excel 第6栏的 saleQty
val excelSalesUnitCode = req.salesUnitCode // Excel 第7栏的 sales unit code(字符串)
val excelSalesUnit = req.salesUnit // Excel 第7栏查找的 uom_conversion(可能为 null)
var saleQty: BigDecimal? = excelSaleQty
var saleUnitId: Long? = null
var saleUnitCode: String? = null
var baseQty: BigDecimal? = null
var baseUnit: Integer? = null
var baseUnitName: String? = null
if (item?.id != null) {
val itemId = item.id!!
try {
// ---- 1) 获取 item 的真实 stock unit ----
val stockItemUom = itemUomService.findStockUnitByItemId(itemId)
val itemStockUnit = stockItemUom?.uom
// saleUnitId: 使用 item 的 stock unit uom id
saleUnitId = itemStockUnit?.id
// saleUnitCode: 从 Excel 数据查找 uom_conversion,如果失败则使用 Excel 数据本身
if (excelSalesUnit != null) {
// Excel 找到了 uom_conversion
saleUnitCode = excelSalesUnit.udfudesc
// 检查 Excel sales unit 与 item stock unit 是否匹配
if (itemStockUnit != null && excelSalesUnit.id != itemStockUnit.id) {
bomMaterialImportIssues.add(
BomMaterialImportIssue(
bomId = bomId,
bomCode = bomCode,
itemId = item.id,
itemCode = item.code,
itemName = item.name,
reason = "Excel sales unit (${excelSalesUnit.code}/${excelSalesUnit.udfudesc}) does not match item stock unit (${itemStockUnit.code}/${itemStockUnit.udfudesc})",
srcQty = excelSaleQty,
srcUomCode = excelSalesUnit.code
)
)
}
} else {
// Excel 没有找到 uom_conversion,使用 Excel 数据本身
saleUnitCode = excelSalesUnitCode ?: itemStockUnit?.udfudesc
if (excelSalesUnitCode != null) {
bomMaterialImportIssues.add(
BomMaterialImportIssue(
bomId = bomId,
bomCode = bomCode,
itemId = item.id,
itemCode = item.code,
itemName = item.name,
reason = "Excel sales unit code '$excelSalesUnitCode' not found in uom_conversion, using Excel data as-is",
srcQty = excelSaleQty,
srcUomCode = excelSalesUnitCode
)
)
}
}
// ---- 2) 从 saleQty + item 的真实 stock unit 转换为 baseQty ----
if (excelSaleQty != null && itemStockUnit != null) {
val baseResult = itemUomService.convertUomByItem(
ConvertUomByItemRequest(
itemId = itemId,
qty = excelSaleQty,
uomId = itemStockUnit.id!!,
targetUnit = "baseUnit"
)
)
baseQty = baseResult.newQty
}
// ---- 3) 获取 item 的真实 base unit ----
val baseItemUom = itemUomService.findBaseUnitByItemId(itemId)
baseUnit = baseItemUom?.uom?.id?.toInt()?.let { Integer.valueOf(it) } as? Integer
baseUnitName = baseItemUom?.uom?.udfudesc
// 如果 baseQty 转换失败,记录问题
if (baseQty == null && excelSaleQty != null && itemStockUnit != null) {
bomMaterialImportIssues.add(
BomMaterialImportIssue(
bomId = bomId,
bomCode = bomCode,
itemId = item.id,
itemCode = item.code,
itemName = item.name,
reason = "Cannot convert saleQty to baseQty: conversion failed",
srcQty = excelSaleQty,
srcUomCode = itemStockUnit.code
)
)
} else if (excelSaleQty != null && itemStockUnit == null) {
bomMaterialImportIssues.add(
BomMaterialImportIssue(
bomId = bomId,
bomCode = bomCode,
itemId = item.id,
itemCode = item.code,
itemName = item.name,
reason = "Cannot convert saleQty to baseQty: item stock unit not found",
srcQty = excelSaleQty,
srcUomCode = null
)
)
}
} catch (e: IllegalArgumentException) {
bomMaterialImportIssues.add(
BomMaterialImportIssue(
bomId = bomId,
bomCode = bomCode,
itemId = item.id,
itemCode = item.code,
itemName = item.name,
reason = e.message ?: "UOM conversion error",
srcQty = excelSaleQty,
srcUomCode = excelSalesUnitCode
)
)
println("【BOM Import Warning】bomCode=$bomCode, item=${item.code} 转 UOM 失败: ${e.message}")
} catch (e: Exception) {
bomMaterialImportIssues.add(
BomMaterialImportIssue(
bomId = bomId,
bomCode = bomCode,
itemId = item.id,
itemCode = item.code,
itemName = item.name,
reason = "Unknown error: ${e.message}",
srcQty = excelSaleQty,
srcUomCode = excelSalesUnitCode
)
)
println("【BOM Import Error】bomCode=$bomCode, item=${item.code}: ${e.message}")
}
} else {
// item 缺失
bomMaterialImportIssues.add(
BomMaterialImportIssue(
bomId = bomId,
bomCode = bomCode,
itemId = item?.id,
itemCode = item?.code,
itemName = item?.name,
reason = "Missing item for conversion",
srcQty = excelSaleQty,
srcUomCode = excelSalesUnitCode
)
)
}
// ===== 写回 BomMaterial =====
bomMaterial.apply {
bomMaterial.apply {
this.item = req.item
this.item = req.item
this.itemName = req.item!!.name
this.itemName = req.item? .name
this.isConsumable = req.isConsumable
this.isConsumable = req.isConsumable
this.qty = req.qty
this.salesUnit = req.salesUnit
this.salesUnitCode = req.salesUnitCode
this.uom = req.uom
this.qty = req.qty // BOM 原始用量(column 2,保留)
this.uom = req.uom // BOM 原始 UOM(column 3,保留)
this.uomName = req.uomName
this.uomName = req.uomName
// 新逻辑:使用 Excel column 6 的 saleQty 和 item stock unit
this.saleQty = saleQty
this.salesUnit = item?.id?.let { itemUomService.findStockUnitByItemId(it)?.uom }
this.salesUnitCode = saleUnitCode
this.baseQty = baseQty
this.baseUnit = baseUnit
this.baseUnitName = baseUnitName
this.bom = req.bom
this.bom = req.bom
}
}
return bomMaterialRepository.saveAndFlush(bomMaterial)
return bomMaterialRepository.saveAndFlush(bomMaterial)
@@ -209,16 +382,21 @@ open class BomService(
val uom = uomConversionRepository.findByCodeAndDeletedFalse(tempCell.stringCellValue.trim())
val uom = uomConversionRepository.findByCodeAndDeletedFalse(tempCell.stringCellValue.trim())
bomMatRequest.uom = uom
bomMatRequest.uom = uom
bomMatRequest.uomName = uom?.udfudesc
bomMatRequest.uomName = uom?.udfudesc
}
}
/*
6 -> {
6 -> {
bomMatRequest.q ty = tempCell.numericCellValue.toBigDecimal()
bomMatRequest.saleQ ty = tempCell.numericCellValue.toBigDecimal()
}
}
*/
7 -> {
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 salesUnit = uomConversionRepository.findByCodeAndDeletedFalse(tempCell.stringCellValue.trim())
bomMatRequest.salesUnit = salesUnit
bomMatRequest.salesUnit = salesUnit
bomMatRequest.salesUnitCode = salesUnit?.udfudesc
// bomMatRequest.salesUnitCode = salesUnit?.udfudesc
bomMatRequest.salesUnitCode = salesUnitCodeStr
}
}
@@ -684,102 +862,87 @@ open class BomService(
}
}
open fun importBOM(): ByteArray {
open fun importBOM(): ByteArray {
// val folderPath = "bomImport"
// val folder = File(folderPath)
bomMaterialImportIssues.clear() // 清空问题列表
val resolver = PathMatchingResourcePatternResolver()
val resolver = PathMatchingResourcePatternResolver()
// val excels = resolver.getResources("bomImport/*.xlsx")
//val excels = resolver.getResources("file:C:/Users/Kelvin YAU/Downloads/bom/*.xlsx")
val excels = resolver.getResources("file:C:/Users/Kelvin YAU/Downloads/datasetup/bom/*.xlsx")
// val excels = resolver.getResources("file:C:/Users/kw093/Downloads/bom/bom/*.xlsx")
// val excels = resolver.getResources("file:C:/Users/2Fi/Desktop/Third Wave of BOM Excel/*.xlsx")
val excels = resolver.getResources("file:C:/Users/kw093/Downloads/bom/bom/*.xlsx")
println("size: ${excels.size}")
println("size: ${excels.size}")
val logExcel = ClassPathResource("excelTemplate/bom_excel_issue_log.xlsx")
val logExcel = ClassPathResource("excelTemplate/bom_excel_issue_log.xlsx")
val templateInputStream = logExcel.inputStream
val templateInputStream = logExcel.inputStream
val workbook: Workbook = XSSFWorkbook(templateInputStream)
val workbook: Workbook = XSSFWorkbook(templateInputStream)
val logSheet: Sheet = workbook.getSheetAt(0)
val logSheet: Sheet = workbook.getSheetAt(0)
var rowIndex = 0
var rowIndex = 0
/////// for outputing issue log ////////////
// excels.forEachIndexed { index, resource ->
//// get sheet
// println(resource.filename)
// val templateInputStream1 = resource.inputStream
// val workbook1: Workbook = XSSFWorkbook(templateInputStream1)
// val sheet: Sheet = workbook1.getSheetAt(0)
// println("sheetName")
// println(sheet.sheetName)
// val successPath: Path = Paths.get("C:/Users/2Fi/Desktop/success/${resource.filename}")
// val failPath: Path = Paths.get("C:/Users/2Fi/Desktop/fail/${resource.filename}")
// val bom = try {
// importExcelBomBasicInfo(sheet) // Updating BOM table
// } catch (e: Exception) {
// println("Error in importExcelBomBasicInfo: ${e.message}")
// rowIndex++
// val row = logSheet.createRow(rowIndex)
// row.createCell(0).setCellValue(resource.filename)
// row.createCell(1).setCellValue(false)
// row.createCell(2).setCellValue(e.message)
// Files.copy(templateInputStream, failPath, StandardCopyOption.REPLACE_EXISTING)
// throw
// return@forEachIndexed
//// throw e // Rethrow to handle in the outer try-catch
// }
// try {
// importExcelBomProcess(bom, sheet)
// } catch (e: Exception) {
// println("Error in importExcelBomProcess: ${e.message}")
// rowIndex++
// val row = logSheet.createRow(rowIndex)
// row.createCell(0).setCellValue(resource.filename)
// row.createCell(1).setCellValue(false)
// row.createCell(2).setCellValue(e.message)
// Files.copy(templateInputStream, failPath, StandardCopyOption.REPLACE_EXISTING)
// throw
// return@forEachIndexed
//// throw e // Rethrow to handle in the outer try-catch
// }
// try {
// importExcelBomMaterial(bom, sheet)
// } catch (e: Exception) {
// println("Error in importExcelBomMaterial: ${e.message}")
// rowIndex++
// val row = logSheet.createRow(rowIndex)
// row.createCell(0).setCellValue(resource.filename)
// row.createCell(1).setCellValue(false)
// row.createCell(2).setCellValue(e.message)
// Files.copy(templateInputStream, failPath, StandardCopyOption.REPLACE_EXISTING)
// throw
// return@forEachIndexed
//// throw e // Rethrow to handle in the outer try-catch
// }
// rowIndex++
// val row = logSheet.createRow(rowIndex)
// row.createCell(0).setCellValue(resource.filename)
// row.createCell(1).setCellValue(true)
// Files.copy(templateInputStream, successPath, StandardCopyOption.REPLACE_EXISTING)
// }
// val outputStream = ByteArrayOutputStream()
// workbook.write(outputStream)
// workbook.close()
// return outputStream.toByteArray()
////////////////////////////////////////////////////////////////////////////////////////////
for (resource in excels) {
for (resource in excels) {
//get sheet
println(resource.filename)
val templateInputStream2 = resource.inputStream
val workbook2: Workbook = XSSFWorkbook(templateInputStream2)
val sheet: Sheet = workbook2.getSheetAt(0)
val bom = importExcelBomBasicInfo(sheet) // updating bom table
// import bom process / create new process
importExcelBomProcess(bom, sheet)
// import bom material
importExcelBomMaterial(bom, sheet)
// break
val filename = resource.filename ?: "unknown"
try {
//get sheet
println("处理文件: $filename")
val templateInputStream2 = resource.inputStream
val workbook2: Workbook = XSSFWorkbook(templateInputStream2)
val sheet: Sheet = workbook2.getSheetAt(0)
val bom = importExcelBomBasicInfo(sheet) // updating bom table
// import bom process / create new process
importExcelBomProcess(bom, sheet)
// import bom material
importExcelBomMaterial(bom, sheet)
workbook2.close()
println("成功处理文件: $filename")
} catch (e: org.apache.poi.openxml4j.exceptions.NotOfficeXmlFileException) {
println("错误:文件 '$filename' 不是有效的 Excel 文件,跳过")
println(" 错误信息: ${e.message}")
} catch (e: java.util.zip.ZipException) {
println("错误:文件 '$filename' 格式错误(可能是临时文件或损坏),跳过")
println(" 错误信息: ${e.message}")
} catch (e: Exception) {
println("错误:处理文件 '$filename' 时发生异常")
println(" 错误类型: ${e.javaClass.simpleName}")
println(" 错误信息: ${e.message}")
e.printStackTrace()
}
}
}
val outputStream = ByteArrayOutputStream()
workbook.write(outputStream)
workbook.close()
return outputStream.toByteArray()
printBomMaterialImportIssues() // 打印问题汇总
val outputStream = ByteArrayOutputStream()
workbook.write(outputStream)
workbook.close()
return outputStream.toByteArray()
}
private fun printBomMaterialImportIssues() {
println("\n========== BOM Material UOM Conversion Issues ==========")
println("Total issues: ${bomMaterialImportIssues.size}")
if (bomMaterialImportIssues.isEmpty()) {
println("No issues.")
println("========================================================\n")
return
}
// 按 BOM 分组统计
val issuesByBom = bomMaterialImportIssues.groupBy {
it.bomCode?.takeIf { it.isNotEmpty() } ?: "BOM-id-${it.bomId}"
}
println("\n按 BOM 分组统计:")
issuesByBom.forEach { (bomKey, issues) ->
println(" $bomKey: ${issues.size} 个问题")
}
println("\n详细问题列表:")
bomMaterialImportIssues
.sortedWith(compareBy({ it.bomCode ?: "BOM-id-${it.bomId}" }, { it.itemCode }))
.forEach { issue ->
val bomIdentifier = issue.bomCode?.takeIf { it.isNotEmpty() }
?: "BOM-id-${issue.bomId}"
println(
"BOM=$bomIdentifier, " +
"Item=${issue.itemCode ?: "null"} (${issue.itemName ?: "null"}), " +
"src=${issue.srcQty} ${issue.srcUomCode} -> reason=${issue.reason}"
)
}
println("========================================================\n")
}
}
}
}