From 19e2039a52543dc339ca0ffd1f081aa784d80359 Mon Sep 17 00:00:00 2001 From: "vluk@2fi-solutions.com.hk" Date: Mon, 5 Jan 2026 19:04:33 +0800 Subject: [PATCH] adding shcedule query --- .../jobOrder/entity/JobOrderRepository.kt | 41 ++++ .../jobOrder/service/JobOrderService.kt | 19 ++ .../ProductionScheduleLineRepository.kt | 1 + .../entity/ProductionScheduleRepository.kt | 10 +- .../service/ProductionScheduleService.kt | 221 +++++++++++++----- 5 files changed, 228 insertions(+), 64 deletions(-) diff --git a/src/main/java/com/ffii/fpsms/modules/jobOrder/entity/JobOrderRepository.kt b/src/main/java/com/ffii/fpsms/modules/jobOrder/entity/JobOrderRepository.kt index bb087b8..60eae3c 100644 --- a/src/main/java/com/ffii/fpsms/modules/jobOrder/entity/JobOrderRepository.kt +++ b/src/main/java/com/ffii/fpsms/modules/jobOrder/entity/JobOrderRepository.kt @@ -172,10 +172,51 @@ interface JobOrderRepository : AbstractRepository { left join inventory_lot_line ill on ill.id = sol.inventoryLotLineId left join inventory_lot il on il.id = ill.inventoryLotId where jo.prodScheduleLineId = :prodScheduleLineId + and jo.actualEnd is null group by jo.id, uc2.udfudesc limit 1 """ ) fun findJobOrderByProdScheduleLineId(prodScheduleLineId: Long): JobOrderDetailWithJsonString?; + @Query( + nativeQuery = true, + value = """ + select + jo.id, + jo.code, + b.name, + jo.reqQty, + b.outputQtyUom as unit, + uc2.udfudesc as uom, + json_arrayagg( + json_object( + 'id', jobm.id, + 'code', i.code, + 'name', i.name, + 'lotNo', il.lotNo, + 'reqQty', jobm.reqQty, + 'uom', uc.udfudesc, + 'status', jobm.status + ) + ) as pickLines, + jo.status + from job_order jo + left join bom b on b.id = jo.bomId + left join item_uom iu on b.itemId = iu.itemId and iu.salesUnit = true + left join uom_conversion uc2 on uc2.id = iu.uomId + left join job_order_bom_material jobm on jo.id = jobm.jobOrderId + left join items i on i.id = jobm.itemId + left join uom_conversion uc on uc.id = jobm.uomId + left join stock_out_line sol on sol.id = jobm.stockOutLineId + left join inventory_lot_line ill on ill.id = sol.inventoryLotLineId + left join inventory_lot il on il.id = ill.inventoryLotId + where b.itemId = :itemId + and jo.actualEnd is null + group by jo.id, uc2.udfudesc + limit 1 + """ + ) + fun findJobOrderByItemId(itemId: Long): JobOrderDetailWithJsonString?; + } \ No newline at end of file diff --git a/src/main/java/com/ffii/fpsms/modules/jobOrder/service/JobOrderService.kt b/src/main/java/com/ffii/fpsms/modules/jobOrder/service/JobOrderService.kt index 3ed0e53..4c19d07 100644 --- a/src/main/java/com/ffii/fpsms/modules/jobOrder/service/JobOrderService.kt +++ b/src/main/java/com/ffii/fpsms/modules/jobOrder/service/JobOrderService.kt @@ -286,6 +286,25 @@ open class JobOrderService( ) } + open fun jobOrderDetailByItemId(itemId: Long): JobOrderDetail { + val sqlResult = jobOrderRepository.findJobOrderByItemId(itemId) ?: throw NoSuchElementException("Job Order not found with itemId: $itemId"); + + val type = object : TypeToken>() {}.type + val jsonResult = sqlResult.pickLines?.let { GsonUtils.stringToJson>(it, type) } + return JobOrderDetail( + id = sqlResult.id, + code = sqlResult.code, + itemCode = sqlResult.itemCode, + name = sqlResult.name, + reqQty = sqlResult.reqQty, + uom = sqlResult.uom, + pickLines = jsonResult, + status = sqlResult.status, + shortUom = sqlResult.shortUom + ) + } + + open fun jobOrderDetailByCode(code: String): JobOrderDetail { val sqlResult = jobOrderRepository.findJobOrderDetailByCode(code) ?: throw NoSuchElementException("Job Order not found: $code"); diff --git a/src/main/java/com/ffii/fpsms/modules/master/entity/ProductionScheduleLineRepository.kt b/src/main/java/com/ffii/fpsms/modules/master/entity/ProductionScheduleLineRepository.kt index f0328d8..2c2f932 100644 --- a/src/main/java/com/ffii/fpsms/modules/master/entity/ProductionScheduleLineRepository.kt +++ b/src/main/java/com/ffii/fpsms/modules/master/entity/ProductionScheduleLineRepository.kt @@ -39,6 +39,7 @@ interface ProductionScheduleLineRepository : AbstractRepository? + @Query(""" SELECT psl FROM ProductionScheduleLine psl JOIN psl.productionSchedule ps diff --git a/src/main/java/com/ffii/fpsms/modules/master/entity/ProductionScheduleRepository.kt b/src/main/java/com/ffii/fpsms/modules/master/entity/ProductionScheduleRepository.kt index 6dba219..7ffc1e0 100644 --- a/src/main/java/com/ffii/fpsms/modules/master/entity/ProductionScheduleRepository.kt +++ b/src/main/java/com/ffii/fpsms/modules/master/entity/ProductionScheduleRepository.kt @@ -7,6 +7,7 @@ import org.springframework.data.domain.Pageable import org.springframework.data.jpa.repository.Query import org.springframework.stereotype.Repository import java.time.LocalDateTime +import java.time.LocalDate @Repository interface ProductionScheduleRepository : AbstractRepository { @@ -117,8 +118,11 @@ interface ProductionScheduleRepository : AbstractRepository= :produceAt + ) and (:totalEstProdCount is null or :totalEstProdCount = '' or totalEstProdCount = :totalEstProdCount) and (coalesce(:types) is null or type in :types) order by id ASC; @@ -126,7 +130,7 @@ interface ProductionScheduleRepository : AbstractRepository?, pageable: Pageable 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 412ec66..6459f3a 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 @@ -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 { 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>, lineMats: List>): ByteArray { + fun exportProdScheduleToExcel( + lines: List>, + lineMats: List> + ): 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( + 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() }