瀏覽代碼

update

master
CANCERYS\kw093 1 天之前
父節點
當前提交
7477100847
共有 8 個檔案被更改,包括 311 行新增52 行删除
  1. +64
    -5
      src/main/java/com/ffii/fpsms/modules/jobOrder/service/JobOrderService.kt
  2. +2
    -2
      src/main/java/com/ffii/fpsms/modules/master/service/BomService.kt
  3. +145
    -41
      src/main/java/com/ffii/fpsms/modules/pickOrder/service/PickExecutionIssueService.kt
  4. +20
    -1
      src/main/java/com/ffii/fpsms/modules/productProcess/entity/ProductProcessLineRepository.kt
  5. +2
    -2
      src/main/java/com/ffii/fpsms/modules/productProcess/entity/projections/ProductProcessInfo.kt
  6. +15
    -0
      src/main/java/com/ffii/fpsms/modules/productProcess/web/ProductProcessController.kt
  7. +60
    -0
      src/main/java/com/ffii/fpsms/modules/productProcess/web/model/SaveProductProcessRequest.kt
  8. +3
    -1
      src/main/resources/db/changelog/changes/20260204_Enson/01_add_bom_materail.sql

+ 64
- 5
src/main/java/com/ffii/fpsms/modules/jobOrder/service/JobOrderService.kt 查看文件

