# Conflicts: # src/main/java/com/ffii/fpsms/modules/productProcess/service/ProductProcessService.ktmaster
| @@ -63,7 +63,7 @@ open class M18DeliveryOrderService( | |||
| val dateTo = request.modifiedDateTo?.let { | |||
| LocalDateTime.parse(it, formatter).toLocalDate().toString() | |||
| } | |||
| val lastModifyDateConds = | |||
| val lastDateConds = | |||
| //"lastModifyDate=largerOrEqual=${request.modifiedDateFrom ?: lastModifyDateStart}=and=lastModifyDate=lessOrEqual=${request.modifiedDateTo ?: lastModifyDateEnd}" | |||
| "dDate=largerOrEqual=${dateFrom ?: lastModifyDateStart}=and=dDate=lessOrEqual=${dateTo ?: lastModifyDateEnd}" | |||
| @@ -74,7 +74,7 @@ open class M18DeliveryOrderService( | |||
| "venId=equal=", | |||
| "=or=" | |||
| ) | |||
| val shopPoConds = "(${shopPoBuyers})=and=(${shopPoSupplier})=and=(${lastModifyDateConds})" | |||
| val shopPoConds = "(${shopPoBuyers})=and=(${shopPoSupplier})=and=(${lastDateConds})" | |||
| println("shopPoConds: ${shopPoConds}") | |||
| val shopPoParams = M18PurchaseOrderListRequest( | |||
| params = null, | |||
| @@ -24,6 +24,7 @@ import org.slf4j.LoggerFactory | |||
| import org.springframework.stereotype.Service | |||
| import java.time.LocalDateTime | |||
| import kotlin.reflect.full.memberProperties | |||
| import java.time.format.DateTimeFormatter | |||
| @Service | |||
| open class M18PurchaseOrderService( | |||
| @@ -57,8 +58,16 @@ open class M18PurchaseOrderService( | |||
| // Include material po, oem po | |||
| open fun getPurchaseOrdersWithType(request: M18CommonRequest): M18PurchaseOrderListResponseWithType? { | |||
| val purchaseOrders = M18PurchaseOrderListResponseWithType(mutableListOf()) | |||
| val lastModifyDateConds = | |||
| "lastModifyDate=largerOrEqual=${request.modifiedDateFrom ?: lastModifyDateStart}=and=lastModifyDate=lessOrEqual=${request.modifiedDateTo ?: lastModifyDateEnd}" | |||
| val formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss") | |||
| val dateFrom = request.modifiedDateFrom?.let { | |||
| LocalDateTime.parse(it, formatter).toLocalDate().toString() | |||
| } | |||
| val dateTo = request.modifiedDateTo?.let { | |||
| LocalDateTime.parse(it, formatter).toLocalDate().toString() | |||
| } | |||
| val lastDateConds = | |||
| //"lastModifyDate=largerOrEqual=${request.modifiedDateFrom ?: lastModifyDateStart}=and=lastModifyDate=lessOrEqual=${request.modifiedDateTo ?: lastModifyDateEnd}" | |||
| "dDate=largerOrEqual=${dateFrom ?: lastModifyDateStart}=and=dDate=lessOrEqual=${dateTo ?: lastModifyDateEnd}" | |||
| // Material PO | |||
| val materialPoBuyers = | |||
| commonUtils.listToString(listOf(m18Config.BEID_PP, m18Config.BEID_PF), "beId=equal=", "=or=") | |||
| @@ -67,7 +76,7 @@ open class M18PurchaseOrderService( | |||
| "venId=unequal=", | |||
| "=or=" | |||
| ) | |||
| val materialPoConds = "(${materialPoBuyers})=and=(${materialPoSupplierNot})=and=(${lastModifyDateConds})" | |||
| val materialPoConds = "(${materialPoBuyers})=and=(${materialPoSupplierNot})=and=(${lastDateConds})" | |||
| println("materialPoConds: ${materialPoConds}") | |||
| val materialPoParams = M18PurchaseOrderListRequest( | |||
| params = null, | |||
| @@ -153,13 +153,24 @@ open class SchedulerService( | |||
| logger.info("Daily Scheduler - PO") | |||
| val currentTime = LocalDateTime.now() | |||
| val today = currentTime.toLocalDate().atStartOfDay() | |||
| val yesterday = today.minusDays(1L) | |||
| /* val yesterday = today.minusDays(1L) | |||
| val request = M18CommonRequest( | |||
| modifiedDateTo = today.format(dataStringFormat), | |||
| modifiedDateFrom = yesterday.format(dataStringFormat) | |||
| )*/ | |||
| val tmr = today.plusDays(1L) | |||
| var request = M18CommonRequest( | |||
| modifiedDateTo = tmr.format(dataStringFormat), | |||
| modifiedDateFrom = tmr.format(dataStringFormat) | |||
| ) | |||
| m18PurchaseOrderService.savePurchaseOrders(request); | |||
| m18DeliveryOrderService.saveDeliveryOrders(request); | |||
| //dDate from tmr to tmr | |||
| var requestDO = M18CommonRequest( | |||
| modifiedDateTo = tmr.format(dataStringFormat), | |||
| modifiedDateFrom = tmr.format(dataStringFormat) | |||
| ) | |||
| m18DeliveryOrderService.saveDeliveryOrders(requestDO); | |||
| // logger.info("today: ${today.format(dataStringFormat)}") | |||
| // logger.info("yesterday: ${yesterday.format(dataStringFormat)}") | |||
| } | |||
| @@ -52,7 +52,7 @@ interface ProductionScheduleRepository : AbstractRepository<ProductionSchedule, | |||
| and (:schedulePeriodTo = '' or datediff(schedulePeriodTo, coalesce(:schedulePeriodTo, schedulePeriodTo)) = 0) | |||
| and (:totalEstProdCount is null or :totalEstProdCount = '' or totalEstProdCount = :totalEstProdCount) | |||
| and (coalesce(:types) is null or type in :types) | |||
| order by id desc; | |||
| order by id ASC; | |||
| """, | |||
| countQuery = | |||
| """ | |||
| @@ -83,7 +83,7 @@ interface ProductionScheduleRepository : AbstractRepository<ProductionSchedule, | |||
| and (:schedulePeriodTo = '' or datediff(schedulePeriodTo, coalesce(:schedulePeriodTo, schedulePeriodTo)) = 0) | |||
| and (:totalEstProdCount is null or :totalEstProdCount = '' or totalEstProdCount = :totalEstProdCount) | |||
| and (coalesce(:types) is null or type in :types) | |||
| order by id desc; | |||
| order by id ASC; | |||
| """, | |||
| ) | |||
| fun findProdScheduleInfoByPage( | |||
| @@ -121,7 +121,7 @@ interface ProductionScheduleRepository : AbstractRepository<ProductionSchedule, | |||
| and (:produceAt = '' or datediff(produceAt, coalesce(:produceAt, produceAt)) = 0) | |||
| and (:totalEstProdCount is null or :totalEstProdCount = '' or totalEstProdCount = :totalEstProdCount) | |||
| and (coalesce(:types) is null or type in :types) | |||
| order by id desc; | |||
| order by id ASC; | |||
| """ | |||
| ) | |||
| fun findProdScheduleInfoByProduceAtByPage( | |||
| @@ -207,10 +207,31 @@ interface ProductionScheduleRepository : AbstractRepository<ProductionSchedule, | |||
| prod.itemPriority, | |||
| prod.batchNeed, | |||
| prod.stockQty, | |||
| prod.outputQty, | |||
| prod.prodQty, | |||
| prod.avgQtyLastMonth, | |||
| json_arrayagg( | |||
| json_object('id', prod.pslId, 'bomMaterials', prod.bomMaterials, 'jobNo', prod.jobNo, 'code', prod.code, 'name', prod.name, 'type', prod.type, 'demandQty', prod.demandQty, 'bomOutputQty', prod.bomOutputQty, 'uomName', prod.uomName, 'prodTimeInMinute', prod.prodTimeInMinute, 'priority', prod.priority, 'approved', prod.approved, 'proportion', prod.proportion, 'daysLeft', prod.daysLeft, 'needNoOfJobOrder', prod.needNoOfJobOrder, 'batchNeed', prod.batchNeed, 'avgQtyLastMonth', prod.avgQtyLastMonth, 'stockQty', prod.stockQty) | |||
| json_object('id', prod.pslId, | |||
| 'bomMaterials', prod.bomMaterials, | |||
| 'jobNo', prod.jobNo, | |||
| 'code', prod.code, | |||
| 'name', prod.name, | |||
| 'type', prod.type, | |||
| 'demandQty', prod.demandQty, | |||
| 'bomOutputQty', prod.bomOutputQty, | |||
| 'uomName', prod.uomName, | |||
| 'prodTimeInMinute', prod.prodTimeInMinute, | |||
| 'priority', prod.priority, | |||
| 'approved', prod.approved, | |||
| 'proportion', prod.proportion, | |||
| 'daysLeft', prod.daysLeft, | |||
| 'needNoOfJobOrder', prod.needNoOfJobOrder, | |||
| 'batchNeed', prod.batchNeed, | |||
| 'avgQtyLastMonth', prod.avgQtyLastMonth, | |||
| 'prodQty', prod.prodQty, | |||
| 'outputQty', prod.outputQty, | |||
| 'stockQty', prod.stockQty) | |||
| ) as prodScheduleLines | |||
| from ( | |||
| select | |||
| @@ -226,6 +247,8 @@ interface ProductionScheduleRepository : AbstractRepository<ProductionSchedule, | |||
| psl.itemPriority, | |||
| psl.batchNeed, | |||
| psl.stockQty, | |||
| psl.outputQty, | |||
| psl.prodQty, | |||
| pm.bomMaterials, | |||
| pm.bomOutputQty, | |||
| uc.udfudesc as uomName, | |||
| @@ -10,6 +10,7 @@ interface PrinterCombo { | |||
| val value: String; | |||
| val code: String?; | |||
| val name: String?; | |||
| val type: String?; | |||
| val description: String?; | |||
| val ip: String?; | |||
| val port: Int?; | |||
| @@ -50,6 +50,8 @@ data class DetailedProdScheduleLineInfo( | |||
| val daysLeft: BigDecimal?, | |||
| val onHandQty: BigDecimal?, | |||
| val stockQty: BigDecimal?, | |||
| val outputQty: BigDecimal?, | |||
| val prodQty: BigDecimal?, | |||
| val lastMonthAvgSales: BigDecimal?, | |||
| val avgQtyLastMonth: BigDecimal?, | |||
| val needNoOfJobOrder: BigDecimal?, | |||
| @@ -46,6 +46,10 @@ 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 | |||
| import org.apache.poi.ss.usermodel.IndexedColors | |||
| import java.io.ByteArrayOutputStream | |||
| @Service | |||
| open class ProductionScheduleService( | |||
| @@ -336,7 +340,7 @@ open class ProductionScheduleService( | |||
| open fun saveProdScheduleLine(request: ReleaseProdScheduleLineRequest): MessageResponse { | |||
| val prodScheduleLine = request.id.let { productionScheduleLineRepository.findById(it).getOrNull() } ?: throw NoSuchElementException() | |||
| val prodSchedule = prodScheduleLine.productionSchedule | |||
| // Update Prod Schedule Type | |||
| prodSchedule.apply { | |||
| type = "manual" | |||
| @@ -351,7 +355,7 @@ open class ProductionScheduleService( | |||
| productionScheduleLineRepository.saveAndFlush(prodScheduleLine) | |||
| val bomMaterials = prodScheduleLine.id?.let { productionScheduleLineRepository.getBomMaterials(it) } | |||
| return MessageResponse( | |||
| id = request.id, | |||
| name = null, | |||
| @@ -422,11 +426,11 @@ open class ProductionScheduleService( | |||
| logger.info("bom?.outputQty: ${bom.outputQty} ${bom.outputQtyUom}") | |||
| logger.info("[releaseProdSchedule] prodScheduleLine.needNoOfJobOrder:" + prodScheduleLine.needNoOfJobOrder) | |||
| repeat(prodScheduleLine.needNoOfJobOrder) { | |||
| //repeat(prodScheduleLine.needNoOfJobOrder) { | |||
| // 6. Create Job Order | |||
| val joRequest = CreateJobOrderRequest( | |||
| bomId = bom.id, // bom is guaranteed non-null here | |||
| reqQty = bom.outputQty, // bom is guaranteed non-null here | |||
| reqQty = bom.outputQty?.multiply(BigDecimal.valueOf(prodScheduleLine.batchNeed.toLong())), | |||
| approverId = approver?.id, | |||
| // CRUCIAL FIX: Use the line ID, not the parent schedule ID | |||
| @@ -442,8 +446,8 @@ open class ProductionScheduleService( | |||
| // 7. Create related job order data | |||
| jobOrderBomMaterialService.createJobOrderBomMaterialsByJoId(createdJobOrderId) | |||
| jobOrderProcessService.createJobOrderProcessesByJoId(createdJobOrderId) | |||
| productProcessService.createProductProcessByJobOrderId(createdJobOrderId) | |||
| } | |||
| productProcessService.createProductProcessByJobOrderId(createdJobOrderId, prodScheduleLine.itemPriority.toInt()) | |||
| //} | |||
| } | |||
| @@ -491,22 +495,45 @@ open class ProductionScheduleService( | |||
| logger.info("bom?.outputQty:" + bom?.outputQty + "" + bom?.outputQtyUom); | |||
| logger.info("prodScheduleLine.needNoOfJobOrder:" + prodScheduleLine.needNoOfJobOrder) | |||
| repeat(prodScheduleLine.needNoOfJobOrder) { | |||
| // Create Job Order | |||
| val prodScheduleLineId = prodScheduleLine.id!! | |||
| try { | |||
| jobOrderService.jobOrderDetailByPsId(prodScheduleLineId) | |||
| } catch (e: NoSuchElementException) { | |||
| // 3. Fetch BOM, handling nullability safely | |||
| val item = prodScheduleLine.item | |||
| val itemId = item.id | |||
| ?: throw IllegalStateException("Item ID is missing for Production Schedule Line $prodScheduleLineId.") | |||
| val bom = bomService.findByItemId(itemId) | |||
| ?: throw NoSuchElementException("BOM not found for Item ID $itemId.") | |||
| val approver = SecurityUtils.getUser().getOrNull() // Get approver once | |||
| val joRequest = CreateJobOrderRequest( | |||
| bomId = bom?.id, | |||
| reqQty = bom?.outputQty, | |||
| bomId = bom.id, // bom is guaranteed non-null here | |||
| reqQty = bom.outputQty?.multiply(BigDecimal.valueOf(prodScheduleLine.batchNeed.toLong())), | |||
| approverId = approver?.id, | |||
| prodScheduleLineId = request.id | |||
| // CRUCIAL FIX: Use the line ID, not the parent schedule ID | |||
| prodScheduleLineId = prodScheduleLine.id!! | |||
| ) | |||
| // Assuming createJobOrder returns the created Job Order (jo) | |||
| val jo = jobOrderService.createJobOrder(joRequest) | |||
| logger.info("jo created:" + jo.id!!) | |||
| jobOrderBomMaterialService.createJobOrderBomMaterialsByJoId(jo.id!!) | |||
| jobOrderProcessService.createJobOrderProcessesByJoId(jo.id!!) | |||
| productProcessService.createProductProcessByJobOrderId(jo.id!!) | |||
| val createdJobOrderId = jo.id | |||
| ?: throw IllegalStateException("Job Order creation failed: returned object ID is null.") | |||
| // 7. Create related job order data | |||
| jobOrderBomMaterialService.createJobOrderBomMaterialsByJoId(createdJobOrderId) | |||
| jobOrderProcessService.createJobOrderProcessesByJoId(createdJobOrderId) | |||
| productProcessService.createProductProcessByJobOrderId(createdJobOrderId, prodScheduleLine.itemPriority.toInt()) | |||
| } | |||
| // Get Latest Data | |||
| @@ -593,7 +620,7 @@ open class ProductionScheduleService( | |||
| CEIL((i.avgQtyLastMonth * 1.9 - stockQty) / i.outputQty) | |||
| ELSE 0 | |||
| END AS batchNeed, | |||
| markDark + markFloat + markDense + markAS as priority, | |||
| 25 + 25 + markDark + markFloat + markDense + markAS + markTimeSequence + markComplexity as priority, | |||
| i.* | |||
| FROM | |||
| (SELECT | |||
| @@ -627,14 +654,29 @@ open class ProductionScheduleService( | |||
| inventory.onHandQty, | |||
| bom.itemId, | |||
| bom.id AS bomId, | |||
| CASE WHEN bom.isDark = 5 THEN 11 | |||
| WHEN bom.isDark = 3 THEN 6 | |||
| WHEN bom.isDark = 1 THEN 2 | |||
| ELSE 0 END as markDark, | |||
| CASE WHEN bom.isFloat = 3 THEN 11 | |||
| CASE WHEN bom.isFloat = 5 THEN 11 | |||
| WHEN bom.isFloat = 3 THEN 6 | |||
| WHEN bom.isFloat = 1 THEN 2 | |||
| ELSE 0 END as markFloat, | |||
| CASE WHEN bom.isDense = 5 THEN 11 | |||
| WHEN bom.isDense = 3 THEN 6 | |||
| WHEN bom.isDense = 1 THEN 2 | |||
| ELSE 0 END as markDense, | |||
| bom.timeSequence as markTimeSequence, | |||
| bom.complexity as markComplexity, | |||
| CASE WHEN bom.allergicSubstances = 5 THEN 11 | |||
| ELSE 0 END as markAS, | |||
| inventory.id AS inventoryId | |||
| FROM | |||
| bom | |||
| @@ -663,6 +705,7 @@ open class ProductionScheduleService( | |||
| needNoOfJobOrder = (row["needNoOfJobOrder"] as Number).toLong() | |||
| daysLeft = (row["daysLeft"] as Number).toDouble() | |||
| batchNeed = row["batchNeed"] as Number | |||
| priority = row["priority"] as Number | |||
| } | |||
| } | |||
| @@ -696,7 +739,7 @@ open class ProductionScheduleService( | |||
| val allergicSubstances = maxOf(record.allergicSubstances.toDouble(), 0.0) | |||
| val priority = isDark + isFloat + isDense + allergicSubstances | |||
| record.itemPriority = priority | |||
| record.itemPriority = record.priority | |||
| productionPriorityMap.put(record, priority) | |||
| } | |||
| @@ -761,6 +804,10 @@ open class ProductionScheduleService( | |||
| record.needNoOfJobOrder = 0 | |||
| record.batchNeed = 0 | |||
| } | |||
| }else{ | |||
| record.needQty = 0.0 | |||
| record.needNoOfJobOrder = 0 | |||
| record.batchNeed = 0 | |||
| } | |||
| logger.info(record.name + " record.batchNeed: " + record.batchNeed + " record.stockQty:" + record.stockQty + " record.daysLeft:" + record.daysLeft) | |||
| @@ -831,7 +878,7 @@ open class ProductionScheduleService( | |||
| savedItem.type = "detailed" | |||
| savedItem.assignDate = LocalDateTime.now().dayOfMonth.toLong(); | |||
| savedItem.weightingRef = detailedScheduleObj.needQty | |||
| savedItem.itemPriority = detailedScheduleObj.needQty.toLong() | |||
| savedItem.itemPriority = detailedScheduleObj.priority.toLong() | |||
| savedItem.outputQty = detailedScheduleObj.outputQty | |||
| savedItem.avgQtyLastMonth = detailedScheduleObj.avgQtyLastMonth | |||
| savedItem.stockQty = detailedScheduleObj.stockQty | |||
| @@ -862,11 +909,15 @@ open class ProductionScheduleService( | |||
| open var stockQty: Double = 0.0 | |||
| open var daysLeft: Double = 0.0 | |||
| open var batchNeed: Number = 0 | |||
| open var priority: Number = 0 | |||
| override fun toString(): String { | |||
| return "NeedQtyRecord(name=${name}," + | |||
| " avgQtyLastMonth=$avgQtyLastMonth" + | |||
| " stockQty=$stockQty" + | |||
| " daysLeft=$daysLeft" + | |||
| " batchNeed=$batchNeed" + | |||
| " priority=$priority" + | |||
| " itemId=$id" + | |||
| " needNoOfJobOrder=$needNoOfJobOrder" + | |||
| " outputQty=$outputQty" + | |||
| @@ -1286,4 +1337,182 @@ open class ProductionScheduleService( | |||
| } | |||
| } | |||
| 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 | |||
| val headerStyle = workbook.createCellStyle().apply { | |||
| fillForegroundColor = IndexedColors.GREY_25_PERCENT.index | |||
| fillPattern = FillPatternType.SOLID_FOREGROUND | |||
| val font = workbook.createFont() | |||
| font.bold = true | |||
| setFont(font) | |||
| } | |||
| // 3. Create Production Worksheets | |||
| groupedData.forEach { (dateKey, dailyLines) -> | |||
| val sheetName = dateKey.replace("[/\\\\?*:\\[\\]]".toRegex(), "-") | |||
| val sheet = workbook.createSheet(sheetName) | |||
| val headers = listOf("Item Name", "Avg Qty Last Month", "Stock Qty", "Days Left", "Output Qty", "Batch Need", "Priority") | |||
| val headerRow = sheet.createRow(0) | |||
| headers.forEachIndexed { i, title -> | |||
| val cell = headerRow.createCell(i) | |||
| cell.setCellValue(title) | |||
| cell.setCellStyle(headerStyle) | |||
| } | |||
| 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"])) | |||
| } | |||
| for (i in headers.indices) { sheet.autoSizeColumn(i) } | |||
| } | |||
| // 4. Create Material Summary Worksheet | |||
| 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", | |||
| "Related Item Code", "Related Item Name" | |||
| ) | |||
| val matHeaderRow = matSheet.createRow(0) | |||
| matHeaders.forEachIndexed { i, title -> | |||
| matHeaderRow.createCell(i).apply { | |||
| setCellValue(title) | |||
| setCellStyle(headerStyle) | |||
| } | |||
| } | |||
| lineMats.forEachIndexed { index, rowData -> | |||
| val row = matSheet.createRow(index + 1) | |||
| 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() ?: "") | |||
| } | |||
| for (i in matHeaders.indices) { matSheet.autoSizeColumn(i) } | |||
| // 5. Finalize and Return | |||
| val out = ByteArrayOutputStream() | |||
| workbook.use { it.write(out) } | |||
| return out.toByteArray() | |||
| } | |||
| private fun asDouble(value: Any?): Double { | |||
| return when (value) { | |||
| is Number -> value.toDouble() | |||
| is String -> value.toDoubleOrNull() ?: 0.0 | |||
| else -> 0.0 | |||
| } | |||
| } | |||
| //====================細排相關 START====================// | |||
| open fun searchExportProdSchedule(fromDate: LocalDate): List<Map<String, Any>> { | |||
| val args = mapOf( | |||
| "fromDate" to fromDate, | |||
| ) | |||
| val sql = """ | |||
| select | |||
| it.code as itemCode, | |||
| it.name as itemName, | |||
| ps.produceAt, | |||
| psl.* | |||
| from production_schedule_line psl | |||
| left join production_schedule ps on psl.prodScheduleId = ps.id | |||
| left join items it on psl.itemId = it.id | |||
| where ps.produceAt >= :fromDate | |||
| and ps.id = (select id from production_schedule where produceAt = ps.produceAt order by id desc limit 1) | |||
| order by ps.produceAt asc, psl.itemPriority desc, itemCode asc | |||
| """; | |||
| return jdbcDao.queryForList(sql, args); | |||
| } | |||
| open fun searchExportProdScheduleMaterial(fromDate: LocalDate): List<Map<String, Any>> { | |||
| val args = mapOf( | |||
| "fromDate" to fromDate, | |||
| ) | |||
| val sql = """ | |||
| select | |||
| group_concat(distinct it.code) as itemCode, | |||
| group_concat(distinct it.name) as itemName, | |||
| itm.code as matCode, | |||
| itm.name as matName, | |||
| sum(bm.qty * psl.batchNeed) as totalMatQtyNeed, | |||
| iv.onHandQty, | |||
| iv.unavailableQty, | |||
| (select sum(qty) from purchase_order_line | |||
| left join purchase_order on purchase_order_line.purchaseOrderId = purchase_order.id | |||
| where purchase_order_line.itemId = itm.id and date(purchase_order.estimatedArrivalDate) >= date(now()) and purchase_order.completeDate is null) as purchasedQty, | |||
| bm.uomName, | |||
| min(ps.produceAt) as toProdcueFrom, | |||
| max(ps.produceAt) as toProdcueAt, | |||
| psl.* | |||
| from production_schedule_line psl | |||
| left join production_schedule ps on psl.prodScheduleId = ps.id | |||
| left join items it on psl.itemId = it.id | |||
| left join bom on it.id = bom.itemId | |||
| left join bom_material bm on bom.id = bm.bomId | |||
| left join items itm on bm.itemId = itm.id | |||
| left join inventory iv on itm.id = iv.itemId | |||
| where ps.produceAt >= :fromDate | |||
| and ps.id = (select id from production_schedule where produceAt = ps.produceAt order by id desc limit 1) | |||
| -- and it.code = 'PP2236' | |||
| -- order by ps.produceAt asc, psl.itemPriority desc, itemCode asc | |||
| group by matCode | |||
| """; | |||
| return jdbcDao.queryForList(sql, args); | |||
| } | |||
| } | |||
| @@ -23,7 +23,10 @@ import java.time.format.DateTimeFormatter | |||
| import java.util.HashMap | |||
| import kotlin.collections.component1 | |||
| import kotlin.collections.component2 | |||
| import org.springframework.http.ResponseEntity | |||
| import org.springframework.http.HttpHeaders | |||
| import org.springframework.http.MediaType | |||
| import org.springframework.web.bind.annotation.GetMapping | |||
| @RestController | |||
| @RequestMapping("/productionSchedule") | |||
| @@ -228,4 +231,20 @@ class ProductionScheduleController( | |||
| throw RuntimeException("Error generate schedule: ${e.message}", e) | |||
| } | |||
| } | |||
| @PostMapping( | |||
| value = ["/export-prod-schedule"], | |||
| produces = ["application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"] | |||
| ) | |||
| fun exportProdSchedule(): ResponseEntity<ByteArray> { | |||
| val data = productionScheduleService.searchExportProdSchedule(LocalDate.now()) | |||
| val dataMat = productionScheduleService.searchExportProdScheduleMaterial(LocalDate.now()) | |||
| val excelContent = productionScheduleService.exportProdScheduleToExcel(data, dataMat) | |||
| return ResponseEntity.ok() | |||
| .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=production_schedule.xlsx") | |||
| .contentType(MediaType.parseMediaType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")) | |||
| .body(excelContent) | |||
| } | |||
| } | |||
| @@ -665,7 +665,7 @@ open class ProductProcessService( | |||
| } else { | |||
| "insufficient" | |||
| } | |||
| jobOrderLineInfo( | |||
| id = line.id?:0, | |||
| itemId = itemId, | |||
| @@ -685,7 +685,7 @@ open class ProductProcessService( | |||
| } | |||
| } | |||
| open fun createProductProcessByJobOrderId(jobOrderId: Long, productionPriority: Int?=50): MessageResponse { | |||
| open fun createProductProcessByJobOrderId(jobOrderId: Long): MessageResponse { | |||
| val jobOrder = jobOrderRepository.findById(jobOrderId).orElse(null) | |||
| val bom = bomRepository.findById(jobOrder?.bom?.id ?: 0L).orElse(null) | |||
| // val bom = jobOrder.bom.let { bomRepository.findById(it).orElse(null) } | |||
| @@ -702,6 +702,7 @@ open class ProductProcessService( | |||
| this.status = ProductProcessStatus.PENDING | |||
| this.date = jobOrder?.planEnd?.toLocalDate() | |||
| this.bom = bom | |||
| this.productionPriority = productionPriority | |||
| } | |||
| @@ -920,10 +921,10 @@ open class ProductProcessService( | |||
| } | |||
| open fun getJobOrderProcessLineDetail(productProcessLineId: Long): JobOrderProcessLineDetailResponse { | |||
| val productProcessLine = productProcessLineRepository.findById(productProcessLineId).orElse(null) | |||
| val bomProcessId = productProcessLine?.bomProcess?.id | |||
| println("bomProcessId ${bomProcessId}") | |||
| val bomProcess: BomProcess? = bomProcessId?.let { | |||
| val bomProcess: BomProcess? = bomProcessId?.let { | |||
| bomProcessRepository.findAll().firstOrNull { it.id == bomProcessId } | |||
| } | |||
| println("bomProcess ${bomProcess?.id}") | |||
| @@ -1024,12 +1025,12 @@ open class ProductProcessService( | |||
| println("📋 Service: ProductProcessLine EndTime: ${productProcessLine.endTime}") | |||
| // 更新状态为 "Pass" | |||
| updateProductProcessLineStatus(productProcessLineId, "Pass") | |||
| // 检查是否所有 lines 都完成(Completed 或 Pass) | |||
| // 注意:这里应该传入 productProcessId,而不是 productProcessLineId | |||
| val productProcessId = productProcessLine?.productProcess?.id ?: 0L | |||
| ifAllLinesCompletedOrPassed(productProcessId) | |||
| return MessageResponse( | |||
| id = productProcessLineId, | |||
| code = "200", | |||
| @@ -1349,20 +1350,20 @@ open class ProductProcessService( | |||
| open fun ifAllLinesCompletedOrPassed(productProcessId: Long): MessageResponse { | |||
| // 获取所有 product process lines | |||
| val allproductProcessLines = productProcessLineRepository.findByProductProcess_Id(productProcessId) | |||
| // 检查是否所有 lines 都是 "Completed" 或 "Pass" | |||
| if(allproductProcessLines.all { it.status == "Completed" || it.status == "Pass" }) { | |||
| // 更新 product process 的 endTime 和状态 | |||
| updateProductProcessEndTime(productProcessId) | |||
| updateProductProcessStatus(productProcessId, ProductProcessStatus.COMPLETED) | |||
| val productProcess = productProcessRepository.findById(productProcessId).orElse(null) | |||
| val jobOrder = jobOrderRepository.findById(productProcess?.jobOrder?.id ?: 0L).orElse(null) | |||
| if(jobOrder != null) { | |||
| jobOrder.status = JobOrderStatus.STORING | |||
| jobOrderRepository.save(jobOrder) | |||
| stockInLineService.create( | |||
| SaveStockInLineRequest( | |||
| itemId = productProcess?.item?.id ?: 0L, | |||
| @@ -1377,7 +1378,7 @@ open class ProductProcessService( | |||
| ) | |||
| } | |||
| } | |||
| return MessageResponse( | |||
| id = productProcessId, | |||
| code = "200", | |||
| @@ -1387,7 +1388,7 @@ open class ProductProcessService( | |||
| errorPosition = null, | |||
| ) | |||
| } | |||
| open fun SaveProductProcessIssueTime(request: SaveProductProcessIssueTimeRequest): MessageResponse { | |||
| println("📋 Service: Saving ProductProcess Issue Time: ${request.productProcessLineId}") | |||
| val productProcessLine = productProcessLineRepository.findById(request.productProcessLineId).orElse(null) | |||
| @@ -22,6 +22,7 @@ public class UpdateUserReq { | |||
| private LocalDate expiryDate; | |||
| private String locale; | |||
| private String remarks; | |||
| private String staffNo; | |||
| // @NotNull | |||
| @@ -126,4 +127,12 @@ public class UpdateUserReq { | |||
| this.remarks = remarks; | |||
| } | |||
| public String getStaffNo() { // Add getter | |||
| return staffNo; | |||
| } | |||
| public void setStaffNo(String staffNo) { // Add setter | |||
| this.staffNo = staffNo; | |||
| } | |||
| } | |||
| @@ -0,0 +1,76 @@ | |||
| package com.ffii.fpsms.modules.user.service | |||
| import com.ffii.core.utils.PdfUtils | |||
| import com.ffii.core.utils.QrCodeUtil | |||
| import com.ffii.fpsms.modules.user.entity.UserRepository | |||
| import com.ffii.fpsms.modules.user.web.ExportUserQrCodeRequest | |||
| import net.sf.jasperreports.engine.JasperCompileManager | |||
| import net.sf.jasperreports.engine.JasperPrint | |||
| import net.sf.jasperreports.engine.export.JRPdfExporter | |||
| import net.sf.jasperreports.export.SimpleExporterInput | |||
| import net.sf.jasperreports.export.SimpleOutputStreamExporterOutput | |||
| import org.springframework.core.io.ClassPathResource | |||
| import org.springframework.stereotype.Service | |||
| import java.io.FileNotFoundException | |||
| import java.awt.GraphicsEnvironment | |||
| import kotlinx.serialization.json.Json | |||
| import kotlinx.serialization.encodeToString | |||
| @Service | |||
| class UserQrCodeService( | |||
| private val userRepository: UserRepository | |||
| ) { | |||
| fun exportUserQrCode(request: ExportUserQrCodeRequest): Map<String, Any> { | |||
| val QRCODE_HANDLE_PDF = "qrCodeHandle/qrCodeHandle.jrxml" | |||
| val resource = ClassPathResource(QRCODE_HANDLE_PDF) | |||
| if (!resource.exists()) { | |||
| throw FileNotFoundException("Report file not found: $QRCODE_HANDLE_PDF") | |||
| } | |||
| val inputStream = resource.inputStream | |||
| val qrCodeHandleReport = JasperCompileManager.compileReport(inputStream) | |||
| val users = userRepository.findAllById(request.userIds) | |||
| val fields = mutableListOf<MutableMap<String, Any>>() | |||
| for (user in users) { | |||
| val field = mutableMapOf<String, Any>() | |||
| val staffNo = user.staffNo ?: "" | |||
| val username = user.username ?: "N/A" | |||
| val qrContentMap = mapOf("staffNo" to staffNo) | |||
| val qrCodeContent = Json.encodeToString(qrContentMap) | |||
| val qrCodeImage = QrCodeUtil.generateQRCodeImage(qrCodeContent) | |||
| field["username"] = username | |||
| field["staffNo"] = staffNo.ifEmpty { "N/A" } | |||
| field["qrCode"] = qrCodeImage | |||
| fields.add(field) | |||
| } | |||
| val params: MutableMap<String, Any> = mutableMapOf() | |||
| // Configure for Chinese character support | |||
| // Try to find a Chinese-supporting font | |||
| val availableFonts = GraphicsEnvironment.getLocalGraphicsEnvironment().availableFontFamilyNames | |||
| val chineseFont = availableFonts.find { | |||
| it.contains("SimSun", ignoreCase = true) || | |||
| it.contains("Microsoft YaHei", ignoreCase = true) || | |||
| it.contains("STSong", ignoreCase = true) || | |||
| it.contains("SimHei", ignoreCase = true) | |||
| } ?: "Arial Unicode MS" // Fallback | |||
| params["net.sf.jasperreports.default.pdf.encoding"] = "Identity-H" | |||
| params["net.sf.jasperreports.default.pdf.embedded"] = true | |||
| params["net.sf.jasperreports.default.pdf.font.name"] = chineseFont | |||
| return mapOf( | |||
| "report" to PdfUtils.fillReport(qrCodeHandleReport, fields, params), | |||
| "fileName" to (users.firstOrNull()?.username ?: "user_qrcode") | |||
| ) | |||
| } | |||
| } | |||
| @@ -111,7 +111,8 @@ public class UserService extends AbstractBaseEntityService<User, Long, UserRepos | |||
| + " u.email," | |||
| + " u.phone1," | |||
| + " u.phone2," | |||
| + " u.remarks " | |||
| + " u.remarks," | |||
| + " u.staffNo" | |||
| + " FROM `user` u" | |||
| + " left join user_group ug on u.id = ug.userId" | |||
| + " where u.deleted = false"); | |||
| @@ -24,6 +24,7 @@ public class UserRecord { | |||
| private String phone1; | |||
| private String phone2; | |||
| private String remarks; | |||
| private String staffNo; | |||
| public Integer getId() { | |||
| return id; | |||
| @@ -151,5 +152,6 @@ public class UserRecord { | |||
| public void setRemarks(String remarks) { | |||
| this.remarks = remarks; | |||
| } | |||
| public String getStaffNo() { return staffNo; } | |||
| public void setStaffNo(String staffNo) { this.staffNo = staffNo; } | |||
| } | |||
| @@ -0,0 +1,5 @@ | |||
| package com.ffii.fpsms.modules.user.web | |||
| data class ExportUserQrCodeRequest( | |||
| val userIds: List<Long> | |||
| ) | |||
| @@ -42,6 +42,14 @@ import com.ffii.fpsms.modules.user.req.UpdateUserReq; | |||
| import com.ffii.fpsms.modules.user.service.UserService; | |||
| import com.ffii.fpsms.modules.user.service.res.LoadUserRes; | |||
| import com.ffii.fpsms.modules.user.web.ExportUserQrCodeRequest; | |||
| import com.ffii.fpsms.modules.user.service.UserQrCodeService; | |||
| import jakarta.servlet.http.HttpServletResponse; | |||
| import net.sf.jasperreports.engine.JasperExportManager; | |||
| import net.sf.jasperreports.engine.JasperPrint; | |||
| import java.io.OutputStream; | |||
| import java.io.UnsupportedEncodingException; | |||
| import jakarta.validation.Valid; | |||
| import jakarta.validation.constraints.NotBlank; | |||
| @@ -53,14 +61,17 @@ public class UserController{ | |||
| private UserService userService; | |||
| private PasswordEncoder passwordEncoder; | |||
| private SettingsService settingsService; | |||
| private UserQrCodeService userQrCodeService; | |||
| public UserController( | |||
| UserService userService, | |||
| PasswordEncoder passwordEncoder, | |||
| SettingsService settingsService) { | |||
| SettingsService settingsService, | |||
| UserQrCodeService userQrCodeService) { | |||
| this.userService = userService; | |||
| this.passwordEncoder = passwordEncoder; | |||
| this.settingsService = settingsService; | |||
| this.userQrCodeService = userQrCodeService; | |||
| } | |||
| // @Operation(summary = "list user", responses = { @ApiResponse(responseCode = "200"), | |||
| @@ -229,6 +240,20 @@ public class UserController{ | |||
| return new PasswordRule(settingsService); | |||
| } | |||
| @PostMapping("/export-qrcode") | |||
| public void exportQrCode(@Valid @RequestBody ExportUserQrCodeRequest request, HttpServletResponse response) | |||
| throws Exception { | |||
| response.setCharacterEncoding("utf-8"); | |||
| response.setContentType("application/pdf"); | |||
| OutputStream out = response.getOutputStream(); | |||
| Map<String, Object> pdf = userQrCodeService.exportUserQrCode(request); | |||
| JasperPrint jasperPrint = (JasperPrint) pdf.get("report"); | |||
| String fileName = (String) pdf.get("fileName"); | |||
| response.addHeader("Content-Disposition", "attachment; filename=\"" + fileName + "_qrcode.pdf\""); | |||
| out.write(JasperExportManager.exportReportToPdf(jasperPrint)); | |||
| out.flush(); | |||
| } | |||
| public static class AdminChangePwdReq { | |||
| private Long id; | |||
| @NotBlank | |||
| @@ -0,0 +1,42 @@ | |||
| <?xml version="1.0" encoding="UTF-8"?> | |||
| <!-- Created with Jaspersoft Studio version 6.21.3.final using JasperReports Library version 6.21.3-4a3078d20785ebe464f18037d738d12fc98c13cf --> | |||
| <jasperReport xmlns="http://jasperreports.sourceforge.net/jasperreports" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://jasperreports.sourceforge.net/jasperreports http://jasperreports.sourceforge.net/xsd/jasperreport.xsd" name="qrCodeHandle" pageWidth="595" pageHeight="842" columnWidth="555" leftMargin="20" rightMargin="20" topMargin="80" bottomMargin="20" uuid="c2f7cd27-3725-47ce-ac85-d8a38dc906fa"> | |||
| <property name="com.jaspersoft.studio.data.defaultdataadapter" value="One Empty Record"/> | |||
| <property name="com.jaspersoft.studio.unit." value="pixel"/> | |||
| <property name="com.jaspersoft.studio.unit.pageHeight" value="pixel"/> | |||
| <property name="com.jaspersoft.studio.unit.pageWidth" value="pixel"/> | |||
| <property name="com.jaspersoft.studio.unit.topMargin" value="pixel"/> | |||
| <property name="com.jaspersoft.studio.unit.bottomMargin" value="pixel"/> | |||
| <property name="com.jaspersoft.studio.unit.leftMargin" value="pixel"/> | |||
| <property name="com.jaspersoft.studio.unit.rightMargin" value="pixel"/> | |||
| <property name="com.jaspersoft.studio.unit.columnWidth" value="pixel"/> | |||
| <property name="com.jaspersoft.studio.unit.columnSpacing" value="pixel"/> | |||
| <queryString> | |||
| <![CDATA[]]> | |||
| </queryString> | |||
| <field name="username" class="java.lang.String"/> | |||
| <field name="staffNo" class="java.lang.String"/> | |||
| <field name="qrCode" class="java.awt.Image"/> | |||
| <background> | |||
| <band splitType="Stretch"/> | |||
| </background> | |||
| <detail> | |||
| <band height="670" splitType="Stretch"> | |||
| <textField isStretchWithOverflow="true" isBlankWhenNull="false"> | |||
| <reportElement stretchType="RelativeToTallestObject" x="37" y="0" width="480" height="120" uuid="e3faf8de-84ba-423f-b6cf-84ba21598686"> | |||
| <property name="com.jaspersoft.studio.unit.x" value="px"/> | |||
| <property name="com.jaspersoft.studio.unit.width" value="px"/> | |||
| <property name="com.jaspersoft.studio.unit.height" value="px"/> | |||
| </reportElement> | |||
| <textElement textAlignment="Center" verticalAlignment="Middle"> | |||
| <font size="54" isBold="true" fontName="微軟正黑體" pdfEncoding="Identity-H" isPdfEmbedded="true"/> | |||
| </textElement> | |||
| <textFieldExpression><![CDATA[$F{username}]]></textFieldExpression> | |||
| </textField> | |||
| <image> | |||
| <reportElement x="27" y="120" width="500" height="500" uuid="b1a8ee23-9f0f-4014-9996-e0025222dcd2"/> | |||
| <imageExpression><![CDATA[$F{qrCode}]]></imageExpression> | |||
| </image> | |||
| </band> | |||
| </detail> | |||
| </jasperReport> | |||