Ver a proveniência

no message

master
ascendente
cometimento
b4d754c33b
1 ficheiros alterados com 122 adições e 106 eliminações
  1. +122
    -106
      src/main/java/com/ffii/fpsms/modules/master/service/ProductionScheduleService.kt

+ 122
- 106
src/main/java/com/ffii/fpsms/modules/master/service/ProductionScheduleService.kt Ver ficheiro

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



Carregando…
Cancelar
Guardar