Przeglądaj źródła

Merge remote-tracking branch 'origin/master'

master
kelvin.yau 1 dzień temu
rodzic
commit
acc5f69788
20 zmienionych plików z 1653 dodań i 153 usunięć
  1. +19
    -3
      src/main/java/com/ffii/fpsms/modules/bag/service/bagService.kt
  2. +1
    -1
      src/main/java/com/ffii/fpsms/modules/jobOrder/service/JobOrderBomMaterialService.kt
  3. +81
    -8
      src/main/java/com/ffii/fpsms/modules/jobOrder/service/JobOrderService.kt
  4. +52
    -0
      src/main/java/com/ffii/fpsms/modules/jobOrder/service/PlasticBagPrinterService.kt
  5. +11
    -0
      src/main/java/com/ffii/fpsms/modules/jobOrder/web/PlasticBagPrinterController.kt
  6. +6
    -1
      src/main/java/com/ffii/fpsms/modules/master/entity/BomMaterial.kt
  7. +210
    -39
      src/main/java/com/ffii/fpsms/modules/master/service/BomService.kt
  8. +6
    -5
      src/main/java/com/ffii/fpsms/modules/master/service/ProductionScheduleService.kt
  9. +145
    -41
      src/main/java/com/ffii/fpsms/modules/pickOrder/service/PickExecutionIssueService.kt
  10. +20
    -0
      src/main/java/com/ffii/fpsms/modules/productProcess/entity/ProductProcessLineRepository.kt
  11. +2
    -2
      src/main/java/com/ffii/fpsms/modules/productProcess/entity/projections/ProductProcessInfo.kt
  12. +36
    -48
      src/main/java/com/ffii/fpsms/modules/productProcess/service/ProductProcessService.kt
  13. +23
    -2
      src/main/java/com/ffii/fpsms/modules/productProcess/web/ProductProcessController.kt
  14. +64
    -2
      src/main/java/com/ffii/fpsms/modules/productProcess/web/model/SaveProductProcessRequest.kt
  15. +232
    -0
      src/main/java/com/ffii/fpsms/modules/report/service/ReportService.kt
  16. +67
    -0
      src/main/java/com/ffii/fpsms/modules/report/web/ReportController.kt
  17. +3
    -0
      src/main/java/com/ffii/fpsms/modules/stock/entity/projection/StockInLineInfo.kt
  18. +2
    -1
      src/main/java/com/ffii/fpsms/modules/stock/service/SuggestedPickLotService.kt
  19. +9
    -0
      src/main/resources/db/changelog/changes/20260204_Enson/01_add_bom_materail.sql
  20. +664
    -0
      src/main/resources/jasper/SemiFGProductionAnalysisReport.jrxml

+ 19
- 3
src/main/java/com/ffii/fpsms/modules/bag/service/bagService.kt Wyświetl plik

