From 55f2bc3a32868add2de522520ce9e3bc2ec1c295 Mon Sep 17 00:00:00 2001 From: "vluk@2fi-solutions.com.hk" Date: Sat, 10 Jan 2026 01:02:30 +0800 Subject: [PATCH] no message --- .../service/ProductionScheduleService.kt | 227 +++++++++++++++++- .../web/ProductionScheduleController.kt | 3 +- 2 files changed, 219 insertions(+), 11 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 88eec5b..e3cc85b 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 @@ -47,12 +47,14 @@ import kotlin.collections.component2 import kotlin.jvm.optionals.getOrNull import kotlin.math.ceil import kotlin.comparisons.maxOf +import java.util.Locale // === POI IMPORTS FOR EXCEL EXPORT WITH PRINT SETUP === import org.apache.poi.ss.usermodel.* import org.apache.poi.ss.usermodel.IndexedColors import org.apache.poi.ss.usermodel.FillPatternType import org.apache.poi.ss.usermodel.VerticalAlignment +import org.apache.poi.ss.util.CellRangeAddress import org.apache.poi.xssf.usermodel.XSSFWorkbook import org.apache.poi.xssf.usermodel.XSSFPrintSetup import java.io.ByteArrayOutputStream @@ -627,16 +629,16 @@ open class ProductionScheduleService( i.avgQtyLastMonth, (i.onHandQty -500), (i.onHandQty -500) * 1.0 / i.avgQtyLastMonth as daysLeft, - i.avgQtyLastMonth * 2 - stockQty as needQty, + i.avgQtyLastMonth * 2.6 - stockQty as needQty, i.stockQty, CASE WHEN stockQty * 1.0 / i.avgQtyLastMonth <= 1.9 THEN - CEIL((i.avgQtyLastMonth * 1.9 - stockQty) / i.outputQty) + CEIL((i.avgQtyLastMonth * 2.6 - stockQty) / i.outputQty) ELSE 0 END AS needNoOfJobOrder, CASE WHEN stockQty * 1.0 / i.avgQtyLastMonth <= 1.9 THEN - CEIL((i.avgQtyLastMonth * 1.9 - stockQty) / i.outputQty) + CEIL((i.avgQtyLastMonth * 2.6 - stockQty) / i.outputQty) ELSE 0 END AS batchNeed, 25 + 25 + markDark + markFloat + markDense + markAS + markTimeSequence + markComplexity as priority, @@ -805,19 +807,39 @@ open class ProductionScheduleService( saveDetailedScheduleOutput(sortedOutputList, accProdCount, fgCount, produceAt) - //do for 7 days to predict - for (i in 1..6) { + //do for 9 days to predict + for (i in 1..8) { + val targetDate = produceAt.plusDays(i.toLong()) + val isSat = targetDate.dayOfWeek == DayOfWeek.SATURDAY + val isFri = targetDate.dayOfWeek == DayOfWeek.FRIDAY + + val isSunday = targetDate.dayOfWeek == DayOfWeek.SUNDAY + val isFriSat = isFri || isSat + + logger.info("##targetDate:" + targetDate + " isFri:"+ isFri + " isSat:" + isSat + " isSunday:" + isSunday) + + fgCount = 0 accProdCount = 0.0 - sortedOutputList.forEach { record -> + sortedOutputList.forEach { record -> record.stockQty = record.stockQty + (record.outputQty * record.needNoOfJobOrder.toInt()) - record.avgQtyLastMonth + //compare if less than 1.9 days record.daysLeft = record.stockQty / record.avgQtyLastMonth - if(record.daysLeft <= 1.9){ - record.needQty = (record.avgQtyLastMonth * 2) - record.stockQty; + var safetyStockDay = 2.0 + var redLine = 1.9 + + if(isFriSat){ + //record.daysLeft = record.daysLeft + safetyStockDay = 2.6 + } + + if(record.daysLeft < redLine){ + record.needQty = (record.avgQtyLastMonth * safetyStockDay) - record.stockQty; - if(record.needQty > 0.0){ + if(!isSunday && record.needQty > 0.0){ + //if(record.needQty > 0.0){ record.batchNeed = ceil(record.needQty / record.outputQty) record.needNoOfJobOrder = record.batchNeed @@ -1362,8 +1384,10 @@ open class ProductionScheduleService( } fun exportProdScheduleToExcel( + fromDate: LocalDate, lines: List>, - lineMats: List> + lineMats: List>, + lineDailys: List> ): ByteArray { val workbook = XSSFWorkbook() @@ -1531,6 +1555,110 @@ 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") + + // 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 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) + } + + // Headers: Two rows for dates + val headerRow1 = dailySheet.createRow(0) + val headerRow2 = dailySheet.createRow(1) + + // Base columns + val baseHeaders = listOf("Mat Code", "Mat Name", "UoM") + + 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 + } + + // 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 + } + + // 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) + lineDailys.forEachIndexed { index, rowData -> + val row = dailySheet.createRow(index + 2) // Start after the two header rows + row.heightInPoints = 30f + + val baseValues = listOf( + rowData["matCode"]?.toString() ?: "", + rowData["matName"]?.toString() ?: "", + rowData["uom"]?.toString() ?: "" + ) + + val dailyQty = (1..9).map { offset -> + asDouble(rowData["day$offset"]) + } + + val allValues = baseValues + dailyQty + + 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) + } + 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) + } + + // Auto-size base columns (optional improvement) + for (i in 0..2) { + dailySheet.autoSizeColumn(i) + } + + // === PRINT SETUP - PORTRAIT === + val dailyPrintSetup = dailySheet.printSetup + dailyPrintSetup.paperSize = XSSFPrintSetup.A4_PAPERSIZE + dailyPrintSetup.landscape = false + dailyPrintSetup.fitWidth = 1.toShort() + dailyPrintSetup.fitHeight = 0.toShort() + dailySheet.fitToPage = true + dailySheet.horizontallyCenter = true + dailySheet.setMargin(Sheet.TopMargin, 0.7) + dailySheet.setMargin(Sheet.BottomMargin, 0.7) + // Finalize val out = ByteArrayOutputStream() workbook.use { it.write(out) } @@ -1620,6 +1748,85 @@ open class ProductionScheduleService( } + open fun searchExportDailyMaterial(fromDate: LocalDate): List> { + + val args = mapOf( + "fromDate" to fromDate, + ) + + val sql = """ + WITH daily_needs AS ( + SELECT + itm.id AS materialId, + itm.code AS matCode, + itm.name AS matName, + bm.uomName AS uom, + iv.onHandQty, + iv.unavailableQty, + COALESCE(( + SELECT SUM(pol.qty) + FROM purchase_order_line pol + JOIN purchase_order po ON pol.purchaseOrderId = po.id + WHERE pol.itemId = itm.id + AND DATE(po.estimatedArrivalDate) >= CURDATE() + AND po.completeDate IS NULL + ), 0) AS purchasedQty, + DATE(ps.produceAt) AS produceDate, + SUM(bm.qty * psl.batchNeed) AS qtyNeeded + FROM production_schedule_line psl + JOIN production_schedule ps ON psl.prodScheduleId = ps.id + JOIN items it ON psl.itemId = it.id + JOIN bom ON it.id = bom.itemId + JOIN bom_material bm ON bom.id = bm.bomId + JOIN items itm ON bm.itemId = itm.id + LEFT JOIN inventory iv ON itm.id = iv.itemId + WHERE DATE(ps.produceAt) >= DATE_ADD(CURDATE(), INTERVAL 1 DAY) + AND DATE(ps.produceAt) < DATE_ADD(CURDATE(), INTERVAL 8 DAY) + AND ps.id = ( + SELECT ps2.id + FROM production_schedule ps2 + WHERE DATE(ps2.produceAt) = DATE(ps.produceAt) + ORDER BY ps2.id DESC + LIMIT 1 + ) + AND bm.itemId IS NOT NULL + GROUP BY + itm.id, + DATE(ps.produceAt) + ) + + SELECT + matCode, + matName, + uom, + onHandQty AS currentStock, + purchasedQty AS incomingPO, + (onHandQty + purchasedQty) AS grossAvailable, + + -- Dynamic columns for next 7 days + COALESCE(MAX(CASE WHEN produceDate = DATE_ADD(CURDATE(), INTERVAL 1 DAY) THEN qtyNeeded END), 0) AS day1, + COALESCE(MAX(CASE WHEN produceDate = DATE_ADD(CURDATE(), INTERVAL 2 DAY) THEN qtyNeeded END), 0) AS day2, + COALESCE(MAX(CASE WHEN produceDate = DATE_ADD(CURDATE(), INTERVAL 3 DAY) THEN qtyNeeded END), 0) AS day3, + COALESCE(MAX(CASE WHEN produceDate = DATE_ADD(CURDATE(), INTERVAL 4 DAY) THEN qtyNeeded END), 0) AS day4, + COALESCE(MAX(CASE WHEN produceDate = DATE_ADD(CURDATE(), INTERVAL 5 DAY) THEN qtyNeeded END), 0) AS day5, + COALESCE(MAX(CASE WHEN produceDate = DATE_ADD(CURDATE(), INTERVAL 6 DAY) THEN qtyNeeded END), 0) AS day6, + COALESCE(MAX(CASE WHEN produceDate = DATE_ADD(CURDATE(), INTERVAL 7 DAY) THEN qtyNeeded END), 0) AS day7, + + -- Total and net + COALESCE(SUM(qtyNeeded), 0) AS totalNeed7Days, + (onHandQty + purchasedQty - COALESCE(SUM(qtyNeeded), 0)) AS netAfter7Days + + FROM daily_needs + GROUP BY + materialId, matCode, matName, uom, onHandQty, purchasedQty + ORDER BY netAfter7Days ASC, totalNeed7Days DESC; + """; + + + return jdbcDao.queryForList(sql, args); + + } + @Transactional open fun clearTodayAndFutureProdSchedule() { val deleteLinesSql = """ diff --git a/src/main/java/com/ffii/fpsms/modules/master/web/ProductionScheduleController.kt b/src/main/java/com/ffii/fpsms/modules/master/web/ProductionScheduleController.kt index 57b8d4e..590378f 100644 --- a/src/main/java/com/ffii/fpsms/modules/master/web/ProductionScheduleController.kt +++ b/src/main/java/com/ffii/fpsms/modules/master/web/ProductionScheduleController.kt @@ -240,7 +240,8 @@ class ProductionScheduleController( fun exportProdSchedule(): ResponseEntity { val data = productionScheduleService.searchExportProdSchedule(LocalDate.now()) val dataMat = productionScheduleService.searchExportProdScheduleMaterial(LocalDate.now()) - val excelContent = productionScheduleService.exportProdScheduleToExcel(data, dataMat) + val dataDaily = productionScheduleService.searchExportDailyMaterial(LocalDate.now()) + val excelContent = productionScheduleService.exportProdScheduleToExcel(LocalDate.now(), data, dataMat, dataDaily) return ResponseEntity.ok() .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=production_schedule.xlsx")