From b4d754c33b2ed967d3f87b3300695d974cc23c08 Mon Sep 17 00:00:00 2001 From: "vluk@2fi-solutions.com.hk" Date: Tue, 3 Feb 2026 13:28:13 +0800 Subject: [PATCH] no message --- .../service/ProductionScheduleService.kt | 228 ++++++++++-------- 1 file changed, 122 insertions(+), 106 deletions(-) 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 d25c9d7..07cc9ec 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 @@ -647,7 +647,8 @@ open class ProductionScheduleService( 0 AS batchNeed, i.pendingJobQty, ((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, i.* FROM @@ -664,14 +665,14 @@ open class ProductionScheduleService( do.deleted = 0 and dol.itemId = items.id -- AND MONTH(do.estimatedArrivalDate) = MONTH(DATE_SUB(NOW(), INTERVAL 1 MONTH)) - AND do.estimatedArrivalDate >= '2026-01-12' AND do.estimatedArrivalDate < '2026-01-16' + AND do.estimatedArrivalDate >= '2026-01-08' AND do.estimatedArrivalDate < '2026-01-18' GROUP BY do.estimatedArrivalDate) AS d) AS avgQtyLastMonth, (select sum(reqQty) from job_order where bomId = bom.id and status != 'completed') AS pendingJobQty, CASE WHEN item_fake_onhand.onHandQty is not null THEN item_fake_onhand.onHandQty ELSE inventory.onHandQty - 500 END AS stockQty, - + bom.baseScore, bom.outputQty, bom.outputQtyUom, (SELECT @@ -820,10 +821,12 @@ open class ProductionScheduleService( tempPriority++ } - saveDetailedScheduleOutput(sortedOutputList, accProdCount, fgCount, produceAt) + println("[INFO] sortedOutputList.size " + sortedOutputList.size + " have bee skipped"); + //ignore the -1 date + //saveDetailedScheduleOutput(sortedOutputList, accProdCount, fgCount, produceAt) //do for 9 days to predict - for (i in 1..(days + 1)) { + for (i in 0..(days)) { val targetDate = produceAt.plusDays(i.toLong()) val isSat = targetDate.dayOfWeek == DayOfWeek.SATURDAY val isFri = targetDate.dayOfWeek == DayOfWeek.FRIDAY @@ -837,7 +840,7 @@ open class ProductionScheduleService( fgCount = 0 accProdCount = 0.0 sortedOutputList.forEach { record -> - if(i > 1) + if(i > 0) record.stockQty = record.stockQty + (record.outputQty * record.needNoOfJobOrder.toInt()) - record.avgQtyLastMonth else record.stockQty = record.stockQty + (record.outputQty * record.needNoOfJobOrder.toInt()) @@ -1411,7 +1414,7 @@ open class ProductionScheduleService( ): ByteArray { val workbook = XSSFWorkbook() - // Header style + // ── Common Styles ── val headerStyle = workbook.createCellStyle().apply { fillForegroundColor = IndexedColors.GREY_25_PERCENT.index fillPattern = FillPatternType.SOLID_FOREGROUND @@ -1422,13 +1425,26 @@ open class ProductionScheduleService( setFont(font) } - // Body style val wrapStyle = workbook.createCellStyle().apply { wrapText = true verticalAlignment = VerticalAlignment.TOP } - // Group production lines by date + // Number style with thousands separator (integer) + val numberStyle = workbook.createCellStyle().apply { + wrapText = true + verticalAlignment = VerticalAlignment.TOP + dataFormat = workbook.createDataFormat().getFormat("#,##0") + } + + val numberDigitStyle = workbook.createCellStyle().apply { + wrapText = true + verticalAlignment = VerticalAlignment.TOP + dataFormat = workbook.createDataFormat().getFormat("#,##0.0") + } + + + // ── Group production lines by date ── val groupedData = lines.groupBy { val produceAt = it["produceAt"] when (produceAt) { @@ -1439,7 +1455,7 @@ open class ProductionScheduleService( } } - // Production sheets (one per date) + // ── Production sheets (one per date) ── groupedData.forEach { (dateKey, dailyLines) -> val safeSheetName = dateKey.replace(Regex("[/\\\\?*:\\[\\]]"), "-").take(31) val sheet = workbook.createSheet(safeSheetName) @@ -1461,38 +1477,42 @@ open class ProductionScheduleService( // Data rows dailyLines.forEachIndexed { index, line -> val row = sheet.createRow(index + 1) - row.heightInPoints = 35f // Slightly taller for portrait readability + row.heightInPoints = 35f - var j = 0; + var j = 0 row.createCell(j++).apply { setCellValue(line["itemCode"]?.toString() ?: ""); cellStyle = wrapStyle } row.createCell(j++).apply { setCellValue(line["itemName"]?.toString() ?: ""); cellStyle = wrapStyle } - row.createCell(j++).apply { setCellValue(asDouble(line["avgQtyLastMonth"])); cellStyle = wrapStyle } - row.createCell(j++).apply { setCellValue(asDouble(line["stockQty"])); cellStyle = wrapStyle } - row.createCell(j++).apply { setCellValue(asDouble(line["daysLeft"])); cellStyle = wrapStyle } - row.createCell(j++).apply { setCellValue(asDouble(line["outputdQty"] ?: line["outputQty"])); cellStyle = wrapStyle } - row.createCell(j++).apply { setCellValue(asDouble(line["batchNeed"])); cellStyle = wrapStyle } - row.createCell(j++).apply { setCellValue(asDouble(line["itemPriority"])); cellStyle = wrapStyle } + row.createCell(j++).apply { setCellValue(asDouble(line["avgQtyLastMonth"])); cellStyle = numberStyle } + row.createCell(j++).apply { setCellValue(asDouble(line["stockQty"])); cellStyle = numberStyle } + row.createCell(j++).apply { setCellValue(asDouble(line["daysLeft"])); cellStyle = numberDigitStyle } + row.createCell(j++).apply { setCellValue(asDouble(line["outputdQty"] ?: line["outputQty"])); cellStyle = numberStyle } + row.createCell(j++).apply { setCellValue(asDouble(line["batchNeed"])); cellStyle = numberStyle } + row.createCell(j++).apply { setCellValue(asDouble(line["itemPriority"])); cellStyle = numberStyle } } - // Auto-size with wider limits for portrait - for (i in headers.indices) { - sheet.autoSizeColumn(i) - val maxWidth = when (i) { - 0 -> 35 * 256 // Item Name can be longer - else -> 18 * 256 - } - if (sheet.getColumnWidth(i) > maxWidth) { - sheet.setColumnWidth(i, maxWidth) - } + // ── FIXED COLUMN WIDTHS ── (much wider, especially for text columns) + sheet.setColumnWidth(0, 24 * 256) // 物料編號 - wider for codes like ABC-12345-XYZ + sheet.setColumnWidth(1, 90 * 256) // 物料名稱 - very generous for long Chinese descriptions + sheet.setColumnWidth(2, 20 * 256) // 每日平均出貨量 + sheet.setColumnWidth(3, 22 * 256) // 出貨前預計存貨量 + sheet.setColumnWidth(4, 14 * 256) // 可用日 + sheet.setColumnWidth(5, 18 * 256) // 每批數量 + sheet.setColumnWidth(6, 18 * 256) // 生產量(批) + sheet.setColumnWidth(7, 14 * 256) // 優先度 + + // Optional: still allow a little auto-sizing on the longest text column + sheet.autoSizeColumn(1) + // But cap it so it doesn't become excessively wide + if (sheet.getColumnWidth(1) > 110 * 256) { + sheet.setColumnWidth(1, 110 * 256) } - // === PORTRAIT PRINT SETUP === + // ── Print setup - Portrait ── val printSetup = sheet.printSetup printSetup.paperSize = XSSFPrintSetup.A4_PAPERSIZE - printSetup.landscape = false // ← Portrait mode - printSetup.fitWidth = 1.toShort() // Crucial: scale to fit width - printSetup.fitHeight = 0.toShort() // Allow multiple pages tall - + printSetup.landscape = false + printSetup.fitWidth = 1.toShort() + printSetup.fitHeight = 0.toShort() sheet.fitToPage = true sheet.horizontallyCenter = true sheet.setMargin(Sheet.LeftMargin, 0.5) @@ -1501,11 +1521,11 @@ open class ProductionScheduleService( sheet.setMargin(Sheet.BottomMargin, 0.7) } - // === MATERIAL SUMMARY SHEET - PORTRAIT OPTIMIZED === - val matSheet = workbook.createSheet("Material Summary") + // ── MATERIAL SUMMARY SHEET ── + val matSheet = workbook.createSheet("物料用量總結") val matHeaders = listOf( - "物料編號", "物料名稱", "欠缺需量", "總用量", + "物料編號", "物料名稱", "需求量", "總用量", "單位", "數量(採購單位)", "採購單位", "已採購數量", "庫存數", "庫存單位", "相關成品編號", "相關成品名稱" @@ -1547,35 +1567,45 @@ open class ProductionScheduleService( values.forEachIndexed { i, value -> val cell = row.createCell(i) when (value) { - is String -> cell.setCellValue(value) - is Number -> cell.setCellValue(value.toDouble()) - else -> cell.setCellValue("") + is String -> { + cell.setCellValue(value) + cell.cellStyle = wrapStyle + } + is Number -> { + cell.setCellValue(value.toDouble()) + cell.cellStyle = when (i) { + 2, 3, 5, 7, 8 -> numberStyle + else -> wrapStyle + } + } + else -> { + cell.setCellValue("") + cell.cellStyle = wrapStyle + } } - cell.cellStyle = wrapStyle } } - // Manual column widths optimized for PORTRAIT A4 - matSheet.setColumnWidth(0, 16 * 256) // Mat Code - matSheet.setColumnWidth(1, 32 * 256) // Mat Name - matSheet.setColumnWidth(2, 14 * 256) // Required Qty - matSheet.setColumnWidth(3, 14 * 256) // Total Qty Need - matSheet.setColumnWidth(4, 10 * 256) // UoM - matSheet.setColumnWidth(5, 14 * 256) // Purchase Qty Need - matSheet.setColumnWidth(6, 10 * 256) // Purchase UoM - matSheet.setColumnWidth(7, 14 * 256) // Purchased Qty - matSheet.setColumnWidth(8, 14 * 256) // On Hand Qty - matSheet.setColumnWidth(9, 14 * 256) // Unavailable Qty - matSheet.setColumnWidth(10, 22 * 256) // Related Item Code - matSheet.setColumnWidth(11, 40 * 256) // Related Item Name (longest) + // Column widths for material summary + matSheet.setColumnWidth(0, 20 * 256) + matSheet.setColumnWidth(1, 50 * 256) // 物料名稱 + matSheet.setColumnWidth(2, 16 * 256) + matSheet.setColumnWidth(3, 16 * 256) + matSheet.setColumnWidth(4, 12 * 256) + matSheet.setColumnWidth(5, 16 * 256) + matSheet.setColumnWidth(6, 12 * 256) + matSheet.setColumnWidth(7, 16 * 256) + matSheet.setColumnWidth(8, 16 * 256) + matSheet.setColumnWidth(9, 12 * 256) + matSheet.setColumnWidth(10, 22 * 256) + matSheet.setColumnWidth(11, 60 * 256) // 相關成品名稱 - wider // Portrait print setup val matPrintSetup = matSheet.printSetup matPrintSetup.paperSize = XSSFPrintSetup.A4_PAPERSIZE - matPrintSetup.landscape = false // ← Portrait + matPrintSetup.landscape = false matPrintSetup.fitWidth = 1.toShort() matPrintSetup.fitHeight = 0.toShort() - matSheet.fitToPage = true matSheet.horizontallyCenter = true matSheet.setMargin(Sheet.LeftMargin, 0.5) @@ -1583,59 +1613,41 @@ open class ProductionScheduleService( matSheet.setMargin(Sheet.TopMargin, 0.7) matSheet.setMargin(Sheet.BottomMargin, 0.7) - // === DAILY MATERIAL NEEDS SHEET - CLEAN & READABLE DATES === - val dailySheet = workbook.createSheet("Daily Material Needs") + // ── DAILY MATERIAL NEEDS SHEET ── + val dailySheet = workbook.createSheet("每天物料用量") - // Generate nicely formatted date headers: "10 Jan" on first row, day of week on second val dateFormatterDayMonth = DateTimeFormatter.ofPattern("d MMM", Locale.ENGLISH) - val dateFormatterDayName = DateTimeFormatter.ofPattern("EEEE", Locale.ENGLISH) // Monday, Tuesday... + val dateFormatterDayName = DateTimeFormatter.ofPattern("EEEE", Locale.ENGLISH) val dateInfo = (1..9).map { offset -> val date = fromDate.plusDays(offset.toLong()) - val dayMonth = date.format(dateFormatterDayMonth) // e.g., "10 Jan" - val dayName = date.format(dateFormatterDayName) // e.g., "Monday" - Pair(dayMonth, dayName) + Pair(date.format(dateFormatterDayMonth), date.format(dateFormatterDayName)) } - // Headers: Two rows for dates + // Two-row headers val headerRow1 = dailySheet.createRow(0) val headerRow2 = dailySheet.createRow(1) - // Base columns - val baseHeaders = listOf("Mat Code", "Mat Name", "UoM") + val baseHeaders = listOf("物料編號", "物料名稱", "採購單位") baseHeaders.forEachIndexed { i, title -> - val cell1 = headerRow1.createCell(i) - cell1.setCellValue(title) - cell1.cellStyle = headerStyle - - val cell2 = headerRow2.createCell(i) - cell2.cellStyle = headerStyle // Empty but merged visually + headerRow1.createCell(i).apply { setCellValue(title); cellStyle = headerStyle } + headerRow2.createCell(i).apply { cellStyle = headerStyle } } - // Date columns: two rows dateInfo.forEachIndexed { index, (dayMonth, dayName) -> - val colIndex = baseHeaders.size + index - - // First row: "10 Jan" - val cellDay = headerRow1.createCell(colIndex) - cellDay.setCellValue(dayMonth) - cellDay.cellStyle = headerStyle - - // Second row: "Monday" - val cellWeekDay = headerRow2.createCell(colIndex) - cellWeekDay.setCellValue(dayName) - cellWeekDay.cellStyle = headerStyle + val col = baseHeaders.size + index + headerRow1.createCell(col).apply { setCellValue(dayMonth); cellStyle = headerStyle } + headerRow2.createCell(col).apply { setCellValue(dayName); cellStyle = headerStyle } } - // Merge the base header cells vertically (optional, for cleaner look) for (i in baseHeaders.indices) { dailySheet.addMergedRegion(CellRangeAddress(0, 1, i, i)) } - // Now write data starting from row 2 (index 2) + // Data rows lineDailys.forEachIndexed { index, rowData -> - val row = dailySheet.createRow(index + 2) // Start after the two header rows + val row = dailySheet.createRow(index + 2) row.heightInPoints = 30f val baseValues = listOf( @@ -1653,30 +1665,31 @@ open class ProductionScheduleService( allValues.forEachIndexed { i, value -> val cell = row.createCell(i) when (value) { - is String -> cell.setCellValue(value) - is Number -> cell.setCellValue(value.toDouble()) - else -> cell.setCellValue(0.0) + is String -> { + cell.setCellValue(value) + cell.cellStyle = wrapStyle + } + is Number -> { + cell.setCellValue(value.toDouble()) + cell.cellStyle = if (i >= baseHeaders.size) numberStyle else wrapStyle + } + else -> { + cell.setCellValue(0.0) + cell.cellStyle = numberStyle + } } - cell.cellStyle = wrapStyle } } - // === COLUMN WIDTHS === - dailySheet.setColumnWidth(0, 16 * 256) // Mat Code - dailySheet.setColumnWidth(1, 35 * 256) // Mat Name - dailySheet.setColumnWidth(2, 10 * 256) // UoM - - // Date columns: slightly wider for readability - for (i in baseHeaders.size until baseHeaders.size + 10) { - dailySheet.setColumnWidth(i, 14 * 256) + // Column widths for daily sheet + dailySheet.setColumnWidth(0, 20 * 256) // 物料編號 + dailySheet.setColumnWidth(1, 55 * 256) // 物料名稱 + dailySheet.setColumnWidth(2, 12 * 256) // 採購單位 + for (i in baseHeaders.size until baseHeaders.size + 9) { + dailySheet.setColumnWidth(i, 16 * 256) // daily qty columns } - // Auto-size base columns (optional improvement) - for (i in 0..2) { - dailySheet.autoSizeColumn(i) - } - - // === PRINT SETUP - PORTRAIT === + // Print setup val dailyPrintSetup = dailySheet.printSetup dailyPrintSetup.paperSize = XSSFPrintSetup.A4_PAPERSIZE dailyPrintSetup.landscape = false @@ -1687,10 +1700,13 @@ open class ProductionScheduleService( dailySheet.setMargin(Sheet.TopMargin, 0.7) dailySheet.setMargin(Sheet.BottomMargin, 0.7) - // Finalize + // ── Write to ByteArray ── val out = ByteArrayOutputStream() - workbook.use { it.write(out) } + workbook.use { wb -> + wb.write(out) + } workbook.close() + return out.toByteArray() }