| @@ -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)}") | |||
| } | |||
| } | |||
| @@ -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 --------------------------------------------- /// | |||
| @@ -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 | |||
| */ | |||
| @@ -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.RoughScheduleObj, Double> = 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<HashMap<RoughScheduleObj, Double>> | |||
| 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)}") | |||
| } | |||
| } | |||
| @@ -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<JobOrderProcess, Long, JobOrderProcessRepository>(jdbcDao, jobOrderProcessRepository) { | |||
| open fun createJobOrderProcessRequests(joId: Long): List<CreateJobOrderProcessRequest> { | |||
| 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<CreateJobOrderProcessRequest>): 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 | |||
| // | |||
| @@ -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 | |||
| } | |||
| @@ -11,6 +11,9 @@ open class ProductionSchedule : BaseEntity<Long>() { | |||
| @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 | |||
| @@ -18,6 +18,9 @@ interface ProductionScheduleRepository : AbstractRepository<ProductionSchedule, | |||
| ) | |||
| fun getLatestRoughScheduleAt(type: String?): LocalDateTime | |||
| fun findByTypeAndProduceAtAndDeletedIsFalse(type: String, produceAt: LocalDateTime): ProductionSchedule? | |||
| // Mainly for rough prod schedule page | |||
| @Query( | |||
| nativeQuery = true, | |||
| value = | |||
| @@ -27,7 +30,6 @@ interface ProductionScheduleRepository : AbstractRepository<ProductionSchedule, | |||
| ps.id, | |||
| ps.deleted, | |||
| ps.scheduleAt, | |||
| -- ps.produceAt, | |||
| date_add(ps.scheduleAt, interval 7 + 1 - if(weekday(ps.scheduleAt) = 6, 0, weekday(ps.scheduleAt) + 1) day) as schedulePeriod, | |||
| date_add(ps.scheduleAt, interval 7 + 1 + 6 - if(weekday(ps.scheduleAt) = 6, 0, weekday(ps.scheduleAt) + 1) day) as schedulePeriodTo, | |||
| ps.totalEstProdCount, | |||
| @@ -38,7 +40,6 @@ interface ProductionScheduleRepository : AbstractRepository<ProductionSchedule, | |||
| select * from prod | |||
| where deleted = false | |||
| and (:scheduleAt = '' or datediff(scheduleAt, coalesce(:scheduleAt, scheduleAt)) = 0) | |||
| -- and (:produceAt = '' or datediff(produceAt, coalesce(:produceAt, produceAt)) = 0) | |||
| and (:schedulePeriod = '' or datediff(schedulePeriod, coalesce(:schedulePeriod, schedulePeriod)) = 0) | |||
| and (:schedulePeriodTo = '' or datediff(schedulePeriodTo, coalesce(:schedulePeriodTo, schedulePeriodTo)) = 0) | |||
| and (:totalEstProdCount is null or :totalEstProdCount = '' or totalEstProdCount = :totalEstProdCount) | |||
| @@ -52,7 +53,6 @@ interface ProductionScheduleRepository : AbstractRepository<ProductionSchedule, | |||
| ps.id, | |||
| ps.deleted, | |||
| ps.scheduleAt, | |||
| -- ps.produceAt, | |||
| date_add(ps.scheduleAt, interval 7 + 1 - if(weekday(ps.scheduleAt) = 6, 0, weekday(ps.scheduleAt) + 1) day) as schedulePeriod, | |||
| date_add(ps.scheduleAt, interval 7 + 1 + 6 - if(weekday(ps.scheduleAt) = 6, 0, weekday(ps.scheduleAt) + 1) day) as schedulePeriodTo, | |||
| ps.totalEstProdCount, | |||
| @@ -63,7 +63,6 @@ interface ProductionScheduleRepository : AbstractRepository<ProductionSchedule, | |||
| select count(*) from prod | |||
| where deleted = false | |||
| and (:scheduleAt = '' or datediff(scheduleAt, coalesce(:scheduleAt, scheduleAt)) = 0) | |||
| -- and (:produceAt = '' or datediff(produceAt, coalesce(:produceAt, produceAt)) = 0) | |||
| and (:schedulePeriod = '' or datediff(schedulePeriod, coalesce(:schedulePeriod, schedulePeriod)) = 0) | |||
| and (:schedulePeriodTo = '' or datediff(schedulePeriodTo, coalesce(:schedulePeriodTo, schedulePeriodTo)) = 0) | |||
| and (:totalEstProdCount is null or :totalEstProdCount = '' or totalEstProdCount = :totalEstProdCount) | |||
| @@ -73,7 +72,6 @@ interface ProductionScheduleRepository : AbstractRepository<ProductionSchedule, | |||
| ) | |||
| fun findProdScheduleInfoByPage( | |||
| scheduleAt: String?, | |||
| // produceAt: String?, | |||
| schedulePeriod: String?, | |||
| schedulePeriodTo: String?, | |||
| totalEstProdCount: Double?, | |||
| @@ -81,6 +79,38 @@ interface ProductionScheduleRepository : AbstractRepository<ProductionSchedule, | |||
| pageable: Pageable | |||
| ): Page<ProdScheduleInfo> | |||
| // 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<String>?, | |||
| pageable: Pageable | |||
| ): Page<ProdScheduleInfo> | |||
| @Query(nativeQuery = true, | |||
| value = | |||
| """ | |||
| @@ -49,7 +49,7 @@ open class ItemsService( | |||
| open fun getRoughScheduleList(): List<Map<String, Any>> { | |||
| 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); | |||
| @@ -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<ProductionSchedule, Long, ProductionScheduleRepository>( | |||
| 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<ProdScheduleInfo>(records, total.toInt()); | |||
| } | |||
| open fun allRoughProdSchedulesByPage(request: SearchProdScheduleRequest): RecordsRes<ProdScheduleInfo> { | |||
| // request.apply { | |||
| // types = listOf("rough") | |||
| // } | |||
| return allProdSchedulesByPage(request); | |||
| } | |||
| open fun allDetailedProdSchedulesByPage(request: SearchProdScheduleRequest): RecordsRes<ProdScheduleInfo> { | |||
| 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<ProdScheduleInfo>(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<ProductionScheduleRecord>() | |||
| // 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<ProductionScheduleRecord>, | |||
| 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) | |||
| @@ -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<Items> { | |||
| @@ -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<ProdScheduleInfo> { | |||
| return productionScheduleService.allRoughProdSchedulesByPage(request); | |||
| } | |||
| @GetMapping("/getRecordByPage/detailed") | |||
| fun allDetailedProdSchedulesByPage(@ModelAttribute request: SearchProdScheduleRequest): RecordsRes<ProdScheduleInfo> { | |||
| 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) | |||
| @@ -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<String>?, | |||
| var types: List<String>?, | |||
| val pageSize: Int?, | |||
| val pageNum: Int?, | |||
| ) | |||
| @@ -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? | |||
| @@ -65,22 +65,52 @@ open class PurchaseOrderService( | |||
| // } | |||
| open fun getPoList(args: MutableMap<String, Any>): List<PurchaseOrderDataClass> { | |||
| 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?, | |||
| @@ -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 | |||
| @@ -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'); | |||
| @@ -0,0 +1,5 @@ | |||
| -- liquibase formatted sql | |||
| -- changeset cyril:update_production_schedule | |||
| ALTER TABLE `production_schedule` | |||
| ADD COLUMN `produceAt` DATETIME NULL AFTER `scheduleAt`; | |||
| @@ -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'); | |||