|
|
|
@@ -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<Map<String, Any>>, |
|
|
|
lineMats: List<Map<String, Any>> |
|
|
|
lineMats: List<Map<String, Any>>, |
|
|
|
lineDailys: List<Map<String, Any>> |
|
|
|
): 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<Any>( |
|
|
|
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<Map<String, Any>> { |
|
|
|
|
|
|
|
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 = """ |
|
|
|
|