@@ -15,6 +15,8 @@ import java.time.LocalDateTime
import java.time.LocalDate import java.time.LocalDate
import com.ffii.fpsms.modules.jobOrder.entity.JobOrderRepository import com.ffii.fpsms.modules.jobOrder.entity.JobOrderRepository
import com.ffii.fpsms.modules.productProcess.entity.ProductProcessRepository import com.ffii.fpsms.modules.productProcess.entity.ProductProcessRepository
import com.ffii.fpsms.modules.master.entity.ItemUomRespository
import java.math.BigDecimal
@Service @Service
open class BagService( open class BagService(
private val bagRepository: BagRepository, private val bagRepository: BagRepository,
@@ -22,20 +24,34 @@ open class BagService(
private val joBagConsumptionRepository: JoBagConsumptionRepository, private val joBagConsumptionRepository: JoBagConsumptionRepository,
private val inventoryLotRepository: InventoryLotRepository, private val inventoryLotRepository: InventoryLotRepository,
private val jobOrderRepository: JobOrderRepository, private val jobOrderRepository: JobOrderRepository,
private val productProcessRepository: ProductProcessRepository
private val productProcessRepository: ProductProcessRepository,
private val itemUomRepository: ItemUomRespository,
) { ) {
open fun createBagLotLinesByBagId(request: CreateBagLotLineRequest): MessageResponse { open fun createBagLotLinesByBagId(request: CreateBagLotLineRequest): MessageResponse {
val bag = bagRepository.findById(request.bagId).orElse(null) val bag = bagRepository.findById(request.bagId).orElse(null)
val lot = inventoryLotRepository.findByLotNoAndItemId(request.lotNo, request.itemId) val lot = inventoryLotRepository.findByLotNoAndItemId(request.lotNo, request.itemId)
val BaseUnitOfMeasure= itemUomRepository.findByItemIdAndStockUnitIsTrueAndDeletedIsFalse(request.itemId)
val baseRatioN = BaseUnitOfMeasure?.ratioN ?: BigDecimal.ONE
println("baseRatioN: $baseRatioN")
val baseRatioD = BaseUnitOfMeasure?.ratioD ?: BigDecimal.ONE
println("baseRatioD: $baseRatioD")
val bagLotLine = BagLotLine().apply { val bagLotLine = BagLotLine().apply {
this.bagId = bag?.id this.bagId = bag?.id
this.lotId = lot?.id this.lotId = lot?.id
this.lotNo = lot?.lotNo this.lotNo = lot?.lotNo
this.startQty = request.stockQty
this.startQty = request.stockQty.toBigDecimal()
.multiply(baseRatioN)
.divide(baseRatioD)
.toInt()
println("startQty: $startQty")
this.consumedQty = 0 this.consumedQty = 0
this.stockOutLineId = request.stockOutLineId this.stockOutLineId = request.stockOutLineId
this.scrapQty = 0 this.scrapQty = 0
this.balanceQty = request.stockQty
this.balanceQty = request.stockQty.toBigDecimal()
.multiply(baseRatioN)
.divide(baseRatioD)
.toInt()
println("balanceQty: $balanceQty")
} }
bagLotLineRepository.save(bagLotLine) bagLotLineRepository.save(bagLotLine)
bag.takenBagBalance = (bag.takenBagBalance ?: 0) + (bagLotLine.balanceQty ?: 0) bag.takenBagBalance = (bag.takenBagBalance ?: 0) + (bagLotLine.balanceQty ?: 0)


+ 1
- 1
src/main/java/com/ffii/fpsms/modules/jobOrder/service/JobOrderBomMaterialService.kt Wyświetl plik

@@ -34,7 +34,7 @@ open class JobOrderBomMaterialService(
joId = joId, joId = joId,
itemId = bm.item?.id, itemId = bm.item?.id,
//reqQty = (bm.qty?.times(proportion) ?: zero).setScale(0,RoundingMode.CEILING), //reqQty = (bm.qty?.times(proportion) ?: zero).setScale(0,RoundingMode.CEILING),
reqQty = (bm.saleQty?.times(proportion) ?: zero).setScale(0, RoundingMode.CEILING),
reqQty = (bm.stockQty?.times(proportion) ?: zero).setScale(0, RoundingMode.CEILING),
uomId = stockUnit?.uom?.id uomId = stockUnit?.uom?.id
) )
} ?: listOf() } ?: listOf()


+ 81
- 8
src/main/java/com/ffii/fpsms/modules/jobOrder/service/JobOrderService.kt Wyświetl plik

@@ -68,6 +68,7 @@ import java.time.LocalDate
import java.time.LocalDateTime import java.time.LocalDateTime
import com.ffii.fpsms.modules.master.entity.BomMaterialRepository import com.ffii.fpsms.modules.master.entity.BomMaterialRepository
import com.ffii.fpsms.modules.master.service.ItemUomService import com.ffii.fpsms.modules.master.service.ItemUomService
import com.ffii.fpsms.modules.master.web.models.ConvertUomByItemRequest
@Service @Service
open class JobOrderService( open class JobOrderService(
val jobOrderRepository: JobOrderRepository, val jobOrderRepository: JobOrderRepository,
@@ -208,7 +209,6 @@ open class JobOrderService(
return RecordsRes<JobOrderInfoWithTypeName>(records, total.toInt()); return RecordsRes<JobOrderInfoWithTypeName>(records, total.toInt());
} }
// 添加辅助方法计算库存统计
private fun calculateStockCounts( private fun calculateStockCounts(
jobOrder: JobOrder, jobOrder: JobOrder,
inventoriesMap: Map<Long?, com.ffii.fpsms.modules.stock.entity.projection.InventoryInfo> inventoriesMap: Map<Long?, com.ffii.fpsms.modules.stock.entity.projection.InventoryInfo>
@@ -216,7 +216,7 @@ open class JobOrderService(
// 过滤掉 consumables 和 CMB 类型的物料 // 过滤掉 consumables 和 CMB 类型的物料
val nonConsumablesJobms = jobOrder.jobms.filter { jobm -> val nonConsumablesJobms = jobOrder.jobms.filter { jobm ->
val itemType = jobm.item?.type?.lowercase() val itemType = jobm.item?.type?.lowercase()
itemType != "consumables" && itemType != "cmb"&& itemType != "nm"
itemType != "consumables" && itemType != "consumable" && itemType != "cmb" && itemType != "nm"
} }
if (nonConsumablesJobms.isEmpty()) { if (nonConsumablesJobms.isEmpty()) {
@@ -226,14 +226,16 @@ open class JobOrderService(
var sufficientCount = 0 var sufficientCount = 0
var insufficientCount = 0 var insufficientCount = 0
println("=== JobOrderService.calculateStockCounts for JobOrder: ${jobOrder.code} ===")
nonConsumablesJobms.forEach { jobm -> nonConsumablesJobms.forEach { jobm ->
val itemId = jobm.item?.id 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) { if (itemId != null) {
val inventory = inventoriesMap[itemId] val inventory = inventoriesMap[itemId]
val availableQty = if (inventory != null) { val availableQty = if (inventory != null) {
// 使用 availableQty,如果没有则计算:onHandQty - onHoldQty - unavailableQty
inventory.availableQty ?: ( inventory.availableQty ?: (
(inventory.onHandQty ?: BigDecimal.ZERO) - (inventory.onHandQty ?: BigDecimal.ZERO) -
(inventory.onHoldQty ?: BigDecimal.ZERO) - (inventory.onHoldQty ?: BigDecimal.ZERO) -
@@ -243,17 +245,74 @@ open class JobOrderService(
BigDecimal.ZERO 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++ sufficientCount++
println("✅ SUFFICIENT - Item: $itemCode ($itemName) - reqQty: $reqQty ($reqUomName) = $baseReqQty ($baseUomName), availableQty: $availableQty ($stockUomName) = $baseAvailableQty ($baseUomName)")
} else { } else {
insufficientCount++ insufficientCount++
println("❌ INSUFFICIENT - Item: $itemCode ($itemName) - reqQty: $reqQty ($reqUomName) = $baseReqQty ($baseUomName), availableQty: $availableQty ($stockUomName) = $baseAvailableQty ($baseUomName)")
} }
} else { } else {
// 如果没有 itemId,视为不足 // 如果没有 itemId,视为不足
insufficientCount++ insufficientCount++
println("❌ INSUFFICIENT - Item: $itemCode ($itemName) - No itemId")
} }
} }
println("=== Result: sufficient=$sufficientCount, insufficient=$insufficientCount ===")
return Pair(sufficientCount, insufficientCount) return Pair(sufficientCount, insufficientCount)
} }
open fun jobOrderDetail(id: Long): JobOrderDetail { open fun jobOrderDetail(id: Long): JobOrderDetail {
@@ -454,16 +513,30 @@ open class JobOrderService(
} }
// ✅ 使用 stockReqQty (bomMaterial.saleQty) 和 stockUom (bomMaterial.salesUnit) // ✅ 使用 stockReqQty (bomMaterial.saleQty) 和 stockUom (bomMaterial.salesUnit)
val stockReqQty = jobm.reqQty ?: bomMaterial?.saleQty ?: BigDecimal.ZERO
val stockUomId = bomMaterial?.salesUnit?.id
val stockReqQty = jobm.reqQty ?: bomMaterial?.stockQty ?: BigDecimal.ZERO
val stockUomId = bomMaterial?.stockUnit?.toLong()
?: itemUomService.findStockUnitByItemId(itemId)?.uom?.id // Fallback: 从 Item 获取库存单位 ?: itemUomService.findStockUnitByItemId(itemId)?.uom?.id // Fallback: 从 Item 获取库存单位
?: jobm.uom?.id // 最后的 fallback
?: jobm.uom?.id // 最后的 fallback
SavePickOrderLineRequest( SavePickOrderLineRequest(
itemId = itemId, itemId = itemId,
qty = stockReqQty, // ✅ 使用库存单位数量 qty = stockReqQty, // ✅ 使用库存单位数量
uomId = stockUomId, // ✅ 使用库存单位 uomId = stockUomId, // ✅ 使用库存单位
) )
}
if (pols.isEmpty()) {
jo.status = JobOrderStatus.PROCESSING
jobOrderRepository.save(jo)
return MessageResponse(
id = jo.id,
code = jo.code,
name = jo.bom?.name,
type = null,
message = null,
errorPosition = null,
entity = mapOf("status" to jo.status?.value)
)
} }
val po = SavePickOrderRequest( val po = SavePickOrderRequest(
joId = jo.id, joId = jo.id,


+ 52
- 0
src/main/java/com/ffii/fpsms/modules/jobOrder/service/PlasticBagPrinterService.kt Wyświetl plik

@@ -419,4 +419,56 @@ open class PlasticBagPrinterService(
println("Han's Laser TCP 連接已關閉") println("Han's Laser TCP 連接已關閉")
} }
} }

fun sendDataFlex6330Zpl(request: PrintRequest) {
Socket().use { socket ->
try {
// Connect with timeout
socket.connect(InetSocketAddress(request.printerIp, request.printerPort), 5000)
socket.soTimeout = 5000 // read timeout if expecting response (optional)

val out = socket.getOutputStream()

// Build ZPL dynamically
val zpl = buildString {
append("^XA\n")
append("^PW500\n") // Print width ~42mm @300dpi; adjust 400-630 based on head
append("^LL280\n") // Label height ~23mm; tune for your pouch
append("^PON\n") // Normal orientation
append("^CI28\n") // UTF-8 / extended char set for Chinese

// Chinese product name / description (top)
append("^FO20,20^A@N,36,36,E:SIMSUN.FNT^FD${request.itemName}^FS\n") // Assumes font loaded

// Item code
append("^FO20,80^A0N,32,32^FD${request.itemCode}^FS\n")

// Expiry date
append("^FO20,120^A0N,28,28^FDEXP: ${request.expiryDate}^FS\n")

// Lot / Batch No.
append("^FO20,160^A0N,28,28^FDLOT: ${request.lotNo}^FS\n")

// QR code encoding lotNo (or combine: item|lot|exp)
// Position right side, mag 6 (~good size), model 2
val qrData = request.lotNo // or "${request.itemCode}|${request.lotNo}|${request.expiryDate}"
append("^FO320,20^BQN,2,6^FDQA,$qrData^FS\n") // QA, prefix for alphanumeric mode

append("^XZ\n")
}

// Send as bytes (UTF-8 safe)
out.write(zpl.toByteArray(Charsets.UTF_8))
out.flush()

println("DataFlex 6330 ZPL: Print job sent to ${request.printerIp}:${request.printerPort}")
// Optional: read response if printer echoes anything (rare in raw ZPL)
// val response = socket.getInputStream().readBytes().decodeToString()
// println("Response: $response")

} catch (e: Exception) {
throw RuntimeException("DataFlex 6330 ZPL communication failed: ${e.message}", e)
}
}
}
} }

+ 11
- 0
src/main/java/com/ffii/fpsms/modules/jobOrder/web/PlasticBagPrinterController.kt Wyświetl plik

@@ -46,6 +46,7 @@ class PlasticBagPrinterController(
response.outputStream.flush() response.outputStream.flush()
} }


/*
@PostMapping("/print-dataflex") @PostMapping("/print-dataflex")
fun printDataFlex(@RequestBody request: PrintRequest): ResponseEntity<String> { fun printDataFlex(@RequestBody request: PrintRequest): ResponseEntity<String> {
return try { return try {
@@ -54,6 +55,16 @@ class PlasticBagPrinterController(
} catch (e: Exception) { } catch (e: Exception) {
ResponseEntity.status(500).body("Error: ${e.message}") ResponseEntity.status(500).body("Error: ${e.message}")
} }
}*/

@PostMapping("/print-dataflex")
fun printDataFlex6330Zpl(@RequestBody request: PrintRequest): ResponseEntity<String> {
return try {
plasticBagPrinterService.sendDataFlex6330Zpl(request)
ResponseEntity.ok("DataFlex 6330 ZPL print command sent successfully to ${request.printerIp}:${request.printerPort}")
} catch (e: Exception) {
ResponseEntity.status(500).body("Error sending ZPL to DataFlex 6330: ${e.message}")
}
} }


@PostMapping("/print-laser") @PostMapping("/print-laser")


+ 6
- 1
src/main/java/com/ffii/fpsms/modules/master/entity/BomMaterial.kt Wyświetl plik

@@ -52,7 +52,12 @@ open class BomMaterial : BaseEntity<Long>() {
open var baseUnit: Integer? = null open var baseUnit: Integer? = null
@Column(name = "baseUnitName", length = 100) @Column(name = "baseUnitName", length = 100)
open var baseUnitName: String? = null open var baseUnitName: String? = null
@Column(name = "stockQty", precision = 14, scale = 2)
open var stockQty: BigDecimal? = null
@Column(name = "stockUnit", nullable = false)
open var stockUnit: Integer? = null
@Column(name = "stockUnitName", length = 100)
open var stockUnitName: String? = null
@NotNull @NotNull
@ManyToOne(optional = false) @ManyToOne(optional = false)
@JoinColumn(name = "bomId", nullable = false) @JoinColumn(name = "bomId", nullable = false)


+ 210
- 39
src/main/java/com/ffii/fpsms/modules/master/service/BomService.kt Wyświetl plik

@@ -150,23 +150,25 @@ open class BomService(
var baseUnit: Integer? = null var baseUnit: Integer? = null
var baseUnitName: String? = null var baseUnitName: String? = null


var stockQty: BigDecimal? = null
var stockUnit: Integer? = null
var stockUnitName: String? = null

if (item?.id != null) { if (item?.id != null) {
val itemId = item.id!! val itemId = item.id!!
try { try {
// ---- 1) 获取 item 的真实 stock unit ----
val stockItemUom = itemUomService.findStockUnitByItemId(itemId)
val itemStockUnit = stockItemUom?.uom
// ---- 1) 获取 item 的真实 sale unit ----
val saleItemUom = itemUomService.findSalesUnitByItemId(itemId)
val itemSaleUnit = saleItemUom?.uom
// saleUnitId: 使用 item 的 stock unit uom id
saleUnitId = itemStockUnit?.id

// saleUnitCode: 从 Excel 数据查找 uom_conversion,如果失败则使用 Excel 数据本身
// ---- 2) 确定 sale unit(来自 Excel column 7)----
if (excelSalesUnit != null) { if (excelSalesUnit != null) {
// Excel 找到了 uom_conversion // Excel 找到了 uom_conversion
saleUnitId = excelSalesUnit.id
saleUnitCode = excelSalesUnit.udfudesc saleUnitCode = excelSalesUnit.udfudesc
// 检查 Excel sales unit 与 item stock unit 是否匹配
if (itemStockUnit != null && excelSalesUnit.id != itemStockUnit.id) {
// 检查 Excel sales unit 与 item sale unit 是否匹配
if (itemSaleUnit != null && excelSalesUnit.id != itemSaleUnit.id) {
bomMaterialImportIssues.add( bomMaterialImportIssues.add(
BomMaterialImportIssue( BomMaterialImportIssue(
bomId = bomId, bomId = bomId,
@@ -174,7 +176,7 @@ open class BomService(
itemId = item.id, itemId = item.id,
itemCode = item.code, itemCode = item.code,
itemName = item.name, itemName = item.name,
reason = "Excel sales unit (${excelSalesUnit.code}/${excelSalesUnit.udfudesc}) does not match item stock unit (${itemStockUnit.code}/${itemStockUnit.udfudesc})",
reason = "Excel sales unit (${excelSalesUnit.code}/${excelSalesUnit.udfudesc}) does not match item sale unit (${itemSaleUnit.code}/${itemSaleUnit.udfudesc})",
srcQty = excelSaleQty, srcQty = excelSaleQty,
srcUomCode = excelSalesUnit.code srcUomCode = excelSalesUnit.code
) )
@@ -182,7 +184,7 @@ open class BomService(
} }
} else { } else {
// Excel 没有找到 uom_conversion,使用 Excel 数据本身 // Excel 没有找到 uom_conversion,使用 Excel 数据本身
saleUnitCode = excelSalesUnitCode ?: itemStockUnit?.udfudesc
saleUnitCode = excelSalesUnitCode
if (excelSalesUnitCode != null) { if (excelSalesUnitCode != null) {
bomMaterialImportIssues.add( bomMaterialImportIssues.add(
@@ -200,26 +202,85 @@ open class BomService(
} }
} }


// ---- 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"
// ---- 3) 从 saleQty(sale unit)转换为 baseQty(base unit)----
if (excelSaleQty != null && excelSalesUnit != null && excelSalesUnit.id != null) {
try {
// 先检查 item 是否有 base unit
val baseItemUom = itemUomService.findBaseUnitByItemId(itemId)
if (baseItemUom == null) {
bomMaterialImportIssues.add(
BomMaterialImportIssue(
bomId = bomId,
bomCode = bomCode,
itemId = item.id,
itemCode = item.code,
itemName = item.name,
reason = "Cannot convert saleQty to baseQty: item base unit not found",
srcQty = excelSaleQty,
srcUomCode = excelSalesUnit.code
)
)
} else {
val baseResult = itemUomService.convertUomByItem(
ConvertUomByItemRequest(
itemId = itemId,
qty = excelSaleQty,
uomId = excelSalesUnit.id!!,
targetUnit = "baseUnit"
)
)
baseQty = baseResult.newQty
// 获取 base unit 信息
baseUnit = baseItemUom.uom?.id?.toInt()?.let { Integer.valueOf(it) } as? Integer
baseUnitName = baseItemUom.uom?.udfudesc
// 验证转换结果
if (baseQty == null) {
bomMaterialImportIssues.add(
BomMaterialImportIssue(
bomId = bomId,
bomCode = bomCode,
itemId = item.id,
itemCode = item.code,
itemName = item.name,
reason = "Cannot convert saleQty to baseQty: conversion returned null",
srcQty = excelSaleQty,
srcUomCode = excelSalesUnit.code
)
)
}
}
} catch (e: IllegalArgumentException) {
bomMaterialImportIssues.add(
BomMaterialImportIssue(
bomId = bomId,
bomCode = bomCode,
itemId = item.id,
itemCode = item.code,
itemName = item.name,
reason = "Cannot convert saleQty to baseQty: ${e.message ?: "IllegalArgumentException"}",
srcQty = excelSaleQty,
srcUomCode = excelSalesUnit.code
)
) )
)
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) {
println("【BOM Import Warning】bomCode=$bomCode, item=${item.code} 转 baseQty 失败: ${e.message}")
} catch (e: Exception) {
bomMaterialImportIssues.add(
BomMaterialImportIssue(
bomId = bomId,
bomCode = bomCode,
itemId = item.id,
itemCode = item.code,
itemName = item.name,
reason = "Cannot convert saleQty to baseQty: ${e.javaClass.simpleName} - ${e.message ?: "Unknown error"}",
srcQty = excelSaleQty,
srcUomCode = excelSalesUnit.code
)
)
println("【BOM Import Error】bomCode=$bomCode, item=${item.code} 转 baseQty 失败: ${e.message}")
}
} else if (excelSaleQty != null && excelSalesUnit == null) {
bomMaterialImportIssues.add( bomMaterialImportIssues.add(
BomMaterialImportIssue( BomMaterialImportIssue(
bomId = bomId, bomId = bomId,
@@ -227,12 +288,92 @@ open class BomService(
itemId = item.id, itemId = item.id,
itemCode = item.code, itemCode = item.code,
itemName = item.name, itemName = item.name,
reason = "Cannot convert saleQty to baseQty: conversion failed",
reason = "Cannot convert saleQty to baseQty: Excel sales unit not found",
srcQty = excelSaleQty, srcQty = excelSaleQty,
srcUomCode = itemStockUnit.code
srcUomCode = excelSalesUnitCode
) )
) )
} else if (excelSaleQty != null && itemStockUnit == null) {
}

// ---- 4) 从 saleQty(sale unit)转换为 stockQty(stock unit)----
if (excelSaleQty != null && excelSalesUnit != null && excelSalesUnit.id != null) {
try {
// 先检查 item 是否有 stock unit
val stockItemUom = itemUomService.findStockUnitByItemId(itemId)
if (stockItemUom == null) {
bomMaterialImportIssues.add(
BomMaterialImportIssue(
bomId = bomId,
bomCode = bomCode,
itemId = item.id,
itemCode = item.code,
itemName = item.name,
reason = "Cannot convert saleQty to stockQty: item stock unit not found",
srcQty = excelSaleQty,
srcUomCode = excelSalesUnit.code
)
)
} else {
val stockResult = itemUomService.convertUomByItem(
ConvertUomByItemRequest(
itemId = itemId,
qty = excelSaleQty,
uomId = excelSalesUnit.id!!,
targetUnit = "stockUnit"
)
)
stockQty = stockResult.newQty
// 获取 stock unit 信息
stockUnit = stockItemUom.uom?.id?.toInt()?.let { Integer.valueOf(it) } as? Integer
stockUnitName = stockItemUom.uom?.udfudesc
// 验证转换结果
if (stockQty == null) {
bomMaterialImportIssues.add(
BomMaterialImportIssue(
bomId = bomId,
bomCode = bomCode,
itemId = item.id,
itemCode = item.code,
itemName = item.name,
reason = "Cannot convert saleQty to stockQty: conversion returned null",
srcQty = excelSaleQty,
srcUomCode = excelSalesUnit.code
)
)
}
}
} catch (e: IllegalArgumentException) {
bomMaterialImportIssues.add(
BomMaterialImportIssue(
bomId = bomId,
bomCode = bomCode,
itemId = item.id,
itemCode = item.code,
itemName = item.name,
reason = "Cannot convert saleQty to stockQty: ${e.message ?: "IllegalArgumentException"}",
srcQty = excelSaleQty,
srcUomCode = excelSalesUnit.code
)
)
println("【BOM Import Warning】bomCode=$bomCode, item=${item.code} 转 stockQty 失败: ${e.message}")
} catch (e: Exception) {
bomMaterialImportIssues.add(
BomMaterialImportIssue(
bomId = bomId,
bomCode = bomCode,
itemId = item.id,
itemCode = item.code,
itemName = item.name,
reason = "Cannot convert saleQty to stockQty: ${e.javaClass.simpleName} - ${e.message ?: "Unknown error"}",
srcQty = excelSaleQty,
srcUomCode = excelSalesUnit.code
)
)
println("【BOM Import Error】bomCode=$bomCode, item=${item.code} 转 stockQty 失败: ${e.message}")
}
} else if (excelSaleQty != null && excelSalesUnit == null) {
bomMaterialImportIssues.add( bomMaterialImportIssues.add(
BomMaterialImportIssue( BomMaterialImportIssue(
bomId = bomId, bomId = bomId,
@@ -240,13 +381,37 @@ open class BomService(
itemId = item.id, itemId = item.id,
itemCode = item.code, itemCode = item.code,
itemName = item.name, itemName = item.name,
reason = "Cannot convert saleQty to baseQty: item stock unit not found",
reason = "Cannot convert saleQty to stockQty: Excel sales unit not found",
srcQty = excelSaleQty, srcQty = excelSaleQty,
srcUomCode = null
srcUomCode = excelSalesUnitCode
) )
) )
} }


// 最终检查:如果 stockQty 仍然为 null,记录问题
if (stockQty == null && excelSaleQty != null && excelSalesUnit != null && excelSalesUnit.id != null) {
// 检查是否已经记录过这个问题(避免重复)
val alreadyReported = bomMaterialImportIssues.any { issue ->
issue.itemId == item.id &&
issue.reason.contains("stockQty", ignoreCase = true) &&
issue.srcQty == excelSaleQty
}
if (!alreadyReported) {
bomMaterialImportIssues.add(
BomMaterialImportIssue(
bomId = bomId,
bomCode = bomCode,
itemId = item.id,
itemCode = item.code,
itemName = item.name,
reason = "Cannot convert saleQty to stockQty: conversion failed (final check)",
srcQty = excelSaleQty,
srcUomCode = excelSalesUnit.code
)
)
}
}

} catch (e: IllegalArgumentException) { } catch (e: IllegalArgumentException) {
bomMaterialImportIssues.add( bomMaterialImportIssues.add(
BomMaterialImportIssue( BomMaterialImportIssue(
@@ -301,15 +466,21 @@ open class BomService(
this.uom = req.uom // BOM 原始 UOM(column 3,保留) this.uom = req.uom // BOM 原始 UOM(column 3,保留)
this.uomName = req.uomName this.uomName = req.uomName


// 新逻辑:使用 Excel column 6 的 saleQty 和 item stock unit
// 新逻辑:使用 Excel column 6 的 saleQty 和 column 7 的 sales unit
this.saleQty = saleQty this.saleQty = saleQty
this.salesUnit = item?.id?.let { itemUomService.findStockUnitByItemId(it)?.uom }
this.salesUnit = excelSalesUnit // 使用 Excel 的 sales unit,不是 item 的 stock unit
this.salesUnitCode = saleUnitCode this.salesUnitCode = saleUnitCode


// 从 sale unit 转换为 base unit
this.baseQty = baseQty this.baseQty = baseQty
this.baseUnit = baseUnit this.baseUnit = baseUnit
this.baseUnitName = baseUnitName this.baseUnitName = baseUnitName


// 从 sale unit 转换为 stock unit(新增)
this.stockQty = stockQty
this.stockUnit = stockUnit
this.stockUnitName = stockUnitName

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


+ 6
- 5
src/main/java/com/ffii/fpsms/modules/master/service/ProductionScheduleService.kt Wyświetl plik

@@ -648,8 +648,8 @@ open class ProductionScheduleService(
i.pendingJobQty, i.pendingJobQty,
((i.stockQty * 1.0) + ifnull(i.pendingJobQty, 0) ) / i.avgQtyLastMonth as daysLeft, ((i.stockQty * 1.0) + ifnull(i.pendingJobQty, 0) ) / i.avgQtyLastMonth as daysLeft,
-- i.baseScore as priority,
25 + 25 + markDark + markFloat + markDense + markAS + markTimeSequence + markComplexity as priority,
ifnull(i.baseScore, 0) as priority,
-- 25 + 25 + markDark + markFloat + markDense + markAS + markTimeSequence + markComplexity as priority,
i.* i.*
FROM FROM
(SELECT (SELECT
@@ -665,7 +665,7 @@ open class ProductionScheduleService(
do.deleted = 0 and do.deleted = 0 and
dol.itemId = items.id dol.itemId = items.id
-- AND MONTH(do.estimatedArrivalDate) = MONTH(DATE_SUB(NOW(), INTERVAL 1 MONTH)) -- AND MONTH(do.estimatedArrivalDate) = MONTH(DATE_SUB(NOW(), INTERVAL 1 MONTH))
AND do.estimatedArrivalDate >= '2026-01-08' AND do.estimatedArrivalDate < '2026-01-18'
AND do.estimatedArrivalDate >= '2026-01-08' AND do.estimatedArrivalDate < '2026-01-31'
GROUP BY do.estimatedArrivalDate) AS d) AS avgQtyLastMonth, GROUP BY do.estimatedArrivalDate) AS d) AS avgQtyLastMonth,


(select sum(reqQty) from job_order where bomId = bom.id and status != 'completed') AS pendingJobQty, (select sum(reqQty) from job_order where bomId = bom.id and status != 'completed') AS pendingJobQty,
@@ -718,8 +718,9 @@ open class ProductionScheduleService(
LEFT JOIN items ON bom.itemId = items.id LEFT JOIN items ON bom.itemId = items.id
LEFT JOIN inventory ON items.id = inventory.itemId LEFT JOIN inventory ON items.id = inventory.itemId
left join item_fake_onhand on items.code = item_fake_onhand.itemCode left join item_fake_onhand on items.code = item_fake_onhand.itemCode
WHERE
bom.itemId != 16771) AS i
WHERE 1
and bom.itemId != 16771
) AS i
WHERE 1 WHERE 1
and i.avgQtyLastMonth is not null and i.avgQtyLastMonth is not null
and i.onHandQty is not null and i.onHandQty is not null


+ 145
- 41
src/main/java/com/ffii/fpsms/modules/pickOrder/service/PickExecutionIssueService.kt Wyświetl plik

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


@Transactional(rollbackFor = [Exception::class])
open fun recordPickExecutionIssue(request: PickExecutionIssueRequest): MessageResponse { open fun recordPickExecutionIssue(request: PickExecutionIssueRequest): MessageResponse {
try { 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 记录 // 1. 检查是否已经存在相同的 pick execution issue 记录
val existingIssues = pickExecutionIssueRepository.findByPickOrderLineIdAndLotIdAndDeletedFalse( val existingIssues = pickExecutionIssueRepository.findByPickOrderLineIdAndLotIdAndDeletedFalse(
@@ -84,7 +98,14 @@ open class PickExecutionIssueService(
request.lotId ?: 0L 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()) { if (existingIssues.isNotEmpty()) {
println("❌ Duplicate issue found - returning DUPLICATE error")
return MessageResponse( return MessageResponse(
id = null, id = null,
name = "Pick execution issue already exists", name = "Pick execution issue already exists",
@@ -96,18 +117,24 @@ open class PickExecutionIssueService(
} }
val pickOrder = pickOrderRepository.findById(request.pickOrderId).orElse(null) 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) // 2. 获取 inventory_lot_line 并计算账面数量 (bookQty)
val inventoryLotLine = request.lotId?.let { val inventoryLotLine = request.lotId?.let {
inventoryLotLineRepository.findById(it).orElse(null) inventoryLotLineRepository.findById(it).orElse(null)
} }
println("Inventory lot line: id=${inventoryLotLine?.id}, lotNo=${inventoryLotLine?.inventoryLot?.lotNo}")
// 计算账面数量(创建 issue 时的快照) // 计算账面数量(创建 issue 时的快照)
val bookQty = if (inventoryLotLine != null) { val bookQty = if (inventoryLotLine != null) {
val inQty = inventoryLotLine.inQty ?: BigDecimal.ZERO val inQty = inventoryLotLine.inQty ?: BigDecimal.ZERO
val outQty = inventoryLotLine.outQty ?: 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 { } else {
println(" No inventory lot line found, bookQty=0")
BigDecimal.ZERO BigDecimal.ZERO
} }
@@ -117,41 +144,53 @@ open class PickExecutionIssueService(
val missQty = request.missQty ?: BigDecimal.ZERO val missQty = request.missQty ?: BigDecimal.ZERO
val badItemQty = request.badItemQty ?: BigDecimal.ZERO val badItemQty = request.badItemQty ?: BigDecimal.ZERO
val badReason = request.badReason ?: "quantity_problem" 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(实际的问题数量) // 4. 计算 issueQty(实际的问题数量)
val issueQty = when { val issueQty = when {
// 情况1: 已拣完但有坏品 // 情况1: 已拣完但有坏品
actualPickQty == requiredQty && badItemQty > BigDecimal.ZERO -> { actualPickQty == requiredQty && badItemQty > BigDecimal.ZERO -> {
println(" Case 1: actualPickQty == requiredQty && badItemQty > 0")
println(" issueQty = badItemQty = $badItemQty")
badItemQty // issueQty = badItemQty badItemQty // issueQty = badItemQty
} }
badReason == "package_problem" && badItemQty > BigDecimal.ZERO -> { badReason == "package_problem" && badItemQty > BigDecimal.ZERO -> {
println(" Case 2: badReason == 'package_problem' && badItemQty > 0")
println(" issueQty = badItemQty = $badItemQty")
badItemQty badItemQty
} }
actualPickQty < requiredQty -> { actualPickQty < requiredQty -> {
println(" Case 3: actualPickQty < requiredQty")
val calculatedIssueQty = bookQty.subtract(actualPickQty) val calculatedIssueQty = bookQty.subtract(actualPickQty)
println(" issueQty = bookQty - actualPickQty = $bookQty - $actualPickQty = $calculatedIssueQty")
if (missQty > BigDecimal.ZERO && missQty > 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 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("================================================") println("================================================")
// 5. 创建 pick execution issue 记录 // 5. 创建 pick execution issue 记录
val issueNo = generateIssueNo()
println("Generated issue number: $issueNo")
val pickExecutionIssue = PickExecutionIssue( val pickExecutionIssue = PickExecutionIssue(
id = null, id = null,
pickOrderId = request.pickOrderId, pickOrderId = request.pickOrderId,
@@ -159,7 +198,7 @@ open class PickExecutionIssueService(
pickOrderCreateDate = request.pickOrderCreateDate, pickOrderCreateDate = request.pickOrderCreateDate,
pickExecutionDate = request.pickExecutionDate ?: LocalDate.now(), pickExecutionDate = request.pickExecutionDate ?: LocalDate.now(),
pickOrderLineId = request.pickOrderLineId, pickOrderLineId = request.pickOrderLineId,
issueNo = generateIssueNo(),
issueNo = issueNo,
joPickOrderId = pickOrder?.jobOrder?.id, joPickOrderId = pickOrder?.jobOrder?.id,
doPickOrderId = if (pickOrder?.type?.value == "do") pickOrder.id else null, doPickOrderId = if (pickOrder?.type?.value == "do") pickOrder.id else null,
issueCategory = IssueCategory.valueOf( issueCategory = IssueCategory.valueOf(
@@ -189,11 +228,18 @@ open class PickExecutionIssueService(
modifiedBy = "system", modifiedBy = "system",
deleted = false deleted = false
) )
println("Creating pick execution issue record...")
val savedIssue = pickExecutionIssueRepository.save(pickExecutionIssue) 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 // 6. NEW: Update inventory_lot_line.issueQty
if (request.lotId != null && inventoryLotLine != null) { if (request.lotId != null && inventoryLotLine != null) {
println("Updating inventory_lot_line.issueQty...")
// ✅ 修改:如果只有 missQty,不更新 issueQty // ✅ 修改:如果只有 missQty,不更新 issueQty
val actualPickQty = request.actualPickQty ?: BigDecimal.ZERO val actualPickQty = request.actualPickQty ?: BigDecimal.ZERO
val missQty = request.missQty ?: BigDecimal.ZERO val missQty = request.missQty ?: BigDecimal.ZERO
@@ -205,6 +251,9 @@ open class PickExecutionIssueService(
val hasMissItemWithPartialPick = missQty > BigDecimal.ZERO val hasMissItemWithPartialPick = missQty > BigDecimal.ZERO
&& actualPickQty > BigDecimal.ZERO && actualPickQty > BigDecimal.ZERO
println(" isMissItemOnly: $isMissItemOnly")
println(" hasMissItemWithPartialPick: $hasMissItemWithPartialPick")
if (!isMissItemOnly && !hasMissItemWithPartialPick) { if (!isMissItemOnly && !hasMissItemWithPartialPick) {
// 只有非 miss item 的情况才更新 issueQty // 只有非 miss item 的情况才更新 issueQty
val currentIssueQty = inventoryLotLine.issueQty ?: BigDecimal.ZERO val currentIssueQty = inventoryLotLine.issueQty ?: BigDecimal.ZERO
@@ -213,42 +262,45 @@ open class PickExecutionIssueService(
inventoryLotLine.modified = LocalDateTime.now() inventoryLotLine.modified = LocalDateTime.now()
inventoryLotLine.modifiedBy = "system" inventoryLotLine.modifiedBy = "system"
inventoryLotLineRepository.saveAndFlush(inventoryLotLine) inventoryLotLineRepository.saveAndFlush(inventoryLotLine)
println("Updated inventory_lot_line ${request.lotId} issueQty: ${currentIssueQty} -> ${newIssueQty}")
println("Updated inventory_lot_line ${request.lotId} issueQty: $currentIssueQty -> $newIssueQty")
} else { } else {
println("Skipped updating issueQty for miss item (lot ${request.lotId})")
println("⏭️ Skipped updating issueQty for miss item (lot ${request.lotId})")
} }
} }
// 7. 获取相关数据用于后续处理 // 7. 获取相关数据用于后续处理
val actualPickQtyForProcessing = request.actualPickQty ?: BigDecimal.ZERO val actualPickQtyForProcessing = request.actualPickQty ?: BigDecimal.ZERO
val missQtyForProcessing = request.missQty ?: BigDecimal.ZERO val missQtyForProcessing = request.missQty ?: BigDecimal.ZERO
val badItemQtyForProcessing = request.badItemQty ?: BigDecimal.ZERO val badItemQtyForProcessing = request.badItemQty ?: BigDecimal.ZERO
val lotId = request.lotId val lotId = request.lotId
val itemId = request.itemId 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("Bad Reason: ${request.badReason}")
println("Lot ID: ${lotId}")
println("Item ID: ${itemId}")
println("Lot ID: $lotId")
println("Item ID: $itemId")
println("================================================") println("================================================")
// 8. 新的统一处理逻辑(根据 badReason 决定处理方式) // 8. 新的统一处理逻辑(根据 badReason 决定处理方式)
when { when {
// 情况1: 只有 miss item (actualPickQty = 0, missQty > 0, badItemQty = 0) // 情况1: 只有 miss item (actualPickQty = 0, missQty > 0, badItemQty = 0)
actualPickQtyForProcessing == BigDecimal.ZERO && missQtyForProcessing > BigDecimal.ZERO && badItemQtyForProcessing == BigDecimal.ZERO -> { actualPickQtyForProcessing == BigDecimal.ZERO && missQtyForProcessing > BigDecimal.ZERO && badItemQtyForProcessing == BigDecimal.ZERO -> {
println("→ Handling: Miss Item Only")
handleMissItemOnly(request, missQtyForProcessing) handleMissItemOnly(request, missQtyForProcessing)
} }
// 情况2: 只有 bad item (badItemQty > 0, missQty = 0) // 情况2: 只有 bad item (badItemQty > 0, missQty = 0)
badItemQtyForProcessing > BigDecimal.ZERO && missQtyForProcessing == BigDecimal.ZERO -> { badItemQtyForProcessing > BigDecimal.ZERO && missQtyForProcessing == BigDecimal.ZERO -> {
println("→ Handling: Bad Item Only")
// NEW: Check bad reason // NEW: Check bad reason
if (request.badReason == "package_problem") { if (request.badReason == "package_problem") {
println(" Bad reason is 'package_problem' - calling handleBadItemPackageProblem")
handleBadItemPackageProblem(request, badItemQtyForProcessing) handleBadItemPackageProblem(request, badItemQtyForProcessing)
} else { } else {
println(" Bad reason is 'quantity_problem' - calling handleBadItemOnly")
// quantity_problem or default: handle as normal bad item // quantity_problem or default: handle as normal bad item
handleBadItemOnly(request, badItemQtyForProcessing) handleBadItemOnly(request, badItemQtyForProcessing)
} }
@@ -256,29 +308,34 @@ open class PickExecutionIssueService(
// 情况3: 既有 miss item 又有 bad item // 情况3: 既有 miss item 又有 bad item
missQtyForProcessing > BigDecimal.ZERO && badItemQtyForProcessing > BigDecimal.ZERO -> { missQtyForProcessing > BigDecimal.ZERO && badItemQtyForProcessing > BigDecimal.ZERO -> {
println("→ Handling: Both Miss and Bad Item")
// NEW: Check bad reason // NEW: Check bad reason
if (request.badReason == "package_problem") { if (request.badReason == "package_problem") {
println(" Bad reason is 'package_problem' - calling handleBothMissAndBadItemPackageProblem")
handleBothMissAndBadItemPackageProblem(request, missQtyForProcessing, badItemQtyForProcessing) handleBothMissAndBadItemPackageProblem(request, missQtyForProcessing, badItemQtyForProcessing)
} else { } else {
println(" Bad reason is 'quantity_problem' - calling handleBothMissAndBadItem")
handleBothMissAndBadItem(request, missQtyForProcessing, badItemQtyForProcessing) handleBothMissAndBadItem(request, missQtyForProcessing, badItemQtyForProcessing)
} }
} }
// 情况4: 有 miss item 的情况(无论 actualPickQty 是多少) // 情况4: 有 miss item 的情况(无论 actualPickQty 是多少)
missQtyForProcessing > BigDecimal.ZERO -> { missQtyForProcessing > BigDecimal.ZERO -> {
println("→ Handling: Miss Item With Partial Pick")
handleMissItemWithPartialPick(request, actualPickQtyForProcessing, missQtyForProcessing) handleMissItemWithPartialPick(request, actualPickQtyForProcessing, missQtyForProcessing)
} }
// 情况5: 正常拣货 (actualPickQty > 0, 没有 miss 或 bad item) // 情况5: 正常拣货 (actualPickQty > 0, 没有 miss 或 bad item)
actualPickQtyForProcessing > BigDecimal.ZERO -> { actualPickQtyForProcessing > BigDecimal.ZERO -> {
println("→ Handling: Normal Pick")
handleNormalPick(request, actualPickQtyForProcessing) handleNormalPick(request, actualPickQtyForProcessing)
} }
else -> { 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 pickOrderForCompletion = pickOrderRepository.findById(request.pickOrderId).orElse(null)
val consoCode = pickOrderForCompletion?.consoCode val consoCode = pickOrderForCompletion?.consoCode
@@ -300,7 +357,9 @@ open class PickExecutionIssueService(
println("⚠️ Error checking pick order completion by pickOrderId: ${e.message}") println("⚠️ Error checking pick order completion by pickOrderId: ${e.message}")
} }
} }
println("=== recordPickExecutionIssue: SUCCESS ===")
println("Issue ID: ${savedIssue.id}, Issue No: ${savedIssue.issueNo}")
return MessageResponse( return MessageResponse(
id = savedIssue.id, id = savedIssue.id,
name = "Pick execution issue recorded successfully", name = "Pick execution issue recorded successfully",
@@ -309,9 +368,10 @@ open class PickExecutionIssueService(
message = "Pick execution issue recorded successfully", message = "Pick execution issue recorded successfully",
errorPosition = null errorPosition = null
) )
} catch (e: Exception) { } catch (e: Exception) {
println("=== ERROR IN recordPickExecutionIssue ===")
println("=== recordPickExecutionIssue: ERROR ===")
println("Error: ${e.message}")
e.printStackTrace() e.printStackTrace()
return MessageResponse( return MessageResponse(
id = null, 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 { open fun submitIssueWithQty(request: SubmitIssueWithQtyRequest): MessageResponse {
try { 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 // Find all issues for this lot and item
val issues = if (request.issueType == "miss") { val issues = if (request.issueType == "miss") {
pickExecutionIssueRepository.findMissItemList(IssueCategory.lot_issue) 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()) { if (issues.isEmpty()) {
println("❌ No issues found for this lot")
return MessageResponse( return MessageResponse(
id = null, id = null,
name = "Error", name = "Error",
@@ -2623,13 +2690,36 @@ open fun submitIssueWithQty(request: SubmitIssueWithQtyRequest): MessageResponse
// Use custom quantity instead of sum // Use custom quantity instead of sum
val submitQty = request.submitQty val submitQty = request.submitQty
if (submitQty <= BigDecimal.ZERO) {
// ✅ 修改:允许提交0,0表示"标记为已处理但无需出库"
if (submitQty < BigDecimal.ZERO) {
println("❌ Submit quantity cannot be negative: $submitQty")
return MessageResponse( return MessageResponse(
id = null, id = null,
name = "Error", name = "Error",
code = "INVALID", code = "INVALID",
type = "stock_issue", 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 errorPosition = null
) )
} }
@@ -2649,14 +2739,17 @@ open fun submitIssueWithQty(request: SubmitIssueWithQtyRequest): MessageResponse
firstIssue.issueRemark, firstIssue.issueRemark,
handler handler
) )
println("Created stock out header: id=${stockOut.id}, type=${if (isMissItem) "MISS_ITEM" else "BAD_ITEM"}")
val pickOrderLine = firstIssue.pickOrderLineId?.let { val pickOrderLine = firstIssue.pickOrderLineId?.let {
pickOrderLineRepository.findById(it).orElse(null) pickOrderLineRepository.findById(it).orElse(null)
} }
println("Pick order line: id=${pickOrderLine?.id}")
val lotLine = request.lotId.let { val lotLine = request.lotId.let {
inventoryLotLineRepository.findById(it).orElse(null) inventoryLotLineRepository.findById(it).orElse(null)
} }
println("Lot line: id=${lotLine?.id}, lotNo=${lotLine?.inventoryLot?.lotNo}")
val item = itemsRepository.findById(request.itemId).orElse(null) val item = itemsRepository.findById(request.itemId).orElse(null)
?: return MessageResponse( ?: return MessageResponse(
@@ -2667,6 +2760,7 @@ open fun submitIssueWithQty(request: SubmitIssueWithQtyRequest): MessageResponse
message = "Item not found", message = "Item not found",
errorPosition = null errorPosition = null
) )
println("Item: id=${item.id}, code=${item.code}")
// Create stock_out_line with custom quantity // Create stock_out_line with custom quantity
val stockOutLine = StockOutLine().apply { val stockOutLine = StockOutLine().apply {
@@ -2679,21 +2773,27 @@ open fun submitIssueWithQty(request: SubmitIssueWithQtyRequest): MessageResponse
this.type = if (isMissItem) "Miss" else "Bad" this.type = if (isMissItem) "Miss" else "Bad"
} }
val savedStockOutLine = stockOutLineRepository.saveAndFlush(stockOutLine) val savedStockOutLine = stockOutLineRepository.saveAndFlush(stockOutLine)
println("Created stock out line: id=${savedStockOutLine.id}, qty=${savedStockOutLine.qty}, status=${savedStockOutLine.status}")
if (!isMissItem && request.lotId != null) { if (!isMissItem && request.lotId != null) {
val lotLineForReset = inventoryLotLineRepository.findById(request.lotId).orElse(null) val lotLineForReset = inventoryLotLineRepository.findById(request.lotId).orElse(null)
if (lotLineForReset != null) { if (lotLineForReset != null) {
val oldIssueQty = lotLineForReset.issueQty
lotLineForReset.issueQty = BigDecimal.ZERO lotLineForReset.issueQty = BigDecimal.ZERO
inventoryLotLineRepository.saveAndFlush(lotLineForReset) 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 // Update inventory_lot_line with custom quantity - pass isMissItem flag
if (request.lotId != null) { if (request.lotId != null) {
println("Updating lot line after issue: lotId=${request.lotId}, submitQty=$submitQty, isMissItem=$isMissItem")
updateLotLineAfterIssue(request.lotId, submitQty, isMissItem) updateLotLineAfterIssue(request.lotId, submitQty, isMissItem)
} }
// Mark all issues as handled // Mark all issues as handled
issues.forEach { issue -> issues.forEach { issue ->
println(" Marking issue ${issue.id} as handled by handler $handler")
markIssueHandled(issue, handler) markIssueHandled(issue, handler)
} }
@@ -2708,6 +2808,7 @@ open fun submitIssueWithQty(request: SubmitIssueWithQtyRequest): MessageResponse
createStockLedgerForStockOutWithBalance(savedStockOutLine, balance, if (isMissItem) "Miss" else "Bad") createStockLedgerForStockOutWithBalance(savedStockOutLine, balance, if (isMissItem) "Miss" else "Bad")
println("=== submitIssueWithQty: SUCCESS ===")
return MessageResponse( return MessageResponse(
id = stockOut.id, id = stockOut.id,
name = "Success", name = "Success",
@@ -2717,6 +2818,9 @@ open fun submitIssueWithQty(request: SubmitIssueWithQtyRequest): MessageResponse
errorPosition = null errorPosition = null
) )
} catch (e: Exception) { } catch (e: Exception) {
println("=== submitIssueWithQty: ERROR ===")
println("Error: ${e.message}")
e.printStackTrace()
return MessageResponse( return MessageResponse(
id = null, id = null,
name = "Error", name = "Error",


+ 20
- 0
src/main/java/com/ffii/fpsms/modules/productProcess/entity/ProductProcessLineRepository.kt Wyświetl plik

@@ -2,6 +2,8 @@ package com.ffii.fpsms.modules.productProcess.entity


import org.springframework.data.jpa.repository.JpaRepository import org.springframework.data.jpa.repository.JpaRepository
import org.springframework.stereotype.Repository import org.springframework.stereotype.Repository
import org.springframework.data.jpa.repository.Query
import java.time.LocalDateTime


@Repository @Repository
interface ProductProcessLineRepository : JpaRepository<ProductProcessLine, Long> { interface ProductProcessLineRepository : JpaRepository<ProductProcessLine, Long> {
@@ -10,4 +12,22 @@ interface ProductProcessLineRepository : JpaRepository<ProductProcessLine, Long>
fun findByHandler_IdAndStartTimeIsNotNullAndEndTimeIsNull(handlerId: Long): List<ProductProcessLine> fun findByHandler_IdAndStartTimeIsNotNullAndEndTimeIsNull(handlerId: Long): List<ProductProcessLine>
fun findByProductProcess_IdIn(ids: List<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>

// 用於 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 Wyświetl plik

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


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




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


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


+ 36
- 48
src/main/java/com/ffii/fpsms/modules/productProcess/service/ProductProcessService.kt Wyświetl plik

@@ -11,12 +11,12 @@ import com.ffii.fpsms.modules.master.entity.EquipmentDetailRepository
import com.ffii.fpsms.modules.productProcess.entity.projections.jobOrderLineInfo import com.ffii.fpsms.modules.productProcess.entity.projections.jobOrderLineInfo
import org.springframework.stereotype.Service import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional import org.springframework.transaction.annotation.Transactional
import java.time.LocalDate
import java.time.LocalDateTime import java.time.LocalDateTime
import java.time.temporal.ChronoUnit import java.time.temporal.ChronoUnit
import com.ffii.fpsms.modules.user.entity.UserRepository import com.ffii.fpsms.modules.user.entity.UserRepository
import com.ffii.fpsms.modules.user.entity.User import com.ffii.fpsms.modules.user.entity.User
import java.time.format.DateTimeFormatter
import com.ffii.fpsms.modules.master.entity.BomRepository import com.ffii.fpsms.modules.master.entity.BomRepository
import com.ffii.fpsms.modules.master.entity.BomProcessRepository import com.ffii.fpsms.modules.master.entity.BomProcessRepository
import com.ffii.fpsms.modules.jobOrder.entity.JobOrderRepository import com.ffii.fpsms.modules.jobOrder.entity.JobOrderRepository
@@ -50,6 +50,9 @@ import com.ffii.fpsms.modules.master.entity.UomConversionRepository
import com.ffii.fpsms.modules.master.entity.ItemUomRespository import com.ffii.fpsms.modules.master.entity.ItemUomRespository
import com.ffii.fpsms.modules.master.web.models.* import com.ffii.fpsms.modules.master.web.models.*
import java.math.RoundingMode import java.math.RoundingMode
import java.time.LocalDate
import java.time.format.DateTimeFormatter

@Service @Service
@Transactional @Transactional
open class ProductProcessService( open class ProductProcessService(
@@ -692,7 +695,7 @@ val sufficientStockQty = bomMaterials
// ✅ 获取 req UOM - 对于 reqQty,使用 bomMaterial.uom(BOM 的 UOM) // ✅ 获取 req UOM - 对于 reqQty,使用 bomMaterial.uom(BOM 的 UOM)
// ✅ 对于 stockReqQty,使用 line.uom(库存单位,已按比例调整) // ✅ 对于 stockReqQty,使用 line.uom(库存单位,已按比例调整)
val reqUomId = bomMaterial?.uom?.id ?: line.uom?.id ?: 0L // BOM 的 UOM val reqUomId = bomMaterial?.uom?.id ?: line.uom?.id ?: 0L // BOM 的 UOM
val stockReqUomId = line.uom?.id ?: bomMaterial?.salesUnit?.id ?: 0L // 库存单位 UOM
val stockReqUomId = line.uom?.id ?: bomMaterial?.stockUnit?.toLong() ?: 0L // 库存单位 UOM
val reqUom = reqUomId.takeIf { it > 0 }?.let { uomConversionRepository.findByIdAndDeletedFalse(it) } val reqUom = reqUomId.takeIf { it > 0 }?.let { uomConversionRepository.findByIdAndDeletedFalse(it) }
val uomName = reqUom?.udfudesc val uomName = reqUom?.udfudesc
@@ -706,7 +709,7 @@ val sufficientStockQty = bomMaterials
println("=== Quantity Calculation for Item: ${line.item?.code} (id=$itemId) ===") println("=== Quantity Calculation for Item: ${line.item?.code} (id=$itemId) ===")
println("JobOrderBomMaterial reqQty (adjusted, stock unit): $actualReqQty in UOM: ${stockReqUom?.udfudesc} (id=$stockReqUomId)") println("JobOrderBomMaterial reqQty (adjusted, stock unit): $actualReqQty in UOM: ${stockReqUom?.udfudesc} (id=$stockReqUomId)")
println("BomMaterial qty (base): ${bomMaterial?.qty}, saleQty (base): ${bomMaterial?.saleQty}")
println("BomMaterial qty (base): ${bomMaterial?.qty}, stockQty (base): ${bomMaterial?.stockQty}")
println("Original stockQty: $stockQtyValue in UOM: $stockUomNameForStock (id=$stockUomId)") println("Original stockQty: $stockQtyValue in UOM: $stockUomNameForStock (id=$stockUomId)")
val jobOrder = jobOrderRepository.findById(joid).orElse(null) val jobOrder = jobOrderRepository.findById(joid).orElse(null)
@@ -721,8 +724,8 @@ val sufficientStockQty = bomMaterials
// ✅ reqQty 使用 bomMaterial.qty * proportion(BOM 单位) // ✅ reqQty 使用 bomMaterial.qty * proportion(BOM 单位)
val reqQtyInBomUnit = (bomMaterial?.qty?.times(proportion) ?: BigDecimal.ZERO) val reqQtyInBomUnit = (bomMaterial?.qty?.times(proportion) ?: BigDecimal.ZERO)
// ✅ stockReqQty 使用 bomMaterial.saleQty * proportion(库存单位,已按比例调整)
val stockReqQtyInStockUnit = (bomMaterial?.saleQty?.times(proportion) ?: BigDecimal.ZERO)
// ✅ stockReqQty 使用 bomMaterial.stockQty * proportion(库存单位,已按比例调整)
val stockReqQtyInStockUnit = (bomMaterial?.stockQty?.times(proportion) ?: BigDecimal.ZERO)
// ✅ Convert reqQty (BOM unit) to base unit // ✅ Convert reqQty (BOM unit) to base unit
val baseReqQtyResult = if (reqUomId > 0 && reqQtyInBomUnit > BigDecimal.ZERO) { val baseReqQtyResult = if (reqUomId > 0 && reqQtyInBomUnit > BigDecimal.ZERO) {
@@ -848,7 +851,7 @@ val sufficientStockQty = bomMaterials
baseReqQty = baseReqQty, baseReqQty = baseReqQty,
stockQty = stockQty, stockQty = stockQty,
// ✅ stockReqQty:使用 bomMaterial.saleQty * proportion(库存单位,已按比例调整)
// ✅ stockReqQty:使用 bomMaterial.stockQty * proportion(库存单位,已按比例调整)
stockReqQty = if (stockReqUomId in decimalUomIds) { stockReqQty = if (stockReqUomId in decimalUomIds) {
stockReqQtyInStockUnit.toDouble() stockReqQtyInStockUnit.toDouble()
} else { } else {
@@ -1510,13 +1513,13 @@ val sufficientStockQty = bomMaterials
productProcessLine.startTime = LocalDateTime.now() productProcessLine.startTime = LocalDateTime.now()
productProcessLineRepository.save(productProcessLine) productProcessLineRepository.save(productProcessLine)
} }
if(allproductProcessLines.all { it.status == "Completed" }) {
if(allproductProcessLines.all { it.status == "Completed"|| it.status == "Pass" }) {
updateProductProcessEndTime(productProcessId) updateProductProcessEndTime(productProcessId)
updateProductProcessStatus(productProcessId, ProductProcessStatus.COMPLETED) updateProductProcessStatus(productProcessId, ProductProcessStatus.COMPLETED)
val productProcess = productProcessRepository.findById(productProcessId).orElse(null) val productProcess = productProcessRepository.findById(productProcessId).orElse(null)
val jobOrder = jobOrderRepository.findById(productProcess?.jobOrder?.id?:0L).orElse(null) val jobOrder = jobOrderRepository.findById(productProcess?.jobOrder?.id?:0L).orElse(null)
if(jobOrder != null) { if(jobOrder != null) {
jobOrder.status = JobOrderStatus.PENDING_QC
jobOrder.status = JobOrderStatus.STORING
jobOrderRepository.save(jobOrder) jobOrderRepository.save(jobOrder)
stockInLineService.create( stockInLineService.create(
@@ -1783,42 +1786,35 @@ val sufficientStockQty = bomMaterials
) )
} }


open fun getJobProcessStatus(): List<JobProcessStatusResponse> {
val productProcesses = productProcessRepository.findAllByDeletedIsFalse()
.filter { it.status != ProductProcessStatus.COMPLETED }
open fun getJobProcessStatus(date: LocalDate?): List<JobProcessStatusResponse> {
val productProcesses = productProcessRepository.findAllByDeletedIsFalse()
.let { list -> if (date == null) list else list.filter { it.date == date } }

return productProcesses.mapNotNull { process -> return productProcesses.mapNotNull { process ->
val jobOrder = jobOrderRepository.findById(process.jobOrder?.id ?: 0L).orElse(null) val jobOrder = jobOrderRepository.findById(process.jobOrder?.id ?: 0L).orElse(null)
// Filter out jobOrders in PLANNING status
if (jobOrder?.status == JobOrderStatus.PLANNING) {
return@mapNotNull null
}

// 仍然保留:PLANNING 不显示(如你也要显示,删掉这段)
if (jobOrder?.status == JobOrderStatus.PLANNING) return@mapNotNull null

val lines = productProcessLineRepository.findByProductProcess_Id(process.id ?: 0L) val lines = productProcessLineRepository.findByProductProcess_Id(process.id ?: 0L)
.sortedBy { it.seqNo } .sortedBy { it.seqNo }
val bom=bomRepository.findById(process.bom?.id ?: 0L).orElse(null)


// Calculate planEndTime based on first start time + remaining processing time
val firstStartTime = lines.firstOrNull { it.startTime != null }?.startTime val firstStartTime = lines.firstOrNull { it.startTime != null }?.startTime
val calculatedPlanEndTime = if (firstStartTime != null) { val calculatedPlanEndTime = if (firstStartTime != null) {
// Calculate total remaining processing time (in minutes) for unfinished processes
var totalRemainingMinutes = 0L var totalRemainingMinutes = 0L
lines.forEach { line -> lines.forEach { line ->
if (line.endTime == null) { if (line.endTime == null) {
// Process is not finished, add its processing time
totalRemainingMinutes += (line.processingTime ?: 0).toLong() totalRemainingMinutes += (line.processingTime ?: 0).toLong()
totalRemainingMinutes += (line.setupTime ?: 0).toLong() totalRemainingMinutes += (line.setupTime ?: 0).toLong()
totalRemainingMinutes += (line.changeoverTime ?: 0).toLong() totalRemainingMinutes += (line.changeoverTime ?: 0).toLong()
} }
} }
// Add remaining time to first start time
firstStartTime.plusMinutes(totalRemainingMinutes) firstStartTime.plusMinutes(totalRemainingMinutes)
} else { } else {
// No process has started yet, use original planEndTime
jobOrder?.planEnd jobOrder?.planEnd
} }
JobProcessStatusResponse( JobProcessStatusResponse(
jobOrderId = jobOrder?.id ?: 0L, jobOrderId = jobOrder?.id ?: 0L,
jobOrderCode = jobOrder?.code ?: "", jobOrderCode = jobOrder?.code ?: "",
@@ -1829,29 +1825,19 @@ open fun getJobProcessStatus(): List<JobProcessStatusResponse> {
processes = (0 until 6).map { index -> processes = (0 until 6).map { index ->
if (index < lines.size) { if (index < lines.size) {
val line = lines[index] val line = lines[index]
val equipmentDetailId = line.equipmentDetailId
// Use line's own data instead of indexing into bomProcesses
val equipmentCode = when {
equipmentDetailId != null -> {
equipmentDetailRepository.findById(equipmentDetailId).orElse(null)?.code ?: ""
}
line.equipment?.code != null -> {
line.equipment?.code ?: ""
}
else -> {
// Safely access bomProcess - it might be deleted
try {
line.bomProcess?.equipment?.code ?: ""
} catch (e: jakarta.persistence.EntityNotFoundException) {
// BomProcess was deleted, fallback to equipmentType
""
}.takeIf { it.isNotEmpty() } ?: (line.equipmentType ?: "")
}

// equipment.description + equipment_detail.name
val equipmentName = try { line.bomProcess?.equipment?.name } catch (_: jakarta.persistence.EntityNotFoundException) { null }

val equipmentDetailName = line.equipmentDetailId?.let { id ->
equipmentDetailRepository.findById(id).orElse(null)?.name
} }

ProcessStatusInfo( ProcessStatusInfo(
equipmentCode = equipmentCode,
processName = line.name, // ✅ 新增:工序名称
equipmentName = equipmentName, // ✅ 替代 equipmentCode
equipmentDetailName = equipmentDetailName, // ✅ 新增
startTime = line.startTime, startTime = line.startTime,
endTime = line.endTime, endTime = line.endTime,
processingTime = line.processingTime, processingTime = line.processingTime,
@@ -1861,13 +1847,15 @@ open fun getJobProcessStatus(): List<JobProcessStatusResponse> {
) )
} else { } else {
ProcessStatusInfo( ProcessStatusInfo(
processName = null,
equipmentName = null,
equipmentDetailName = null,
startTime = null, startTime = null,
endTime = null, endTime = null,
processingTime = null, processingTime = null,
setupTime = null, setupTime = null,
changeoverTime = null, changeoverTime = null,
isRequired = false,
equipmentCode = null,
isRequired = false
) )
} }
} }


+ 23
- 2
src/main/java/com/ffii/fpsms/modules/productProcess/web/ProductProcessController.kt Wyświetl plik

@@ -9,6 +9,9 @@ import org.springframework.data.domain.Pageable
import org.springframework.web.bind.annotation.* import org.springframework.web.bind.annotation.*
import com.ffii.fpsms.modules.productProcess.entity.projections.ProductProcessInfo import com.ffii.fpsms.modules.productProcess.entity.projections.ProductProcessInfo
import com.ffii.fpsms.modules.master.web.models.MessageResponse import com.ffii.fpsms.modules.master.web.models.MessageResponse
import java.time.LocalDate
import java.time.format.DateTimeFormatter

@RestController @RestController
@RequestMapping("/product-process") @RequestMapping("/product-process")
class ProductProcessController( class ProductProcessController(
@@ -226,11 +229,29 @@ class ProductProcessController(
return productProcessService.UpdateProductProcessLineProcessingTimeSetupTimeChangeoverTime(request) return productProcessService.UpdateProductProcessLineProcessingTimeSetupTimeChangeoverTime(request)
} }
@GetMapping("/Demo/JobProcessStatus") @GetMapping("/Demo/JobProcessStatus")
fun getJobProcessStatus(): List<JobProcessStatusResponse> {
return productProcessService.getJobProcessStatus()
fun getJobProcessStatus(@RequestParam(required = false) date: String?): List<JobProcessStatusResponse> {
val parsedDate = date?.takeIf { it.isNotBlank() }?.let {
LocalDate.parse(it, DateTimeFormatter.ISO_DATE) // yyyy-MM-dd
}
return productProcessService.getJobProcessStatus(parsedDate)
} }
@PostMapping("/Demo/ProcessLine/delete/{lineId}") @PostMapping("/Demo/ProcessLine/delete/{lineId}")
fun deleteProductProcessLine(@PathVariable lineId: Long): MessageResponse { fun deleteProductProcessLine(@PathVariable lineId: Long): MessageResponse {
return productProcessService.deleteProductProcessLine(lineId) 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()
}
} }

+ 64
- 2
src/main/java/com/ffii/fpsms/modules/productProcess/web/model/SaveProductProcessRequest.kt Wyświetl plik

@@ -220,7 +220,9 @@ data class UpdateProductProcessLineProcessingTimeSetupTimeChangeoverTimeRequest(
val changeoverTime: Int? val changeoverTime: Int?
) )
data class ProcessStatusInfo( data class ProcessStatusInfo(
val equipmentCode: String?,
val processName: String?,
val equipmentName: String?,
val equipmentDetailName: String?,
val startTime: LocalDateTime?, val startTime: LocalDateTime?,
val endTime: LocalDateTime?, val endTime: LocalDateTime?,
val processingTime: Int?, val processingTime: Int?,
@@ -235,6 +237,66 @@ data class JobProcessStatusResponse(
val itemCode: String, val itemCode: String,
val itemName: String, val itemName: String,
val planEndTime: LocalDateTime?, val planEndTime: LocalDateTime?,
val status: String,
val status: String,
val processes: List<ProcessStatusInfo> 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>,
) )

+ 232
- 0
src/main/java/com/ffii/fpsms/modules/report/service/ReportService.kt Wyświetl plik

@@ -97,6 +97,30 @@ open class ReportService(
return "AND (${conditions.joinToString(" OR ")})" return "AND (${conditions.joinToString(" OR ")})"
} }


/**
* Helper function to build SQL clause for comma-separated values with exact match.
* Supports multiple values like "val1, val2, val3" and generates OR conditions with =.
*/
private fun buildMultiValueExactClause(
paramValue: String?,
columnName: String,
paramPrefix: String,
args: MutableMap<String, Any>
): String {
if (paramValue.isNullOrBlank()) return ""
val values = paramValue.split(",").map { it.trim() }.filter { it.isNotBlank() }
if (values.isEmpty()) return ""
val conditions = values.mapIndexed { index, value ->
val paramName = "${paramPrefix}_$index"
args[paramName] = value
"$columnName = :$paramName"
}
return "AND (${conditions.joinToString(" OR ")})"
}

/** /**
* Queries the database for Stock In Traceability Report data. * Queries the database for Stock In Traceability Report data.
* Joins stock_in_line, stock_in, items, item_category, qc_result, inventory_lot, inventory_lot_line, warehouse, and shop tables. * Joins stock_in_line, stock_in, items, item_category, qc_result, inventory_lot, inventory_lot_line, warehouse, and shop tables.
@@ -106,6 +130,7 @@ open class ReportService(
stockCategory: String?, stockCategory: String?,
stockSubCategory: String?, stockSubCategory: String?,
itemCode: String?, itemCode: String?,
year: String?,
lastInDateStart: String?, lastInDateStart: String?,
lastInDateEnd: String? lastInDateEnd: String?
): List<Map<String, Any>> { ): List<Map<String, Any>> {
@@ -115,6 +140,13 @@ open class ReportService(
val stockSubCategorySql = buildMultiValueLikeClause(stockSubCategory, "ic.sub", "stockSubCategory", args) val stockSubCategorySql = buildMultiValueLikeClause(stockSubCategory, "ic.sub", "stockSubCategory", args)
val itemCodeSql = buildMultiValueLikeClause(itemCode, "it.code", "itemCode", args) val itemCodeSql = buildMultiValueLikeClause(itemCode, "it.code", "itemCode", args)
val yearSql = if (!year.isNullOrBlank()) {
args["year"] = year
"AND YEAR(sil.receiptDate) = :year"
} else {
""
}
val lastInDateStartSql = if (!lastInDateStart.isNullOrBlank()) { val lastInDateStartSql = if (!lastInDateStart.isNullOrBlank()) {
args["lastInDateStart"] = lastInDateStart args["lastInDateStart"] = lastInDateStart
"AND sil.receiptDate >= :lastInDateStart" "AND sil.receiptDate >= :lastInDateStart"
@@ -168,6 +200,7 @@ open class ReportService(
$stockCategorySql $stockCategorySql
$stockSubCategorySql $stockSubCategorySql
$itemCodeSql $itemCodeSql
$yearSql
$lastInDateStartSql $lastInDateStartSql
$lastInDateEndSql $lastInDateEndSql
ORDER BY ic.sub, it.code, sil.lotNo ORDER BY ic.sub, it.code, sil.lotNo
@@ -176,6 +209,205 @@ open class ReportService(
return jdbcDao.queryForList(sql, args) return jdbcDao.queryForList(sql, args)
} }


/**
* Queries the database for Semi FG Production Analysis Report data.
* Flow:
* 1. Filter bom by description (FG/WIP) to get bom.code values
* 2. Match bom.code with stock_ledger.itemCode
* 3. Aggregate stock_ledger data by month for each item based on inQty
* Supports comma-separated values for stockCategory, stockSubCategory, and itemCode.
*/
fun searchSemiFGProductionAnalysisReport(
stockCategory: String?,
stockSubCategory: String?,
itemCode: String?,
year: String?,
lastOutDateStart: String?,
lastOutDateEnd: String?
): List<Map<String, Any>> {
val args = mutableMapOf<String, Any>()
// Filter by stockCategory from bom.description (FG/WIP) - this finds which bom.code values match
// Supports multiple categories separated by comma (e.g., "FG,WIP")
// If "All" is selected or contains "All", don't filter by description
val stockCategorySql = if (!itemCode.isNullOrBlank()) {
// When itemCode is provided, skip stockCategory filter
""
} else if (!stockCategory.isNullOrBlank() && stockCategory != "All" && !stockCategory.contains("All")) {
// Handle multiple categories (comma-separated)
val categories = stockCategory.split(",").map { it.trim() }.filter { it.isNotBlank() && it != "All" }
if (categories.isNotEmpty()) {
val conditions = categories.mapIndexed { index, cat ->
val paramName = "stockCategory_$index"
args[paramName] = cat
"b.description = :$paramName"
}
"AND (${conditions.joinToString(" OR ")})"
} else {
""
}
} else {
""
}
val stockSubCategorySql = buildMultiValueLikeClause(stockSubCategory, "ic.sub", "stockSubCategory", args)
// Filter by itemCode - match bom.code (user input should match bom.code, which then matches stock_ledger.itemCode)
val itemCodeSql = buildMultiValueExactClause(itemCode, "b.code", "itemCode", args)
val yearSql = if (!year.isNullOrBlank()) {
args["year"] = year
"AND YEAR(sl.modified) = :year"
} else {
""
}
val lastOutDateStartSql = if (!lastOutDateStart.isNullOrBlank()) {
args["lastOutDateStart"] = lastOutDateStart
"AND DATE(sl.modified) >= :lastOutDateStart"
} else ""
val lastOutDateEndSql = if (!lastOutDateEnd.isNullOrBlank()) {
args["lastOutDateEnd"] = lastOutDateEnd
"AND DATE(sl.modified) < :lastOutDateEnd"
} else ""

val sql = """
SELECT
COALESCE(ic.sub, '') as stockSubCategory,
COALESCE(sl.itemCode, '') as itemNo,
COALESCE(b.name, '') as itemName,
COALESCE(uc.code, '') as unitOfMeasure,
CAST(COALESCE(SUM(CASE WHEN MONTH(sl.modified) = 1 THEN sl.inQty ELSE 0 END), 0) AS DECIMAL(18,2)) as qtyJan,
CAST(COALESCE(SUM(CASE WHEN MONTH(sl.modified) = 2 THEN sl.inQty ELSE 0 END), 0) AS DECIMAL(18,2)) as qtyFeb,
CAST(COALESCE(SUM(CASE WHEN MONTH(sl.modified) = 3 THEN sl.inQty ELSE 0 END), 0) AS DECIMAL(18,2)) as qtyMar,
CAST(COALESCE(SUM(CASE WHEN MONTH(sl.modified) = 4 THEN sl.inQty ELSE 0 END), 0) AS DECIMAL(18,2)) as qtyApr,
CAST(COALESCE(SUM(CASE WHEN MONTH(sl.modified) = 5 THEN sl.inQty ELSE 0 END), 0) AS DECIMAL(18,2)) as qtyMay,
CAST(COALESCE(SUM(CASE WHEN MONTH(sl.modified) = 6 THEN sl.inQty ELSE 0 END), 0) AS DECIMAL(18,2)) as qtyJun,
CAST(COALESCE(SUM(CASE WHEN MONTH(sl.modified) = 7 THEN sl.inQty ELSE 0 END), 0) AS DECIMAL(18,2)) as qtyJul,
CAST(COALESCE(SUM(CASE WHEN MONTH(sl.modified) = 8 THEN sl.inQty ELSE 0 END), 0) AS DECIMAL(18,2)) as qtyAug,
CAST(COALESCE(SUM(CASE WHEN MONTH(sl.modified) = 9 THEN sl.inQty ELSE 0 END), 0) AS DECIMAL(18,2)) as qtySep,
CAST(COALESCE(SUM(CASE WHEN MONTH(sl.modified) = 10 THEN sl.inQty ELSE 0 END), 0) AS DECIMAL(18,2)) as qtyOct,
CAST(COALESCE(SUM(CASE WHEN MONTH(sl.modified) = 11 THEN sl.inQty ELSE 0 END), 0) AS DECIMAL(18,2)) as qtyNov,
CAST(COALESCE(SUM(CASE WHEN MONTH(sl.modified) = 12 THEN sl.inQty ELSE 0 END), 0) AS DECIMAL(18,2)) as qtyDec,
CAST(COALESCE(SUM(sl.inQty), 0) AS CHAR) as totalProductionQty
FROM stock_ledger sl
INNER JOIN bom b ON sl.itemCode = b.code AND b.deleted = false
LEFT JOIN items it ON sl.itemId = it.id
LEFT JOIN item_category ic ON it.categoryId = ic.id
LEFT JOIN item_uom iu ON it.id = iu.itemId AND iu.stockUnit = true
LEFT JOIN uom_conversion uc ON iu.uomId = uc.id
WHERE sl.deleted = false
AND sl.inQty IS NOT NULL
AND sl.inQty > 0
$stockCategorySql
$stockSubCategorySql
$itemCodeSql
$yearSql
$lastOutDateStartSql
$lastOutDateEndSql
GROUP BY sl.itemCode, ic.sub, it.id, b.name, uc.code, b.description
ORDER BY ic.sub, sl.itemCode
""".trimIndent()
return jdbcDao.queryForList(sql, args)
}

/**
* Gets list of item codes (bom.code) with names based on stockCategory filter.
* Supports multiple categories separated by comma (e.g., "FG,WIP").
* If stockCategory is "All" or null, returns all codes.
* If stockCategory is "FG" or "WIP" or "FG,WIP", returns codes matching those descriptions.
* Returns a list of maps with "code" and "name" keys.
*/
fun getSemiFGItemCodes(stockCategory: String?): List<Map<String, String>> {
val args = mutableMapOf<String, Any>()
val stockCategorySql = if (!stockCategory.isNullOrBlank() && stockCategory != "All" && !stockCategory.contains("All")) {
// Handle multiple categories (comma-separated)
val categories = stockCategory.split(",").map { it.trim() }.filter { it.isNotBlank() && it != "All" }
if (categories.isNotEmpty()) {
val conditions = categories.mapIndexed { index, cat ->
val paramName = "stockCategory_$index"
args[paramName] = cat
"b.description = :$paramName"
}
"AND (${conditions.joinToString(" OR ")})"
} else {
""
}
} else {
""
}

val sql = """
SELECT DISTINCT b.code, COALESCE(b.name, '') as name
FROM bom b
WHERE b.deleted = false
AND b.code IS NOT NULL
AND b.code != ''
$stockCategorySql
ORDER BY b.code
""".trimIndent()
val results = jdbcDao.queryForList(sql, args)
return results.mapNotNull {
val code = it["code"]?.toString()
val name = it["name"]?.toString() ?: ""
if (code != null) {
mapOf("code" to code, "name" to name)
} else {
null
}
}
}

/**
* Gets list of item codes with their category (FG/WIP) and name based on stockCategory filter.
* Supports multiple categories separated by comma (e.g., "FG,WIP").
* Returns a list of maps with "code", "category", and "name" keys.
*/
fun getSemiFGItemCodesWithCategory(stockCategory: String?): List<Map<String, String>> {
val args = mutableMapOf<String, Any>()
val stockCategorySql = if (!stockCategory.isNullOrBlank() && stockCategory != "All" && !stockCategory.contains("All")) {
// Handle multiple categories (comma-separated)
val categories = stockCategory.split(",").map { it.trim() }.filter { it.isNotBlank() && it != "All" }
if (categories.isNotEmpty()) {
val conditions = categories.mapIndexed { index, cat ->
val paramName = "stockCategory_$index"
args[paramName] = cat
"b.description = :$paramName"
}
"AND (${conditions.joinToString(" OR ")})"
} else {
""
}
} else {
""
}

val sql = """
SELECT DISTINCT b.code, COALESCE(b.description, '') as category, COALESCE(b.name, '') as name
FROM bom b
WHERE b.deleted = false
AND b.code IS NOT NULL
AND b.code != ''
$stockCategorySql
ORDER BY b.code
""".trimIndent()
val results = jdbcDao.queryForList(sql, args)
return results.mapNotNull {
val code = it["code"]?.toString()
val category = it["category"]?.toString() ?: ""
val name = it["name"]?.toString() ?: ""
if (code != null) {
mapOf("code" to code, "category" to category, "name" to name)
} else {
null
}
}
}

/** /**
* Compiles and fills a Jasper Report, returning the PDF as a ByteArray. * Compiles and fills a Jasper Report, returning the PDF as a ByteArray.
*/ */


+ 67
- 0
src/main/java/com/ffii/fpsms/modules/report/web/ReportController.kt Wyświetl plik

@@ -85,6 +85,7 @@ class ReportController(
@RequestParam(required = false) stockCategory: String?, @RequestParam(required = false) stockCategory: String?,
@RequestParam(required = false) stockSubCategory: String?, @RequestParam(required = false) stockSubCategory: String?,
@RequestParam(required = false) itemCode: String?, @RequestParam(required = false) itemCode: String?,
@RequestParam(required = false) year: String?,
@RequestParam(required = false) lastInDateStart: String?, @RequestParam(required = false) lastInDateStart: String?,
@RequestParam(required = false) lastInDateEnd: String? @RequestParam(required = false) lastInDateEnd: String?
): ResponseEntity<ByteArray> { ): ResponseEntity<ByteArray> {
@@ -94,6 +95,7 @@ class ReportController(
parameters["stockCategory"] = stockCategory ?: "All" parameters["stockCategory"] = stockCategory ?: "All"
parameters["stockSubCategory"] = stockSubCategory ?: "All" parameters["stockSubCategory"] = stockSubCategory ?: "All"
parameters["itemNo"] = itemCode ?: "All" parameters["itemNo"] = itemCode ?: "All"
parameters["year"] = year ?: LocalDate.now().year.toString()
parameters["reportDate"] = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd")) parameters["reportDate"] = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd"))
parameters["reportTime"] = LocalTime.now().format(DateTimeFormatter.ofPattern("HH:mm:ss")) parameters["reportTime"] = LocalTime.now().format(DateTimeFormatter.ofPattern("HH:mm:ss"))
parameters["lastInDateStart"] = lastInDateStart ?: "" parameters["lastInDateStart"] = lastInDateStart ?: ""
@@ -104,6 +106,7 @@ class ReportController(
stockCategory, stockCategory,
stockSubCategory, stockSubCategory,
itemCode, itemCode,
year,
lastInDateStart, lastInDateStart,
lastInDateEnd lastInDateEnd
) )
@@ -122,4 +125,68 @@ class ReportController(


return ResponseEntity(pdfBytes, headers, HttpStatus.OK) return ResponseEntity(pdfBytes, headers, HttpStatus.OK)
} }

@GetMapping("/print-semi-fg-production-analysis")
fun generateSemiFGProductionAnalysisReport(
@RequestParam(required = false) stockCategory: String?,
@RequestParam(required = false) stockSubCategory: String?,
@RequestParam(required = false) itemCode: String?,
@RequestParam(required = false) year: String?,
@RequestParam(required = false) lastOutDateStart: String?,
@RequestParam(required = false) lastOutDateEnd: String?
): ResponseEntity<ByteArray> {
val parameters = mutableMapOf<String, Any>()
// Set report header parameters
parameters["stockCategory"] = stockCategory ?: "All"
parameters["stockSubCategory"] = stockSubCategory ?: "All"
parameters["itemNo"] = itemCode ?: "All"
parameters["year"] = year ?: LocalDate.now().year.toString()
parameters["reportDate"] = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd"))
parameters["reportTime"] = LocalTime.now().format(DateTimeFormatter.ofPattern("HH:mm:ss"))
parameters["lastOutDateStart"] = lastOutDateStart ?: ""
parameters["lastOutDateEnd"] = lastOutDateEnd ?: ""
parameters["deliveryPeriodStart"] = ""
parameters["deliveryPeriodEnd"] = ""

// Query the DB to get a list of data
val dbData = reportService.searchSemiFGProductionAnalysisReport(
stockCategory,
stockSubCategory,
itemCode,
year,
lastOutDateStart,
lastOutDateEnd
)

val pdfBytes = reportService.createPdfResponse(
"/jasper/SemiFGProductionAnalysisReport.jrxml",
parameters,
dbData
)

val headers = HttpHeaders().apply {
contentType = MediaType.APPLICATION_PDF
setContentDispositionFormData("attachment", "SemiFGProductionAnalysisReport.pdf")
set("filename", "SemiFGProductionAnalysisReport.pdf")
}

return ResponseEntity(pdfBytes, headers, HttpStatus.OK)
}

@GetMapping("/semi-fg-item-codes")
fun getSemiFGItemCodes(
@RequestParam(required = false) stockCategory: String?
): ResponseEntity<List<Map<String, String>>> {
val itemCodes = reportService.getSemiFGItemCodes(stockCategory)
return ResponseEntity(itemCodes, HttpStatus.OK)
}

@GetMapping("/semi-fg-item-codes-with-category")
fun getSemiFGItemCodesWithCategory(
@RequestParam(required = false) stockCategory: String?
): ResponseEntity<List<Map<String, String>>> {
val itemCodesWithCategory = reportService.getSemiFGItemCodesWithCategory(stockCategory)
return ResponseEntity(itemCodesWithCategory, HttpStatus.OK)
}
} }

+ 3
- 0
src/main/java/com/ffii/fpsms/modules/stock/entity/projection/StockInLineInfo.kt Wyświetl plik

@@ -13,6 +13,8 @@ interface StockInLineInfo {
val itemId: Long val itemId: Long
@get:Value("#{target.item?.name}") @get:Value("#{target.item?.name}")
val itemName: String? val itemName: String?
@get:Value("#{target.item?.LocationCode}")
val locationCode: String?
val itemNo: String val itemNo: String
@get:Value("#{target.stockIn?.id}") @get:Value("#{target.stockIn?.id}")
val stockInId: Long val stockInId: Long
@@ -74,4 +76,5 @@ interface PutAwayLineForSil {
val putawayDate: LocalDateTime?; val putawayDate: LocalDateTime?;
@get:Value("#{target.createdBy}") @get:Value("#{target.createdBy}")
val putawayUser: String?; val putawayUser: String?;

} }

+ 2
- 1
src/main/java/com/ffii/fpsms/modules/stock/service/SuggestedPickLotService.kt Wyświetl plik

@@ -310,7 +310,8 @@ open class SuggestedPickLotService(
} }
// 规则 2:原有逻辑:跳过 3F // 规则 2:原有逻辑:跳过 3F
if (warehouseStoreId == "3F") {
val isJobOrder = pickOrder?.type?.value == "jo" || pickOrder?.type?.value == "jo"
if (warehouseStoreId == "3F" && !isJobOrder) {
return@forEachIndexed return@forEachIndexed
} }


+ 9
- 0
src/main/resources/db/changelog/changes/20260204_Enson/01_add_bom_materail.sql Wyświetl plik

@@ -0,0 +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`;

+ 664
- 0
src/main/resources/jasper/SemiFGProductionAnalysisReport.jrxml Wyświetl plik

@@ -0,0 +1,664 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Created with Jaspersoft Studio version 6.21.3.final using JasperReports Library version 6.21.3-4a3078d20785ebe464f18037d738d12fc98c13cf -->
<jasperReport xmlns="http://jasperreports.sourceforge.net/jasperreports" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://jasperreports.sourceforge.net/jasperreports http://jasperreports.sourceforge.net/xsd/jasperreport.xsd" name="SemiFGProductionAnalysisReport" pageWidth="842" pageHeight="595" orientation="Landscape" columnWidth="802" leftMargin="20" rightMargin="20" topMargin="20" bottomMargin="20" uuid="055685d6-c02e-403a-8511-48e9df2752d3">
<property name="com.jaspersoft.studio.unit." value="pixel"/>
<property name="com.jaspersoft.studio.unit.pageHeight" value="pixel"/>
<property name="com.jaspersoft.studio.unit.pageWidth" value="pixel"/>
<property name="com.jaspersoft.studio.unit.topMargin" value="pixel"/>
<property name="com.jaspersoft.studio.unit.bottomMargin" value="pixel"/>
<property name="com.jaspersoft.studio.unit.leftMargin" value="pixel"/>
<property name="com.jaspersoft.studio.unit.rightMargin" value="pixel"/>
<property name="com.jaspersoft.studio.unit.columnWidth" value="pixel"/>
<property name="com.jaspersoft.studio.unit.columnSpacing" value="pixel"/>
<property name="com.jaspersoft.studio.data.defaultdataadapter" value="One Empty Record"/>
<property name="com.jaspersoft.studio.data.sql.tables" value=""/>
<parameter name="stockSubCategory" class="java.lang.String">
<defaultValueExpression><![CDATA["stockSubCategory"]]></defaultValueExpression>
</parameter>
<parameter name="stockCategory" class="java.lang.String">
<defaultValueExpression><![CDATA["stockCategory"]]></defaultValueExpression>
</parameter>
<parameter name="itemNo" class="java.lang.String">
<defaultValueExpression><![CDATA["itemCode"]]></defaultValueExpression>
</parameter>
<parameter name="year" class="java.lang.String"/>
<parameter name="reportDate" class="java.lang.String"/>
<parameter name="reportTime" class="java.lang.String"/>
<parameter name="deliveryPeriodStart" class="java.lang.String"/>
<parameter name="deliveryPeriodEnd" class="java.lang.String"/>
<parameter name="lastOutDateStart" class="java.lang.String">
<parameterDescription><![CDATA["lastOutDateStart"]]></parameterDescription>
</parameter>
<parameter name="lastOutDateEnd" class="java.lang.String">
<parameterDescription><![CDATA["lastOutDateStart"]]></parameterDescription>
</parameter>
<queryString>
<![CDATA[select * from fpsmsdb.items , fpsmsdb.stock_out_line]]>
</queryString>
<field name="id" class="java.lang.Integer">
<property name="com.jaspersoft.studio.field.name" value="id"/>
<property name="com.jaspersoft.studio.field.label" value="id"/>
<property name="com.jaspersoft.studio.field.tree.path" value="items"/>
<fieldDescription><![CDATA[]]></fieldDescription>
</field>
<field name="modified" class="java.time.LocalDateTime">
<property name="com.jaspersoft.studio.field.name" value="modified"/>
<property name="com.jaspersoft.studio.field.label" value="modified"/>
<property name="com.jaspersoft.studio.field.tree.path" value="items"/>
<fieldDescription><![CDATA[]]></fieldDescription>
</field>
<field name="itemNo" class="java.lang.String">
<property name="com.jaspersoft.studio.field.name" value="code"/>
<property name="com.jaspersoft.studio.field.label" value="code"/>
<property name="com.jaspersoft.studio.field.tree.path" value="items"/>
<fieldDescription><![CDATA[]]></fieldDescription>
</field>
<field name="itemName" class="java.lang.String">
<property name="com.jaspersoft.studio.field.name" value="name"/>
<property name="com.jaspersoft.studio.field.label" value="name"/>
<property name="com.jaspersoft.studio.field.tree.path" value="items"/>
<fieldDescription><![CDATA[]]></fieldDescription>
</field>
<field name="description" class="java.lang.String">
<property name="com.jaspersoft.studio.field.name" value="description"/>
<property name="com.jaspersoft.studio.field.label" value="description"/>
<property name="com.jaspersoft.studio.field.tree.path" value="items"/>
<fieldDescription><![CDATA[]]></fieldDescription>
</field>
<field name="remarks" class="java.lang.String">
<property name="com.jaspersoft.studio.field.name" value="remarks"/>
<property name="com.jaspersoft.studio.field.label" value="remarks"/>
<property name="com.jaspersoft.studio.field.tree.path" value="items"/>
<fieldDescription><![CDATA[]]></fieldDescription>
</field>
<field name="type" class="java.lang.String">
<property name="com.jaspersoft.studio.field.name" value="type"/>
<property name="com.jaspersoft.studio.field.label" value="type"/>
<property name="com.jaspersoft.studio.field.tree.path" value="items"/>
<fieldDescription><![CDATA[]]></fieldDescription>
</field>
<field name="stockSubCategory" class="java.lang.Integer">
<property name="com.jaspersoft.studio.field.name" value="categoryId"/>
<property name="com.jaspersoft.studio.field.label" value="categoryId"/>
<property name="com.jaspersoft.studio.field.tree.path" value="items"/>
<fieldDescription><![CDATA[]]></fieldDescription>
</field>
<field name="qcCategoryId" class="java.lang.Integer">
<property name="com.jaspersoft.studio.field.name" value="qcCategoryId"/>
<property name="com.jaspersoft.studio.field.label" value="qcCategoryId"/>
<property name="com.jaspersoft.studio.field.tree.path" value="items"/>
<fieldDescription><![CDATA[]]></fieldDescription>
</field>
<field name="uomId" class="java.lang.Integer">
<property name="com.jaspersoft.studio.field.name" value="uomId"/>
<property name="com.jaspersoft.studio.field.label" value="uomId"/>
<property name="com.jaspersoft.studio.field.tree.path" value="items"/>
<fieldDescription><![CDATA[]]></fieldDescription>
</field>
<field name="itemId" class="java.lang.Integer">
<property name="com.jaspersoft.studio.field.name" value="itemId"/>
<property name="com.jaspersoft.studio.field.label" value="itemId"/>
<property name="com.jaspersoft.studio.field.tree.path" value="stock_out_line"/>
<fieldDescription><![CDATA[]]></fieldDescription>
</field>
<field name="qty" class="java.math.BigDecimal">
<property name="com.jaspersoft.studio.field.name" value="qty"/>
<property name="com.jaspersoft.studio.field.label" value="qty"/>
<property name="com.jaspersoft.studio.field.tree.path" value="stock_out_line"/>
<fieldDescription><![CDATA[]]></fieldDescription>
</field>
<field name="stockOutId" class="java.lang.Integer">
<property name="com.jaspersoft.studio.field.name" value="stockOutId"/>
<property name="com.jaspersoft.studio.field.label" value="stockOutId"/>
<property name="com.jaspersoft.studio.field.tree.path" value="stock_out_line"/>
<fieldDescription><![CDATA[]]></fieldDescription>
</field>
<field name="pickOrderLineId" class="java.lang.Integer">
<property name="com.jaspersoft.studio.field.name" value="pickOrderLineId"/>
<property name="com.jaspersoft.studio.field.label" value="pickOrderLineId"/>
<property name="com.jaspersoft.studio.field.tree.path" value="stock_out_line"/>
<fieldDescription><![CDATA[]]></fieldDescription>
</field>
<field name="status" class="java.lang.String">
<property name="com.jaspersoft.studio.field.name" value="status"/>
<property name="com.jaspersoft.studio.field.label" value="status"/>
<property name="com.jaspersoft.studio.field.tree.path" value="stock_out_line"/>
<fieldDescription><![CDATA[]]></fieldDescription>
</field>
<field name="pickTime" class="java.time.LocalDateTime">
<property name="com.jaspersoft.studio.field.name" value="pickTime"/>
<property name="com.jaspersoft.studio.field.label" value="pickTime"/>
<property name="com.jaspersoft.studio.field.tree.path" value="stock_out_line"/>
<fieldDescription><![CDATA[]]></fieldDescription>
</field>
<field name="qtyJan" class="java.math.BigDecimal">
<property name="com.jaspersoft.studio.field.name" value="qty"/>
<property name="com.jaspersoft.studio.field.label" value="qty"/>
<property name="com.jaspersoft.studio.field.tree.path" value="stock_out_line"/>
<fieldDescription><![CDATA[]]></fieldDescription>
</field>
<field name="qtyFeb" class="java.math.BigDecimal">
<property name="com.jaspersoft.studio.field.name" value="qty"/>
<property name="com.jaspersoft.studio.field.label" value="qty"/>
<property name="com.jaspersoft.studio.field.tree.path" value="stock_out_line"/>
<fieldDescription><![CDATA[]]></fieldDescription>
</field>
<field name="qtyMar" class="java.math.BigDecimal">
<property name="com.jaspersoft.studio.field.name" value="qty"/>
<property name="com.jaspersoft.studio.field.label" value="qty"/>
<property name="com.jaspersoft.studio.field.tree.path" value="stock_out_line"/>
<fieldDescription><![CDATA[]]></fieldDescription>
</field>
<field name="qtyApr" class="java.math.BigDecimal">
<property name="com.jaspersoft.studio.field.name" value="qty"/>
<property name="com.jaspersoft.studio.field.label" value="qty"/>
<property name="com.jaspersoft.studio.field.tree.path" value="stock_out_line"/>
<fieldDescription><![CDATA[]]></fieldDescription>
</field>
<field name="qtyMay" class="java.math.BigDecimal">
<property name="com.jaspersoft.studio.field.name" value="qty"/>
<property name="com.jaspersoft.studio.field.label" value="qty"/>
<property name="com.jaspersoft.studio.field.tree.path" value="stock_out_line"/>
<fieldDescription><![CDATA[]]></fieldDescription>
</field>
<field name="qtyJun" class="java.math.BigDecimal">
<property name="com.jaspersoft.studio.field.name" value="qty"/>
<property name="com.jaspersoft.studio.field.label" value="qty"/>
<property name="com.jaspersoft.studio.field.tree.path" value="stock_out_line"/>
<fieldDescription><![CDATA[]]></fieldDescription>
</field>
<field name="qtyJul" class="java.math.BigDecimal">
<property name="com.jaspersoft.studio.field.name" value="qty"/>
<property name="com.jaspersoft.studio.field.label" value="qty"/>
<property name="com.jaspersoft.studio.field.tree.path" value="stock_out_line"/>
<fieldDescription><![CDATA[]]></fieldDescription>
</field>
<field name="qtyAug" class="java.math.BigDecimal">
<property name="com.jaspersoft.studio.field.name" value="qty"/>
<property name="com.jaspersoft.studio.field.label" value="qty"/>
<property name="com.jaspersoft.studio.field.tree.path" value="stock_out_line"/>
<fieldDescription><![CDATA[]]></fieldDescription>
</field>
<field name="qtySep" class="java.math.BigDecimal">
<property name="com.jaspersoft.studio.field.name" value="qty"/>
<property name="com.jaspersoft.studio.field.label" value="qty"/>
<property name="com.jaspersoft.studio.field.tree.path" value="stock_out_line"/>
<fieldDescription><![CDATA[]]></fieldDescription>
</field>
<field name="qtyOct" class="java.math.BigDecimal">
<property name="com.jaspersoft.studio.field.name" value="qty"/>
<property name="com.jaspersoft.studio.field.label" value="qty"/>
<property name="com.jaspersoft.studio.field.tree.path" value="stock_out_line"/>
<fieldDescription><![CDATA[]]></fieldDescription>
</field>
<field name="qtyNov" class="java.math.BigDecimal">
<property name="com.jaspersoft.studio.field.name" value="qty"/>
<property name="com.jaspersoft.studio.field.label" value="qty"/>
<property name="com.jaspersoft.studio.field.tree.path" value="stock_out_line"/>
<fieldDescription><![CDATA[]]></fieldDescription>
</field>
<field name="qtyDec" class="java.math.BigDecimal">
<property name="com.jaspersoft.studio.field.name" value="qty"/>
<property name="com.jaspersoft.studio.field.label" value="qty"/>
<property name="com.jaspersoft.studio.field.tree.path" value="stock_out_line"/>
<fieldDescription><![CDATA[]]></fieldDescription>
</field>
<field name="unitOfMeasure" class="java.lang.String"/>
<field name="totalProductionQty" class="java.lang.String"/>
<group name="Group1">
<groupExpression><![CDATA[$F{itemNo}]]></groupExpression>
<groupHeader>
<band height="92">
<staticText>
<reportElement x="0" y="10" width="81" height="20" uuid="09bcfab9-9520-48dd-b851-3c00951cc550">
<property name="com.jaspersoft.studio.unit.y" value="px"/>
<property name="com.jaspersoft.studio.unit.width" value="px"/>
</reportElement>
<textElement textAlignment="Left" verticalAlignment="Middle">
<font fontName="微軟正黑體" size="12"/>
</textElement>
<text><![CDATA[貨品編號:]]></text>
</staticText>
<textField>
<reportElement x="81" y="10" width="119" height="20" uuid="6f905a5d-cf6b-4d62-8433-ff3d570829ae"/>
<textElement textAlignment="Left" verticalAlignment="Middle">
<font fontName="微軟正黑體" size="12"/>
</textElement>
<textFieldExpression><![CDATA[$F{itemNo}]]></textFieldExpression>
</textField>
<staticText>
<reportElement x="280" y="10" width="80" height="20" uuid="f0d35ecc-bc8d-4f5e-a94c-5cc6e5cde51e">
<property name="com.jaspersoft.studio.unit.y" value="px"/>
<property name="com.jaspersoft.studio.unit.height" value="px"/>
<property name="com.jaspersoft.studio.unit.width" value="px"/>
</reportElement>
<textElement textAlignment="Right" verticalAlignment="Middle">
<font fontName="微軟正黑體" size="12"/>
</textElement>
<text><![CDATA[貨品名稱:]]></text>
</staticText>
<textField>
<reportElement x="360" y="10" width="130" height="20" uuid="11265d83-f36f-4cf2-ae3f-60d504226ab0">
<property name="com.jaspersoft.studio.unit.width" value="px"/>
</reportElement>
<textElement verticalAlignment="Middle">
<font fontName="微軟正黑體" size="12"/>
</textElement>
<textFieldExpression><![CDATA[$F{itemName}]]></textFieldExpression>
</textField>
<staticText>
<reportElement x="610" y="10" width="41" height="20" uuid="a9054f9d-f217-4daa-a35c-076fe16a2df6">
<property name="com.jaspersoft.studio.unit.y" value="px"/>
<property name="com.jaspersoft.studio.unit.width" value="px"/>
</reportElement>
<textElement textAlignment="Right" verticalAlignment="Middle">
<font fontName="微軟正黑體" size="12"/>
</textElement>
<text><![CDATA[單位:]]></text>
</staticText>
<textField>
<reportElement x="651" y="10" width="129" height="20" uuid="1a6c5348-668b-40cd-b8c7-a7b53c5dbcbe">
<property name="com.jaspersoft.studio.unit.height" value="px"/>
<property name="com.jaspersoft.studio.unit.width" value="px"/>
</reportElement>
<textElement textAlignment="Left" verticalAlignment="Middle">
<font fontName="微軟正黑體" size="12"/>
</textElement>
<textFieldExpression><![CDATA[$F{unitOfMeasure}]]></textFieldExpression>
</textField>
<staticText>
<reportElement x="0" y="40" width="56" height="20" uuid="e7a7df0c-bb6b-48da-9612-f9ca6b11f845">
<property name="com.jaspersoft.studio.unit.y" value="px"/>
</reportElement>
<textElement textAlignment="Left" verticalAlignment="Middle">
<font fontName="微軟正黑體" size="10"/>
</textElement>
<text><![CDATA[一月]]></text>
</staticText>
<staticText>
<reportElement x="224" y="40" width="56" height="20" uuid="1502529a-e0bf-4fff-aa34-7b4cff72ddf0">
<property name="com.jaspersoft.studio.unit.y" value="px"/>
<property name="com.jaspersoft.studio.unit.width" value="px"/>
</reportElement>
<textElement textAlignment="Left" verticalAlignment="Middle">
<font fontName="微軟正黑體" size="10"/>
</textElement>
<text><![CDATA[五月]]></text>
</staticText>
<staticText>
<reportElement x="56" y="40" width="56" height="20" uuid="afd73da5-fb15-4768-ae44-ccc1f57123af">
<property name="com.jaspersoft.studio.unit.y" value="px"/>
<property name="com.jaspersoft.studio.unit.width" value="px"/>
</reportElement>
<textElement textAlignment="Left" verticalAlignment="Middle" markup="html">
<font fontName="微軟正黑體" size="10"/>
</textElement>
<text><![CDATA[二月]]></text>
</staticText>
<staticText>
<reportElement x="560" y="40" width="56" height="20" uuid="741b9a24-b91b-4ccb-aa7b-2eee600e7994">
<property name="com.jaspersoft.studio.unit.y" value="px"/>
<property name="com.jaspersoft.studio.unit.width" value="px"/>
</reportElement>
<textElement textAlignment="Left" verticalAlignment="Middle">
<font fontName="微軟正黑體" size="10"/>
</textElement>
<text><![CDATA[十一月]]></text>
</staticText>
<staticText>
<reportElement x="392" y="40" width="56" height="20" uuid="b2935034-a5b9-4508-aa4a-0029704f8a3b">
<property name="com.jaspersoft.studio.unit.y" value="px"/>
<property name="com.jaspersoft.studio.unit.width" value="px"/>
</reportElement>
<textElement textAlignment="Left" verticalAlignment="Middle">
<font fontName="微軟正黑體" size="10"/>
</textElement>
<text><![CDATA[八月]]></text>
</staticText>
<staticText>
<reportElement x="112" y="40" width="56" height="20" uuid="7d81e729-e12d-4f2e-a857-6224163177bd">
<property name="com.jaspersoft.studio.unit.y" value="px"/>
<property name="com.jaspersoft.studio.unit.width" value="px"/>
</reportElement>
<textElement textAlignment="Left" verticalAlignment="Middle">
<font fontName="微軟正黑體" size="10"/>
</textElement>
<text><![CDATA[三月]]></text>
</staticText>
<staticText>
<reportElement x="168" y="40" width="56" height="20" uuid="cc73619f-39f9-4165-a8d7-e4679704b7ee">
<property name="com.jaspersoft.studio.unit.y" value="px"/>
<property name="com.jaspersoft.studio.unit.width" value="px"/>
</reportElement>
<textElement textAlignment="Left" verticalAlignment="Middle">
<font fontName="微軟正黑體" size="10"/>
</textElement>
<text><![CDATA[四月]]></text>
</staticText>
<staticText>
<reportElement x="336" y="40" width="56" height="20" uuid="7af699eb-2dc9-4f4a-861c-0e54c284fbbd">
<property name="com.jaspersoft.studio.unit.y" value="px"/>
<property name="com.jaspersoft.studio.unit.height" value="px"/>
</reportElement>
<textElement textAlignment="Left" verticalAlignment="Middle">
<font fontName="微軟正黑體" size="10"/>
</textElement>
<text><![CDATA[七月]]></text>
</staticText>
<staticText>
<reportElement x="280" y="40" width="56" height="20" uuid="0fbf45b7-f56b-4e19-a130-1adcb46f294d">
<property name="com.jaspersoft.studio.unit.y" value="px"/>
<property name="com.jaspersoft.studio.unit.width" value="px"/>
</reportElement>
<textElement textAlignment="Left" verticalAlignment="Middle">
<font fontName="微軟正黑體" size="10"/>
</textElement>
<text><![CDATA[六月]]></text>
</staticText>
<staticText>
<reportElement x="616" y="40" width="74" height="20" uuid="3403153f-8bc9-4108-b3e2-45af3fd95305">
<property name="com.jaspersoft.studio.unit.y" value="px"/>
<property name="com.jaspersoft.studio.unit.width" value="px"/>
</reportElement>
<textElement textAlignment="Left" verticalAlignment="Middle">
<font fontName="微軟正黑體" size="10"/>
</textElement>
<text><![CDATA[十二月]]></text>
</staticText>
<staticText>
<reportElement x="448" y="40" width="56" height="20" uuid="40912a1d-624d-496f-999c-41cd3210a441">
<property name="com.jaspersoft.studio.unit.y" value="px"/>
<property name="com.jaspersoft.studio.unit.width" value="px"/>
</reportElement>
<textElement textAlignment="Left" verticalAlignment="Middle">
<font fontName="微軟正黑體" size="10"/>
</textElement>
<text><![CDATA[九月]]></text>
</staticText>
<staticText>
<reportElement x="504" y="40" width="56" height="20" uuid="0db0531d-3805-4d02-be13-746e6b97fd14">
<property name="com.jaspersoft.studio.unit.y" value="px"/>
<property name="com.jaspersoft.studio.unit.width" value="px"/>
</reportElement>
<textElement textAlignment="Left" verticalAlignment="Middle">
<font fontName="微軟正黑體" size="10"/>
</textElement>
<text><![CDATA[十月]]></text>
</staticText>
<textField>
<reportElement x="690" y="62" width="90" height="20" uuid="803f19df-9c30-4b63-a930-69955daf442e">
<property name="com.jaspersoft.studio.unit.height" value="px"/>
<property name="com.jaspersoft.studio.unit.width" value="px"/>
</reportElement>
<textElement textAlignment="Left" verticalAlignment="Top">
<font fontName="微軟正黑體" size="10"/>
</textElement>
<textFieldExpression><![CDATA[$F{totalProductionQty}]]></textFieldExpression>
</textField>
<line>
<reportElement x="0" y="89" width="799" height="1" uuid="72083934-812f-42b9-b29b-b73d98fe6925">
<property name="com.jaspersoft.studio.unit.height" value="px"/>
</reportElement>
</line>
<line>
<reportElement x="0" y="90" width="799" height="1" uuid="f0436daf-da7a-42fd-8514-c5c70716792d">
<property name="com.jaspersoft.studio.unit.height" value="px"/>
</reportElement>
</line>
<staticText>
<reportElement x="690" y="40" width="90" height="20" uuid="4de75182-a992-4144-a36b-e49a56fbe89f">
<property name="com.jaspersoft.studio.unit.y" value="px"/>
<property name="com.jaspersoft.studio.unit.width" value="px"/>
</reportElement>
<textElement textAlignment="Left" verticalAlignment="Middle">
<font fontName="微軟正黑體" size="10"/>
</textElement>
<text><![CDATA[總和]]></text>
</staticText>
</band>
</groupHeader>
</group>
<pageHeader>
<band height="140" splitType="Stretch">
<property name="com.jaspersoft.studio.unit.height" value="px"/>
<textField evaluationTime="Report">
<reportElement x="770" y="0" width="30" height="23" uuid="6e9ed0a2-e1e6-4533-a786-f086b868a84c">
<property name="com.jaspersoft.studio.unit.width" value="px"/>
<property name="com.jaspersoft.studio.unit.height" value="px"/>
</reportElement>
<textElement textAlignment="Center" verticalAlignment="Middle"/>
<textFieldExpression><![CDATA[$V{PAGE_NUMBER}]]></textFieldExpression>
</textField>
<staticText>
<reportElement x="690" y="0" width="30" height="23" uuid="5a9e0fa9-418d-4838-9f82-e2f336e5bce7">
<property name="com.jaspersoft.studio.unit.y" value="px"/>
<property name="com.jaspersoft.studio.unit.width" value="px"/>
</reportElement>
<textElement textAlignment="Center" verticalAlignment="Middle">
<font fontName="微軟正黑體" size="12"/>
</textElement>
<text><![CDATA[頁數]]></text>
</staticText>
<staticText>
<reportElement x="750" y="0" width="30" height="23" uuid="26d3d09c-48aa-4870-822a-c403e7faddfa">
<property name="com.jaspersoft.studio.unit.y" value="px"/>
<property name="com.jaspersoft.studio.unit.width" value="px"/>
</reportElement>
<textElement textAlignment="Center" verticalAlignment="Justified">
<font fontName="微軟正黑體" size="12"/>
</textElement>
<text><![CDATA[/]]></text>
</staticText>
<textField>
<reportElement x="730" y="0" width="30" height="23" uuid="0c758a26-1c7f-484d-919e-d215917e9216">
<property name="com.jaspersoft.studio.unit.width" value="px"/>
</reportElement>
<textElement textAlignment="Center" verticalAlignment="Middle"/>
<textFieldExpression><![CDATA[$V{PAGE_NUMBER}]]></textFieldExpression>
</textField>
<staticText>
<reportElement x="0" y="60" width="90" height="23" uuid="0daddd8b-ca61-42af-9c2b-cbf11b7d7dac">
<property name="com.jaspersoft.studio.unit.y" value="px"/>
<property name="com.jaspersoft.studio.unit.height" value="px"/>
</reportElement>
<textElement textAlignment="Left" verticalAlignment="Middle">
<font fontName="微軟正黑體" size="16" isBold="true"/>
</textElement>
<text><![CDATA[報告日期:]]></text>
</staticText>
<textField>
<reportElement x="651" y="90" width="148" height="23" uuid="53b66bc3-4925-4340-add0-b4f2069a76c1"/>
<textElement textAlignment="Right" verticalAlignment="Middle">
<font fontName="微軟正黑體" size="16" isBold="true"/>
</textElement>
<textFieldExpression><![CDATA[$P{year}]]></textFieldExpression>
</textField>
<staticText>
<reportElement x="560" y="60" width="90" height="23" uuid="e2be43a3-3570-4a4e-a671-d96f872a5ad7">
<property name="com.jaspersoft.studio.unit.y" value="px"/>
<property name="com.jaspersoft.studio.unit.height" value="px"/>
</reportElement>
<textElement textAlignment="Right" verticalAlignment="Middle">
<font fontName="微軟正黑體" size="16" isBold="true"/>
</textElement>
<text><![CDATA[報告時間:]]></text>
</staticText>
<staticText>
<reportElement x="560" y="90" width="90" height="23" uuid="29ed871d-3417-4978-9799-964389143451">
<property name="com.jaspersoft.studio.unit.y" value="px"/>
<property name="com.jaspersoft.studio.unit.height" value="px"/>
</reportElement>
<textElement textAlignment="Right" verticalAlignment="Middle">
<font fontName="微軟正黑體" size="16" isBold="true"/>
</textElement>
<text><![CDATA[年份:]]></text>
</staticText>
<textField>
<reportElement x="651" y="60" width="148" height="23" uuid="cb1fbb06-e953-40e5-bdbe-5fc219f7c884"/>
<textElement textAlignment="Right" verticalAlignment="Middle">
<font fontName="微軟正黑體" size="16" isBold="true"/>
</textElement>
<textFieldExpression><![CDATA[$P{reportTime}]]></textFieldExpression>
</textField>
<textField>
<reportElement x="90" y="60" width="190" height="23" uuid="d398cf17-318c-4c16-a8bf-6f064e911965"/>
<textElement>
<font fontName="微軟正黑體" size="16" isBold="true"/>
</textElement>
<textFieldExpression><![CDATA[$P{reportDate}]]></textFieldExpression>
</textField>
<line>
<reportElement x="0" y="129" width="799" height="1" uuid="a7fde15d-ebf2-4516-88ab-fb01689a414e">
<property name="com.jaspersoft.studio.unit.height" value="px"/>
</reportElement>
</line>
<line>
<reportElement x="0" y="130" width="799" height="1" uuid="9e9ba4e3-e369-4180-b01b-1b581e8fa00d">
<property name="com.jaspersoft.studio.unit.height" value="px"/>
</reportElement>
</line>
<staticText>
<reportElement x="280" y="10" width="210" height="39" uuid="7451001b-6d5a-438c-82db-2b9e687d9a27">
<property name="com.jaspersoft.studio.unit.y" value="px"/>
<property name="com.jaspersoft.studio.unit.height" value="px"/>
</reportElement>
<textElement textAlignment="Center" verticalAlignment="Middle">
<font fontName="微軟正黑體" size="16" isBold="true"/>
</textElement>
<text><![CDATA[成品/半成品生產分析報告]]></text>
</staticText>
<textField>
<reportElement x="90" y="90" width="336" height="23" uuid="b51949e1-c40a-4db0-a799-4243555893fd">
<property name="com.jaspersoft.studio.unit.height" value="px"/>
</reportElement>
<textElement textAlignment="Left" verticalAlignment="Middle">
<font fontName="微軟正黑體" size="12" isBold="true"/>
</textElement>
<textFieldExpression><![CDATA[$P{lastOutDateStart} + " 到 " + $P{lastOutDateEnd}]]></textFieldExpression>
</textField>
<staticText>
<reportElement x="0" y="90" width="90" height="23" uuid="fb09a559-f5fa-4e56-a891-87c600a2745a">
<property name="com.jaspersoft.studio.unit.y" value="px"/>
<property name="com.jaspersoft.studio.unit.width" value="px"/>
<property name="com.jaspersoft.studio.unit.height" value="px"/>
</reportElement>
<textElement textAlignment="Left" verticalAlignment="Top">
<font fontName="微軟正黑體" size="12" isBold="true"/>
</textElement>
<text><![CDATA[完成生產日期:]]></text>
</staticText>
</band>
</pageHeader>
<detail>
<band height="22" splitType="Stretch">
<textField>
<reportElement x="280" y="-30" width="56" height="20" uuid="3c77d2e4-afbf-4d52-9ee0-c90174f35809">
<property name="com.jaspersoft.studio.unit.width" value="px"/>
</reportElement>
<textElement textAlignment="Left" verticalAlignment="Top">
<font fontName="微軟正黑體" size="10"/>
</textElement>
<textFieldExpression><![CDATA[$F{qtyJun}]]></textFieldExpression>
</textField>
<textField>
<reportElement x="336" y="-30" width="56" height="20" uuid="b7f7d359-7a9a-4d8f-8f0d-3a579fd92af6">
<property name="com.jaspersoft.studio.unit.width" value="px"/>
</reportElement>
<textElement textAlignment="Left" verticalAlignment="Top">
<font fontName="微軟正黑體" size="10"/>
</textElement>
<textFieldExpression><![CDATA[$F{qtyJul}]]></textFieldExpression>
</textField>
<textField>
<reportElement x="0" y="-30" width="56" height="20" uuid="09cd34d3-ca81-4e96-8e62-9e2b5309b748">
<property name="com.jaspersoft.studio.unit.width" value="px"/>
</reportElement>
<textElement textAlignment="Left" verticalAlignment="Top">
<font fontName="微軟正黑體" size="10"/>
</textElement>
<textFieldExpression><![CDATA[$F{qtyJan}]]></textFieldExpression>
</textField>
<textField>
<reportElement x="560" y="-30" width="56" height="20" uuid="30c01289-b963-4063-be9f-42a87e0e37d1">
<property name="com.jaspersoft.studio.unit.width" value="px"/>
</reportElement>
<textElement textAlignment="Left" verticalAlignment="Top">
<font fontName="微軟正黑體" size="10"/>
</textElement>
<textFieldExpression><![CDATA[$F{qtyNov}]]></textFieldExpression>
</textField>
<textField>
<reportElement x="616" y="-30" width="74" height="20" uuid="b4f317e7-6c8f-4f37-b78d-00005559e398">
<property name="com.jaspersoft.studio.unit.width" value="px"/>
</reportElement>
<textElement textAlignment="Left" verticalAlignment="Top">
<font fontName="微軟正黑體" size="10"/>
</textElement>
<textFieldExpression><![CDATA[$F{qtyDec}]]></textFieldExpression>
</textField>
<textField>
<reportElement x="392" y="-30" width="56" height="20" uuid="a84b08f0-123f-40ff-988b-7f9ac62cec09">
<property name="com.jaspersoft.studio.unit.width" value="px"/>
</reportElement>
<textElement textAlignment="Left" verticalAlignment="Top">
<font fontName="微軟正黑體" size="10"/>
</textElement>
<textFieldExpression><![CDATA[$F{qtyAug}]]></textFieldExpression>
</textField>
<textField>
<reportElement x="448" y="-30" width="56" height="20" uuid="b4a7d70f-ab5a-4303-b6a8-70f76fda74df">
<property name="com.jaspersoft.studio.unit.width" value="px"/>
</reportElement>
<textElement textAlignment="Left" verticalAlignment="Top">
<font fontName="微軟正黑體" size="10"/>
</textElement>
<textFieldExpression><![CDATA[$F{qtySep}]]></textFieldExpression>
</textField>
<textField>
<reportElement x="504" y="-30" width="56" height="20" uuid="3661155a-e86a-4fe7-9753-2f3893aed786">
<property name="com.jaspersoft.studio.unit.width" value="px"/>
</reportElement>
<textElement textAlignment="Left" verticalAlignment="Top">
<font fontName="微軟正黑體" size="10"/>
</textElement>
<textFieldExpression><![CDATA[$F{qtyOct}]]></textFieldExpression>
</textField>
<textField>
<reportElement x="112" y="-30" width="56" height="20" uuid="6393dfd5-1fc6-4ee0-8044-9b2bae5d5019">
<property name="com.jaspersoft.studio.unit.width" value="px"/>
</reportElement>
<textElement textAlignment="Left" verticalAlignment="Top">
<font fontName="微軟正黑體" size="10"/>
</textElement>
<textFieldExpression><![CDATA[$F{qtyMar}]]></textFieldExpression>
</textField>
<textField>
<reportElement x="168" y="-30" width="56" height="20" uuid="e4f89d0a-4dc5-4408-a99e-51f5458c77ac">
<property name="com.jaspersoft.studio.unit.width" value="px"/>
</reportElement>
<textElement textAlignment="Left" verticalAlignment="Top">
<font fontName="微軟正黑體" size="10"/>
</textElement>
<textFieldExpression><![CDATA[$F{qtyApr}]]></textFieldExpression>
</textField>
<textField>
<reportElement x="224" y="-30" width="56" height="20" uuid="c2b581c3-979e-4450-b077-5017c7f485b0">
<property name="com.jaspersoft.studio.unit.width" value="px"/>
</reportElement>
<textElement textAlignment="Left" verticalAlignment="Top">
<font fontName="微軟正黑體" size="10"/>
</textElement>
<textFieldExpression><![CDATA[$F{qtyMay}]]></textFieldExpression>
</textField>
<textField>
<reportElement x="56" y="-30" width="56" height="20" uuid="0153c92a-ff58-457d-9ece-bd61f349530b">
<property name="com.jaspersoft.studio.unit.width" value="px"/>
</reportElement>
<textElement textAlignment="Left" verticalAlignment="Top">
<font fontName="微軟正黑體" size="10"/>
</textElement>
<textFieldExpression><![CDATA[$F{qtyFeb}]]></textFieldExpression>
</textField>
</band>
</detail>
</jasperReport>

Ładowanie…
Anuluj
Zapisz