From 81378f6b8abffec712ecb0f5a8ab87ae08c3c463 Mon Sep 17 00:00:00 2001 From: tommy Date: Mon, 2 Nov 2026 15:50:51 +0800 Subject: [PATCH] =?UTF-8?q?=E6=8E=92=E7=94=A2=20Excel=20=E5=9C=A8=E3=80=8C?= =?UTF-8?q?=E7=89=A9=E6=96=99=E7=94=A8=E9=87=8F=E7=B8=BD=E7=B5=90=E3=80=8D?= =?UTF-8?q?=E5=8A=A0=E5=85=A5=E3=80=8C=E9=9C=80=E6=8E=A1=E8=B3=BC/?= =?UTF-8?q?=E7=94=9F=E7=94=A2=E6=95=B8=E9=87=8F=E3=80=8D=EF=BC=9D=E4=B8=8B?= =?UTF-8?q?=E9=80=B1=E7=94=A8=E9=87=8F=20=E2=88=92=20=E5=BA=AB=E5=AD=98?= =?UTF-8?q?=EF=BC=88=E4=B8=8D=E6=89=A3=20PO=EF=BC=8C=E5=8F=AF=E7=82=BA?= =?UTF-8?q?=E8=B2=A0=EF=BC=89=20=E6=AC=84=E5=AF=AC=E5=8A=A0=E5=AF=AC?= =?UTF-8?q?=EF=BC=9B=E4=B8=8D=E6=94=BE=E5=9C=A8=E6=AF=8F=E6=97=A5=E5=88=86?= =?UTF-8?q?=E9=A0=81=20=E5=B7=A5=E5=96=AE=20=E5=88=97=E8=A1=A8=E9=A1=AF?= =?UTF-8?q?=E7=A4=BA=20BOM=20=E5=B7=A5=E5=BA=8F=E6=A8=99=E6=BA=96=E5=B7=A5?= =?UTF-8?q?=E6=99=82=EF=BC=88=E9=A0=90=E4=BC=B0=E5=88=86=E9=90=98=EF=BC=89?= =?UTF-8?q?=20=E3=80=8CBOM=E6=9D=90=E6=96=99=E3=80=8D=E2=86=92=E3=80=8C?= =?UTF-8?q?=E6=9D=90=E6=96=99=E3=80=8D=20=E8=80=97=E6=9D=90=E3=80=8C?= =?UTF-8?q?=E4=B8=8D=E9=81=A9=E7=94=A8=E3=80=8D=E2=86=92=E3=80=8C=E4=B8=8D?= =?UTF-8?q?=E7=94=A8=E6=8F=90=E6=96=99=E3=80=8D=20BOM=20=E5=B7=A5=E8=97=9D?= =?UTF-8?q?=20=E6=96=B0=E5=A2=9E=20bom.details=EF=BC=88=E5=90=AB=20Liquiba?= =?UTF-8?q?se=EF=BC=89=20=E3=80=8C=E5=B7=A5=E8=97=9D=E6=98=8E=E7=B4=B0?= =?UTF-8?q?=E3=80=8D=E6=94=B9=E7=82=BA=E5=8F=AF=E5=84=B2=E5=AD=98=E7=9A=84?= =?UTF-8?q?=E8=87=AA=E7=94=B1=E6=96=87=E5=AD=97=EF=BC=9B=E3=80=8C=E5=B7=A5?= =?UTF-8?q?=E8=97=9D=E6=B5=81=E7=A8=8B=E3=80=8D=E4=BB=8D=E7=94=A8=E5=90=8D?= =?UTF-8?q?=E7=A8=B1=20=E7=9C=8B=E6=9D=BF=20/=20=E5=88=86=E9=A0=81=20?= =?UTF-8?q?=E5=B7=A5=E5=96=AE=E7=8B=80=E6=85=8B=E7=9C=8B=E6=9D=BF=E7=B6=93?= =?UTF-8?q?=E6=89=8B=E4=BA=BA=EF=BC=9A=E7=84=A1=20handler=20=E6=99=82?= =?UTF-8?q?=E5=9B=9E=E9=80=80=E9=A1=AF=E7=A4=BA=20operator=20=E9=9A=B1?= =?UTF-8?q?=E8=97=8F=E9=A3=B2=E6=96=99=E7=94=9F=E7=94=A2=E6=95=B8=E9=87=8F?= =?UTF-8?q?=E5=88=86=E9=A0=81=20=E6=8E=92=E7=94=A2=E8=A6=8F=E5=89=87?= =?UTF-8?q?=E6=96=87=E6=A1=88=20=E3=80=8C=E4=B8=80=E8=88=AC=E6=97=A5?= =?UTF-8?q?=E3=80=8D=E2=86=92=E3=80=8C=E4=B8=80=E8=88=AC=E8=A6=8F=E5=89=87?= =?UTF-8?q?=E3=80=8D=20=E3=80=8C=E5=A4=9A=E5=82=99=E6=97=A5=E3=80=8D?= =?UTF-8?q?=E7=AD=89=E3=80=8C=E5=A4=9A=E5=82=99=E3=80=8D=E7=9B=B8=E9=97=9C?= =?UTF-8?q?=20=E2=86=92=E3=80=8C=E7=89=B9=E5=88=A5=E8=A6=8F=E5=89=87?= =?UTF-8?q?=E3=80=8D=20=E3=80=8C=E5=A4=9A=E5=82=99=E6=98=9F=E6=9C=9F?= =?UTF-8?q?=E3=80=8D=E2=86=92=E3=80=8C=E7=89=B9=E5=88=A5=E8=A6=8F=E5=89=87?= =?UTF-8?q?=E9=81=A9=E7=94=A8=E6=98=9F=E6=9C=9F=E3=80=8D=EF=BC=88=E5=89=8D?= =?UTF-8?q?=E5=BE=8C=E7=AB=AF=E5=90=8C=E6=AD=A5=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../entity/projections/JobOrderInfo.kt | 4 +++- .../jobOrder/service/JobOrderService.kt | 17 ++++++++++++++++- .../jobOrder/service/PsScheduleRuleService.kt | 6 +++--- .../ffii/fpsms/modules/master/entity/Bom.kt | 4 ++++ .../master/entity/BomProcessRepository.kt | 18 ++++++++++++++++++ .../fpsms/modules/master/service/BomService.kt | 13 +++++++++++++ .../service/ProductionScheduleService.kt | 10 ++++++---- .../fpsms/modules/master/web/BomController.kt | 10 ++++++++++ .../entity/projections/ProductProcessInfo.kt | 2 ++ .../service/ProductProcessService.kt | 5 ++++- 10 files changed, 79 insertions(+), 10 deletions(-) diff --git a/src/main/java/com/ffii/fpsms/modules/jobOrder/entity/projections/JobOrderInfo.kt b/src/main/java/com/ffii/fpsms/modules/jobOrder/entity/projections/JobOrderInfo.kt index 3bbcf154..288ffe8c 100644 --- a/src/main/java/com/ffii/fpsms/modules/jobOrder/entity/projections/JobOrderInfo.kt +++ b/src/main/java/com/ffii/fpsms/modules/jobOrder/entity/projections/JobOrderInfo.kt @@ -68,7 +68,9 @@ data class JobOrderInfoWithTypeName( val status: String, val jobTypeId: Long?, val jobTypeName: String?, - val lotNo: String? + val lotNo: String?, + /** Standard BOM process minutes (prep + duration + post). Null if unknown. */ + val estimatedMinutes: Int? = null, ) // Job Order interface JobOrderDetailWithJsonString { diff --git a/src/main/java/com/ffii/fpsms/modules/jobOrder/service/JobOrderService.kt b/src/main/java/com/ffii/fpsms/modules/jobOrder/service/JobOrderService.kt index ce3b1b0e..20f738ca 100644 --- a/src/main/java/com/ffii/fpsms/modules/jobOrder/service/JobOrderService.kt +++ b/src/main/java/com/ffii/fpsms/modules/jobOrder/service/JobOrderService.kt @@ -69,6 +69,7 @@ import java.math.RoundingMode import java.time.LocalDate import java.time.LocalDateTime import com.ffii.fpsms.modules.master.entity.BomMaterialRepository +import com.ffii.fpsms.modules.master.entity.BomProcessRepository import com.ffii.fpsms.modules.master.service.ItemUomService import com.ffii.fpsms.modules.master.web.models.ConvertUomByItemRequest import com.ffii.fpsms.modules.stock.service.StockInLineService @@ -96,6 +97,7 @@ open class JobOrderService( val productProcessRepository: ProductProcessRepository, val jobOrderBomMaterialRepository: JobOrderBomMaterialRepository, val bomMaterialRepository: BomMaterialRepository, + val bomProcessRepository: BomProcessRepository, val itemUomService: ItemUomService ) { @@ -171,6 +173,15 @@ open class JobOrderService( } else { emptyMap() } + + val bomIds = jobOrdersMap.values.mapNotNull { it.bom?.id }.distinct() + val estimatedMinutesByBomId: Map = if (bomIds.isNotEmpty()) { + bomProcessRepository.sumStandardMinutesByBomIds(bomIds).associate { agg -> + agg.bomId to agg.totalMinutes.toInt().coerceAtLeast(0) + } + } else { + emptyMap() + } // 移除内存中的日期过滤,因为已经在数据库层面过滤了 val records = response.content @@ -184,6 +195,9 @@ open class JobOrderService( Pair(null, null) } + + val bomId = jobOrder?.bom?.id + val estimatedMinutes = bomId?.let { estimatedMinutesByBomId[it] } JobOrderInfoWithTypeName( id = info.id, @@ -210,7 +224,8 @@ open class JobOrderService( }, jobTypeId = info.jobTypeId, jobTypeName = info.jobTypeId?.let { jobTypes[it]?.name }, - lotNo = info.lotNo + lotNo = info.lotNo, + estimatedMinutes = estimatedMinutes, ) } .filter { info -> diff --git a/src/main/java/com/ffii/fpsms/modules/jobOrder/service/PsScheduleRuleService.kt b/src/main/java/com/ffii/fpsms/modules/jobOrder/service/PsScheduleRuleService.kt index 7f81c455..b13782b8 100644 --- a/src/main/java/com/ffii/fpsms/modules/jobOrder/service/PsScheduleRuleService.kt +++ b/src/main/java/com/ffii/fpsms/modules/jobOrder/service/PsScheduleRuleService.kt @@ -134,12 +134,12 @@ open class PsScheduleRuleService( } fun savePlantRules(request: SavePsPlantRulesRequest) { - requireDays(request.redLineDays, request.safetyStockDays, UiText.t("一般日", "Normal days")) - requireDays(request.prepareMoreRedLineDays, request.prepareMoreSafetyStockDays, UiText.t("多備日", "Extra-prep days")) + requireDays(request.redLineDays, request.safetyStockDays, UiText.t("一般規則", "Normal rules")) + requireDays(request.prepareMoreRedLineDays, request.prepareMoreSafetyStockDays, UiText.t("特別規則", "Special rules")) val coffee = requireProduceDays(request.coffeeDays, UiText.t("類別 A", "Category A")) val tea = requireProduceDays(request.teaDays, UiText.t("類別 B", "Category B")) val lemon = requireProduceDays(request.lemonDays, UiText.t("類別 C", "Category C")) - val prepareMore = parseDaysStrict(request.prepareMoreDays, UiText.t("多備星期", "Extra-prep weekdays")) + val prepareMore = parseDaysStrict(request.prepareMoreDays, UiText.t("特別規則適用星期", "Special-rule weekdays")) val noProduction = parseDaysStrict(request.noProductionDays, UiText.t("不生產星期", "Non-production weekdays")) upsert(RED_LINE, formatNumber(request.redLineDays), "float") diff --git a/src/main/java/com/ffii/fpsms/modules/master/entity/Bom.kt b/src/main/java/com/ffii/fpsms/modules/master/entity/Bom.kt index 3307651b..38adc3c8 100644 --- a/src/main/java/com/ffii/fpsms/modules/master/entity/Bom.kt +++ b/src/main/java/com/ffii/fpsms/modules/master/entity/Bom.kt @@ -56,6 +56,10 @@ open class Bom : BaseEntity() { @Column(name = "description", nullable = false, length = 100) open var description: String? = null + /** Free-text process / craft details shown on job order 工藝明細. */ + @Column(name = "details", columnDefinition = "TEXT") + open var details: String? = null + @Column(name = "outputQty", precision = 14, scale = 2) open var outputQty: BigDecimal? = null diff --git a/src/main/java/com/ffii/fpsms/modules/master/entity/BomProcessRepository.kt b/src/main/java/com/ffii/fpsms/modules/master/entity/BomProcessRepository.kt index d15a0e9d..a61dcd6a 100644 --- a/src/main/java/com/ffii/fpsms/modules/master/entity/BomProcessRepository.kt +++ b/src/main/java/com/ffii/fpsms/modules/master/entity/BomProcessRepository.kt @@ -30,4 +30,22 @@ interface BomProcessRepository : AbstractRepository { """ ) fun sumDurationByBomIds(@Param("bomIds") bomIds: Collection): List + + /** prep + duration + post for JO list estimated time */ + @Query( + """ + SELECT + bp.bom.id AS bomId, + COALESCE(SUM( + COALESCE(bp.prepTimeInMinute, 0) + + COALESCE(bp.durationInMinute, 0) + + COALESCE(bp.postProdTimeInMinute, 0) + ), 0) AS totalMinutes + FROM BomProcess bp + WHERE bp.deleted = false + AND bp.bom.id IN :bomIds + GROUP BY bp.bom.id + """ + ) + fun sumStandardMinutesByBomIds(@Param("bomIds") bomIds: Collection): List } \ No newline at end of file diff --git a/src/main/java/com/ffii/fpsms/modules/master/service/BomService.kt b/src/main/java/com/ffii/fpsms/modules/master/service/BomService.kt index d9b1c0d6..6b961a16 100644 --- a/src/main/java/com/ffii/fpsms/modules/master/service/BomService.kt +++ b/src/main/java/com/ffii/fpsms/modules/master/service/BomService.kt @@ -212,6 +212,19 @@ open class BomService( return response } + /** Update free-text craft details (工藝明細) on BOM. */ + @Transactional + open fun updateBomDetails(id: Long, details: String?): Map { + val bom = bomRepository.findByIdAndDeletedIsFalse(id) + ?: throw NotFoundException() + bom.details = details + bomRepository.saveAndFlush(bom) + return mapOf( + "id" to bom.id, + "details" to bom.details, + ) + } + /** * Edit BOM (basic fields + BOM materials + BOM process lines). * diff --git a/src/main/java/com/ffii/fpsms/modules/master/service/ProductionScheduleService.kt b/src/main/java/com/ffii/fpsms/modules/master/service/ProductionScheduleService.kt index d0f1bb48..8ee83afd 100644 --- a/src/main/java/com/ffii/fpsms/modules/master/service/ProductionScheduleService.kt +++ b/src/main/java/com/ffii/fpsms/modules/master/service/ProductionScheduleService.kt @@ -1564,7 +1564,8 @@ open class ProductionScheduleService( val matSheet = workbook.createSheet(UiText.t("物料用量總結", "Material usage summary")) val matHeaders = listOf( - UiText.t("物料編號", "Material code"), UiText.t("物料名稱", "Material name"), UiText.t("需求量", "Required qty"), UiText.t("總用量", "Total usage"), + UiText.t("物料編號", "Material code"), UiText.t("物料名稱", "Material name"), + UiText.t("需採購/生產數量", "Qty to purchase/produce"), UiText.t("總用量", "Total usage"), UiText.t("單位", "UOM"), UiText.t("數量(採購單位)", "Qty (purchase UOM)"), UiText.t("採購單位", "Purchase UOM"), UiText.t("已採購數量", "Purchased qty"), UiText.t("庫存數", "On hand"), UiText.t("庫存單位", "Stock UOM"), UiText.t("相關成品編號", "Related finished-good code"), UiText.t("相關成品名稱", "Related finished-good name") @@ -1586,12 +1587,13 @@ open class ProductionScheduleService( val purchaseQtyNeed = asDouble(rowData["purchaseQtyNeed"]) val purchased = asDouble(rowData["purchasedQty"]) val onHand = asDouble(rowData["onHandQty"]) - val requiredQty = (totalNeed - purchased - onHand).coerceAtLeast(0.0) + // 總用量 − 庫存;不扣在途採購。負數代表有餘量。 + val qtyToPurchaseOrProduce = totalNeed - onHand val values = listOf( rowData["matCode"]?.toString() ?: "", rowData["matName"]?.toString() ?: "", - requiredQty, + qtyToPurchaseOrProduce, totalNeed, rowData["uomName"]?.toString() ?: "", purchaseQtyNeed, @@ -1628,7 +1630,7 @@ open class ProductionScheduleService( // Column widths for material summary matSheet.setColumnWidth(0, 20 * 256) matSheet.setColumnWidth(1, 50 * 256) // 物料名稱 - matSheet.setColumnWidth(2, 16 * 256) + matSheet.setColumnWidth(2, 22 * 256) // 需採購/生產數量 matSheet.setColumnWidth(3, 16 * 256) matSheet.setColumnWidth(4, 12 * 256) matSheet.setColumnWidth(5, 16 * 256) diff --git a/src/main/java/com/ffii/fpsms/modules/master/web/BomController.kt b/src/main/java/com/ffii/fpsms/modules/master/web/BomController.kt index 67210170..47a93299 100644 --- a/src/main/java/com/ffii/fpsms/modules/master/web/BomController.kt +++ b/src/main/java/com/ffii/fpsms/modules/master/web/BomController.kt @@ -29,6 +29,7 @@ import com.ffii.fpsms.modules.master.web.models.BomFormatCheckRequest import com.ffii.fpsms.modules.master.web.models.ImportBomRequestPayload import com.ffii.fpsms.modules.master.web.models.BomDetailResponse import com.ffii.fpsms.modules.master.web.models.EditBomRequest +import com.ffii.fpsms.modules.master.web.models.UpdateBomDetailsRequest import com.ffii.fpsms.modules.master.web.models.BomExcelCheckProgress import com.ffii.fpsms.modules.master.web.models.BomIdByItemCodeResponse import com.ffii.fpsms.modules.master.web.models.MasterDataIssueResponse @@ -169,4 +170,13 @@ fun getBomDetail(@PathVariable id: Long): BomDetailResponse { ): BomDetailResponse { return bomService.editBom(id, request) } + + /** Update free-text craft details (工藝明細) only. */ + @PutMapping("/{id}/details") + fun updateBomDetails( + @PathVariable id: Long, + @RequestBody request: UpdateBomDetailsRequest, + ): Map { + return bomService.updateBomDetails(id, request.details) + } } \ No newline at end of file diff --git a/src/main/java/com/ffii/fpsms/modules/productProcess/entity/projections/ProductProcessInfo.kt b/src/main/java/com/ffii/fpsms/modules/productProcess/entity/projections/ProductProcessInfo.kt index c1a2429a..4aa5b620 100644 --- a/src/main/java/com/ffii/fpsms/modules/productProcess/entity/projections/ProductProcessInfo.kt +++ b/src/main/java/com/ffii/fpsms/modules/productProcess/entity/projections/ProductProcessInfo.kt @@ -24,6 +24,8 @@ data class ProductProcessInfo( val jobOrderStatus: String?, val jobType: String?, val bomDescription: String?, + /** Free-text craft details from bom.details */ + val bomDetails: String? = null, val isDark: String?, val isDense: Int?, val isFloat: String?, diff --git a/src/main/java/com/ffii/fpsms/modules/productProcess/service/ProductProcessService.kt b/src/main/java/com/ffii/fpsms/modules/productProcess/service/ProductProcessService.kt index bd274731..a37f9170 100644 --- a/src/main/java/com/ffii/fpsms/modules/productProcess/service/ProductProcessService.kt +++ b/src/main/java/com/ffii/fpsms/modules/productProcess/service/ProductProcessService.kt @@ -674,6 +674,7 @@ open class ProductProcessService( jobOrderStatus = jobOrder?.status?.value ?: "", jobType = jobType?.name ?: "", bomDescription = bom?.description ?: "", + bomDetails = bom?.details, itemId = bom?.item?.id ?: 0, itemCode = bom?.item?.code ?: "", itemName = bom?.item?.name ?: "", @@ -2602,7 +2603,9 @@ open class ProductProcessService( processName = line.name, equipmentName = equipmentName, equipmentDetailName = equipmentDetailName, - handlerName = line.handler?.name, + // Scan staff writes operator; startLine writes handler. Prefer handler, fall back to operator. + handlerName = line.handler?.name?.takeIf { it.isNotBlank() } + ?: line.operator?.name, startTime = line.startTime, endTime = line.endTime, processingTime = line.processingTime,