|
|
|
@@ -11,6 +11,7 @@ import com.ffii.fpsms.modules.jobOrder.service.JobOrderService |
|
|
|
import com.ffii.fpsms.modules.jobOrder.web.model.CreateJobOrderBomMaterialRequest |
|
|
|
import com.ffii.fpsms.modules.jobOrder.web.model.CreateJobOrderProcessRequest |
|
|
|
import com.ffii.fpsms.modules.jobOrder.web.model.CreateJobOrderRequest |
|
|
|
import com.ffii.fpsms.modules.jobOrder.entity.projections.JobOrderDetail |
|
|
|
import com.ffii.fpsms.modules.master.entity.* |
|
|
|
import com.ffii.fpsms.modules.master.entity.projections.* |
|
|
|
import com.ffii.fpsms.modules.master.web.models.MessageResponse |
|
|
|
@@ -46,10 +47,16 @@ import kotlin.collections.component2 |
|
|
|
import kotlin.jvm.optionals.getOrNull |
|
|
|
import kotlin.math.ceil |
|
|
|
import kotlin.comparisons.maxOf |
|
|
|
import org.apache.poi.xssf.usermodel.XSSFWorkbook |
|
|
|
import org.apache.poi.ss.usermodel.FillPatternType |
|
|
|
|
|
|
|
// === 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.xssf.usermodel.XSSFWorkbook |
|
|
|
import org.apache.poi.xssf.usermodel.XSSFPrintSetup |
|
|
|
import java.io.ByteArrayOutputStream |
|
|
|
import java.sql.Timestamp |
|
|
|
|
|
|
|
@Service |
|
|
|
open class ProductionScheduleService( |
|
|
|
@@ -118,10 +125,15 @@ open class ProductionScheduleService( |
|
|
|
open fun allDetailedProdSchedulesByPage(request: SearchProdScheduleRequest): RecordsRes<ProdScheduleInfo> { |
|
|
|
val pageable = PageRequest.of(request.pageNum ?: 0, request.pageSize ?: 10); |
|
|
|
|
|
|
|
val produceAtDate: LocalDate = request.produceAt?.takeIf { it.isNotBlank() } |
|
|
|
?.let { |
|
|
|
LocalDate.parse(it.trim()) |
|
|
|
} |
|
|
|
?: LocalDate.now() |
|
|
|
|
|
|
|
val response = productionScheduleRepository.findProdScheduleInfoByProduceAtByPage( |
|
|
|
produceAt = request.produceAt, |
|
|
|
produceAt = produceAtDate!!, |
|
|
|
totalEstProdCount = request.totalEstProdCount, |
|
|
|
// types = listOf("detailed", "manual"), |
|
|
|
types = request.types, |
|
|
|
pageable = pageable |
|
|
|
) |
|
|
|
@@ -397,18 +409,22 @@ open class ProductionScheduleService( |
|
|
|
val prodScheduleLine = productionScheduleLineRepository.findById(prodScheduleLineId).getOrNull() |
|
|
|
?: throw NoSuchElementException("Production Schedule Line with ID $prodScheduleLineId not found.") |
|
|
|
|
|
|
|
try { |
|
|
|
jobOrderService.jobOrderDetailByPsId(prodScheduleLineId) |
|
|
|
} catch (e: NoSuchElementException) { |
|
|
|
|
|
|
|
// 3. Fetch BOM, handling nullability safely |
|
|
|
val item = prodScheduleLine.item |
|
|
|
?: throw IllegalStateException("Item object is missing for Production Schedule Line $prodScheduleLineId.") |
|
|
|
// 3. Fetch BOM, handling nullability safely |
|
|
|
val item = prodScheduleLine.item |
|
|
|
?: throw IllegalStateException("Item object is missing for Production Schedule Line $prodScheduleLineId.") |
|
|
|
|
|
|
|
val itemId = item.id |
|
|
|
?: throw IllegalStateException("Item ID is missing for Production Schedule Line $prodScheduleLineId.") |
|
|
|
val itemId = item.id |
|
|
|
?: throw IllegalStateException("Item ID is missing for Production Schedule Line $prodScheduleLineId.") |
|
|
|
|
|
|
|
val bom = bomService.findByItemId(itemId) |
|
|
|
try { |
|
|
|
jobOrderService.jobOrderDetailByItemId(itemId) |
|
|
|
} catch (e: NoSuchElementException) { |
|
|
|
//only do with no JO is working |
|
|
|
|
|
|
|
try { |
|
|
|
jobOrderService.jobOrderDetailByItemId(itemId) |
|
|
|
} catch (e: NoSuchElementException) { |
|
|
|
val bom = bomService.findByItemId(itemId) |
|
|
|
?: throw NoSuchElementException("BOM not found for Item ID $itemId.") |
|
|
|
|
|
|
|
// 4. Update Prod Schedule Line fields |
|
|
|
@@ -448,6 +464,7 @@ open class ProductionScheduleService( |
|
|
|
jobOrderProcessService.createJobOrderProcessesByJoId(createdJobOrderId) |
|
|
|
productProcessService.createProductProcessByJobOrderId(createdJobOrderId, prodScheduleLine.itemPriority.toInt()) |
|
|
|
//} |
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
@@ -1338,60 +1355,106 @@ open class ProductionScheduleService( |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
fun exportProdScheduleToExcel(lines: List<Map<String, Any>>, lineMats: List<Map<String, Any>>): ByteArray { |
|
|
|
fun exportProdScheduleToExcel( |
|
|
|
lines: List<Map<String, Any>>, |
|
|
|
lineMats: List<Map<String, Any>> |
|
|
|
): ByteArray { |
|
|
|
val workbook = XSSFWorkbook() |
|
|
|
|
|
|
|
// 1. Group Production Lines by Date |
|
|
|
val groupedData = lines.groupBy { |
|
|
|
val produceAt = it["produceAt"] |
|
|
|
when (produceAt) { |
|
|
|
is LocalDateTime -> produceAt.toLocalDate().toString() |
|
|
|
is java.sql.Timestamp -> produceAt.toLocalDateTime().toLocalDate().toString() |
|
|
|
else -> produceAt?.toString()?.substring(0, 10) ?: "Unknown_Date" |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
// 2. Define Header Style |
|
|
|
// Header style |
|
|
|
val headerStyle = workbook.createCellStyle().apply { |
|
|
|
fillForegroundColor = IndexedColors.GREY_25_PERCENT.index |
|
|
|
fillPattern = FillPatternType.SOLID_FOREGROUND |
|
|
|
wrapText = true |
|
|
|
verticalAlignment = VerticalAlignment.CENTER |
|
|
|
val font = workbook.createFont() |
|
|
|
font.bold = true |
|
|
|
setFont(font) |
|
|
|
} |
|
|
|
|
|
|
|
// 3. Create Production Worksheets |
|
|
|
// Body style |
|
|
|
val wrapStyle = workbook.createCellStyle().apply { |
|
|
|
wrapText = true |
|
|
|
verticalAlignment = VerticalAlignment.TOP |
|
|
|
} |
|
|
|
|
|
|
|
// Group production lines by date |
|
|
|
val groupedData = lines.groupBy { |
|
|
|
val produceAt = it["produceAt"] |
|
|
|
when (produceAt) { |
|
|
|
is LocalDateTime -> produceAt.toLocalDate().toString() |
|
|
|
is Timestamp -> produceAt.toLocalDateTime().toLocalDate().toString() |
|
|
|
is String -> produceAt.take(10) |
|
|
|
else -> produceAt?.toString()?.substring(0, 10) ?: "Unknown_Date" |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
// Production sheets (one per date) |
|
|
|
groupedData.forEach { (dateKey, dailyLines) -> |
|
|
|
val sheetName = dateKey.replace("[/\\\\?*:\\[\\]]".toRegex(), "-") |
|
|
|
val sheet = workbook.createSheet(sheetName) |
|
|
|
val safeSheetName = dateKey.replace(Regex("[/\\\\?*:\\[\\]]"), "-").take(31) |
|
|
|
val sheet = workbook.createSheet(safeSheetName) |
|
|
|
|
|
|
|
val headers = listOf("Item Name", "Avg Qty Last Month", "Stock Qty", "Days Left", "Output Qty", "Batch Need", "Priority") |
|
|
|
val headers = listOf( |
|
|
|
"Item Name", "Avg Qty Last Month", "Stock Qty", "Days Left", |
|
|
|
"Output Qty", "Batch Need", "Priority" |
|
|
|
) |
|
|
|
|
|
|
|
// Header row |
|
|
|
val headerRow = sheet.createRow(0) |
|
|
|
headers.forEachIndexed { i, title -> |
|
|
|
val cell = headerRow.createCell(i) |
|
|
|
cell.setCellValue(title) |
|
|
|
cell.setCellStyle(headerStyle) |
|
|
|
headerRow.createCell(i).apply { |
|
|
|
setCellValue(title) |
|
|
|
cellStyle = headerStyle |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
// Data rows |
|
|
|
dailyLines.forEachIndexed { index, line -> |
|
|
|
val row = sheet.createRow(index + 1) |
|
|
|
row.createCell(0).setCellValue(line["itemName"]?.toString() ?: "") |
|
|
|
row.createCell(1).setCellValue(asDouble(line["avgQtyLastMonth"])) |
|
|
|
row.createCell(2).setCellValue(asDouble(line["stockQty"])) |
|
|
|
row.createCell(3).setCellValue(asDouble(line["daysLeft"])) |
|
|
|
row.createCell(4).setCellValue(asDouble(line["outputdQty"])) // Note: Matching your snippet's "outputdQty" key |
|
|
|
row.createCell(5).setCellValue(asDouble(line["batchNeed"])) |
|
|
|
row.createCell(6).setCellValue(asDouble(line["itemPriority"])) |
|
|
|
row.heightInPoints = 35f // Slightly taller for portrait readability |
|
|
|
|
|
|
|
row.createCell(0).apply { setCellValue(line["itemName"]?.toString() ?: ""); cellStyle = wrapStyle } |
|
|
|
row.createCell(1).apply { setCellValue(asDouble(line["avgQtyLastMonth"])); cellStyle = wrapStyle } |
|
|
|
row.createCell(2).apply { setCellValue(asDouble(line["stockQty"])); cellStyle = wrapStyle } |
|
|
|
row.createCell(3).apply { setCellValue(asDouble(line["daysLeft"])); cellStyle = wrapStyle } |
|
|
|
row.createCell(4).apply { setCellValue(asDouble(line["outputdQty"] ?: line["outputQty"])); cellStyle = wrapStyle } |
|
|
|
row.createCell(5).apply { setCellValue(asDouble(line["batchNeed"])); cellStyle = wrapStyle } |
|
|
|
row.createCell(6).apply { setCellValue(asDouble(line["itemPriority"])); cellStyle = wrapStyle } |
|
|
|
} |
|
|
|
|
|
|
|
for (i in headers.indices) { sheet.autoSizeColumn(i) } |
|
|
|
// 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) |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
// === PORTRAIT PRINT SETUP === |
|
|
|
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 |
|
|
|
|
|
|
|
sheet.fitToPage = true |
|
|
|
sheet.horizontallyCenter = true |
|
|
|
sheet.setMargin(Sheet.LeftMargin, 0.5) |
|
|
|
sheet.setMargin(Sheet.RightMargin, 0.5) |
|
|
|
sheet.setMargin(Sheet.TopMargin, 0.7) |
|
|
|
sheet.setMargin(Sheet.BottomMargin, 0.7) |
|
|
|
} |
|
|
|
|
|
|
|
// 4. Create Material Summary Worksheet |
|
|
|
// === MATERIAL SUMMARY SHEET - PORTRAIT OPTIMIZED === |
|
|
|
val matSheet = workbook.createSheet("Material Summary") |
|
|
|
|
|
|
|
val matHeaders = listOf( |
|
|
|
"Mat Code", "Mat Name", "Required Qty", "Total Qty Need", |
|
|
|
"UoM", "Purchased Qty", "On Hand Qty", "Unavailable Qty", |
|
|
|
"Mat Code", "Mat Name", "Required Qty", "Total Qty Need", |
|
|
|
"UoM", "Purchased Qty", "On Hand Qty", "Unavailable Qty", |
|
|
|
"Related Item Code", "Related Item Name" |
|
|
|
) |
|
|
|
|
|
|
|
@@ -1399,37 +1462,73 @@ open class ProductionScheduleService( |
|
|
|
matHeaders.forEachIndexed { i, title -> |
|
|
|
matHeaderRow.createCell(i).apply { |
|
|
|
setCellValue(title) |
|
|
|
setCellStyle(headerStyle) |
|
|
|
cellStyle = headerStyle |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
lineMats.forEachIndexed { index, rowData -> |
|
|
|
val row = matSheet.createRow(index + 1) |
|
|
|
|
|
|
|
row.heightInPoints = 35f |
|
|
|
|
|
|
|
val totalNeed = asDouble(rowData["totalMatQtyNeed"]) |
|
|
|
val purchased = asDouble(rowData["purchasedQty"]) |
|
|
|
val onHand = asDouble(rowData["onHandQty"]) |
|
|
|
|
|
|
|
// Calculation: Required Qty = totalMatQtyNeed - purchasedQty - onHandQty (minimum 0) |
|
|
|
val requiredQty = (totalNeed - purchased - onHand).coerceAtLeast(0.0) |
|
|
|
|
|
|
|
row.createCell(0).setCellValue(rowData["matCode"]?.toString() ?: "") |
|
|
|
row.createCell(1).setCellValue(rowData["matName"]?.toString() ?: "") |
|
|
|
row.createCell(2).setCellValue(requiredQty) |
|
|
|
row.createCell(3).setCellValue(totalNeed) |
|
|
|
row.createCell(4).setCellValue(rowData["uomName"]?.toString() ?: "") |
|
|
|
row.createCell(5).setCellValue(purchased) |
|
|
|
row.createCell(6).setCellValue(onHand) |
|
|
|
row.createCell(7).setCellValue(asDouble(rowData["unavailableQty"])) |
|
|
|
row.createCell(8).setCellValue(rowData["itemCode"]?.toString() ?: "") |
|
|
|
row.createCell(9).setCellValue(rowData["itemName"]?.toString() ?: "") |
|
|
|
} |
|
|
|
val values = listOf<Any>( |
|
|
|
rowData["matCode"]?.toString() ?: "", |
|
|
|
rowData["matName"]?.toString() ?: "", |
|
|
|
requiredQty, |
|
|
|
totalNeed, |
|
|
|
rowData["uomName"]?.toString() ?: "", |
|
|
|
purchased, |
|
|
|
onHand, |
|
|
|
asDouble(rowData["unavailableQty"]), |
|
|
|
rowData["itemCode"]?.toString() ?: "", |
|
|
|
rowData["itemName"]?.toString() ?: "" |
|
|
|
) |
|
|
|
|
|
|
|
for (i in matHeaders.indices) { matSheet.autoSizeColumn(i) } |
|
|
|
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("") |
|
|
|
} |
|
|
|
cell.cellStyle = wrapStyle |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
// 5. Finalize and Return |
|
|
|
// 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) // Purchased Qty |
|
|
|
matSheet.setColumnWidth(6, 14 * 256) // On Hand Qty |
|
|
|
matSheet.setColumnWidth(7, 14 * 256) // Unavailable Qty |
|
|
|
matSheet.setColumnWidth(8, 22 * 256) // Related Item Code |
|
|
|
matSheet.setColumnWidth(9, 40 * 256) // Related Item Name (longest) |
|
|
|
|
|
|
|
// Portrait print setup |
|
|
|
val matPrintSetup = matSheet.printSetup |
|
|
|
matPrintSetup.paperSize = XSSFPrintSetup.A4_PAPERSIZE |
|
|
|
matPrintSetup.landscape = false // ← Portrait |
|
|
|
matPrintSetup.fitWidth = 1.toShort() |
|
|
|
matPrintSetup.fitHeight = 0.toShort() |
|
|
|
|
|
|
|
matSheet.fitToPage = true |
|
|
|
matSheet.horizontallyCenter = true |
|
|
|
matSheet.setMargin(Sheet.LeftMargin, 0.5) |
|
|
|
matSheet.setMargin(Sheet.RightMargin, 0.5) |
|
|
|
matSheet.setMargin(Sheet.TopMargin, 0.7) |
|
|
|
matSheet.setMargin(Sheet.BottomMargin, 0.7) |
|
|
|
|
|
|
|
// Finalize |
|
|
|
val out = ByteArrayOutputStream() |
|
|
|
workbook.use { it.write(out) } |
|
|
|
workbook.close() |
|
|
|
return out.toByteArray() |
|
|
|
} |
|
|
|
|
|
|
|
|