@@ -68,6 +68,7 @@ import java.time.LocalDate
import java.time.LocalDateTime
import com.ffii.fpsms.modules.master.entity.BomMaterialRepository
import com.ffii.fpsms.modules.master.service.ItemUomService
import com.ffii.fpsms.modules.master.web.models.ConvertUomByItemRequest
@Service
open class JobOrderService(
val jobOrderRepository: JobOrderRepository,
@@ -208,7 +209,6 @@ open class JobOrderService(
return RecordsRes<JobOrderInfoWithTypeName>(records, total.toInt());
}
// 添加辅助方法计算库存统计
private fun calculateStockCounts(
jobOrder: JobOrder,
inventoriesMap: Map<Long?, com.ffii.fpsms.modules.stock.entity.projection.InventoryInfo>
@@ -216,7 +216,7 @@ open class JobOrderService(
// 过滤掉 consumables 和 CMB 类型的物料
val nonConsumablesJobms = jobOrder.jobms.filter { jobm ->
val itemType = jobm.item?.type?.lowercase()
itemType != "consumables" && itemType != "cmb"&& itemType != "nm"
itemType != "consumables" && itemType != "consumable" && itemType != "cmb" && itemType != "nm"
}
if (nonConsumablesJobms.isEmpty()) {
@@ -226,14 +226,16 @@ open class JobOrderService(
var sufficientCount = 0
var insufficientCount = 0
println("=== JobOrderService.calculateStockCounts for JobOrder: ${jobOrder.code} ===")
nonConsumablesJobms.forEach { jobm ->
val itemId = jobm.item?.id
val reqQty = jobm.reqQty ?: BigDecimal.ZERO
val itemCode = jobm.item?.code ?: "N/A"
val itemName = jobm.item?.name ?: "N/A"
if (itemId != null) {
val inventory = inventoriesMap[itemId]
val availableQty = if (inventory != null) {
// 使用 availableQty,如果没有则计算:onHandQty - onHoldQty - unavailableQty
inventory.availableQty ?: (
(inventory.onHandQty ?: BigDecimal.ZERO) -
(inventory.onHoldQty ?: BigDecimal.ZERO) -
@@ -243,17 +245,74 @@ open class JobOrderService(
BigDecimal.ZERO
}
if (availableQty >= reqQty) {
// ✅ 获取 reqQty 和 availableQty 的单位信息
val reqQty = jobm.reqQty ?: BigDecimal.ZERO
val reqUomId = jobm.uom?.id ?: 0L
val reqUomName = jobm.uom?.udfudesc ?: "N/A"
// ✅ 修复:使用 itemUomService 获取 stockUomId(与 ProductProcessService 保持一致)
val stockUnitItemUom = itemUomService.findStockUnitByItemId(itemId)
val stockUomId = stockUnitItemUom?.uom?.id ?: 0L
val stockUomName = stockUnitItemUom?.uom?.udfudesc ?: "N/A"
// ✅ 转换为 base unit 进行比较(与 ProductProcessService 保持一致)
val baseReqQtyResult = if (reqUomId > 0 && reqQty > BigDecimal.ZERO) {
try {
itemUomService.convertUomByItem(
ConvertUomByItemRequest(
itemId = itemId,
qty = reqQty,
uomId = reqUomId,
targetUnit = "baseUnit"
)
)
} catch (e: Exception) {
println("Error converting reqQty to base unit: ${e.message}")
null
}
} else {
null
}
val baseAvailableQtyResult = if (stockUomId > 0 && availableQty > BigDecimal.ZERO) {
try {
itemUomService.convertUomByItem(
ConvertUomByItemRequest(
itemId = itemId,
qty = availableQty,
uomId = stockUomId,
targetUnit = "baseUnit"
)
)
} catch (e: Exception) {
println("Error converting availableQty to base unit: ${e.message}")
null
}
} else {
null
}
val baseReqQty = baseReqQtyResult?.newQty ?: BigDecimal.ZERO
val baseAvailableQty = baseAvailableQtyResult?.newQty ?: BigDecimal.ZERO
val baseUomName = baseReqQtyResult?.udfudesc ?: baseAvailableQtyResult?.udfudesc ?: "N/A"
// ✅ 使用 base unit 进行比较
if (baseAvailableQty >= baseReqQty) {
sufficientCount++
println("✅ SUFFICIENT - Item: $itemCode ($itemName) - reqQty: $reqQty ($reqUomName) = $baseReqQty ($baseUomName), availableQty: $availableQty ($stockUomName) = $baseAvailableQty ($baseUomName)")
} else {
insufficientCount++
println("❌ INSUFFICIENT - Item: $itemCode ($itemName) - reqQty: $reqQty ($reqUomName) = $baseReqQty ($baseUomName), availableQty: $availableQty ($stockUomName) = $baseAvailableQty ($baseUomName)")
}
} else {
// 如果没有 itemId,视为不足
insufficientCount++
println("❌ INSUFFICIENT - Item: $itemCode ($itemName) - No itemId")
}
}
println("=== Result: sufficient=$sufficientCount, insufficient=$insufficientCount ===")
return Pair(sufficientCount, insufficientCount)
}
open fun jobOrderDetail(id: Long): JobOrderDetail {


+ 2
- 2
src/main/java/com/ffii/fpsms/modules/master/service/BomService.kt 查看文件

@@ -757,8 +757,8 @@ open class BomService(
// 创建 equipment
equipment = Equipment().apply {
this.name = equipmentName // 完整值 XXX-YYY
this.code = secondPart
this.code = equipmentName // 完整值 XXX-YYY
this.name = secondPart
this.description = firstPart // XXX 写入 description
}
equipment = equipmentRepository.saveAndFlush(equipment)


+ 145
- 41
src/main/java/com/ffii/fpsms/modules/pickOrder/service/PickExecutionIssueService.kt 查看文件

@@ -74,9 +74,23 @@ open class PickExecutionIssueService(
@Value("\${pick.execution.auto-resuggest-on-rejection:false}")
private val autoResuggestOnLotRejection: Boolean = false

@Transactional(rollbackFor = [Exception::class])
open fun recordPickExecutionIssue(request: PickExecutionIssueRequest): MessageResponse {
try {
println("=== recordPickExecutionIssue: START ===")
println("Request details:")
println(" pickOrderId: ${request.pickOrderId}")
println(" pickOrderLineId: ${request.pickOrderLineId}")
println(" itemId: ${request.itemId}")
println(" itemCode: ${request.itemCode}")
println(" lotId: ${request.lotId}")
println(" lotNo: ${request.lotNo}")
println(" requiredQty: ${request.requiredQty}")
println(" actualPickQty: ${request.actualPickQty}")
println(" missQty: ${request.missQty}")
println(" badItemQty: ${request.badItemQty}")
println(" badReason: ${request.badReason}")
println(" issueCategory: ${request.issueCategory}")
println("========================================")
// 1. 检查是否已经存在相同的 pick execution issue 记录
val existingIssues = pickExecutionIssueRepository.findByPickOrderLineIdAndLotIdAndDeletedFalse(
@@ -84,7 +98,14 @@ open class PickExecutionIssueService(
request.lotId ?: 0L
)
println("Checking for existing issues...")
println(" Found ${existingIssues.size} existing issues")
existingIssues.forEachIndexed { index, issue ->
println(" Existing[$index]: id=${issue.id}, issueNo=${issue.issueNo}, handleStatus=${issue.handleStatus}, issueQty=${issue.issueQty}")
}
if (existingIssues.isNotEmpty()) {
println("❌ Duplicate issue found - returning DUPLICATE error")
return MessageResponse(
id = null,
name = "Pick execution issue already exists",
@@ -96,18 +117,24 @@ open class PickExecutionIssueService(
}
val pickOrder = pickOrderRepository.findById(request.pickOrderId).orElse(null)
println("Pick order: id=${pickOrder?.id}, code=${pickOrder?.code}, type=${pickOrder?.type?.value}")
// 2. 获取 inventory_lot_line 并计算账面数量 (bookQty)
val inventoryLotLine = request.lotId?.let {
inventoryLotLineRepository.findById(it).orElse(null)
}
println("Inventory lot line: id=${inventoryLotLine?.id}, lotNo=${inventoryLotLine?.inventoryLot?.lotNo}")
// 计算账面数量(创建 issue 时的快照)
val bookQty = if (inventoryLotLine != null) {
val inQty = inventoryLotLine.inQty ?: BigDecimal.ZERO
val outQty = inventoryLotLine.outQty ?: BigDecimal.ZERO
inQty.subtract(outQty) // bookQty = inQty - outQty
val calculated = inQty.subtract(outQty) // bookQty = inQty - outQty
println(" BookQty calculation: inQty=$inQty, outQty=$outQty, bookQty=$calculated")
calculated
} else {
println(" No inventory lot line found, bookQty=0")
BigDecimal.ZERO
}
@@ -117,41 +144,53 @@ open class PickExecutionIssueService(
val missQty = request.missQty ?: BigDecimal.ZERO
val badItemQty = request.badItemQty ?: BigDecimal.ZERO
val badReason = request.badReason ?: "quantity_problem"
println("=== Quantity Summary ===")
println(" Required Qty: $requiredQty")
println(" Actual Pick Qty: $actualPickQty")
println(" Miss Qty: $missQty")
println(" Bad Item Qty: $badItemQty")
println(" Bad Reason: $badReason")
println(" Book Qty: $bookQty")
// 4. 计算 issueQty(实际的问题数量)
val issueQty = when {
// 情况1: 已拣完但有坏品
actualPickQty == requiredQty && badItemQty > BigDecimal.ZERO -> {
println(" Case 1: actualPickQty == requiredQty && badItemQty > 0")
println(" issueQty = badItemQty = $badItemQty")
badItemQty // issueQty = badItemQty
}
badReason == "package_problem" && badItemQty > BigDecimal.ZERO -> {
println(" Case 2: badReason == 'package_problem' && badItemQty > 0")
println(" issueQty = badItemQty = $badItemQty")
badItemQty
}
actualPickQty < requiredQty -> {
println(" Case 3: actualPickQty < requiredQty")
val calculatedIssueQty = bookQty.subtract(actualPickQty)
println(" issueQty = bookQty - actualPickQty = $bookQty - $actualPickQty = $calculatedIssueQty")
if (missQty > BigDecimal.ZERO && missQty > calculatedIssueQty) {
println("⚠️ Warning: User reported missQty (${missQty}) exceeds calculated issueQty (${calculatedIssueQty})")
println(" BookQty: ${bookQty}, ActualPickQty: ${actualPickQty}")
println("⚠️ Warning: User reported missQty ($missQty) exceeds calculated issueQty ($calculatedIssueQty)")
println(" BookQty: $bookQty, ActualPickQty: $actualPickQty")
}
calculatedIssueQty
}
else -> BigDecimal.ZERO
else -> {
println(" Case 4: Default case")
println(" issueQty = 0")
BigDecimal.ZERO
}
}
println("=== PICK EXECUTION ISSUE PROCESSING ===")
println("Required Qty: ${requiredQty}")
println("Actual Pick Qty: ${actualPickQty}")
println("Miss Qty (Reported): ${missQty}")
println("Bad Item Qty: ${badItemQty}")
println("Book Qty (inQty - outQty): ${bookQty}")
println("Issue Qty (Calculated): ${issueQty}")
println("Bad Reason: ${request.badReason}")
println("Lot ID: ${request.lotId}")
println("Item ID: ${request.itemId}")
println("=== Final IssueQty Calculation ===")
println(" Calculated IssueQty: $issueQty")
println("================================================")
// 5. 创建 pick execution issue 记录
val issueNo = generateIssueNo()
println("Generated issue number: $issueNo")
val pickExecutionIssue = PickExecutionIssue(
id = null,
pickOrderId = request.pickOrderId,
@@ -159,7 +198,7 @@ open class PickExecutionIssueService(
pickOrderCreateDate = request.pickOrderCreateDate,
pickExecutionDate = request.pickExecutionDate ?: LocalDate.now(),
pickOrderLineId = request.pickOrderLineId,
issueNo = generateIssueNo(),
issueNo = issueNo,
joPickOrderId = pickOrder?.jobOrder?.id,
doPickOrderId = if (pickOrder?.type?.value == "do") pickOrder.id else null,
issueCategory = IssueCategory.valueOf(
@@ -189,11 +228,18 @@ open class PickExecutionIssueService(
modifiedBy = "system",
deleted = false
)
println("Creating pick execution issue record...")
val savedIssue = pickExecutionIssueRepository.save(pickExecutionIssue)
println("✅ Issue record created successfully!")
println(" Issue ID: ${savedIssue.id}")
println(" Issue No: ${savedIssue.issueNo}")
println(" Handle Status: ${savedIssue.handleStatus}")
println(" Issue Qty: ${savedIssue.issueQty}")
// 6. NEW: Update inventory_lot_line.issueQty
if (request.lotId != null && inventoryLotLine != null) {
println("Updating inventory_lot_line.issueQty...")
// ✅ 修改:如果只有 missQty,不更新 issueQty
val actualPickQty = request.actualPickQty ?: BigDecimal.ZERO
val missQty = request.missQty ?: BigDecimal.ZERO
@@ -205,6 +251,9 @@ open class PickExecutionIssueService(
val hasMissItemWithPartialPick = missQty > BigDecimal.ZERO
&& actualPickQty > BigDecimal.ZERO
println(" isMissItemOnly: $isMissItemOnly")
println(" hasMissItemWithPartialPick: $hasMissItemWithPartialPick")
if (!isMissItemOnly && !hasMissItemWithPartialPick) {
// 只有非 miss item 的情况才更新 issueQty
val currentIssueQty = inventoryLotLine.issueQty ?: BigDecimal.ZERO
@@ -213,42 +262,45 @@ open class PickExecutionIssueService(
inventoryLotLine.modified = LocalDateTime.now()
inventoryLotLine.modifiedBy = "system"
inventoryLotLineRepository.saveAndFlush(inventoryLotLine)
println("Updated inventory_lot_line ${request.lotId} issueQty: ${currentIssueQty} -> ${newIssueQty}")
println("Updated inventory_lot_line ${request.lotId} issueQty: $currentIssueQty -> $newIssueQty")
} else {
println("Skipped updating issueQty for miss item (lot ${request.lotId})")
println("⏭️ Skipped updating issueQty for miss item (lot ${request.lotId})")
}
}
// 7. 获取相关数据用于后续处理
val actualPickQtyForProcessing = request.actualPickQty ?: BigDecimal.ZERO
val missQtyForProcessing = request.missQty ?: BigDecimal.ZERO
val badItemQtyForProcessing = request.badItemQty ?: BigDecimal.ZERO
val lotId = request.lotId
val itemId = request.itemId
println("=== PICK EXECUTION ISSUE PROCESSING (NEW LOGIC) ===")
println("Actual Pick Qty: ${actualPickQtyForProcessing}")
println("Miss Qty: ${missQtyForProcessing}")
println("Bad Item Qty: ${badItemQtyForProcessing}")
println("=== Processing Logic Selection ===")
println("Actual Pick Qty: $actualPickQtyForProcessing")
println("Miss Qty: $missQtyForProcessing")
println("Bad Item Qty: $badItemQtyForProcessing")
println("Bad Reason: ${request.badReason}")
println("Lot ID: ${lotId}")
println("Item ID: ${itemId}")
println("Lot ID: $lotId")
println("Item ID: $itemId")
println("================================================")
// 8. 新的统一处理逻辑(根据 badReason 决定处理方式)
when {
// 情况1: 只有 miss item (actualPickQty = 0, missQty > 0, badItemQty = 0)
actualPickQtyForProcessing == BigDecimal.ZERO && missQtyForProcessing > BigDecimal.ZERO && badItemQtyForProcessing == BigDecimal.ZERO -> {
println("→ Handling: Miss Item Only")
handleMissItemOnly(request, missQtyForProcessing)
}
// 情况2: 只有 bad item (badItemQty > 0, missQty = 0)
badItemQtyForProcessing > BigDecimal.ZERO && missQtyForProcessing == BigDecimal.ZERO -> {
println("→ Handling: Bad Item Only")
// NEW: Check bad reason
if (request.badReason == "package_problem") {
println(" Bad reason is 'package_problem' - calling handleBadItemPackageProblem")
handleBadItemPackageProblem(request, badItemQtyForProcessing)
} else {
println(" Bad reason is 'quantity_problem' - calling handleBadItemOnly")
// quantity_problem or default: handle as normal bad item
handleBadItemOnly(request, badItemQtyForProcessing)
}
@@ -256,29 +308,34 @@ open class PickExecutionIssueService(
// 情况3: 既有 miss item 又有 bad item
missQtyForProcessing > BigDecimal.ZERO && badItemQtyForProcessing > BigDecimal.ZERO -> {
println("→ Handling: Both Miss and Bad Item")
// NEW: Check bad reason
if (request.badReason == "package_problem") {
println(" Bad reason is 'package_problem' - calling handleBothMissAndBadItemPackageProblem")
handleBothMissAndBadItemPackageProblem(request, missQtyForProcessing, badItemQtyForProcessing)
} else {
println(" Bad reason is 'quantity_problem' - calling handleBothMissAndBadItem")
handleBothMissAndBadItem(request, missQtyForProcessing, badItemQtyForProcessing)
}
}
// 情况4: 有 miss item 的情况(无论 actualPickQty 是多少)
missQtyForProcessing > BigDecimal.ZERO -> {
println("→ Handling: Miss Item With Partial Pick")
handleMissItemWithPartialPick(request, actualPickQtyForProcessing, missQtyForProcessing)
}
// 情况5: 正常拣货 (actualPickQty > 0, 没有 miss 或 bad item)
actualPickQtyForProcessing > BigDecimal.ZERO -> {
println("→ Handling: Normal Pick")
handleNormalPick(request, actualPickQtyForProcessing)
}
else -> {
println("Unknown case: actualPickQty=${actualPickQtyForProcessing}, missQty=${missQtyForProcessing}, badItemQty=${badItemQtyForProcessing}")
println("⚠️ Unknown case: actualPickQty=$actualPickQtyForProcessing, missQty=$missQtyForProcessing, badItemQty=$badItemQtyForProcessing")
}
}
val pickOrderForCompletion = pickOrderRepository.findById(request.pickOrderId).orElse(null)
val consoCode = pickOrderForCompletion?.consoCode
@@ -300,7 +357,9 @@ open class PickExecutionIssueService(
println("⚠️ Error checking pick order completion by pickOrderId: ${e.message}")
}
}
println("=== recordPickExecutionIssue: SUCCESS ===")
println("Issue ID: ${savedIssue.id}, Issue No: ${savedIssue.issueNo}")
return MessageResponse(
id = savedIssue.id,
name = "Pick execution issue recorded successfully",
@@ -309,9 +368,10 @@ open class PickExecutionIssueService(
message = "Pick execution issue recorded successfully",
errorPosition = null
)
} catch (e: Exception) {
println("=== ERROR IN recordPickExecutionIssue ===")
println("=== recordPickExecutionIssue: ERROR ===")
println("Error: ${e.message}")
e.printStackTrace()
return MessageResponse(
id = null,
@@ -2585,10 +2645,11 @@ open fun getLotIssueDetails(lotId: Long, itemId: Long, issueType: String): LotIs
)
}

// New: Submit with custom quantity
@Transactional(rollbackFor = [Exception::class])
open fun submitIssueWithQty(request: SubmitIssueWithQtyRequest): MessageResponse {
try {
println("=== submitIssueWithQty: START ===")
println("Request: lotId=${request.lotId}, itemId=${request.itemId}, issueType=${request.issueType}, submitQty=${request.submitQty}, handler=${request.handler}")
// Find all issues for this lot and item
val issues = if (request.issueType == "miss") {
pickExecutionIssueRepository.findMissItemList(IssueCategory.lot_issue)
@@ -2606,7 +2667,13 @@ open fun submitIssueWithQty(request: SubmitIssueWithQtyRequest): MessageResponse
}
}
println("Found ${issues.size} issues to process")
issues.forEachIndexed { index, issue ->
println(" Issue[$index]: id=${issue.id}, issueQty=${issue.issueQty}, handleStatus=${issue.handleStatus}")
}
if (issues.isEmpty()) {
println("❌ No issues found for this lot")
return MessageResponse(
id = null,
name = "Error",
@@ -2623,13 +2690,36 @@ open fun submitIssueWithQty(request: SubmitIssueWithQtyRequest): MessageResponse
// Use custom quantity instead of sum
val submitQty = request.submitQty
if (submitQty <= BigDecimal.ZERO) {
// ✅ 修改:允许提交0,0表示"标记为已处理但无需出库"
if (submitQty < BigDecimal.ZERO) {
println("❌ Submit quantity cannot be negative: $submitQty")
return MessageResponse(
id = null,
name = "Error",
code = "INVALID",
type = "stock_issue",
message = "Submit quantity must be greater than 0",
message = "Submit quantity cannot be negative",
errorPosition = null
)
}
// ✅ 新增:如果提交数量为0,只标记issue为已处理,不创建stock_out_line
if (submitQty == BigDecimal.ZERO) {
println("ℹ️ Submit quantity is 0 - marking issues as handled without creating stock out")
// Mark all issues as handled
issues.forEach { issue ->
println(" Marking issue ${issue.id} as handled by handler $handler")
markIssueHandled(issue, handler)
}
println("✅ All issues marked as handled (no stock out created)")
return MessageResponse(
id = null,
name = "Success",
code = "SUCCESS",
type = "stock_issue",
message = "Issues marked as handled (no stock out - quantity is 0)",
errorPosition = null
)
}
@@ -2649,14 +2739,17 @@ open fun submitIssueWithQty(request: SubmitIssueWithQtyRequest): MessageResponse
firstIssue.issueRemark,
handler
)
println("Created stock out header: id=${stockOut.id}, type=${if (isMissItem) "MISS_ITEM" else "BAD_ITEM"}")
val pickOrderLine = firstIssue.pickOrderLineId?.let {
pickOrderLineRepository.findById(it).orElse(null)
}
println("Pick order line: id=${pickOrderLine?.id}")
val lotLine = request.lotId.let {
inventoryLotLineRepository.findById(it).orElse(null)
}
println("Lot line: id=${lotLine?.id}, lotNo=${lotLine?.inventoryLot?.lotNo}")
val item = itemsRepository.findById(request.itemId).orElse(null)
?: return MessageResponse(
@@ -2667,6 +2760,7 @@ open fun submitIssueWithQty(request: SubmitIssueWithQtyRequest): MessageResponse
message = "Item not found",
errorPosition = null
)
println("Item: id=${item.id}, code=${item.code}")
// Create stock_out_line with custom quantity
val stockOutLine = StockOutLine().apply {
@@ -2679,21 +2773,27 @@ open fun submitIssueWithQty(request: SubmitIssueWithQtyRequest): MessageResponse
this.type = if (isMissItem) "Miss" else "Bad"
}
val savedStockOutLine = stockOutLineRepository.saveAndFlush(stockOutLine)
println("Created stock out line: id=${savedStockOutLine.id}, qty=${savedStockOutLine.qty}, status=${savedStockOutLine.status}")
if (!isMissItem && request.lotId != null) {
val lotLineForReset = inventoryLotLineRepository.findById(request.lotId).orElse(null)
if (lotLineForReset != null) {
val oldIssueQty = lotLineForReset.issueQty
lotLineForReset.issueQty = BigDecimal.ZERO
inventoryLotLineRepository.saveAndFlush(lotLineForReset)
println("✅ Reset issueQty to 0 for lot ${request.lotId} before bad item submission (submitIssueWithQty)")
println("✅ Reset issueQty for lot ${request.lotId}: $oldIssueQty -> 0")
}
}
// Update inventory_lot_line with custom quantity - pass isMissItem flag
if (request.lotId != null) {
println("Updating lot line after issue: lotId=${request.lotId}, submitQty=$submitQty, isMissItem=$isMissItem")
updateLotLineAfterIssue(request.lotId, submitQty, isMissItem)
}
// Mark all issues as handled
issues.forEach { issue ->
println(" Marking issue ${issue.id} as handled by handler $handler")
markIssueHandled(issue, handler)
}
@@ -2708,6 +2808,7 @@ open fun submitIssueWithQty(request: SubmitIssueWithQtyRequest): MessageResponse
createStockLedgerForStockOutWithBalance(savedStockOutLine, balance, if (isMissItem) "Miss" else "Bad")
println("=== submitIssueWithQty: SUCCESS ===")
return MessageResponse(
id = stockOut.id,
name = "Success",
@@ -2717,6 +2818,9 @@ open fun submitIssueWithQty(request: SubmitIssueWithQtyRequest): MessageResponse
errorPosition = null
)
} catch (e: Exception) {
println("=== submitIssueWithQty: ERROR ===")
println("Error: ${e.message}")
e.printStackTrace()
return MessageResponse(
id = null,
name = "Error",


+ 20
- 1
src/main/java/com/ffii/fpsms/modules/productProcess/entity/ProductProcessLineRepository.kt 查看文件

@@ -3,12 +3,31 @@ package com.ffii.fpsms.modules.productProcess.entity
import org.springframework.data.jpa.repository.JpaRepository
import org.springframework.stereotype.Repository
import org.springframework.data.jpa.repository.Query
import java.time.LocalDateTime

@Repository
interface ProductProcessLineRepository : JpaRepository<ProductProcessLine, Long> {
fun findByProductProcess_Id(productProcessId: Long): List<ProductProcessLine>
fun findByProductProcess_IdAndHandler_Id(productProcessId: Long, handlerId: Long): List<ProductProcessLine>
fun findByHandler_IdAndStartTimeIsNotNullAndEndTimeIsNull(handlerId: Long): List<ProductProcessLine>
fun findByProductProcess_IdIn(ids: List<Long>): List<ProductProcessLine>

@Query("SELECT l FROM ProductProcessLine l LEFT JOIN FETCH l.equipment WHERE l.productProcess.id = :productProcessId")
fun findByProductProcess_IdWithEquipment(productProcessId: Long): List<ProductProcessLine>
fun findByProductProcess_IdWithEquipment(productProcessId: Long): List<ProductProcessLine>

// 用於 Operator KPI:抓取在指定時間範圍內有重疊的所有行
@Query(
"""
SELECT l FROM ProductProcessLine l
WHERE l.deleted = false
AND l.startTime IS NOT NULL
AND (
(l.startTime <= :endOfDay AND (l.endTime IS NULL OR l.endTime >= :startOfDay))
)
"""
)
fun findAllOverlappingWithDateRange(startOfDay: LocalDateTime, endOfDay: LocalDateTime): List<ProductProcessLine>

// 用於 Equipment 狀態:查詢指定 equipmentDetailId 目前仍未完成的所有行
fun findByEquipmentDetailIdAndDeletedFalseAndEndTimeIsNull(equipmentDetailId: Long): List<ProductProcessLine>
}

+ 2
- 2
src/main/java/com/ffii/fpsms/modules/productProcess/entity/projections/ProductProcessInfo.kt 查看文件

@@ -97,12 +97,12 @@ data class jobOrderLineInfo(
val type: String?,

val reqQty: Double?,
val baseReqQty: Int?,
val baseReqQty: Long?,


val stockQty: Int?,
val stockReqQty: Double?,
val baseStockQty: Int?,
val baseStockQty: Long?,

val reqUom: String?,
val reqBaseUom: String?,


+ 15
- 0
src/main/java/com/ffii/fpsms/modules/productProcess/web/ProductProcessController.kt 查看文件

@@ -239,4 +239,19 @@ class ProductProcessController(
fun deleteProductProcessLine(@PathVariable lineId: Long): MessageResponse {
return productProcessService.deleteProductProcessLine(lineId)
}

// ===== Dashboards =====

@GetMapping("/Demo/OperatorKpi")
fun getOperatorKpi(@RequestParam(required = false) date: String?): List<OperatorKpiResponse> {
val parsedDate = date?.takeIf { it.isNotBlank() }?.let {
LocalDate.parse(it, DateTimeFormatter.ISO_DATE)
}
return productProcessService.getOperatorKpi(parsedDate)
}

@GetMapping("/Demo/EquipmentStatus")
fun getEquipmentStatus(): List<EquipmentStatusByTypeResponse> {
return productProcessService.getEquipmentStatusByType()
}
}

+ 60
- 0
src/main/java/com/ffii/fpsms/modules/productProcess/web/model/SaveProductProcessRequest.kt 查看文件

@@ -239,4 +239,64 @@ data class JobProcessStatusResponse(
val planEndTime: LocalDateTime?,
val status: String,
val processes: List<ProcessStatusInfo>
)

// ===== Operator KPI Dashboard =====

data class OperatorKpiProcessInfo(
val jobOrderId: Long?,
val jobOrderCode: String?,
val productProcessId: Long?,
val productProcessLineId: Long?,
val processName: String?,
val equipmentName: String?,
val equipmentDetailName: String?,
val startTime: LocalDateTime?,
val endTime: LocalDateTime?,
val processingTime: Int?,
val itemCode: String?,
val itemName: String?,
)

data class OperatorKpiResponse(
val operatorId: Long,
val operatorName: String?,
val staffNo: String?,
val totalProcessingMinutes: Long,
val totalJobOrderCount: Int,
val currentProcesses: List<OperatorKpiProcessInfo>,
)

// ===== Equipment Status Dashboard =====

data class EquipmentStatusProcessInfo(
val jobOrderId: Long?,
val jobOrderCode: String?,
val productProcessId: Long?,
val productProcessLineId: Long?,
val processName: String?,
val operatorName: String?,
val startTime: LocalDateTime?,
val processingTime: Int?,
)

data class EquipmentStatusPerDetail(
val equipmentDetailId: Long,
val equipmentDetailCode: String?,
val equipmentDetailName: String?,
val equipmentId: Long?,
val equipmentTypeName: String?,
val status: String,
val repairAndMaintenanceStatus: Boolean?,
val latestRepairAndMaintenanceDate: LocalDateTime?,
val lastRepairAndMaintenanceDate: LocalDateTime?,
val repairAndMaintenanceRemarks: String?,
val currentProcess: EquipmentStatusProcessInfo?,
)

data class EquipmentStatusByTypeResponse(
val equipmentTypeId: Long,
val equipmentTypeName: String?,
val details: List<EquipmentStatusPerDetail>,
)

+ 3
- 1
src/main/resources/db/changelog/changes/20260204_Enson/01_add_bom_materail.sql 查看文件

@@ -1,7 +1,9 @@
-- liquibase formatted sql
-- changeset KelvinY:add_baseScore_to_bom
-- preconditions onFail:MARK_RAN
-- precondition-sql-check expectedResult:0 SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = 'fpsmsdb' AND TABLE_NAME = 'bom_material' AND COLUMN_NAME = 'stockQty';

ALTER TABLE `fpsmsdb`.`bom_material`
ADD COLUMN `stockQty` DECIMAL(14, 2) NULL DEFAULT NULL AFTER `salesUnitCode`,
ADD COLUMN `stockUnit` INTEGER NULL DEFAULT NULL AFTER `stockQty`,
ADD COLUMN `stockUnitName` VARCHAR(255) NULL DEFAULT NULL AFTER `stockUnit`;
ADD COLUMN `stockUnitName` VARCHAR(255) NULL DEFAULT NULL AFTER `stockUnit`;

Loading…
取消
儲存