Ver código fonte

update import bom

master
CANCERYS\kw093 1 dia atrás
pai
commit
7966cc7817
6 arquivos alterados com 335 adições e 107 exclusões
  1. +10
    -1
      src/main/java/com/ffii/fpsms/modules/master/entity/BomMaterial.kt
  2. +265
    -102
      src/main/java/com/ffii/fpsms/modules/master/service/BomService.kt
  3. +37
    -2
      src/main/java/com/ffii/fpsms/modules/master/service/ItemUomService.kt
  4. +2
    -1
      src/main/java/com/ffii/fpsms/modules/master/web/models/SaveBomRequest.kt
  5. +13
    -1
      src/main/java/com/ffii/fpsms/modules/master/web/models/SaveBomResponse.kt
  6. +8
    -0
      src/main/resources/db/changelog/changes/20260202_Enson/01_add_bom_materail.sql

+ 10
- 1
src/main/java/com/ffii/fpsms/modules/master/entity/BomMaterial.kt Ver arquivo

@@ -37,13 +37,22 @@ open class BomMaterial : BaseEntity<Long>() {
@Column(name = "uomName", length = 100)
open var uomName: String? = null

@Column(name = "saleQty", precision = 14, scale = 2)
open var saleQty: BigDecimal? = null

@ManyToOne
@JoinColumn(name = "salesUnitId")
open var salesUnit: UomConversion? = null

@Column(name = "salesUnitCode")
open var salesUnitCode: String? = null

@Column(name = "baseQty", precision = 14, scale = 2)
open var baseQty: BigDecimal? = null
@Column(name = "baseUnit", nullable = false)
open var baseUnit: Integer? = null
@Column(name = "baseUnitName", length = 100)
open var baseUnitName: String? = null
@NotNull
@ManyToOne(optional = false)
@JoinColumn(name = "bomId", nullable = false)


+ 265
- 102
src/main/java/com/ffii/fpsms/modules/master/service/BomService.kt Ver arquivo

@@ -20,6 +20,9 @@ import com.ffii.fpsms.modules.master.entity.EquipmentDetailRepository
import com.ffii.fpsms.modules.settings.entity.BomWeightingScoreRepository
import java.math.BigDecimal
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
@@ -36,6 +39,7 @@ open class BomService(
private val processRepository: ProcessRepository,
private val equipmentDetailRepository: EquipmentDetailRepository,
private val bomWeightingScoreRepository: BomWeightingScoreRepository,
private val itemUomService: ItemUomService,
) {
open fun findAll(): List<Bom> {
return bomRepository.findAll()
@@ -123,20 +127,189 @@ open class BomService(
}

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 {
this.item = req.item
this.itemName = req.item!!.name
this.itemName = req.item?.name
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

// 新逻辑:使用 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
}
return bomMaterialRepository.saveAndFlush(bomMaterial)
@@ -209,16 +382,21 @@ open class BomService(
val uom = uomConversionRepository.findByCodeAndDeletedFalse(tempCell.stringCellValue.trim())
bomMatRequest.uom = uom
bomMatRequest.uomName = uom?.udfudesc
}
/*
6 -> {
bomMatRequest.qty = tempCell.numericCellValue.toBigDecimal()
bomMatRequest.saleQty = tempCell.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())
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 {
// val folderPath = "bomImport"
// val folder = File(folderPath)
bomMaterialImportIssues.clear() // 清空问题列表
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}")
val logExcel = ClassPathResource("excelTemplate/bom_excel_issue_log.xlsx")
val templateInputStream = logExcel.inputStream
val workbook: Workbook = XSSFWorkbook(templateInputStream)
val logSheet: Sheet = workbook.getSheetAt(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) {
//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")
}
}

+ 37
- 2
src/main/java/com/ffii/fpsms/modules/master/service/ItemUomService.kt Ver arquivo

@@ -164,6 +164,7 @@ open class ItemUomService(
)
}
}
if (request.uomId == 784L) {
val targetItemUom = findTargetItemUom(request.itemId, request.targetUnit)
val targetUomId = targetItemUom?.uom?.id
@@ -182,9 +183,43 @@ open class ItemUomService(
)
}
}
// Find source ItemUom by itemId and uomId
val sourceItemUom = itemUomRespository.findFirstByItemIdAndUomIdAndDeletedIsFalse(request.itemId, request.uomId)
?: throw IllegalArgumentException("Source ItemUom not found for itemId=${request.itemId}, uomId=${request.uomId}")
var sourceItemUom = itemUomRespository.findFirstByItemIdAndUomIdAndDeletedIsFalse(request.itemId, request.uomId)
// Special handling: If source is KG (784) but ItemUom doesn't exist, try indirect conversion via baseUnit
if (sourceItemUom == null && request.uomId == 784L) {
val baseUnitItemUom = findBaseUnitByItemId(request.itemId)
val baseUnitUomId = baseUnitItemUom?.uom?.id
// If base unit is gram (id=4), convert KG -> G first, then G -> target
if (baseUnitUomId == 4L) {
// Step 1: Convert KG to G (baseUnit)
val baseQty = request.qty.multiply(BigDecimal(1000))
// Step 2: Convert from baseUnit to targetUnit
val targetItemUom = findTargetItemUom(request.itemId, request.targetUnit)
?: throw IllegalArgumentException("Target ItemUom not found for itemId=${request.itemId}, targetUnit=${request.targetUnit}")
val targetRatioN = targetItemUom.ratioN ?: BigDecimal.ONE
val targetRatioD = targetItemUom.ratioD ?: BigDecimal.ONE
// Convert base (G) to target: baseQty * ratioD / ratioN
val newQty = baseQty.multiply(targetRatioD).divide(targetRatioN, 2, RoundingMode.UP)
val uomConversion = targetItemUom.uom
?: throw IllegalArgumentException("Target UomConversion not found for target ItemUom")
return ConvertUomByItemResponse(
newQty = newQty,
udfudesc = uomConversion.udfudesc,
udfShortDesc = uomConversion.udfShortDesc
)
}
}
// If still no source ItemUom found, throw error
sourceItemUom ?: throw IllegalArgumentException("Source ItemUom not found for itemId=${request.itemId}, uomId=${request.uomId}")
// Find target ItemUom by itemId and targetUnit
val targetItemUom = findTargetItemUom(request.itemId, request.targetUnit)


+ 2
- 1
src/main/java/com/ffii/fpsms/modules/master/web/models/SaveBomRequest.kt Ver arquivo

@@ -49,6 +49,7 @@ data class ImportBomMatRequest (
var isConsumable: Boolean? = false,
var qty: BigDecimal? = null,
var uom: UomConversion? = null,
var saleQty: BigDecimal? = null,
var salesUnit: UomConversion? = null,
var salesUnitCode: String? = null,
var uomName: String? = null,
@@ -68,4 +69,4 @@ data class ImportBomProcessRequest(
data class ImportBomProcessMaterialRequest(
var bomProcess: BomProcess? = null,
var bomMaterial: BomMaterial? = null,
)
)

+ 13
- 1
src/main/java/com/ffii/fpsms/modules/master/web/models/SaveBomResponse.kt Ver arquivo

@@ -11,4 +11,16 @@ data class SaveBomResponse (
val outputQtyUom: String? = null,
val yield: BigDecimal? = null,
val uomName: String? = null,
)
)
data class BomMaterialImportIssue(
val bomId: Long?,
val bomCode: String?,
val itemId: Long?,
val itemCode: String?,
val itemName: String?,
val reason: String,
val srcQty: BigDecimal?,
val srcUomCode: String?,
)
val bomMaterialImportIssues = mutableListOf<BomMaterialImportIssue>()

+ 8
- 0
src/main/resources/db/changelog/changes/20260202_Enson/01_add_bom_materail.sql Ver arquivo

@@ -0,0 +1,8 @@
-- liquibase formatted sql
-- changeset KelvinY:add_baseScore_to_bom

ALTER TABLE `fpsmsdb`.`bom_material`
ADD COLUMN `saleQty` DECIMAL(14, 2) NULL DEFAULT NULL AFTER `uomName`,
ADD COLUMN `baseQty` DECIMAL(14, 2) NULL DEFAULT NULL AFTER `salesUnitCode`,
ADD COLUMN `baseUnit` INTEGER NULL DEFAULT NULL AFTER `baseQty`,
ADD COLUMN `baseUnitName` VARCHAR(255) NULL DEFAULT NULL AFTER `baseUnit`;

Carregando…
Cancelar
Salvar