From c1520be4df20a4a05d3a4c767b428135c4a73a07 Mon Sep 17 00:00:00 2001 From: "cyril.tsui" Date: Thu, 7 Aug 2025 18:19:01 +0800 Subject: [PATCH 1/6] update --- src/main/java/com/ffii/fpsms/m18/model/M18MasterDataResponse.kt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/main/java/com/ffii/fpsms/m18/model/M18MasterDataResponse.kt b/src/main/java/com/ffii/fpsms/m18/model/M18MasterDataResponse.kt index ff8ffec..7004e9e 100644 --- a/src/main/java/com/ffii/fpsms/m18/model/M18MasterDataResponse.kt +++ b/src/main/java/com/ffii/fpsms/m18/model/M18MasterDataResponse.kt @@ -64,6 +64,7 @@ data class M18VendorVen ( /** name */ val desc: String, val `desc_zh-TW`: String, + val `desc_zh-CN`: String, /** contactNo */ val tel: String, val email: String, @@ -213,6 +214,7 @@ data class M18BusinessUnitVirdept ( /** name */ val desc: String, val `desc_zh-TW`: String, + val `desc_zh-CN`: String, /** contactNo */ val tel: String, val email: String, From 60da4657454f10739f0137b020f774507a24b0db Mon Sep 17 00:00:00 2001 From: "MSI\\derek" Date: Fri, 8 Aug 2025 11:30:53 +0800 Subject: [PATCH 2/6] check qty --- .../fpsms/modules/stock/service/InventoryService.kt | 2 +- .../modules/stock/service/StockInLineService.kt | 13 +++++++++++-- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/ffii/fpsms/modules/stock/service/InventoryService.kt b/src/main/java/com/ffii/fpsms/modules/stock/service/InventoryService.kt index ada9713..2d2d4d9 100644 --- a/src/main/java/com/ffii/fpsms/modules/stock/service/InventoryService.kt +++ b/src/main/java/com/ffii/fpsms/modules/stock/service/InventoryService.kt @@ -268,7 +268,7 @@ open class InventoryService( val oneYearExpiry = 1L val stockInLineEntries = polList.map { pol -> val newLotNo = CodeGenerator.generateCode( - prefix = "POLOT", + prefix = "MPO", itemId = pol.item!!.id!!, count = pol.id!!.toInt() ) diff --git a/src/main/java/com/ffii/fpsms/modules/stock/service/StockInLineService.kt b/src/main/java/com/ffii/fpsms/modules/stock/service/StockInLineService.kt index 99cf666..7af2282 100644 --- a/src/main/java/com/ffii/fpsms/modules/stock/service/StockInLineService.kt +++ b/src/main/java/com/ffii/fpsms/modules/stock/service/StockInLineService.kt @@ -1,5 +1,6 @@ package com.ffii.fpsms.modules.stock.service +import com.ffii.core.exception.BadRequestException import com.ffii.core.support.AbstractBaseEntityService import com.ffii.core.support.JdbcDao import com.ffii.core.utils.QrCodeUtil @@ -74,7 +75,7 @@ open class StockInLineService( purchaseOrderLine.apply { status = PurchaseOrderLineStatus.RECEIVING } - polRepository.saveAndFlush(purchaseOrderLine) + val pol = polRepository.saveAndFlush(purchaseOrderLine) if (stockIn == null) { stockIn = stockInService.create(SaveStockInRequest(purchaseOrderId = request.purchaseOrderId)).entity as StockIn // update po status to receiving @@ -84,6 +85,10 @@ open class StockInLineService( } purchaseOrderRepository.save(po) } + val allStockInLine = stockInLineRepository.findAllStockInLineInfoByStockInIdAndDeletedFalse(stockIn.id!!) + if (pol.qty!! < request.acceptedQty) { + throw BadRequestException() + } stockInLine.apply { this.item = item itemNo = item.code @@ -116,7 +121,7 @@ open class StockInLineService( "itemId" to stockInLine.item!!.id ) val inventoryCount = jdbcDao.queryForInt(INVENTORY_COUNT.toString(), args) - val newLotNo = CodeGenerator.generateCode(prefix = "POLOT", itemId = stockInLine.item!!.id!!, count = inventoryCount) + val newLotNo = CodeGenerator.generateCode(prefix = "MPO", itemId = stockInLine.item!!.id!!, count = inventoryCount) inventoryLot.apply { this.item = stockInLine.item this.stockInLine = stockInLine @@ -247,11 +252,15 @@ open class StockInLineService( message = "stock in line id is null", errorPosition = null, ) +// val allStockInLine = stockInLineRepository.findAllStockInLineInfoByStockInIdAndDeletedFalse(stockInLine!!.stockIn!!.id!!) + if (stockInLine.expiryDate != null && request.expiryDate == null) { request.apply { expiryDate = stockInLine.expiryDate } } + // TODO: check all status to prevent reverting progress due to multiple users access to the same po? + // return list of stock in line, update data grid with the list if (request.acceptedQty.compareTo(stockInLine.acceptedQty) == 0) { var savedInventoryLot: InventoryLot? = null From 33bfdb12b46e6fa97c1d8d10e8786543730d86ed Mon Sep 17 00:00:00 2001 From: "cyril.tsui" Date: Fri, 8 Aug 2025 12:19:37 +0800 Subject: [PATCH 3/6] Update scheduler, po, jo, prod schedule --- .../fpsms/m18/service/M18SchedulerService.kt | 46 ++++- .../ffii/fpsms/m18/web/M18TestController.kt | 7 +- .../fpsms/modules/common/SettingNames.java | 5 + .../common/scheduler/SchedulerService.kt | 186 ++++++++++++++++++ .../service/JobOrderProcessService.kt | 20 ++ .../jobOrder/web/JobOrderController.kt | 3 + .../master/entity/ProductionSchedule.kt | 3 + .../entity/ProductionScheduleRepository.kt | 40 +++- .../modules/master/service/ItemsService.kt | 3 +- .../service/ProductionScheduleService.kt | 68 ++++++- .../web/ProductionScheduleController.kt | 22 ++- .../web/models/SearchProdScheduleRequest.kt | 9 +- .../entity/projections/PurchaseOrderInfo.kt | 1 + .../service/PurchaseOrderService.kt | 80 ++++++-- .../web/PurchaseOrderController.kt | 4 + .../01_insert_schedule_setting.sql | 4 + .../02_update_production_schedule.sql | 5 + .../01_insert_schedule_setting.sql | 7 + 18 files changed, 465 insertions(+), 48 deletions(-) create mode 100644 src/main/java/com/ffii/fpsms/modules/common/scheduler/SchedulerService.kt create mode 100644 src/main/resources/db/changelog/changes/20250801_01_cyril/01_insert_schedule_setting.sql create mode 100644 src/main/resources/db/changelog/changes/20250801_01_cyril/02_update_production_schedule.sql create mode 100644 src/main/resources/db/changelog/changes/20250804_01_cyril/01_insert_schedule_setting.sql diff --git a/src/main/java/com/ffii/fpsms/m18/service/M18SchedulerService.kt b/src/main/java/com/ffii/fpsms/m18/service/M18SchedulerService.kt index a73a2a3..1a0a239 100644 --- a/src/main/java/com/ffii/fpsms/m18/service/M18SchedulerService.kt +++ b/src/main/java/com/ffii/fpsms/m18/service/M18SchedulerService.kt @@ -28,10 +28,14 @@ open class M18SchedulerService( ) { var logger: Logger = LoggerFactory.getLogger(JwtTokenUtil::class.java) val dataStringFormat = DateTimeFormatter.ofPattern("yyyy-MM-dd") + val defaultCronExpression = "0 0 2 * * *"; @Volatile var scheduledM18Po: ScheduledFuture<*>? = null + @Volatile + var scheduledM18Master: ScheduledFuture<*>? = null + fun isValidCronExpression(cronExpression: String): Boolean { return try { CronTrigger(cronExpression) @@ -43,35 +47,39 @@ open class M18SchedulerService( @PostConstruct fun init() { scheduleM18PoTask() + scheduleM18MasterData() } - fun scheduleM18PoTask() { - val defaultCronExpression = "0 0 2 * * *"; - scheduledM18Po?.cancel(false) + fun commonSchedule(scheduled: ScheduledFuture<*>?, settingName: String, scheduleFunc: () -> Unit) { + scheduled?.cancel(false) - var cron = settingsService.findByName(SettingNames.SCHEDULE_M18_PO).getOrNull()?.value ?: defaultCronExpression; + var cron = settingsService.findByName(settingName).getOrNull()?.value ?: defaultCronExpression; if (!isValidCronExpression(cron)) { cron = defaultCronExpression } scheduledM18Po = taskScheduler.schedule( { -// testTask(); - getM18Pos() + scheduleFunc() }, CronTrigger(cron) ) - println("Cron: $cron") } - fun testTask() { - println("Test: ${LocalDateTime.now()}") + // Scheduler + fun scheduleM18PoTask() { + commonSchedule(scheduledM18Po, SettingNames.SCHEDULE_M18_PO, ::getM18Pos) } + fun scheduleM18MasterData() { + commonSchedule(scheduledM18Master, SettingNames.SCHEDULE_M18_MASTER, ::getM18MasterData) + } + + // Tasks // @Async -// @Scheduled(cron = "0 0 2 * * *") // (SS/MM/HH/DD/MM/YY) // @Scheduled(cron = "0 0 2 * * *") // (SS/MM/HH/DD/MM/YY) open fun getM18Pos() { + logger.info("Daily Scheduler - PO") val currentTime = LocalDateTime.now() val today = currentTime.toLocalDate().atStartOfDay() val yesterday = today.minusDays(1L) @@ -84,4 +92,22 @@ open class M18SchedulerService( logger.info("today: ${today.format(dataStringFormat)}") logger.info("yesterday: ${yesterday.format(dataStringFormat)}") } + + open fun getM18MasterData() { + logger.info("Daily Scheduler - Master Data") + val currentTime = LocalDateTime.now() + val today = currentTime.toLocalDate().atStartOfDay() + val yesterday = today.minusDays(1L) + val request = M18CommonRequest( + modifiedDateTo = today.format(dataStringFormat), + modifiedDateFrom = yesterday.format(dataStringFormat) + ) + m18MasterDataService.saveUnits(request) + m18MasterDataService.saveProducts(request) + m18MasterDataService.saveVendors(request) + m18MasterDataService.saveBusinessUnits(request) + m18MasterDataService.saveCurrencies(request) + logger.info("today: ${today.format(dataStringFormat)}") + logger.info("yesterday: ${yesterday.format(dataStringFormat)}") + } } \ No newline at end of file diff --git a/src/main/java/com/ffii/fpsms/m18/web/M18TestController.kt b/src/main/java/com/ffii/fpsms/m18/web/M18TestController.kt index 999515c..23412ae 100644 --- a/src/main/java/com/ffii/fpsms/m18/web/M18TestController.kt +++ b/src/main/java/com/ffii/fpsms/m18/web/M18TestController.kt @@ -5,6 +5,7 @@ import com.ffii.fpsms.m18.M18Config import com.ffii.fpsms.m18.service.* import com.ffii.fpsms.m18.web.models.M18CommonRequest import com.ffii.fpsms.modules.common.SettingNames +import com.ffii.fpsms.modules.common.scheduler.SchedulerService import com.ffii.fpsms.modules.master.entity.ItemUom import com.ffii.fpsms.modules.master.entity.Items import com.ffii.fpsms.modules.master.entity.ShopRepository @@ -31,7 +32,7 @@ class M18TestController ( private val itemUomService: ItemUomService, private val m18PurchaseQuotationService: M18PurchaseQuotationService, private val m18DeliveryOrderService: M18DeliveryOrderService, - val m18SchedulerService: M18SchedulerService, + val schedulerService: SchedulerService, private val settingsService: SettingsService, ) { var logger: Logger = LoggerFactory.getLogger(JwtTokenUtil::class.java) @@ -60,13 +61,13 @@ class M18TestController ( @GetMapping("/test4") fun test4(): Any { - return m18SchedulerService.getM18Pos(); + return schedulerService.getM18Pos(); } // --------------------------------------------- Scheduler --------------------------------------------- /// @GetMapping("/schedule/po") // fun schedulePo(@RequestParam @Valid newCron: String) { settingsService.update(SettingNames.SCHEDULE_M18_PO, newCron); - m18SchedulerService.scheduleM18PoTask() + schedulerService.scheduleM18PoTask() } // --------------------------------------------- Master Data --------------------------------------------- /// diff --git a/src/main/java/com/ffii/fpsms/modules/common/SettingNames.java b/src/main/java/com/ffii/fpsms/modules/common/SettingNames.java index ce63b5d..69620b4 100644 --- a/src/main/java/com/ffii/fpsms/modules/common/SettingNames.java +++ b/src/main/java/com/ffii/fpsms/modules/common/SettingNames.java @@ -25,6 +25,11 @@ public abstract class SettingNames { */ public static final String SCHEDULE_M18_PO = "SCHEDULE.m18.po"; + public static final String SCHEDULE_M18_MASTER = "SCHEDULE.m18.master"; + + public static final String SCHEDULE_PROD_ROUGH = "SCHEDULE.prod.rough"; + + public static final String SCHEDULE_PROD_DETAILED = "SCHEDULE.prod.detailed"; /* * Mail settings */ diff --git a/src/main/java/com/ffii/fpsms/modules/common/scheduler/SchedulerService.kt b/src/main/java/com/ffii/fpsms/modules/common/scheduler/SchedulerService.kt new file mode 100644 index 0000000..a08082c --- /dev/null +++ b/src/main/java/com/ffii/fpsms/modules/common/scheduler/SchedulerService.kt @@ -0,0 +1,186 @@ +package com.ffii.fpsms.modules.common.scheduler + +import com.ffii.core.utils.JwtTokenUtil +import com.ffii.fpsms.m18.service.M18DeliveryOrderService +import com.ffii.fpsms.m18.service.M18MasterDataService +import com.ffii.fpsms.m18.service.M18PurchaseOrderService +import com.ffii.fpsms.m18.web.models.M18CommonRequest +import com.ffii.fpsms.modules.common.SettingNames +import com.ffii.fpsms.modules.master.service.ProductionScheduleService +import com.ffii.fpsms.modules.settings.service.SettingsService +import jakarta.annotation.PostConstruct +import org.slf4j.Logger +import org.slf4j.LoggerFactory +import org.springframework.scheduling.TaskScheduler +import org.springframework.scheduling.support.CronTrigger +import org.springframework.stereotype.Service +import java.time.LocalDate +import java.time.LocalDateTime +import java.time.format.DateTimeFormatter +import java.util.HashMap +import java.util.concurrent.ScheduledFuture +import kotlin.jvm.optionals.getOrNull + +@Service +open class SchedulerService( + val settingsService: SettingsService, + val taskScheduler: TaskScheduler, + val productionScheduleService: ProductionScheduleService, + val m18PurchaseOrderService: M18PurchaseOrderService, + val m18DeliveryOrderService: M18DeliveryOrderService, + val m18MasterDataService: M18MasterDataService, +) { + var logger: Logger = LoggerFactory.getLogger(JwtTokenUtil::class.java) + val dataStringFormat = DateTimeFormatter.ofPattern("yyyy-MM-dd") + val defaultCronExpression = "0 0 2 * * *"; + + @Volatile + var scheduledM18Po: ScheduledFuture<*>? = null + + @Volatile + var scheduledM18Master: ScheduledFuture<*>? = null + + @Volatile + var scheduledRoughProd: ScheduledFuture<*>? = null + + @Volatile + var scheduledDetailedProd: ScheduledFuture<*>? = null + + // Common Function + fun isValidCronExpression(cronExpression: String): Boolean { + return try { + CronTrigger(cronExpression) + true + } catch (e: IllegalArgumentException) { + false + } + } + + fun commonSchedule(scheduled: ScheduledFuture<*>?, settingName: String, scheduleFunc: () -> Unit) { + scheduled?.cancel(false) + + var cron = settingsService.findByName(settingName).getOrNull()?.value ?: defaultCronExpression; + + if (!isValidCronExpression(cron)) { + cron = defaultCronExpression + } + scheduledM18Po = taskScheduler.schedule( + { + scheduleFunc() + }, + CronTrigger(cron) + ) + } + + // Init Scheduler + @PostConstruct + fun init() { + scheduleM18PoTask(); + scheduleM18MasterData(); + scheduleRoughProd(); + scheduleDetailedProd(); + } + + // Scheduler + // --------------------------- FP-MTMS --------------------------- // + fun scheduleRoughProd() { + commonSchedule(scheduledRoughProd, SettingNames.SCHEDULE_PROD_ROUGH, ::getRoughProdSchedule) + } + + fun scheduleDetailedProd() { + commonSchedule(scheduledDetailedProd, SettingNames.SCHEDULE_PROD_DETAILED, ::getDetailedProdSchedule) + } + + // --------------------------- M18 --------------------------- // + fun scheduleM18PoTask() { + commonSchedule(scheduledM18Po, SettingNames.SCHEDULE_M18_PO, ::getM18Pos) + } + + fun scheduleM18MasterData() { + commonSchedule(scheduledM18Master, SettingNames.SCHEDULE_M18_MASTER, ::getM18MasterData) + } + + // Function for schedule + // --------------------------- FP-MTMS --------------------------- // + open fun getRoughProdSchedule() { + try { + logger.info("Daily Scheduler - Rough Prod") + val demoFGList = productionScheduleService.convertToFinishedGoodList(); + + val result: HashMap = productionScheduleService.generateRoughScheduleByWeek(demoFGList) + val sortedEntries = result.entries.sortedBy { it.value } + + var accProdCount = 0.0; + var fgCount = 0L; + for ((roughScheduleRecord, totalDifference) in sortedEntries) { + accProdCount += roughScheduleRecord.totalProductionCount; + fgCount ++; + println("[totalDifference:" + totalDifference + "] - " + roughScheduleRecord.toString()) + } + + // Convert to List> + productionScheduleService.saveRoughScheduleOutput(sortedEntries, accProdCount, fgCount); + } catch (e: Exception) { + throw RuntimeException("Error generate schedule: ${e.message}", e) + } + } + + open fun getDetailedProdSchedule() { + try { + logger.info("Daily Scheduler - Detailed Prod") + // For test + val today = LocalDateTime.now() + + // T, T+1, T+2 + for (day in longArrayOf(0L, 1L, 2L)) { + val produceAt = today.plusDays(day) + val latestRoughScheduleAt = productionScheduleService.getScheduledAtByDate(produceAt.toLocalDate()); + // assume schedule period is monday to sunday + val assignDate = produceAt.dayOfWeek.value + + //TODO: update this to receive selectedDate and assignDate from schedule + println("produceAt: $produceAt | today: $today | latestRoughScheduleAt: $latestRoughScheduleAt | assignDate: $assignDate ") + productionScheduleService.generateDetailedScheduleByDay(assignDate, latestRoughScheduleAt.atStartOfDay(), produceAt) + } + } catch (e: Exception) { + throw RuntimeException("Error generate schedule: ${e.message}", e) + } + } + + // --------------------------- M18 --------------------------- // +// @Async +// @Scheduled(cron = "0 0 2 * * *") // (SS/MM/HH/DD/MM/YY) + open fun getM18Pos() { + logger.info("Daily Scheduler - PO") + val currentTime = LocalDateTime.now() + val today = currentTime.toLocalDate().atStartOfDay() + val yesterday = today.minusDays(1L) + val request = M18CommonRequest( + modifiedDateTo = today.format(dataStringFormat), + modifiedDateFrom = yesterday.format(dataStringFormat) + ) + m18PurchaseOrderService.savePurchaseOrders(request); + m18DeliveryOrderService.saveDeliveryOrders(request); + logger.info("today: ${today.format(dataStringFormat)}") + logger.info("yesterday: ${yesterday.format(dataStringFormat)}") + } + + open fun getM18MasterData() { + logger.info("Daily Scheduler - Master Data") + val currentTime = LocalDateTime.now() + val today = currentTime.toLocalDate().atStartOfDay() + val yesterday = today.minusDays(1L) + val request = M18CommonRequest( + modifiedDateTo = today.format(dataStringFormat), + modifiedDateFrom = yesterday.format(dataStringFormat) + ) + m18MasterDataService.saveUnits(request) + m18MasterDataService.saveProducts(request) +// m18MasterDataService.saveBoms(request) + m18MasterDataService.saveVendors(request) + m18MasterDataService.saveBusinessUnits(request) + m18MasterDataService.saveCurrencies(request) + logger.info("today: ${today.format(dataStringFormat)}") + logger.info("yesterday: ${yesterday.format(dataStringFormat)}") + } +} \ No newline at end of file diff --git a/src/main/java/com/ffii/fpsms/modules/jobOrder/service/JobOrderProcessService.kt b/src/main/java/com/ffii/fpsms/modules/jobOrder/service/JobOrderProcessService.kt index a60c647..0c19cd7 100644 --- a/src/main/java/com/ffii/fpsms/modules/jobOrder/service/JobOrderProcessService.kt +++ b/src/main/java/com/ffii/fpsms/modules/jobOrder/service/JobOrderProcessService.kt @@ -39,6 +39,7 @@ import org.springframework.data.domain.Pageable import org.springframework.stereotype.Service import org.springframework.transaction.annotation.Transactional import java.io.IOException +import java.math.BigDecimal import java.util.HashMap import java.util.Objects import kotlin.jvm.optionals.getOrDefault @@ -52,6 +53,21 @@ open class JobOrderProcessService( private val jobOrderRepository: JobOrderRepository, ) : AbstractBaseEntityService(jdbcDao, jobOrderProcessRepository) { + open fun createJobOrderProcessRequests(joId: Long): List { + val zero = BigDecimal.ZERO + val jo = jobOrderRepository.findById(joId).getOrNull() ?: throw NoSuchElementException() + + val jopRequests = jo.bom?.bomProcesses?.map { bp -> + CreateJobOrderProcessRequest( + joId = jo.id, + processId = bp.process?.id, + seqNo = bp.seqNo, + ) + } ?: listOf(); + + return jopRequests + } + open fun createJobOrderProcesses(request: List): MessageResponse{ val joProcesses = request.map { req -> val jo = req.joId?.let { jobOrderRepository.findById(it).getOrNull() } @@ -110,6 +126,10 @@ open class JobOrderProcessService( } } + fun createJobOrderProcessesByJoId(joId: Long): MessageResponse { + return createJobOrderProcesses(createJobOrderProcessRequests(joId)); + } + // open fun isCorrectMachineUsed(request: MachineRequest): MessageResponse{ // // Further development, this with check equipment for the job process // diff --git a/src/main/java/com/ffii/fpsms/modules/jobOrder/web/JobOrderController.kt b/src/main/java/com/ffii/fpsms/modules/jobOrder/web/JobOrderController.kt index fd254bb..38717f1 100644 --- a/src/main/java/com/ffii/fpsms/modules/jobOrder/web/JobOrderController.kt +++ b/src/main/java/com/ffii/fpsms/modules/jobOrder/web/JobOrderController.kt @@ -4,6 +4,7 @@ import com.ffii.core.response.RecordsRes import com.ffii.fpsms.modules.jobOrder.entity.projections.JobOrderDetail import com.ffii.fpsms.modules.jobOrder.entity.projections.JobOrderInfo import com.ffii.fpsms.modules.jobOrder.service.JobOrderBomMaterialService +import com.ffii.fpsms.modules.jobOrder.service.JobOrderProcessService import com.ffii.fpsms.modules.jobOrder.service.JobOrderService import com.ffii.fpsms.modules.jobOrder.web.model.CreateJobOrderRequest import com.ffii.fpsms.modules.jobOrder.web.model.JobOrderReleaseRequest @@ -23,6 +24,7 @@ import org.springframework.web.bind.annotation.RestController class JobOrderController( private val jobOrderService: JobOrderService, private val jobOrderBomMaterialService: JobOrderBomMaterialService, + private val jobOrderProcessService: JobOrderProcessService ) { @GetMapping("/getRecordByPage") @@ -47,6 +49,7 @@ class JobOrderController( throw NoSuchElementException() } jobOrderBomMaterialService.createJobOrderBomMaterialsByJoId(jo.id) + jobOrderProcessService.createJobOrderProcessesByJoId(jo.id) return jo } diff --git a/src/main/java/com/ffii/fpsms/modules/master/entity/ProductionSchedule.kt b/src/main/java/com/ffii/fpsms/modules/master/entity/ProductionSchedule.kt index 32685df..b05bdda 100644 --- a/src/main/java/com/ffii/fpsms/modules/master/entity/ProductionSchedule.kt +++ b/src/main/java/com/ffii/fpsms/modules/master/entity/ProductionSchedule.kt @@ -11,6 +11,9 @@ open class ProductionSchedule : BaseEntity() { @Column(name = "scheduleAt") open var scheduleAt: LocalDateTime = LocalDateTime.now() + @Column(name = "produceAt") + open var produceAt: LocalDateTime? = null; + @Column(name = "totalEstProdCount") open var totalEstProdCount: Double = 0.0 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 3012b77..8ed84ad 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 @@ -18,6 +18,9 @@ interface ProductionScheduleRepository : AbstractRepository + // Mainly for detailed prod schedule page + @Query( + nativeQuery = true, + value = + """ + select + ps.id, + ps.deleted, + ps.scheduleAt, + ps.produceAt, + ps.totalEstProdCount, + ps.totalFGType, + ps.`type` + from production_schedule ps + where deleted = false + and produceAt is not null + and scheduleAt in (select max(ps2.scheduleAt) from production_schedule ps2 group by ps2.produceAt) + -- and (:scheduleAt = '' or datediff(scheduleAt, coalesce(:scheduleAt, scheduleAt)) = 0) + 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; + """ + ) + fun findProdScheduleInfoByProduceAtByPage( + // scheduleAt: String?, + produceAt: String?, + totalEstProdCount: Double?, + types: List?, + pageable: Pageable + ): Page + @Query(nativeQuery = true, value = """ diff --git a/src/main/java/com/ffii/fpsms/modules/master/service/ItemsService.kt b/src/main/java/com/ffii/fpsms/modules/master/service/ItemsService.kt index 6cd3676..f7adc56 100644 --- a/src/main/java/com/ffii/fpsms/modules/master/service/ItemsService.kt +++ b/src/main/java/com/ffii/fpsms/modules/master/service/ItemsService.kt @@ -49,7 +49,7 @@ open class ItemsService( open fun getRoughScheduleList(): List> { val now = LocalDateTime.now() - val lastMonthStart = now.minusMonths(1).withDayOfMonth(1) // Start of last month + val lastMonthStart = now.minusMonths(1).withDayOfMonth(1).withHour(0).withMinute(0).withSecond(0) // Start of last month val lastMonthEnd = now.minusDays(now.dayOfMonth.toLong()).withHour(23).withMinute(59).withSecond(59) // End of last month val curMonthStart = now.withDayOfMonth(1) // Start of last month @@ -82,6 +82,7 @@ open class ItemsService( + " WHERE do.deleted = false " + " AND do.estimatedArrivalDate >= :lastMonthStart " + " AND do.estimatedArrivalDate <= :lastMonthEnd " + + " AND dol.itemId is not null " + " GROUP BY dol.itemId, dol.itemNo, i.name " ); return jdbcDao.queryForList(sql.toString(), args); 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 a290fde..7e8add9 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 @@ -16,6 +16,7 @@ import com.ffii.fpsms.modules.master.entity.projections.* import com.ffii.fpsms.modules.master.web.models.MessageResponse import com.ffii.fpsms.modules.master.web.models.ReleaseProdScheduleLineRequest import com.ffii.fpsms.modules.master.web.models.SearchProdScheduleRequest +import com.ffii.fpsms.modules.settings.service.SettingsService import com.ffii.fpsms.modules.stock.entity.Inventory import com.ffii.fpsms.modules.stock.entity.InventoryRepository import com.ffii.fpsms.modules.stock.service.InventoryService @@ -26,6 +27,7 @@ import com.google.gson.JsonDeserializer import com.google.gson.JsonElement import com.google.gson.reflect.TypeToken import org.springframework.data.domain.PageRequest +import org.springframework.scheduling.support.CronTrigger import org.springframework.stereotype.Service import org.springframework.transaction.annotation.Transactional import java.lang.reflect.Type @@ -35,6 +37,7 @@ import java.time.DayOfWeek import java.time.LocalDate import java.time.LocalDateTime import java.time.format.DateTimeFormatter +import java.util.concurrent.ScheduledFuture import kotlin.collections.component1 import kotlin.collections.component2 import kotlin.jvm.optionals.getOrNull @@ -54,13 +57,15 @@ open class ProductionScheduleService( private val jobOrderProcessService: JobOrderProcessService, private val inventoryService: InventoryService, private val inventoryRepository: InventoryRepository, - private val itemUomService: ItemUomService + private val itemUomService: ItemUomService, + private val settingsService: SettingsService, ) : AbstractBaseEntityService( jdbcDao, productionScheduleRepository ) { // do mapping with projection open val formatter: DateTimeFormatter? = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss") + val defaultCronExpression = "0 0 2 * * *"; open fun getLatestScheduleAt(type: String?): LocalDateTime { return productionScheduleRepository.getLatestRoughScheduleAt(type) @@ -69,7 +74,7 @@ open class ProductionScheduleService( open fun getScheduledAtByDate(inputDate: LocalDate): LocalDate { val daysToSubtract = when (inputDate.dayOfWeek) { DayOfWeek.WEDNESDAY -> 7 - else -> (inputDate.dayOfWeek.value + 7 - DayOfWeek.WEDNESDAY.value) % 7 + else -> inputDate.dayOfWeek.value + 7 - DayOfWeek.WEDNESDAY.value } val lastWednesday = inputDate.minusDays(daysToSubtract.toLong()) @@ -82,7 +87,6 @@ open class ProductionScheduleService( val response = productionScheduleRepository.findProdScheduleInfoByPage( scheduleAt = request.scheduleAt, -// produceAt = request.produceAt, schedulePeriod = request.schedulePeriod, schedulePeriodTo = request.schedulePeriodTo, totalEstProdCount = request.totalEstProdCount, @@ -95,6 +99,29 @@ open class ProductionScheduleService( return RecordsRes(records, total.toInt()); } + open fun allRoughProdSchedulesByPage(request: SearchProdScheduleRequest): RecordsRes { +// request.apply { +// types = listOf("rough") +// } + return allProdSchedulesByPage(request); + } + + open fun allDetailedProdSchedulesByPage(request: SearchProdScheduleRequest): RecordsRes { + val pageable = PageRequest.of(request.pageNum ?: 0, request.pageSize ?: 10); + + val response = productionScheduleRepository.findProdScheduleInfoByProduceAtByPage( + produceAt = request.produceAt, + totalEstProdCount = request.totalEstProdCount, +// types = listOf("detailed", "manual"), + types = request.types, + pageable = pageable + ) + + val records = response.content + val total = response.totalElements + return RecordsRes(records, total.toInt()); + } + open fun roughProdScheduleDetail(id: Long): RoughProdScheduleWithLine { val zero = BigDecimal.ZERO val prodSchedule = productionScheduleRepository.findById(id).getOrNull() ?: throw NoSuchElementException() @@ -303,10 +330,18 @@ open class ProductionScheduleService( @Transactional(rollbackFor = [java.lang.Exception::class]) 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" + } + productionScheduleRepository.saveAndFlush(prodSchedule) // Update Prod Schedule Line Prod qty prodScheduleLine.apply { prodQty = request.demandQty.toDouble() + type = "manual" } productionScheduleLineRepository.saveAndFlush(prodScheduleLine) @@ -329,10 +364,21 @@ open class ProductionScheduleService( val bom = prodScheduleLine.item.id?.let { bomService.findByItemId(it) } val approver = SecurityUtils.getUser().getOrNull() val proportion = request.demandQty.divide(bom?.outputQty ?: BigDecimal.ONE, 5, RoundingMode.HALF_UP) + val isSameQty = request.demandQty.equals(prodScheduleLine.prodQty) + + // Update Prod Schedule Type + if (!isSameQty) { + val prodSchedule = prodScheduleLine.productionSchedule + prodSchedule.apply { + type = "manual" + } + productionScheduleRepository.save(prodSchedule) + } // Update Prod Schedule Line Prod qty prodScheduleLine.apply { prodQty = request.demandQty.toDouble() + this.type = if (isSameQty) this.type else "manual" approverId = approver?.id } productionScheduleLineRepository.save(prodScheduleLine) @@ -459,9 +505,15 @@ open class ProductionScheduleService( return jdbcDao.queryForInt(sql.toString(), args); } - open fun generateDetailedScheduleByDay(assignDate: Int, selectedDate: LocalDateTime) { + open fun generateDetailedScheduleByDay(assignDate: Int, selectedDate: LocalDateTime, produceAt: LocalDateTime) { val detailedScheduleOutputList = ArrayList() + // check the produce date have manual changes. + val manualChange = productionScheduleRepository.findByTypeAndProduceAtAndDeletedIsFalse("detailed", produceAt) + if (manualChange != null) { + return; + } + //increasement available var idleProductionCount = 22000.0 - getDailyProductionCount(assignDate, selectedDate); @@ -518,17 +570,19 @@ open class ProductionScheduleService( tempPriority++ } - saveDetailedScheduleOutput(sortedOutputList, accProdCount, fgCount) + saveDetailedScheduleOutput(sortedOutputList, accProdCount, fgCount, produceAt) } open fun saveDetailedScheduleOutput( sortedEntries: List, accProdCount: Double, - fgCount: Long + fgCount: Long, + produceAt: LocalDateTime, ) { val tempObj = ProductionSchedule() tempObj.id = -1; tempObj.scheduleAt = LocalDateTime.now() + tempObj.produceAt = produceAt; tempObj.totalFGType = fgCount; tempObj.totalEstProdCount = accProdCount; tempObj.type = "detailed" @@ -539,7 +593,6 @@ open class ProductionScheduleService( } } - private fun saveDetailedScheduleLineToDatabase(parentId: Long, detailedScheduleObj: ProductionScheduleRecord) { try { val prodSchedule = productionScheduleRepository.findById(parentId).get() @@ -627,7 +680,6 @@ open class ProductionScheduleService( sql.append(" ORDER BY psl.assignDate, psl.itemPriority ASC "); print(sql) - print(args.toString()) val resultList = jdbcDao.queryForList(sql.toString(), args); print(resultList) diff --git a/src/main/java/com/ffii/fpsms/modules/master/web/ProductionScheduleController.kt b/src/main/java/com/ffii/fpsms/modules/master/web/ProductionScheduleController.kt index ed34049..a2fa062 100644 --- a/src/main/java/com/ffii/fpsms/modules/master/web/ProductionScheduleController.kt +++ b/src/main/java/com/ffii/fpsms/modules/master/web/ProductionScheduleController.kt @@ -2,6 +2,7 @@ package com.ffii.fpsms.modules.master.web import com.ffii.core.response.RecordsRes import com.ffii.core.utils.CriteriaArgsBuilder +import com.ffii.fpsms.modules.common.scheduler.SchedulerService import com.ffii.fpsms.modules.master.entity.ProductionScheduleRepository import com.ffii.fpsms.modules.master.entity.projections.DetailedProdScheduleWithLine import com.ffii.fpsms.modules.master.entity.projections.ProdScheduleInfo @@ -30,6 +31,7 @@ import kotlin.math.abs class ProductionScheduleController( private val productionScheduleService: ProductionScheduleService, private val productionScheduleRepository: ProductionScheduleRepository, + private val schedulerService: SchedulerService, ) { // @GetMapping // fun allItems(): List { @@ -48,6 +50,16 @@ class ProductionScheduleController( return productionScheduleService.getLatestScheduleAt("rough") } + @GetMapping("/test1") + fun test1(): Any { + return schedulerService.getRoughProdSchedule(); + } + + @GetMapping("/test2") + fun test2(): Any { + return schedulerService.getDetailedProdSchedule(); + } + @GetMapping("/detail/rough/{id}") fun getScheduleDetail(@PathVariable id: Long): RoughProdScheduleWithLine { return productionScheduleService.roughProdScheduleDetail(id) @@ -73,7 +85,15 @@ class ProductionScheduleController( return productionScheduleService.allProdSchedulesByPage(request); } + @GetMapping("/getRecordByPage/rough") + fun allRoughProdSchedulesByPage(@ModelAttribute request: SearchProdScheduleRequest): RecordsRes { + return productionScheduleService.allRoughProdSchedulesByPage(request); + } + @GetMapping("/getRecordByPage/detailed") + fun allDetailedProdSchedulesByPage(@ModelAttribute request: SearchProdScheduleRequest): RecordsRes { + return productionScheduleService.allDetailedProdSchedulesByPage(request); + } @RequestMapping(value = ["/testDetailedSchedule"], method = [RequestMethod.GET]) fun generateDetailSchedule(request: HttpServletRequest?): Int { @@ -98,7 +118,7 @@ class ProductionScheduleController( //TODO: update this to receive selectedDate and assignDate from schedule // productionScheduleService.generateDetailedScheduleByDay(1, LocalDateTime.of(2025,6,25,0,0,0)) println("genDate: $genDate | today: $today | latestRoughScheduleAt: $latestRoughScheduleAt | assignDate: $assignDate ") - productionScheduleService.generateDetailedScheduleByDay(assignDate, latestRoughScheduleAt ?: LocalDateTime.now()) + productionScheduleService.generateDetailedScheduleByDay(assignDate, latestRoughScheduleAt ?: LocalDateTime.now(), genDate ?: today) return 200 } catch (e: Exception) { throw RuntimeException("Error generate schedule: ${e.message}", e) diff --git a/src/main/java/com/ffii/fpsms/modules/master/web/models/SearchProdScheduleRequest.kt b/src/main/java/com/ffii/fpsms/modules/master/web/models/SearchProdScheduleRequest.kt index c894d82..819eda3 100644 --- a/src/main/java/com/ffii/fpsms/modules/master/web/models/SearchProdScheduleRequest.kt +++ b/src/main/java/com/ffii/fpsms/modules/master/web/models/SearchProdScheduleRequest.kt @@ -4,11 +4,12 @@ import java.time.LocalDate data class SearchProdScheduleRequest ( val id: Long?, - val scheduleAt: String?, - val schedulePeriod: String?, - val schedulePeriodTo: String?, + val scheduleAt: String? = "", + val produceAt: String? = "", + val schedulePeriod: String? = "", + val schedulePeriodTo: String? = "", val totalEstProdCount: Double?, - val types: List?, + var types: List?, val pageSize: Int?, val pageNum: Int?, ) \ No newline at end of file diff --git a/src/main/java/com/ffii/fpsms/modules/purchaseOrder/entity/projections/PurchaseOrderInfo.kt b/src/main/java/com/ffii/fpsms/modules/purchaseOrder/entity/projections/PurchaseOrderInfo.kt index 6f19647..ff8134e 100644 --- a/src/main/java/com/ffii/fpsms/modules/purchaseOrder/entity/projections/PurchaseOrderInfo.kt +++ b/src/main/java/com/ffii/fpsms/modules/purchaseOrder/entity/projections/PurchaseOrderInfo.kt @@ -23,6 +23,7 @@ data class PurchaseOrderDataClass( val orderDate: LocalDateTime?, val estimatedArrivalDate: LocalDateTime?, val completeDate: LocalDateTime?, + val itemDetail: String, val status: String, val supplier: String?, var escalated: Boolean? diff --git a/src/main/java/com/ffii/fpsms/modules/purchaseOrder/service/PurchaseOrderService.kt b/src/main/java/com/ffii/fpsms/modules/purchaseOrder/service/PurchaseOrderService.kt index 8368108..ccb6616 100644 --- a/src/main/java/com/ffii/fpsms/modules/purchaseOrder/service/PurchaseOrderService.kt +++ b/src/main/java/com/ffii/fpsms/modules/purchaseOrder/service/PurchaseOrderService.kt @@ -65,22 +65,52 @@ open class PurchaseOrderService( // } open fun getPoList(args: MutableMap): List { val sql = StringBuilder( - " select " - + " po.*, " - + " s.name as supplier, " - + " CASE " - + " WHEN sil.purchaseOrderId IS NOT NULL THEN 1 " - + " ELSE 0 " - + " END AS escalated " - + " from purchase_order po " - + " left join shop s on s.id = po.supplierId " - + " left join ( " - + " select " - + " sil.purchaseOrderId " - + " from stock_in_line sil " - + " where sil.status like 'determine%' " - + " ) sil on sil.purchaseOrderId = po.id " - + " where po.deleted = false " + "select * from ( " + + "select " + + " po.*," + + " group_concat(" + + " coalesce(i.code, \"N/A\"), " + + " '-', " + + " coalesce(i.name, \"N/A\")," + + " ' ('," + + " coalesce(uc.udfudesc, \"N/A\")," + + " ')'," + + " ': ', " + + " coalesce(pol.qty, 0), " + + " ' (', " + + " coalesce(sil2.sumAcceptedQty, 0)," + + " ')' " + + " SEPARATOR ','" + + " ) as itemDetail," + + " s.name as supplier, " + + " CASE " + + " WHEN sil.purchaseOrderId IS NOT NULL THEN 1 " + + " ELSE 0 " + + " END AS escalated " + + " from purchase_order po" + + " left join purchase_order_line pol on pol.purchaseOrderId = po.id" + + " left join items i on i.id = pol.itemId" + + " left join shop s on s.id = po.supplierId " + + " left join ( " + + " select " + + " sil.purchaseOrderId " + + " from stock_in_line sil " + + " where sil.status like 'determine%'" + + " and sil.deleted = false" + + " ) sil on sil.purchaseOrderId = po.id" + + " left join ( " + + " select " + + " sil.purchaseOrderLineId, " + + " sum(coalesce(sil.acceptedQty, 0)) as sumAcceptedQty " + + " from stock_in_line sil " + + " where sil.deleted = false " + + " and sil.status = 'completed' " + + " group by sil.purchaseOrderLineId " + + " ) sil2 on sil2.purchaseOrderLineId = pol.id" + + " left join item_uom iu on iu.itemId = pol.itemId and iu.purchaseUnit = true" + + " left join uom_conversion uc on uc.id = iu.uomId" + + " where po.deleted = false " + + " and pol.deleted = false " ) if (args.containsKey("code")){ sql.append(" AND po.code like :code "); @@ -95,7 +125,24 @@ open class PurchaseOrderService( sql.append(" and sil.purchaseOrderId IS NULL "); } } + if (args.containsKey("orderDate")){ + sql.append(" AND po.orderDate >= :orderDate "); + } + if (args.containsKey("orderDateTo")){ + sql.append(" AND po.orderDate <= :orderDateTo "); + } + if (args.containsKey("estimatedArrivalDate")){ + sql.append(" AND po.estimatedArrivalDate >= :estimatedArrivalDate "); + } + if (args.containsKey("estimatedArrivalDateTo")){ + sql.append(" AND po.estimatedArrivalDate <= :estimatedArrivalDateTo "); + } + sql.append(" group by po.id"); sql.append(" order by po.orderDate desc") + sql.append(" ) r") + if (args.containsKey("itemDetail")){ + sql.append(" AND r.itemDetail like :itemDetail "); + } val list = jdbcDao.queryForList(sql.toString(), args); println(list) @@ -105,6 +152,7 @@ open class PurchaseOrderService( code = it["code"] as String, orderDate = it["orderDate"] as LocalDateTime?, estimatedArrivalDate = it["estimatedArrivalDate"] as LocalDateTime?, + itemDetail = it["itemDetail"] as String, completeDate = it["completeDate"] as LocalDateTime?, status = it["status"] as String, supplier = it["supplier"] as String?, diff --git a/src/main/java/com/ffii/fpsms/modules/purchaseOrder/web/PurchaseOrderController.kt b/src/main/java/com/ffii/fpsms/modules/purchaseOrder/web/PurchaseOrderController.kt index 769ced1..9bcde5d 100644 --- a/src/main/java/com/ffii/fpsms/modules/purchaseOrder/web/PurchaseOrderController.kt +++ b/src/main/java/com/ffii/fpsms/modules/purchaseOrder/web/PurchaseOrderController.kt @@ -35,6 +35,10 @@ class PurchaseOrderController( .addStringLike("code") .addString("status") .addBoolean("escalated") + .addDate("orderDate") + .addDate("orderDateTo") + .addDate("estimatedArrivalDate") + .addDate("estimatedArrivalDateTo") .build() println(criteriaArgs) val pageSize = request.getParameter("pageSize")?.toIntOrNull() ?: 10 // Default to 10 if not provided diff --git a/src/main/resources/db/changelog/changes/20250801_01_cyril/01_insert_schedule_setting.sql b/src/main/resources/db/changelog/changes/20250801_01_cyril/01_insert_schedule_setting.sql new file mode 100644 index 0000000..8cb5e61 --- /dev/null +++ b/src/main/resources/db/changelog/changes/20250801_01_cyril/01_insert_schedule_setting.sql @@ -0,0 +1,4 @@ +-- liquibase formatted sql +-- changeset cyril:insert_schedule_setting + +INSERT INTO `settings` (`name`, `value`, `category`, `type`) VALUES ('SCHEDULE.m18.master', '0 0 1 * * *', 'SCHEDULE', 'string'); diff --git a/src/main/resources/db/changelog/changes/20250801_01_cyril/02_update_production_schedule.sql b/src/main/resources/db/changelog/changes/20250801_01_cyril/02_update_production_schedule.sql new file mode 100644 index 0000000..9d6bce1 --- /dev/null +++ b/src/main/resources/db/changelog/changes/20250801_01_cyril/02_update_production_schedule.sql @@ -0,0 +1,5 @@ +-- liquibase formatted sql +-- changeset cyril:update_production_schedule + +ALTER TABLE `production_schedule` + ADD COLUMN `produceAt` DATETIME NULL AFTER `scheduleAt`; diff --git a/src/main/resources/db/changelog/changes/20250804_01_cyril/01_insert_schedule_setting.sql b/src/main/resources/db/changelog/changes/20250804_01_cyril/01_insert_schedule_setting.sql new file mode 100644 index 0000000..1536ad0 --- /dev/null +++ b/src/main/resources/db/changelog/changes/20250804_01_cyril/01_insert_schedule_setting.sql @@ -0,0 +1,7 @@ +-- liquibase formatted sql +-- changeset cyril:insert_schedule_setting + +INSERT INTO `settings` (`name`, `value`, `category`, `type`) +VALUES + ('SCHEDULE.prod.rough', '0 0 4 * * Wed', 'SCHEDULE', 'string'), + ('SCHEDULE.prod.detailed', '0 0 3 * * *', 'SCHEDULE', 'string'); \ No newline at end of file From c1b21003d3734c0221a0badc85852cf9971d11cf Mon Sep 17 00:00:00 2001 From: "vluk@2fi-solutions.com.hk" Date: Sat, 9 Aug 2025 17:36:26 +0800 Subject: [PATCH 4/6] Added Zebra print util for direct print a pdf file --- build.gradle | 2 + .../com/ffii/core/utils/ZebraPrinterUtil.kt | 109 ++++++++++++++++++ 2 files changed, 111 insertions(+) create mode 100644 src/main/java/com/ffii/core/utils/ZebraPrinterUtil.kt diff --git a/build.gradle b/build.gradle index 4300c01..b5e486b 100644 --- a/build.gradle +++ b/build.gradle @@ -31,6 +31,8 @@ dependencies { implementation 'org.springframework.security:spring-security-ldap' implementation 'org.liquibase:liquibase-core' implementation 'com.google.code.gson:gson:2.8.5' + implementation("org.apache.pdfbox:pdfbox:2.0.29") + implementation("org.apache.pdfbox:fontbox:2.0.34") // // https://mvnrepository.com/artifact/org.springdoc/springdoc-openapi-starter-webmvc-ui // implementation("org.springdoc:springdoc-openapi-starter-webmvc-ui:2.8.8") diff --git a/src/main/java/com/ffii/core/utils/ZebraPrinterUtil.kt b/src/main/java/com/ffii/core/utils/ZebraPrinterUtil.kt new file mode 100644 index 0000000..12c21ca --- /dev/null +++ b/src/main/java/com/ffii/core/utils/ZebraPrinterUtil.kt @@ -0,0 +1,109 @@ +package com.ffii.core.utils + +import org.apache.pdfbox.pdmodel.PDDocument +import org.apache.pdfbox.rendering.PDFRenderer +import org.apache.pdfbox.rendering.ImageType +import java.awt.image.BufferedImage +import java.io.File +import java.io.OutputStream +import java.net.Socket +import java.util.* + +/** + * Utility class for generating and sending print jobs to a Zebra printer using ZPL. + * This class requires the 'org.apache.pdfbox:pdfbox' dependency to be included in your project. + */ +open class ZebraPrinterUtil { + + companion object { + + /** + * Converts the first page of a PDF document into a ZPL command and sends it to the specified printer. + * + * @param pdfFile The PDF file to be printed. + * @param printerIp The IP address of the Zebra printer. + * @param printerPort The port of the Zebra printer, typically 9100. + * @throws Exception if there is an error during file processing or printing. + */ + @JvmStatic + fun printPdfToZebra(pdfFile: File, printerIp: String, printerPort: Int) { + + // Check if the file exists and is readable + if (!pdfFile.exists() || !pdfFile.canRead()) { + throw IllegalArgumentException("Error: File not found or not readable at path: ${pdfFile.absolutePath}") + } + + try { + // 1. Load the PDF document + PDDocument.load(pdfFile).use { document -> + val renderer = PDFRenderer(document) + + // 2. Render the first page of the PDF as a monochrome image + // The '300 / 72f' scales the image to 300 DPI. + val image = renderer.renderImage(0, 300 / 72f, ImageType.BINARY) + + // 3. Convert the image to a ZPL format string + val zplCommand = convertImageToZpl(image) + + // 4. Send the ZPL command to the printer via a network socket + val printData = zplCommand.toByteArray() + Socket(printerIp, printerPort).use { socket -> + val os: OutputStream = socket.getOutputStream() + os.write(printData) + os.flush() + } + } + } catch (e: Exception) { + // Re-throw the exception with a more descriptive message + throw Exception("Error processing print job for PDF: ${e.message}", e) + } + } + + /** + * Converts a BufferedImage (monochrome) to a ZPL string using the ^GFA command. + * This function handles the conversion of pixel data to a compressed hex string. + * + * @param image The BufferedImage to convert. + * @return A ZPL-formatted string ready to be sent to the printer. + */ + private fun convertImageToZpl(image: BufferedImage): String { + val zpl = StringBuilder() + zpl.append("^XA\n") + + val width = image.width + val height = image.height + + // ZPL format for a graphical image is ^GFA + val bytesPerRow = (width + 7) / 8 + val totalBytes = bytesPerRow * height + + zpl.append("^FO0,0^GFA,").append(totalBytes).append(",").append(totalBytes).append(",").append(bytesPerRow).append(",") + + // Extract pixel data and convert to ZPL hex string + for (y in 0 until height) { + val rowBits = BitSet(width) + for (x in 0 until width) { + // Check for a black pixel (0xFF000000 in ARGB) + if (image.getRGB(x, y) == 0xFF000000.toInt()) { + rowBits.set(x) + } + } + + for (i in 0 until bytesPerRow) { + var byteValue = 0 + for (bit in 0 until 8) { + if (rowBits.get(i * 8 + bit)) { + byteValue = byteValue or (1 shl (7 - bit)) + } + } + zpl.append(String.format("%02X", byteValue)) + } + } + + zpl.append("^FS\n") + zpl.append("^XZ\n") + + return zpl.toString() + } + } +} From b3bebdf3e79deb07d66cb72ffb9d82d3f3e3af01 Mon Sep 17 00:00:00 2001 From: "vluk@2fi-solutions.com.hk" Date: Sun, 10 Aug 2025 14:37:15 +0800 Subject: [PATCH 5/6] added a demo code in po controller for easy testing --- .../web/PurchaseOrderController.kt | 37 ++++++++++++++++++- 1 file changed, 36 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/ffii/fpsms/modules/purchaseOrder/web/PurchaseOrderController.kt b/src/main/java/com/ffii/fpsms/modules/purchaseOrder/web/PurchaseOrderController.kt index 9bcde5d..2fa8153 100644 --- a/src/main/java/com/ffii/fpsms/modules/purchaseOrder/web/PurchaseOrderController.kt +++ b/src/main/java/com/ffii/fpsms/modules/purchaseOrder/web/PurchaseOrderController.kt @@ -4,6 +4,7 @@ import com.ffii.core.response.RecordsRes import com.ffii.core.support.JdbcDao import com.ffii.core.utils.CriteriaArgsBuilder import com.ffii.core.utils.PagingUtils +import com.ffii.core.utils.ZebraPrinterUtil import com.ffii.fpsms.modules.master.entity.Items import com.ffii.fpsms.modules.master.service.ItemsService import com.ffii.fpsms.modules.master.web.models.MessageResponse @@ -12,16 +13,22 @@ import com.ffii.fpsms.modules.purchaseOrder.entity.projections.PurchaseOrderData import com.ffii.fpsms.modules.purchaseOrder.entity.projections.PurchaseOrderInfo import com.ffii.fpsms.modules.purchaseOrder.service.PurchaseOrderService import com.ffii.fpsms.modules.purchaseOrder.web.model.PagingRequest +import com.ffii.fpsms.modules.stock.service.StockInLineService +import com.ffii.fpsms.modules.stock.web.model.ExportQrCodeRequest import com.ffii.fpsms.modules.stock.web.model.SaveStockInLineRequest import jakarta.servlet.http.HttpServletRequest +import net.sf.jasperreports.engine.JasperExportManager +import net.sf.jasperreports.engine.JasperPrint import org.springframework.data.domain.Page import org.springframework.data.domain.PageRequest import org.springframework.web.bind.annotation.* +import java.io.File @RestController @RequestMapping("/po") class PurchaseOrderController( - private val purchaseOrderService: PurchaseOrderService + private val purchaseOrderService: PurchaseOrderService, + private val stockInLineService: StockInLineService ) { @GetMapping("/list") fun getPoList( @@ -46,9 +53,11 @@ class PurchaseOrderController( val fullList = purchaseOrderService.getPoList(criteriaArgs) val paginatedList = PagingUtils.getPaginatedList(fullList,pageSize, pageNum) + return RecordsRes(paginatedList, fullList.size) } + @GetMapping("/testing") fun testing(request: HttpServletRequest) { val criteriaArgs = CriteriaArgsBuilder.withRequest(request) @@ -62,6 +71,32 @@ class PurchaseOrderController( @GetMapping("/detail/{id}") // purchaseOrderId fun getDetailedPo(@PathVariable id: Long): Map { + + /* tested the sample printing function by a pdf file + // just get a pdf file for demo , it can be any files? didn't try pdf with pages or multi pdf files + val request = ExportQrCodeRequest( + stockInLineIds = listOf(1L) + ) + val pdf = stockInLineService.exportStockInLineQrcode(request) + val jasperPrint = pdf["report"] as JasperPrint + + // 1. Create a temporary file to save the PDF. + val tempPdfFile = File.createTempFile("print_job_", ".pdf") + + try { + + // 2. Export the JasperPrint to the temporary PDF file. + JasperExportManager.exportReportToPdfFile(jasperPrint, tempPdfFile.absolutePath) + + // 3. Call the utility function with the temporary file. + ZebraPrinterUtil.printPdfToZebra(tempPdfFile, "192.168.245.16", 9100) + + } finally { + // 4. Ensure the temporary file is deleted after the print job is sent. + tempPdfFile.delete() + } + */ + return purchaseOrderService.getDetailedPo(id) } From 5babd1ad5023604a21826cf7d3ac03bd710886f2 Mon Sep 17 00:00:00 2001 From: "vluk@2fi-solutions.com.hk" Date: Sun, 10 Aug 2025 14:43:23 +0800 Subject: [PATCH 6/6] tested the printing with another file --- .../modules/purchaseOrder/web/PurchaseOrderController.kt | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/ffii/fpsms/modules/purchaseOrder/web/PurchaseOrderController.kt b/src/main/java/com/ffii/fpsms/modules/purchaseOrder/web/PurchaseOrderController.kt index 2fa8153..52a7ec0 100644 --- a/src/main/java/com/ffii/fpsms/modules/purchaseOrder/web/PurchaseOrderController.kt +++ b/src/main/java/com/ffii/fpsms/modules/purchaseOrder/web/PurchaseOrderController.kt @@ -72,10 +72,11 @@ class PurchaseOrderController( @GetMapping("/detail/{id}") // purchaseOrderId fun getDetailedPo(@PathVariable id: Long): Map { - /* tested the sample printing function by a pdf file + /* + // tested the sample printing function by a pdf file // just get a pdf file for demo , it can be any files? didn't try pdf with pages or multi pdf files val request = ExportQrCodeRequest( - stockInLineIds = listOf(1L) + stockInLineIds = listOf(2L) ) val pdf = stockInLineService.exportStockInLineQrcode(request) val jasperPrint = pdf["report"] as JasperPrint @@ -95,7 +96,8 @@ class PurchaseOrderController( // 4. Ensure the temporary file is deleted after the print job is sent. tempPdfFile.delete() } - */ + + */ return purchaseOrderService.getDetailedPo(id) }