# Conflicts: # src/main/java/com/ffii/fpsms/modules/productProcess/service/ProductProcessService.ktmaster
| @@ -63,7 +63,7 @@ open class M18DeliveryOrderService( | |||||
| val dateTo = request.modifiedDateTo?.let { | val dateTo = request.modifiedDateTo?.let { | ||||
| LocalDateTime.parse(it, formatter).toLocalDate().toString() | LocalDateTime.parse(it, formatter).toLocalDate().toString() | ||||
| } | } | ||||
| val lastModifyDateConds = | |||||
| val lastDateConds = | |||||
| //"lastModifyDate=largerOrEqual=${request.modifiedDateFrom ?: lastModifyDateStart}=and=lastModifyDate=lessOrEqual=${request.modifiedDateTo ?: lastModifyDateEnd}" | //"lastModifyDate=largerOrEqual=${request.modifiedDateFrom ?: lastModifyDateStart}=and=lastModifyDate=lessOrEqual=${request.modifiedDateTo ?: lastModifyDateEnd}" | ||||
| "dDate=largerOrEqual=${dateFrom ?: lastModifyDateStart}=and=dDate=lessOrEqual=${dateTo ?: lastModifyDateEnd}" | "dDate=largerOrEqual=${dateFrom ?: lastModifyDateStart}=and=dDate=lessOrEqual=${dateTo ?: lastModifyDateEnd}" | ||||
| @@ -74,7 +74,7 @@ open class M18DeliveryOrderService( | |||||
| "venId=equal=", | "venId=equal=", | ||||
| "=or=" | "=or=" | ||||
| ) | ) | ||||
| val shopPoConds = "(${shopPoBuyers})=and=(${shopPoSupplier})=and=(${lastModifyDateConds})" | |||||
| val shopPoConds = "(${shopPoBuyers})=and=(${shopPoSupplier})=and=(${lastDateConds})" | |||||
| println("shopPoConds: ${shopPoConds}") | println("shopPoConds: ${shopPoConds}") | ||||
| val shopPoParams = M18PurchaseOrderListRequest( | val shopPoParams = M18PurchaseOrderListRequest( | ||||
| params = null, | params = null, | ||||
| @@ -24,6 +24,7 @@ import org.slf4j.LoggerFactory | |||||
| import org.springframework.stereotype.Service | import org.springframework.stereotype.Service | ||||
| import java.time.LocalDateTime | import java.time.LocalDateTime | ||||
| import kotlin.reflect.full.memberProperties | import kotlin.reflect.full.memberProperties | ||||
| import java.time.format.DateTimeFormatter | |||||
| @Service | @Service | ||||
| open class M18PurchaseOrderService( | open class M18PurchaseOrderService( | ||||
| @@ -57,8 +58,16 @@ open class M18PurchaseOrderService( | |||||
| // Include material po, oem po | // Include material po, oem po | ||||
| open fun getPurchaseOrdersWithType(request: M18CommonRequest): M18PurchaseOrderListResponseWithType? { | open fun getPurchaseOrdersWithType(request: M18CommonRequest): M18PurchaseOrderListResponseWithType? { | ||||
| val purchaseOrders = M18PurchaseOrderListResponseWithType(mutableListOf()) | 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 | // Material PO | ||||
| val materialPoBuyers = | val materialPoBuyers = | ||||
| commonUtils.listToString(listOf(m18Config.BEID_PP, m18Config.BEID_PF), "beId=equal=", "=or=") | commonUtils.listToString(listOf(m18Config.BEID_PP, m18Config.BEID_PF), "beId=equal=", "=or=") | ||||
| @@ -67,7 +76,7 @@ open class M18PurchaseOrderService( | |||||
| "venId=unequal=", | "venId=unequal=", | ||||
| "=or=" | "=or=" | ||||
| ) | ) | ||||
| val materialPoConds = "(${materialPoBuyers})=and=(${materialPoSupplierNot})=and=(${lastModifyDateConds})" | |||||
| val materialPoConds = "(${materialPoBuyers})=and=(${materialPoSupplierNot})=and=(${lastDateConds})" | |||||
| println("materialPoConds: ${materialPoConds}") | println("materialPoConds: ${materialPoConds}") | ||||
| val materialPoParams = M18PurchaseOrderListRequest( | val materialPoParams = M18PurchaseOrderListRequest( | ||||
| params = null, | params = null, | ||||
| @@ -153,13 +153,24 @@ open class SchedulerService( | |||||
| logger.info("Daily Scheduler - PO") | logger.info("Daily Scheduler - PO") | ||||
| val currentTime = LocalDateTime.now() | val currentTime = LocalDateTime.now() | ||||
| val today = currentTime.toLocalDate().atStartOfDay() | val today = currentTime.toLocalDate().atStartOfDay() | ||||
| val yesterday = today.minusDays(1L) | |||||
| /* val yesterday = today.minusDays(1L) | |||||
| val request = M18CommonRequest( | val request = M18CommonRequest( | ||||
| modifiedDateTo = today.format(dataStringFormat), | modifiedDateTo = today.format(dataStringFormat), | ||||
| modifiedDateFrom = yesterday.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); | 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("today: ${today.format(dataStringFormat)}") | ||||
| // logger.info("yesterday: ${yesterday.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 (:schedulePeriodTo = '' or datediff(schedulePeriodTo, coalesce(:schedulePeriodTo, schedulePeriodTo)) = 0) | ||||
| and (:totalEstProdCount is null or :totalEstProdCount = '' or totalEstProdCount = :totalEstProdCount) | and (:totalEstProdCount is null or :totalEstProdCount = '' or totalEstProdCount = :totalEstProdCount) | ||||
| and (coalesce(:types) is null or type in :types) | and (coalesce(:types) is null or type in :types) | ||||
| order by id desc; | |||||
| order by id ASC; | |||||
| """, | """, | ||||
| countQuery = | countQuery = | ||||
| """ | """ | ||||
| @@ -83,7 +83,7 @@ interface ProductionScheduleRepository : AbstractRepository<ProductionSchedule, | |||||
| and (:schedulePeriodTo = '' or datediff(schedulePeriodTo, coalesce(:schedulePeriodTo, schedulePeriodTo)) = 0) | and (:schedulePeriodTo = '' or datediff(schedulePeriodTo, coalesce(:schedulePeriodTo, schedulePeriodTo)) = 0) | ||||
| and (:totalEstProdCount is null or :totalEstProdCount = '' or totalEstProdCount = :totalEstProdCount) | and (:totalEstProdCount is null or :totalEstProdCount = '' or totalEstProdCount = :totalEstProdCount) | ||||
| and (coalesce(:types) is null or type in :types) | and (coalesce(:types) is null or type in :types) | ||||
| order by id desc; | |||||
| order by id ASC; | |||||
| """, | """, | ||||
| ) | ) | ||||
| fun findProdScheduleInfoByPage( | fun findProdScheduleInfoByPage( | ||||
| @@ -121,7 +121,7 @@ interface ProductionScheduleRepository : AbstractRepository<ProductionSchedule, | |||||
| and (:produceAt = '' or datediff(produceAt, coalesce(:produceAt, produceAt)) = 0) | and (:produceAt = '' or datediff(produceAt, coalesce(:produceAt, produceAt)) = 0) | ||||
| and (:totalEstProdCount is null or :totalEstProdCount = '' or totalEstProdCount = :totalEstProdCount) | and (:totalEstProdCount is null or :totalEstProdCount = '' or totalEstProdCount = :totalEstProdCount) | ||||
| and (coalesce(:types) is null or type in :types) | and (coalesce(:types) is null or type in :types) | ||||
| order by id desc; | |||||
| order by id ASC; | |||||
| """ | """ | ||||
| ) | ) | ||||
| fun findProdScheduleInfoByProduceAtByPage( | fun findProdScheduleInfoByProduceAtByPage( | ||||
| @@ -207,10 +207,31 @@ interface ProductionScheduleRepository : AbstractRepository<ProductionSchedule, | |||||
| prod.itemPriority, | prod.itemPriority, | ||||
| prod.batchNeed, | prod.batchNeed, | ||||
| prod.stockQty, | prod.stockQty, | ||||
| prod.outputQty, | |||||
| prod.prodQty, | |||||
| prod.avgQtyLastMonth, | prod.avgQtyLastMonth, | ||||
| json_arrayagg( | 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 | ) as prodScheduleLines | ||||
| from ( | from ( | ||||
| select | select | ||||
| @@ -226,6 +247,8 @@ interface ProductionScheduleRepository : AbstractRepository<ProductionSchedule, | |||||
| psl.itemPriority, | psl.itemPriority, | ||||
| psl.batchNeed, | psl.batchNeed, | ||||
| psl.stockQty, | psl.stockQty, | ||||
| psl.outputQty, | |||||
| psl.prodQty, | |||||
| pm.bomMaterials, | pm.bomMaterials, | ||||
| pm.bomOutputQty, | pm.bomOutputQty, | ||||
| uc.udfudesc as uomName, | uc.udfudesc as uomName, | ||||
| @@ -10,6 +10,7 @@ interface PrinterCombo { | |||||
| val value: String; | val value: String; | ||||
| val code: String?; | val code: String?; | ||||
| val name: String?; | val name: String?; | ||||
| val type: String?; | |||||
| val description: String?; | val description: String?; | ||||
| val ip: String?; | val ip: String?; | ||||
| val port: Int?; | val port: Int?; | ||||
| @@ -50,6 +50,8 @@ data class DetailedProdScheduleLineInfo( | |||||
| val daysLeft: BigDecimal?, | val daysLeft: BigDecimal?, | ||||
| val onHandQty: BigDecimal?, | val onHandQty: BigDecimal?, | ||||
| val stockQty: BigDecimal?, | val stockQty: BigDecimal?, | ||||
| val outputQty: BigDecimal?, | |||||
| val prodQty: BigDecimal?, | |||||
| val lastMonthAvgSales: BigDecimal?, | val lastMonthAvgSales: BigDecimal?, | ||||
| val avgQtyLastMonth: BigDecimal?, | val avgQtyLastMonth: BigDecimal?, | ||||
| val needNoOfJobOrder: BigDecimal?, | val needNoOfJobOrder: BigDecimal?, | ||||
| @@ -46,6 +46,10 @@ import kotlin.collections.component2 | |||||
| import kotlin.jvm.optionals.getOrNull | import kotlin.jvm.optionals.getOrNull | ||||
| import kotlin.math.ceil | import kotlin.math.ceil | ||||
| import kotlin.comparisons.maxOf | 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 | @Service | ||||
| open class ProductionScheduleService( | open class ProductionScheduleService( | ||||
| @@ -336,7 +340,7 @@ open class ProductionScheduleService( | |||||
| open fun saveProdScheduleLine(request: ReleaseProdScheduleLineRequest): MessageResponse { | open fun saveProdScheduleLine(request: ReleaseProdScheduleLineRequest): MessageResponse { | ||||
| val prodScheduleLine = request.id.let { productionScheduleLineRepository.findById(it).getOrNull() } ?: throw NoSuchElementException() | val prodScheduleLine = request.id.let { productionScheduleLineRepository.findById(it).getOrNull() } ?: throw NoSuchElementException() | ||||
| val prodSchedule = prodScheduleLine.productionSchedule | val prodSchedule = prodScheduleLine.productionSchedule | ||||
| // Update Prod Schedule Type | // Update Prod Schedule Type | ||||
| prodSchedule.apply { | prodSchedule.apply { | ||||
| type = "manual" | type = "manual" | ||||
| @@ -351,7 +355,7 @@ open class ProductionScheduleService( | |||||
| productionScheduleLineRepository.saveAndFlush(prodScheduleLine) | productionScheduleLineRepository.saveAndFlush(prodScheduleLine) | ||||
| val bomMaterials = prodScheduleLine.id?.let { productionScheduleLineRepository.getBomMaterials(it) } | val bomMaterials = prodScheduleLine.id?.let { productionScheduleLineRepository.getBomMaterials(it) } | ||||
| return MessageResponse( | return MessageResponse( | ||||
| id = request.id, | id = request.id, | ||||
| name = null, | name = null, | ||||
| @@ -422,11 +426,11 @@ open class ProductionScheduleService( | |||||
| logger.info("bom?.outputQty: ${bom.outputQty} ${bom.outputQtyUom}") | logger.info("bom?.outputQty: ${bom.outputQty} ${bom.outputQtyUom}") | ||||
| logger.info("[releaseProdSchedule] prodScheduleLine.needNoOfJobOrder:" + prodScheduleLine.needNoOfJobOrder) | logger.info("[releaseProdSchedule] prodScheduleLine.needNoOfJobOrder:" + prodScheduleLine.needNoOfJobOrder) | ||||
| repeat(prodScheduleLine.needNoOfJobOrder) { | |||||
| //repeat(prodScheduleLine.needNoOfJobOrder) { | |||||
| // 6. Create Job Order | // 6. Create Job Order | ||||
| val joRequest = CreateJobOrderRequest( | val joRequest = CreateJobOrderRequest( | ||||
| bomId = bom.id, // bom is guaranteed non-null here | 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, | approverId = approver?.id, | ||||
| // CRUCIAL FIX: Use the line ID, not the parent schedule 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 | // 7. Create related job order data | ||||
| jobOrderBomMaterialService.createJobOrderBomMaterialsByJoId(createdJobOrderId) | jobOrderBomMaterialService.createJobOrderBomMaterialsByJoId(createdJobOrderId) | ||||
| jobOrderProcessService.createJobOrderProcessesByJoId(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("bom?.outputQty:" + bom?.outputQty + "" + bom?.outputQtyUom); | ||||
| logger.info("prodScheduleLine.needNoOfJobOrder:" + prodScheduleLine.needNoOfJobOrder) | 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( | 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, | 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) | 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 | // Get Latest Data | ||||
| @@ -593,7 +620,7 @@ open class ProductionScheduleService( | |||||
| CEIL((i.avgQtyLastMonth * 1.9 - stockQty) / i.outputQty) | CEIL((i.avgQtyLastMonth * 1.9 - stockQty) / i.outputQty) | ||||
| ELSE 0 | ELSE 0 | ||||
| END AS batchNeed, | END AS batchNeed, | ||||
| markDark + markFloat + markDense + markAS as priority, | |||||
| 25 + 25 + markDark + markFloat + markDense + markAS + markTimeSequence + markComplexity as priority, | |||||
| i.* | i.* | ||||
| FROM | FROM | ||||
| (SELECT | (SELECT | ||||
| @@ -627,14 +654,29 @@ open class ProductionScheduleService( | |||||
| inventory.onHandQty, | inventory.onHandQty, | ||||
| bom.itemId, | bom.itemId, | ||||
| bom.id AS bomId, | bom.id AS bomId, | ||||
| CASE WHEN bom.isDark = 5 THEN 11 | CASE WHEN bom.isDark = 5 THEN 11 | ||||
| WHEN bom.isDark = 3 THEN 6 | |||||
| WHEN bom.isDark = 1 THEN 2 | |||||
| ELSE 0 END as markDark, | 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, | ELSE 0 END as markFloat, | ||||
| CASE WHEN bom.isDense = 5 THEN 11 | CASE WHEN bom.isDense = 5 THEN 11 | ||||
| WHEN bom.isDense = 3 THEN 6 | |||||
| WHEN bom.isDense = 1 THEN 2 | |||||
| ELSE 0 END as markDense, | ELSE 0 END as markDense, | ||||
| bom.timeSequence as markTimeSequence, | |||||
| bom.complexity as markComplexity, | |||||
| CASE WHEN bom.allergicSubstances = 5 THEN 11 | CASE WHEN bom.allergicSubstances = 5 THEN 11 | ||||
| ELSE 0 END as markAS, | ELSE 0 END as markAS, | ||||
| inventory.id AS inventoryId | inventory.id AS inventoryId | ||||
| FROM | FROM | ||||
| bom | bom | ||||
| @@ -663,6 +705,7 @@ open class ProductionScheduleService( | |||||
| needNoOfJobOrder = (row["needNoOfJobOrder"] as Number).toLong() | needNoOfJobOrder = (row["needNoOfJobOrder"] as Number).toLong() | ||||
| daysLeft = (row["daysLeft"] as Number).toDouble() | daysLeft = (row["daysLeft"] as Number).toDouble() | ||||
| batchNeed = row["batchNeed"] as Number | 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 allergicSubstances = maxOf(record.allergicSubstances.toDouble(), 0.0) | ||||
| val priority = isDark + isFloat + isDense + allergicSubstances | val priority = isDark + isFloat + isDense + allergicSubstances | ||||
| record.itemPriority = priority | |||||
| record.itemPriority = record.priority | |||||
| productionPriorityMap.put(record, priority) | productionPriorityMap.put(record, priority) | ||||
| } | } | ||||
| @@ -761,6 +804,10 @@ open class ProductionScheduleService( | |||||
| record.needNoOfJobOrder = 0 | record.needNoOfJobOrder = 0 | ||||
| record.batchNeed = 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) | 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.type = "detailed" | ||||
| savedItem.assignDate = LocalDateTime.now().dayOfMonth.toLong(); | savedItem.assignDate = LocalDateTime.now().dayOfMonth.toLong(); | ||||
| savedItem.weightingRef = detailedScheduleObj.needQty | savedItem.weightingRef = detailedScheduleObj.needQty | ||||
| savedItem.itemPriority = detailedScheduleObj.needQty.toLong() | |||||
| savedItem.itemPriority = detailedScheduleObj.priority.toLong() | |||||
| savedItem.outputQty = detailedScheduleObj.outputQty | savedItem.outputQty = detailedScheduleObj.outputQty | ||||
| savedItem.avgQtyLastMonth = detailedScheduleObj.avgQtyLastMonth | savedItem.avgQtyLastMonth = detailedScheduleObj.avgQtyLastMonth | ||||
| savedItem.stockQty = detailedScheduleObj.stockQty | savedItem.stockQty = detailedScheduleObj.stockQty | ||||
| @@ -862,11 +909,15 @@ open class ProductionScheduleService( | |||||
| open var stockQty: Double = 0.0 | open var stockQty: Double = 0.0 | ||||
| open var daysLeft: Double = 0.0 | open var daysLeft: Double = 0.0 | ||||
| open var batchNeed: Number = 0 | open var batchNeed: Number = 0 | ||||
| open var priority: Number = 0 | |||||
| override fun toString(): String { | override fun toString(): String { | ||||
| return "NeedQtyRecord(name=${name}," + | return "NeedQtyRecord(name=${name}," + | ||||
| " avgQtyLastMonth=$avgQtyLastMonth" + | " avgQtyLastMonth=$avgQtyLastMonth" + | ||||
| " stockQty=$stockQty" + | " stockQty=$stockQty" + | ||||
| " daysLeft=$daysLeft" + | |||||
| " batchNeed=$batchNeed" + | |||||
| " priority=$priority" + | |||||
| " itemId=$id" + | " itemId=$id" + | ||||
| " needNoOfJobOrder=$needNoOfJobOrder" + | " needNoOfJobOrder=$needNoOfJobOrder" + | ||||
| " outputQty=$outputQty" + | " 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 java.util.HashMap | ||||
| import kotlin.collections.component1 | import kotlin.collections.component1 | ||||
| import kotlin.collections.component2 | 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 | @RestController | ||||
| @RequestMapping("/productionSchedule") | @RequestMapping("/productionSchedule") | ||||
| @@ -228,4 +231,20 @@ class ProductionScheduleController( | |||||
| throw RuntimeException("Error generate schedule: ${e.message}", e) | 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 { | } else { | ||||
| "insufficient" | "insufficient" | ||||
| } | } | ||||
| jobOrderLineInfo( | jobOrderLineInfo( | ||||
| id = line.id?:0, | id = line.id?:0, | ||||
| itemId = itemId, | 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 jobOrder = jobOrderRepository.findById(jobOrderId).orElse(null) | ||||
| val bom = bomRepository.findById(jobOrder?.bom?.id ?: 0L).orElse(null) | val bom = bomRepository.findById(jobOrder?.bom?.id ?: 0L).orElse(null) | ||||
| // val bom = jobOrder.bom.let { bomRepository.findById(it).orElse(null) } | // val bom = jobOrder.bom.let { bomRepository.findById(it).orElse(null) } | ||||
| @@ -702,6 +702,7 @@ open class ProductProcessService( | |||||
| this.status = ProductProcessStatus.PENDING | this.status = ProductProcessStatus.PENDING | ||||
| this.date = jobOrder?.planEnd?.toLocalDate() | this.date = jobOrder?.planEnd?.toLocalDate() | ||||
| this.bom = bom | this.bom = bom | ||||
| this.productionPriority = productionPriority | this.productionPriority = productionPriority | ||||
| } | } | ||||
| @@ -920,10 +921,10 @@ open class ProductProcessService( | |||||
| } | } | ||||
| open fun getJobOrderProcessLineDetail(productProcessLineId: Long): JobOrderProcessLineDetailResponse { | open fun getJobOrderProcessLineDetail(productProcessLineId: Long): JobOrderProcessLineDetailResponse { | ||||
| val productProcessLine = productProcessLineRepository.findById(productProcessLineId).orElse(null) | val productProcessLine = productProcessLineRepository.findById(productProcessLineId).orElse(null) | ||||
| val bomProcessId = productProcessLine?.bomProcess?.id | val bomProcessId = productProcessLine?.bomProcess?.id | ||||
| println("bomProcessId ${bomProcessId}") | println("bomProcessId ${bomProcessId}") | ||||
| val bomProcess: BomProcess? = bomProcessId?.let { | |||||
| val bomProcess: BomProcess? = bomProcessId?.let { | |||||
| bomProcessRepository.findAll().firstOrNull { it.id == bomProcessId } | bomProcessRepository.findAll().firstOrNull { it.id == bomProcessId } | ||||
| } | } | ||||
| println("bomProcess ${bomProcess?.id}") | println("bomProcess ${bomProcess?.id}") | ||||
| @@ -1024,12 +1025,12 @@ open class ProductProcessService( | |||||
| println("📋 Service: ProductProcessLine EndTime: ${productProcessLine.endTime}") | println("📋 Service: ProductProcessLine EndTime: ${productProcessLine.endTime}") | ||||
| // 更新状态为 "Pass" | // 更新状态为 "Pass" | ||||
| updateProductProcessLineStatus(productProcessLineId, "Pass") | updateProductProcessLineStatus(productProcessLineId, "Pass") | ||||
| // 检查是否所有 lines 都完成(Completed 或 Pass) | // 检查是否所有 lines 都完成(Completed 或 Pass) | ||||
| // 注意:这里应该传入 productProcessId,而不是 productProcessLineId | // 注意:这里应该传入 productProcessId,而不是 productProcessLineId | ||||
| val productProcessId = productProcessLine?.productProcess?.id ?: 0L | val productProcessId = productProcessLine?.productProcess?.id ?: 0L | ||||
| ifAllLinesCompletedOrPassed(productProcessId) | ifAllLinesCompletedOrPassed(productProcessId) | ||||
| return MessageResponse( | return MessageResponse( | ||||
| id = productProcessLineId, | id = productProcessLineId, | ||||
| code = "200", | code = "200", | ||||
| @@ -1349,20 +1350,20 @@ open class ProductProcessService( | |||||
| open fun ifAllLinesCompletedOrPassed(productProcessId: Long): MessageResponse { | open fun ifAllLinesCompletedOrPassed(productProcessId: Long): MessageResponse { | ||||
| // 获取所有 product process lines | // 获取所有 product process lines | ||||
| val allproductProcessLines = productProcessLineRepository.findByProductProcess_Id(productProcessId) | val allproductProcessLines = productProcessLineRepository.findByProductProcess_Id(productProcessId) | ||||
| // 检查是否所有 lines 都是 "Completed" 或 "Pass" | // 检查是否所有 lines 都是 "Completed" 或 "Pass" | ||||
| if(allproductProcessLines.all { it.status == "Completed" || it.status == "Pass" }) { | if(allproductProcessLines.all { it.status == "Completed" || it.status == "Pass" }) { | ||||
| // 更新 product process 的 endTime 和状态 | // 更新 product process 的 endTime 和状态 | ||||
| updateProductProcessEndTime(productProcessId) | updateProductProcessEndTime(productProcessId) | ||||
| updateProductProcessStatus(productProcessId, ProductProcessStatus.COMPLETED) | updateProductProcessStatus(productProcessId, ProductProcessStatus.COMPLETED) | ||||
| val productProcess = productProcessRepository.findById(productProcessId).orElse(null) | val productProcess = productProcessRepository.findById(productProcessId).orElse(null) | ||||
| val jobOrder = jobOrderRepository.findById(productProcess?.jobOrder?.id ?: 0L).orElse(null) | val jobOrder = jobOrderRepository.findById(productProcess?.jobOrder?.id ?: 0L).orElse(null) | ||||
| if(jobOrder != null) { | if(jobOrder != null) { | ||||
| jobOrder.status = JobOrderStatus.STORING | jobOrder.status = JobOrderStatus.STORING | ||||
| jobOrderRepository.save(jobOrder) | jobOrderRepository.save(jobOrder) | ||||
| stockInLineService.create( | stockInLineService.create( | ||||
| SaveStockInLineRequest( | SaveStockInLineRequest( | ||||
| itemId = productProcess?.item?.id ?: 0L, | itemId = productProcess?.item?.id ?: 0L, | ||||
| @@ -1377,7 +1378,7 @@ open class ProductProcessService( | |||||
| ) | ) | ||||
| } | } | ||||
| } | } | ||||
| return MessageResponse( | return MessageResponse( | ||||
| id = productProcessId, | id = productProcessId, | ||||
| code = "200", | code = "200", | ||||
| @@ -1387,7 +1388,7 @@ open class ProductProcessService( | |||||
| errorPosition = null, | errorPosition = null, | ||||
| ) | ) | ||||
| } | } | ||||
| open fun SaveProductProcessIssueTime(request: SaveProductProcessIssueTimeRequest): MessageResponse { | open fun SaveProductProcessIssueTime(request: SaveProductProcessIssueTimeRequest): MessageResponse { | ||||
| println("📋 Service: Saving ProductProcess Issue Time: ${request.productProcessLineId}") | println("📋 Service: Saving ProductProcess Issue Time: ${request.productProcessLineId}") | ||||
| val productProcessLine = productProcessLineRepository.findById(request.productProcessLineId).orElse(null) | val productProcessLine = productProcessLineRepository.findById(request.productProcessLineId).orElse(null) | ||||
| @@ -22,6 +22,7 @@ public class UpdateUserReq { | |||||
| private LocalDate expiryDate; | private LocalDate expiryDate; | ||||
| private String locale; | private String locale; | ||||
| private String remarks; | private String remarks; | ||||
| private String staffNo; | |||||
| // @NotNull | // @NotNull | ||||
| @@ -126,4 +127,12 @@ public class UpdateUserReq { | |||||
| this.remarks = remarks; | 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.email," | ||||
| + " u.phone1," | + " u.phone1," | ||||
| + " u.phone2," | + " u.phone2," | ||||
| + " u.remarks " | |||||
| + " u.remarks," | |||||
| + " u.staffNo" | |||||
| + " FROM `user` u" | + " FROM `user` u" | ||||
| + " left join user_group ug on u.id = ug.userId" | + " left join user_group ug on u.id = ug.userId" | ||||
| + " where u.deleted = false"); | + " where u.deleted = false"); | ||||
| @@ -24,6 +24,7 @@ public class UserRecord { | |||||
| private String phone1; | private String phone1; | ||||
| private String phone2; | private String phone2; | ||||
| private String remarks; | private String remarks; | ||||
| private String staffNo; | |||||
| public Integer getId() { | public Integer getId() { | ||||
| return id; | return id; | ||||
| @@ -151,5 +152,6 @@ public class UserRecord { | |||||
| public void setRemarks(String remarks) { | public void setRemarks(String remarks) { | ||||
| this.remarks = 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.UserService; | ||||
| import com.ffii.fpsms.modules.user.service.res.LoadUserRes; | 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.Valid; | ||||
| import jakarta.validation.constraints.NotBlank; | import jakarta.validation.constraints.NotBlank; | ||||
| @@ -53,14 +61,17 @@ public class UserController{ | |||||
| private UserService userService; | private UserService userService; | ||||
| private PasswordEncoder passwordEncoder; | private PasswordEncoder passwordEncoder; | ||||
| private SettingsService settingsService; | private SettingsService settingsService; | ||||
| private UserQrCodeService userQrCodeService; | |||||
| public UserController( | public UserController( | ||||
| UserService userService, | UserService userService, | ||||
| PasswordEncoder passwordEncoder, | PasswordEncoder passwordEncoder, | ||||
| SettingsService settingsService) { | |||||
| SettingsService settingsService, | |||||
| UserQrCodeService userQrCodeService) { | |||||
| this.userService = userService; | this.userService = userService; | ||||
| this.passwordEncoder = passwordEncoder; | this.passwordEncoder = passwordEncoder; | ||||
| this.settingsService = settingsService; | this.settingsService = settingsService; | ||||
| this.userQrCodeService = userQrCodeService; | |||||
| } | } | ||||
| // @Operation(summary = "list user", responses = { @ApiResponse(responseCode = "200"), | // @Operation(summary = "list user", responses = { @ApiResponse(responseCode = "200"), | ||||
| @@ -229,6 +240,20 @@ public class UserController{ | |||||
| return new PasswordRule(settingsService); | 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 { | public static class AdminChangePwdReq { | ||||
| private Long id; | private Long id; | ||||
| @NotBlank | @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> | |||||