| @@ -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<Any>( | |||
| @@ -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() | |||
| } | |||