From 7a29cda3a5aeac3208598fbf886e2b6783cabba1 Mon Sep 17 00:00:00 2001 From: "cyril.tsui" Date: Mon, 7 Jul 2025 17:34:34 +0800 Subject: [PATCH] [Prod Schedule] Update Prod Schedule Entity & Rough Schedule Search + detail --- .../master/entity/ProductionSchedule.kt | 5 + .../master/entity/ProductionScheduleLine.kt | 17 +- .../ProductionScheduleLineRepository.kt | 2 + .../entity/ProductionScheduleRepository.kt | 74 ++++ .../entity/projections/ProdScheduleInfo.kt | 84 ++++ .../service/ProductionScheduleService.kt | 403 ++++++++++++++---- .../web/ProductionScheduleController.kt | 43 +- .../web/models/SearchProdScheduleRequest.kt | 14 + 8 files changed, 550 insertions(+), 92 deletions(-) create mode 100644 src/main/java/com/ffii/fpsms/modules/master/entity/projections/ProdScheduleInfo.kt create mode 100644 src/main/java/com/ffii/fpsms/modules/master/web/models/SearchProdScheduleRequest.kt 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 597d486..32685df 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 @@ -1,5 +1,6 @@ package com.ffii.fpsms.modules.master.entity; +import com.fasterxml.jackson.annotation.JsonManagedReference import com.ffii.core.entity.BaseEntity import jakarta.persistence.* import java.time.LocalDateTime @@ -18,4 +19,8 @@ open class ProductionSchedule : BaseEntity() { @Column(name = "type") open var type: String = "manual" + + @JsonManagedReference + @OneToMany(mappedBy = "productionSchedule", cascade = [CascadeType.ALL], orphanRemoval = true) + open var productionScheduleLines: MutableList = mutableListOf() } diff --git a/src/main/java/com/ffii/fpsms/modules/master/entity/ProductionScheduleLine.kt b/src/main/java/com/ffii/fpsms/modules/master/entity/ProductionScheduleLine.kt index 35af7c9..12da0b1 100644 --- a/src/main/java/com/ffii/fpsms/modules/master/entity/ProductionScheduleLine.kt +++ b/src/main/java/com/ffii/fpsms/modules/master/entity/ProductionScheduleLine.kt @@ -1,5 +1,6 @@ package com.ffii.fpsms.modules.master.entity; +import com.fasterxml.jackson.annotation.JsonBackReference import com.ffii.core.entity.BaseEntity import jakarta.persistence.* import java.time.LocalDateTime @@ -7,11 +8,14 @@ import java.time.LocalDateTime @Entity @Table(name = "production_schedule_line") open class ProductionScheduleLine : BaseEntity() { - @Column(name = "prodScheduleId") - open var prodScheduleId: Long = 0L +// @Column(name = "prodScheduleId") +// open var prodScheduleId: Long = 0L - @Column(name = "itemId") - open var itemId: Long = 0L + @OneToOne + @JoinColumn(name = "itemId") + open var item: Items = Items() +// @Column(name = "itemId") +// open var itemId: Long = 0L @Column(name = "lastMonthAvgSales") open var lastMonthAvgSales: Double = 0.0 @@ -40,4 +44,9 @@ open class ProductionScheduleLine : BaseEntity() { @Column(name = "weightingRef") open var weightingRef: Double = 0.0 + + @JsonBackReference + @ManyToOne + @JoinColumn(name = "prodScheduleId", nullable = false) + open var productionSchedule: ProductionSchedule = ProductionSchedule() } diff --git a/src/main/java/com/ffii/fpsms/modules/master/entity/ProductionScheduleLineRepository.kt b/src/main/java/com/ffii/fpsms/modules/master/entity/ProductionScheduleLineRepository.kt index d734dce..9f74af2 100644 --- a/src/main/java/com/ffii/fpsms/modules/master/entity/ProductionScheduleLineRepository.kt +++ b/src/main/java/com/ffii/fpsms/modules/master/entity/ProductionScheduleLineRepository.kt @@ -1,7 +1,9 @@ package com.ffii.fpsms.modules.master.entity import com.ffii.core.support.AbstractRepository +import org.springframework.data.jpa.repository.Query import org.springframework.stereotype.Repository +import java.time.LocalDateTime @Repository interface ProductionScheduleLineRepository : AbstractRepository { 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 91c32a4..cc6896c 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 @@ -1,8 +1,82 @@ package com.ffii.fpsms.modules.master.entity import com.ffii.core.support.AbstractRepository +import com.ffii.fpsms.modules.master.entity.projections.ProdScheduleInfo +import com.ffii.fpsms.modules.master.entity.projections.ProdScheduleWithLine +import org.springframework.data.domain.Page +import org.springframework.data.domain.Pageable +import org.springframework.data.jpa.repository.Query import org.springframework.stereotype.Repository +import java.io.Serializable +import java.time.LocalDate +import java.time.LocalDateTime @Repository interface ProductionScheduleRepository : AbstractRepository { + @Query( + """ + select max(ps.scheduleAt) from ProductionSchedule ps + where (:type is null or ps.type = :type) + or ps.type = 'manual' + """ + ) + fun getLatestRoughScheduleAt(type: String?): LocalDateTime + + @Query( + nativeQuery = true, + value = + """ + with prod as ( + select + ps.id, + ps.deleted, + ps.scheduleAt, + date_add(ps.scheduleAt, interval 7 - if(weekday(ps.scheduleAt) = 6, 0, weekday(ps.scheduleAt) + 1) day) as schedulePeriod, + date_add(ps.scheduleAt, interval 7 + 6 - if(weekday(ps.scheduleAt) = 6, 0, weekday(ps.scheduleAt) + 1) day) as schedulePeriodTo, + ps.totalEstProdCount, + ps.totalFGType, + ps.`type` + from production_schedule ps + ) + select * from prod + where deleted = false + and (:scheduleAt = '' or datediff(scheduleAt, coalesce(:scheduleAt, scheduleAt)) = 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) + and (:type is null or type = :type) + order by id desc; + """, + countQuery = + """ + with prod as ( + select + ps.id, + ps.deleted, + ps.scheduleAt, + date_add(ps.scheduleAt, interval 7 - if(weekday(ps.scheduleAt) = 6, 0, weekday(ps.scheduleAt) + 1) day) as schedulePeriod, + date_add(ps.scheduleAt, interval 7 + 6 - if(weekday(ps.scheduleAt) = 6, 0, weekday(ps.scheduleAt) + 1) day) as schedulePeriodTo, + ps.totalEstProdCount, + ps.totalFGType, + ps.`type` + from production_schedule ps + ) + select count(*) from prod + where deleted = false + and (:scheduleAt = '' or datediff(scheduleAt, coalesce(:scheduleAt, scheduleAt)) = 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) + and (:type is null or type = :type) + order by id desc; + """, + ) + fun findProdScheduleInfoByPage( + scheduleAt: String?, + schedulePeriod: String?, + schedulePeriodTo: String?, + totalEstProdCount: Double?, + type: String?, + pageable: Pageable + ): Page } \ No newline at end of file diff --git a/src/main/java/com/ffii/fpsms/modules/master/entity/projections/ProdScheduleInfo.kt b/src/main/java/com/ffii/fpsms/modules/master/entity/projections/ProdScheduleInfo.kt new file mode 100644 index 0000000..d71e57f --- /dev/null +++ b/src/main/java/com/ffii/fpsms/modules/master/entity/projections/ProdScheduleInfo.kt @@ -0,0 +1,84 @@ +package com.ffii.fpsms.modules.master.entity.projections + +import org.springframework.beans.factory.annotation.Value +import java.math.BigDecimal +import java.time.LocalDate +import java.time.LocalDateTime + +// Production Schedule +interface ProdScheduleInfo { + val id: Long? + val scheduleAt: LocalDate? + val schedulePeriod: LocalDate? + val schedulePeriodTo: LocalDate? + val totalEstProdCount: BigDecimal? + val totalFGType: Long? + val type: String? +} + +// Production Schedule With Line +data class ProdScheduleWithLine( + val id: Long?, + val scheduleAt: LocalDateTime?, + val schedulePeriod: LocalDateTime?, + val schedulePeriodTo: LocalDateTime?, + val totalEstProdCount: BigDecimal?, + val totalFGType: Long?, + val type: String?, + val prodScheduleLinesByFg: List, + val prodScheduleLinesByFgByDate: Map>, + val prodScheduleLinesByBom: List, + val prodScheduleLinesByBomByDate: Map> +) + +data class ProdScheduleLineInfoByFg( + val id: Long?, + val code: String?, + val name: String?, + val type: String?, + val availableQty: BigDecimal?, + val prodQty: BigDecimal?, + val lastMonthAvgSales: BigDecimal?, + val estCloseBal: BigDecimal?, + val priority: Long?, + val bomMaterials: List?, + val assignDate: Long?, +) + +data class ProdScheduleLineBomMaterialInfoByFg( + val id: Long?, + val code: String?, + val name: String?, + val type: String?, + val availableQty: BigDecimal?, + val demandQty: BigDecimal?, + val uomName: String? +) + +data class ProdScheduleLineInfoByBom( + val id: Long?, + val code: String?, + val name: String?, + val type: String?, + val availableQty: BigDecimal?, + val totalDemandQty: BigDecimal?, + val demandQty1: BigDecimal?, + val demandQty2: BigDecimal?, + val demandQty3: BigDecimal?, + val demandQty4: BigDecimal?, + val demandQty5: BigDecimal?, + val demandQty6: BigDecimal?, + val demandQty7: BigDecimal?, + val uomName: String?, +) + +data class ProdScheduleLineInfoByBomByDate( + val id: Long?, + val code: String?, + val name: String?, + val type: String?, + val availableQty: BigDecimal?, + val demandQty: BigDecimal?, + val assignDate: Long?, + val uomName: String?, +) \ No newline at end of file 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 59602a4..1ab259a 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 @@ -1,15 +1,22 @@ package com.ffii.fpsms.modules.master.service +import com.ffii.core.response.RecordsRes import com.ffii.core.support.AbstractBaseEntityService import com.ffii.core.support.JdbcDao import com.ffii.fpsms.modules.master.entity.* -import com.ffii.fpsms.modules.master.web.models.MessageResponse +import com.ffii.fpsms.modules.master.entity.projections.* +import com.ffii.fpsms.modules.master.web.models.SearchProdScheduleRequest +import org.springframework.data.domain.PageRequest import org.springframework.stereotype.Service +import java.math.BigDecimal +import java.math.RoundingMode import java.time.LocalDateTime import java.time.format.DateTimeFormatter import java.util.* +import kotlin.NoSuchElementException import kotlin.collections.component1 import kotlin.collections.component2 +import kotlin.jvm.optionals.getOrNull import kotlin.math.ceil @Service @@ -18,10 +25,214 @@ open class ProductionScheduleService( private val itemService: ItemsService, private val productionScheduleRepository: ProductionScheduleRepository, private val productionScheduleLineRepository: ProductionScheduleLineRepository, -): AbstractBaseEntityService(jdbcDao, productionScheduleRepository) { + private val bomMaterialService: BomMaterialService, private val bomMaterialRepository: BomMaterialRepository, +) : AbstractBaseEntityService( + jdbcDao, + productionScheduleRepository +) { // do mapping with projection open val formatter: DateTimeFormatter? = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss") + open fun getLatestScheduleAt(type: String?): LocalDateTime { + return productionScheduleRepository.getLatestRoughScheduleAt(type) + } + + open fun allProdSchedulesByPage(request: SearchProdScheduleRequest): RecordsRes { + val pageable = PageRequest.of(request.pageNum ?: 0, request.pageSize ?: 10); + + val response = productionScheduleRepository.findProdScheduleInfoByPage( + scheduleAt = request.scheduleAt, + schedulePeriod = request.schedulePeriod, + schedulePeriodTo = request.schedulePeriodTo, + totalEstProdCount = request.totalEstProdCount, + type = request.type, + pageable = pageable + ) + + val records = response.content + val total = response.totalElements + return RecordsRes(records, total.toInt()); + } + + open fun prodScheduleDetail(id: Long): ProdScheduleWithLine { + val zero = BigDecimal.ZERO + val prodSchedule = productionScheduleRepository.findById(id).getOrNull() ?: throw NoSuchElementException() + val dayOfWeekValue = + if (prodSchedule.scheduleAt.dayOfWeek.value >= 7) prodSchedule.scheduleAt.dayOfWeek.value - 7 else prodSchedule.scheduleAt.dayOfWeek.value + val schedulePeriod = prodSchedule.scheduleAt.plusDays((7 - dayOfWeekValue).toLong()) + val schedulePeriodTo = prodSchedule.scheduleAt.plusDays((7 + 6 - dayOfWeekValue).toLong()) + val prodScheduleLines = prodSchedule.productionScheduleLines.filter { !it.deleted } + + // ---------------------------------- By FG ----------------------------------// + // Production Schedule By Date + val prodScheduleLineInfosByFg = prodScheduleLines.map { line -> + val bomMaterials = line.item.id?.let { bomMaterialRepository.findAllByBomItemIdAndDeletedIsFalse(it) } + ?.map { bm -> + val proportion = + if (line.prodQty > 0 && bm.bom?.outputQty != null && (bm.bom?.outputQty ?: zero) > zero) { + BigDecimal(line.prodQty).divide(bm.bom!!.outputQty, 2, RoundingMode.HALF_UP) + } else { + zero + } + + val demandQty = bm.qty?.times(proportion) ?: zero + + ProdScheduleLineBomMaterialInfoByFg( + id = bm.id, + code = bm.item?.code, + name = bm.item?.name, + type = bm.item?.type, + availableQty = bm.item?.inventories?.sumOf { + (it.onHandQty ?: zero) - (it.onHoldQty ?: zero) - (it.unavailableQty ?: zero) + }, + demandQty = demandQty, + uomName = bm.uomName + ) + } + + ProdScheduleLineInfoByFg( + id = line.id, + code = line.item.code, + name = line.item.name, + type = line.item.type, + availableQty = line.item.inventories.sumOf { + (it.onHandQty ?: zero) - (it.onHoldQty ?: zero) - (it.unavailableQty ?: zero) + }, + prodQty = BigDecimal(line.prodQty), + lastMonthAvgSales = BigDecimal(line.lastMonthAvgSales), + estCloseBal = BigDecimal(line.estCloseBal), + priority = line.itemPriority, + bomMaterials = bomMaterials, + assignDate = line.assignDate, + ) + }.filter { !it.bomMaterials.isNullOrEmpty() } + + // Sum of the Production Schedule + val sumProdScheduleLineInfosByFg = prodScheduleLineInfosByFg + .groupBy { it.code } + .map { (code, infos) -> + val sumBomMaterials = infos + .flatMap { it.bomMaterials ?: mutableListOf() } + .groupBy { it.code } + .map { (code, bm) -> + ProdScheduleLineBomMaterialInfoByFg( + id = if (bm.isNotEmpty()) bm[0].id else null, + code = if (bm.isNotEmpty()) bm[0].code else null, + name = if (bm.isNotEmpty()) bm[0].name else null, + type = if (bm.isNotEmpty()) bm[0].type else null, + availableQty = bm.sumOf { it.availableQty ?: zero }, + demandQty = bm.sumOf { it.demandQty ?: zero }, + uomName = if (bm.isNotEmpty()) bm[0].uomName else null + ) + } + + ProdScheduleLineInfoByFg( + id = if (infos.isNotEmpty()) infos[0].assignDate else null, + code = if (infos.isNotEmpty()) infos[0].code else null, + name = if (infos.isNotEmpty()) infos[0].name else null, + type = if (infos.isNotEmpty()) infos[0].type else null, + availableQty = infos.sumOf { (it.availableQty ?: zero) }, + prodQty = infos.sumOf { (it.prodQty ?: zero) }, + lastMonthAvgSales = infos.sumOf { (it.lastMonthAvgSales ?: zero) }, + estCloseBal = infos.sumOf { (it.estCloseBal ?: zero) }, + priority = if (infos.isNotEmpty()) infos[0].priority else null, + bomMaterials = sumBomMaterials, + assignDate = null, + ) + } + + // Production Schedule By Date group by assign date + val groupedProdScheduleLinesByFg = prodScheduleLineInfosByFg.groupBy { it.assignDate } + + // ---------------------------------- By BoM ----------------------------------// + // Seven Days of Bom Materials + val prodScheduleLinesInfoByBom7Days = prodScheduleLines.mapNotNull { line -> + val bomMaterial = line.item.id?.let { bomMaterialRepository.findAllByBomItemIdAndDeletedIsFalse(it) } + + bomMaterial?.map { bm -> + val proportion = + if (line.prodQty > 0 && bm.bom?.outputQty != null && (bm.bom?.outputQty ?: zero) > zero) { + BigDecimal(line.prodQty).divide(bm.bom!!.outputQty, 2, RoundingMode.HALF_UP) + } else { + zero + } + + val demandQty = bm.qty?.times(proportion) ?: zero + + ProdScheduleLineInfoByBomByDate( + id = bm.item?.id, + code = bm.item?.code, + name = bm.item?.name, + type = bm.item?.type, + availableQty = bm.item?.inventories?.sumOf { + (it.onHandQty ?: zero) - (it.onHoldQty ?: zero) - (it.unavailableQty ?: zero) + } ?: zero, + demandQty = demandQty, + assignDate = line.assignDate, + uomName = bm.uomName + ) + } + } + .flatten() + +// println(prodScheduleLinesInfoByBom7Days) + + // Production Schedule By Bom By Date + val prodScheduleLinesInfoByBom = prodScheduleLinesInfoByBom7Days + .groupBy { Pair(it.assignDate, it.code) } + .map { (key, line) -> + ProdScheduleLineInfoByBomByDate( + id = if (line.isNotEmpty()) line[0].id else null, + code = if (line.isNotEmpty()) line[0].code else null, + name = if (line.isNotEmpty()) line[0].name else null, + type = if (line.isNotEmpty()) line[0].type else null, + availableQty = if (line.isNotEmpty()) line[0].availableQty else zero, + demandQty = line.sumOf { it.demandQty ?: zero }, + assignDate = key.first, + uomName = if (line.isNotEmpty()) line[0].uomName else null, + ) + } + + val groupedProdScheduleLinesByBom = prodScheduleLinesInfoByBom.groupBy { it.assignDate } + + // Sum of the Production Schedule By Bom + val sumProdScheduleLinesInfoByBom = prodScheduleLinesInfoByBom7Days + .groupBy { it.code } + .map { (_, line) -> + ProdScheduleLineInfoByBom( + id = if (line.isNotEmpty()) line[0].id else null, + code = if (line.isNotEmpty()) line[0].code else null, + name = if (line.isNotEmpty()) line[0].name else null, + type = if (line.isNotEmpty()) line[0].type else null, + availableQty = if (line.isNotEmpty()) line[0].availableQty else null, + totalDemandQty = line.sumOf { it.demandQty ?: zero }, + demandQty1 = line.filter { it.assignDate == 1L }.sumOf { it.demandQty ?: zero }, + demandQty2 = line.filter { it.assignDate == 2L }.sumOf { it.demandQty ?: zero }, + demandQty3 = line.filter { it.assignDate == 3L }.sumOf { it.demandQty ?: zero }, + demandQty4 = line.filter { it.assignDate == 4L }.sumOf { it.demandQty ?: zero }, + demandQty5 = line.filter { it.assignDate == 5L }.sumOf { it.demandQty ?: zero }, + demandQty6 = line.filter { it.assignDate == 6L }.sumOf { it.demandQty ?: zero }, + demandQty7 = line.filter { it.assignDate == 7L }.sumOf { it.demandQty ?: zero }, + uomName = if (line.isNotEmpty()) line[0].uomName else null, + ) + } + + // ---------------------------------- Response ----------------------------------// + return ProdScheduleWithLine( + id = prodSchedule.id, + scheduleAt = prodSchedule.scheduleAt, + schedulePeriod = schedulePeriod, + schedulePeriodTo = schedulePeriodTo, + totalEstProdCount = BigDecimal(prodSchedule.totalEstProdCount), + totalFGType = prodSchedule.totalFGType, + type = prodSchedule.type, + prodScheduleLinesByFg = sumProdScheduleLineInfosByFg, + prodScheduleLinesByFgByDate = groupedProdScheduleLinesByFg, + prodScheduleLinesByBom = sumProdScheduleLinesInfoByBom, + prodScheduleLinesByBomByDate = groupedProdScheduleLinesByBom + ) + } + //====================細排相關 START====================// open fun getDailyProductionCount(assignDate: Int, selectedDate: LocalDateTime): Int { @@ -33,14 +244,14 @@ open class ProductionScheduleService( val sql = StringBuilder( " SELECT " - + " SUM(psl.prodQty) AS dailyProdCount " - + " FROM production_schedule ps " - + " LEFT JOIN production_schedule_line psl ON psl.prodScheduleId = ps.id " - + " WHERE ps.deleted = FALSE " - + " AND psl.deleted = FALSE " - + " AND DATE(ps.scheduleAt) LIKE DATE(:selectedDate) " - + " AND psl.assignDate = :assignDate " - + " GROUP BY ps.id " + + " SUM(psl.prodQty) AS dailyProdCount " + + " FROM production_schedule ps " + + " LEFT JOIN production_schedule_line psl ON psl.prodScheduleId = ps.id " + + " WHERE ps.deleted = FALSE " + + " AND psl.deleted = FALSE " + + " AND DATE(ps.scheduleAt) LIKE DATE(:selectedDate) " + + " AND psl.assignDate = :assignDate " + + " GROUP BY ps.id " ); return jdbcDao.queryForInt(sql.toString(), args); } @@ -64,11 +275,12 @@ open class ProductionScheduleService( val productionPriorityMap: HashMap = HashMap(); //TODO: update to use SQL get shop order record for detailed scheduling (real prodQty and openBal) - for (record in scheduledList){ - println("Object - " + record.toString() ); - val tempRecord= record; - tempRecord.prodQty = tempRecord.prodQty *2; - val difference = -(tempRecord.targetMinStock + tempRecord.prodQty - tempRecord.estCloseBal) /*TODO: this should be real close bal*/; + for (record in scheduledList) { + println("Object - " + record.toString()); + val tempRecord = record; + tempRecord.prodQty = tempRecord.prodQty * 2; + val difference = + -(tempRecord.targetMinStock + tempRecord.prodQty - tempRecord.estCloseBal) /*TODO: this should be real close bal*/; productionPriorityMap.put(tempRecord, difference) } @@ -80,17 +292,16 @@ open class ProductionScheduleService( for ((updatedScheduleRecord, totalDifference) in sortedEntries) { //match id with rough schedule record to create new record - val targetRecord = scheduledList.find { it.itemId == updatedScheduleRecord.itemId } + val targetRecord = scheduledList.find { it.item.id == updatedScheduleRecord.item.id } val prodDifference = updatedScheduleRecord.prodQty - (targetRecord?.prodQty ?: 0.0) - if(idleProductionCount - prodDifference >= 0){ + if (idleProductionCount - prodDifference >= 0) { //have enough quoter for adjustment idleProductionCount -= prodDifference; detailedScheduleOutputList.add(updatedScheduleRecord) accProdCount += updatedScheduleRecord.prodQty fgCount++ - } - else{ + } else { println("[INFO] item " + updatedScheduleRecord.name + " have bee skipped"); } } @@ -108,7 +319,11 @@ open class ProductionScheduleService( saveDetailedScheduleOutput(sortedOutputList, accProdCount, fgCount) } - open fun saveDetailedScheduleOutput(sortedEntries: List, accProdCount: Double, fgCount: Long) { + open fun saveDetailedScheduleOutput( + sortedEntries: List, + accProdCount: Double, + fgCount: Long + ) { val tempObj = ProductionSchedule() tempObj.id = -1; tempObj.scheduleAt = LocalDateTime.now() @@ -117,7 +332,7 @@ open class ProductionScheduleService( tempObj.id = saveProductionScheduleToDatabase(tempObj); tempObj.type = "detailed" - for ( detailedScheduleRecord in sortedEntries) { + for (detailedScheduleRecord in sortedEntries) { saveDetailedScheduleLineToDatabase(tempObj.id ?: -1, detailedScheduleRecord) } } @@ -125,10 +340,12 @@ open class ProductionScheduleService( private fun saveDetailedScheduleLineToDatabase(parentId: Long, detailedScheduleObj: ProductionScheduleRecord) { try { + val prodSchedule = productionScheduleRepository.findById(parentId).get() var savedItem = ProductionScheduleLine() savedItem.id = -1; - savedItem.prodScheduleId = parentId; - savedItem.itemId = detailedScheduleObj.itemId ?: -1; +// savedItem.prodScheduleId = parentId; + savedItem.productionSchedule = prodSchedule; + savedItem.item.id = detailedScheduleObj.item.id ?: -1; savedItem.lastMonthAvgSales = detailedScheduleObj.lastMonthAvgSales ?: 0.0; savedItem.refScheduleId = detailedScheduleObj.id; savedItem.approverId = null; @@ -155,8 +372,8 @@ open class ProductionScheduleService( open var targetMinStock: Double = 0.0; override fun toString(): String { - return "ProductionScheduleLine(prodScheduleId=$prodScheduleId," + - " itemId=$itemId, lastMonthAvgSales=$lastMonthAvgSales," + + return "ProductionScheduleLine(prodScheduleId=${productionSchedule.id}," + + " itemId=${item.id}, lastMonthAvgSales=$lastMonthAvgSales," + " prodQty=$prodQty, estCloseBal=$estCloseBal, type='$type'," + " approverId=$approverId, refScheduleId=$refScheduleId," + " assignDate=$assignDate, itemPriority=$itemPriority)" + @@ -190,22 +407,24 @@ open class ProductionScheduleService( + " AND psl.deleted = FALSE " ); - if(args.containsKey("selectedDate")){ + if (args.containsKey("selectedDate")) { sql.append(" AND DATE(ps.scheduleAt) LIKE DATE(:selectedDate) "); } - if(args.containsKey("name")){ + if (args.containsKey("name")) { sql.append(" AND i.name LIKE :name "); } - if(args.containsKey("assignDate")){ + if (args.containsKey("assignDate")) { sql.append(" AND psl.assignDate = :assignDate "); } - if(args.containsKey("id")){ + if (args.containsKey("id")) { sql.append(" AND i.id = :id "); } sql.append(" ORDER BY psl.assignDate, psl.itemPriority ASC "); - val resultList = jdbcDao.queryForList(sql.toString(), args, ); + print(sql) + val resultList = jdbcDao.queryForList(sql.toString(), args); + print(resultList) //TODO: From Global config val DARK_MAX_VALUE = 1; @@ -214,13 +433,13 @@ open class ProductionScheduleService( val DARK_WEIGHTING = 0.34; val FLOAT_WEIGHTING = 0.33; val DENSE_WEIGHTING = 0.33; - + // Convert the list of maps to a list of ProductionScheduleRecord return resultList.map { row -> ProductionScheduleRecord().apply { this.id = row["id"].toString().toLong() this.itemPriority = row["itemPriority"].toString().toLong() - this.itemId = row["itemId"].toString().toLong() + this.item.id = row["itemId"].toString().toLong() this.name = row["name"] as String this.lastMonthAvgSales = row["lastMonthAvgSales"].toString().toDouble() this.targetMinStock = row["targetMinStock"].toString().toDouble() // Ensure correct type @@ -239,20 +458,20 @@ open class ProductionScheduleService( this.weightingRef = if (isDarkValue > 0) { - (DARK_MAX_VALUE+1 - isDarkValue) / DARK_MAX_VALUE.toDouble() * DARK_WEIGHTING - } else { - 0.0 - } + - if (isDenseValue > 0) { - (DENSE_MAX_VALUE+1 - isDenseValue) / DENSE_MAX_VALUE.toDouble() * DENSE_WEIGHTING + (DARK_MAX_VALUE + 1 - isDarkValue) / DARK_MAX_VALUE.toDouble() * DARK_WEIGHTING } else { 0.0 } + - if (isFloatValue > 0) { - (FLOAT_MAX_VALUE+1 - isFloatValue) / FLOAT_MAX_VALUE.toDouble() * FLOAT_WEIGHTING - } else { - 0.0 - } + if (isDenseValue > 0) { + (DENSE_MAX_VALUE + 1 - isDenseValue) / DENSE_MAX_VALUE.toDouble() * DENSE_WEIGHTING + } else { + 0.0 + } + + if (isFloatValue > 0) { + (FLOAT_MAX_VALUE + 1 - isFloatValue) / FLOAT_MAX_VALUE.toDouble() * FLOAT_WEIGHTING + } else { + 0.0 + } } } } @@ -277,11 +496,11 @@ open class ProductionScheduleService( return finishedGoodList } - open fun generateRoughScheduleByWeek(fgList : ArrayList): HashMap { + open fun generateRoughScheduleByWeek(fgList: ArrayList): HashMap { val roughScheduleOutput: HashMap = HashMap(); var itemCount = 1 - for(fg:FinishedGood in fgList) { + for (fg: FinishedGood in fgList) { val roughScheduleRecord: RoughScheduleObj = roughScheduleByItem(fg); itemCount++ roughScheduleOutput.put(roughScheduleRecord, roughScheduleRecord.totalDifference); @@ -307,17 +526,19 @@ open class ProductionScheduleService( roughScheduleRecord.totalDifference += roughScheduleRecord.productionSchedule[i] roughScheduleOutput[roughScheduleRecord] = roughScheduleRecord.totalDifference - } - else{//超過每日產量 - var maxExtraProduction = Math.max((22000L - accProductionCount),0L); - roughScheduleRecord.totalForgoneCount = roughScheduleRecord.totalForgoneCount + roughScheduleRecord.productionSchedule[i] - maxExtraProduction; + } else {//超過每日產量 + var maxExtraProduction = Math.max((22000L - accProductionCount), 0L); + roughScheduleRecord.totalForgoneCount = + roughScheduleRecord.totalForgoneCount + roughScheduleRecord.productionSchedule[i] - maxExtraProduction; roughScheduleRecord.productionSchedule[i] = maxExtraProduction.toDouble(); accProductionCount += maxExtraProduction; //update close balance - roughScheduleRecord.closeBalance[i] = roughScheduleRecord.closeBalance[i] - roughScheduleRecord.productionSchedule[i] + maxExtraProduction.toDouble(); - for (j in i+1 until 12){ - roughScheduleRecord.closeBalance[j] = roughScheduleRecord.closeBalance[j] - roughScheduleRecord.productionSchedule[i] + maxExtraProduction.toDouble(); + roughScheduleRecord.closeBalance[i] = + roughScheduleRecord.closeBalance[i] - roughScheduleRecord.productionSchedule[i] + maxExtraProduction.toDouble(); + for (j in i + 1 until 12) { + roughScheduleRecord.closeBalance[j] = + roughScheduleRecord.closeBalance[j] - roughScheduleRecord.productionSchedule[i] + maxExtraProduction.toDouble(); } // Update totalDifference in roughScheduleRecord @@ -328,12 +549,13 @@ open class ProductionScheduleService( } //解決拖欠的數量 - for ((roughScheduleRecord, totalDifference) in sortedEntries){ + for ((roughScheduleRecord, totalDifference) in sortedEntries) { if (accProductionCount + ceil(roughScheduleRecord.productionSchedule[i]).toLong() <= 22000) { //沒有超過每日產量 - if(roughScheduleRecord.totalForgoneCount > 0.0){ - if(accProductionCount + ceil(roughScheduleRecord.totalForgoneCount).toLong() <=22000){ + if (roughScheduleRecord.totalForgoneCount > 0.0) { + if (accProductionCount + ceil(roughScheduleRecord.totalForgoneCount).toLong() <= 22000) { //可以全做 - roughScheduleRecord.productionSchedule[i] = roughScheduleRecord.productionSchedule[i] + ceil(roughScheduleRecord.totalForgoneCount).toLong(); + roughScheduleRecord.productionSchedule[i] = + roughScheduleRecord.productionSchedule[i] + ceil(roughScheduleRecord.totalForgoneCount).toLong(); accProductionCount += ceil(roughScheduleRecord.totalForgoneCount).toLong(); @@ -341,23 +563,26 @@ open class ProductionScheduleService( roughScheduleOutput[roughScheduleRecord] = roughScheduleRecord.totalDifference //update close balance - for (j in i until 12){ - roughScheduleRecord.closeBalance[j] = roughScheduleRecord.closeBalance[j] + roughScheduleRecord.totalForgoneCount; + for (j in i until 12) { + roughScheduleRecord.closeBalance[j] = + roughScheduleRecord.closeBalance[j] + roughScheduleRecord.totalForgoneCount; } roughScheduleRecord.totalForgoneCount = 0.0; - } - else{ + } else { //只能做部分 var maxExtraProduction = Math.max((22000L - accProductionCount), 0L); - roughScheduleRecord.totalForgoneCount = Math.max((roughScheduleRecord.totalForgoneCount - maxExtraProduction), 0.0); + roughScheduleRecord.totalForgoneCount = + Math.max((roughScheduleRecord.totalForgoneCount - maxExtraProduction), 0.0); - roughScheduleRecord.productionSchedule[i] = roughScheduleRecord.productionSchedule[i] + maxExtraProduction; + roughScheduleRecord.productionSchedule[i] = + roughScheduleRecord.productionSchedule[i] + maxExtraProduction; accProductionCount += maxExtraProduction; roughScheduleRecord.totalDifference += maxExtraProduction; //update close balance - for (j in i until 12){ - roughScheduleRecord.closeBalance[j] = roughScheduleRecord.closeBalance[j] + maxExtraProduction; + for (j in i until 12) { + roughScheduleRecord.closeBalance[j] = + roughScheduleRecord.closeBalance[j] + maxExtraProduction; } } } @@ -366,7 +591,7 @@ open class ProductionScheduleService( } //debug use - for ((roughScheduleRecord, totalDifference) in sortedEntries){ + for ((roughScheduleRecord, totalDifference) in sortedEntries) { println("[RUN" + i + "][index:" + totalDifference + "] - " + roughScheduleRecord.fgDetail?.name + " - " + roughScheduleRecord.productionSchedule[i]); } println("Total Production Count $accProductionCount"); @@ -377,33 +602,38 @@ open class ProductionScheduleService( return roughScheduleOutput } - open fun roughScheduleByItem(fg: FinishedGood): RoughScheduleObj{ + open fun roughScheduleByItem(fg: FinishedGood): RoughScheduleObj { val minStockCount: Double = fg.lastMonthAvgSalesCount * 2 //TODO: change to multiple by user config setting val stockDifference: Double = minStockCount - fg.openBalance val productionSchedule: Array = Array(12, { 0.0 }) //首日粗排產量 —> 取結果,或是fg本身生產上限,並不可小於0 productionSchedule[0] = Math.max( - ceil(Math.min( - (stockDifference + fg.lastMonthAvgSalesCount), - fg.fgProductionLimit - )), + ceil( + Math.min( + (stockDifference + fg.lastMonthAvgSalesCount), + fg.fgProductionLimit + ) + ), 0.0 ) val closeBalance: Array = Array(12, { 0.0 }) closeBalance[0] = /*Math.floor(*/fg.openBalance + productionSchedule[0] - fg.lastMonthAvgSalesCount/*)*/; - for (i in 1 until 12){ + for (i in 1 until 12) { //最少庫存-closeBalance 有可能小於0,所以必須要確保輸出>=0 - productionSchedule[i] = Math.max( - ceil(Math.min( - (minStockCount - closeBalance[i-1] + fg.lastMonthAvgSalesCount), - fg.fgProductionLimit - )), + productionSchedule[i] = Math.max( + ceil( + Math.min( + (minStockCount - closeBalance[i - 1] + fg.lastMonthAvgSalesCount), + fg.fgProductionLimit + ) + ), 0.0 ) - closeBalance[i] = /*Math.floor(*/closeBalance[i-1] + productionSchedule[i] - fg.lastMonthAvgSalesCount/*)*/; + closeBalance[i] = /*Math.floor(*/ + closeBalance[i - 1] + productionSchedule[i] - fg.lastMonthAvgSalesCount/*)*/; } var totalProductionCount: Double = 0.0 @@ -425,7 +655,11 @@ open class ProductionScheduleService( ); } - open fun saveRoughScheduleOutput(sortedEntries: List>, accProdCount: Double, fgCount: Long) { + open fun saveRoughScheduleOutput( + sortedEntries: List>, + accProdCount: Double, + fgCount: Long + ) { val tempObj = ProductionSchedule() tempObj.id = -1; tempObj.scheduleAt = LocalDateTime.now() @@ -439,7 +673,7 @@ open class ProductionScheduleService( } } - private fun saveProductionScheduleToDatabase(newRecord: ProductionSchedule): Long?{ + private fun saveProductionScheduleToDatabase(newRecord: ProductionSchedule): Long? { try { val savedItem = productionScheduleRepository.saveAndFlush(newRecord) return savedItem.id @@ -449,19 +683,21 @@ open class ProductionScheduleService( } private fun saveRoughScheduleLineToDatabase(parentId: Long, roughScheduleObj: RoughScheduleObj) { - for (i in 5 until 12){ + for (i in 5 until 12) { try { + val prodSchedule = productionScheduleRepository.findById(parentId).get() var savedItem = ProductionScheduleLine() savedItem.id = -1; - savedItem.prodScheduleId = parentId; - savedItem.itemId = roughScheduleObj.fgDetail?.id ?: -1; +// savedItem.prodScheduleId = parentId; + savedItem.productionSchedule = prodSchedule; + savedItem.item.id = roughScheduleObj.fgDetail?.id ?: -1; savedItem.lastMonthAvgSales = roughScheduleObj.fgDetail?.lastMonthAvgSalesCount ?: 0.0; savedItem.refScheduleId = null; savedItem.approverId = null; savedItem.estCloseBal = roughScheduleObj.closeBalance[i] savedItem.prodQty = roughScheduleObj.productionSchedule[i] savedItem.type = "rough" - savedItem.assignDate = i-4L; + savedItem.assignDate = i - 4L; savedItem.itemPriority = roughScheduleObj.itemPriority[i] productionScheduleLineRepository.saveAndFlush(savedItem) @@ -530,5 +766,4 @@ open class ProductionScheduleService( } } - } \ No newline at end of file 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 d558b17..c29e3fd 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 @@ -1,34 +1,69 @@ package com.ffii.fpsms.modules.master.web -import com.ffii.fpsms.modules.master.entity.ProductionSchedule -import com.ffii.fpsms.modules.master.entity.UomConversion +import com.ffii.core.response.RecordsRes +import com.ffii.core.utils.CriteriaArgsBuilder +import com.ffii.fpsms.modules.master.entity.ProductionScheduleRepository +import com.ffii.fpsms.modules.master.entity.projections.ProdScheduleInfo +import com.ffii.fpsms.modules.master.entity.projections.ProdScheduleWithLine import com.ffii.fpsms.modules.master.service.ProductionScheduleService import com.ffii.fpsms.modules.master.service.ProductionScheduleService.FinishedGood import com.ffii.fpsms.modules.master.service.ProductionScheduleService.RoughScheduleObj +import com.ffii.fpsms.modules.master.web.models.SearchProdScheduleRequest import jakarta.servlet.http.HttpServletRequest import org.springframework.web.bind.annotation.* +import java.time.Duration import java.time.LocalDateTime -import java.util.ArrayList import java.util.HashMap import kotlin.collections.component1 import kotlin.collections.component2 +import kotlin.math.abs @RestController @RequestMapping("/productionSchedule") class ProductionScheduleController( private val productionScheduleService: ProductionScheduleService, + private val productionScheduleRepository: ProductionScheduleRepository, ) { // @GetMapping // fun allItems(): List { // return uomConversionService.allItems() // } + @GetMapping("/test") + fun test(request: HttpServletRequest): Any { + val criteriaArgs = CriteriaArgsBuilder.withRequest(request) + .addStringLike("type") + .build() + if (!criteriaArgs.containsKey("type")) { + criteriaArgs["type"] = "rough" + } + return productionScheduleService.getLatestScheduleAt("rough") + } + + @GetMapping("/detail/{id}") + fun getScheduleDetail(@PathVariable id: Long): ProdScheduleWithLine { + return productionScheduleService.prodScheduleDetail(id) + } + + @GetMapping("/getRecordByPage") + fun allProdSchedulesByPage(@ModelAttribute request: SearchProdScheduleRequest): RecordsRes { + return productionScheduleService.allProdSchedulesByPage(request); + } + @RequestMapping(value = ["/testDetailSchedule"], method = [RequestMethod.GET]) fun generateDetailSchedule(request: HttpServletRequest?): Int { try { + val today = LocalDateTime.now() + val latestRoughScheduleAt = productionScheduleService.getLatestScheduleAt("rough") + val assignDate = abs(Duration.between(latestRoughScheduleAt, today).toDays() % 7) + val finalAssignDate = if (assignDate.toInt() == 0) { + 1 + } else assignDate.toInt() //TODO: update this to receive selectedDate and assignDate from schedule - productionScheduleService.generateDetailedScheduleByDay(1, LocalDateTime.of(2025,6,25,0,0,0)) +// productionScheduleService.generateDetailedScheduleByDay(1, LocalDateTime.of(2025,6,25,0,0,0)) + println("today: $today | latestRoughScheduleAty: $latestRoughScheduleAt | assignDate: $assignDate ") + productionScheduleService.generateDetailedScheduleByDay(finalAssignDate, LocalDateTime.now()) 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 new file mode 100644 index 0000000..fe0a0de --- /dev/null +++ b/src/main/java/com/ffii/fpsms/modules/master/web/models/SearchProdScheduleRequest.kt @@ -0,0 +1,14 @@ +package com.ffii.fpsms.modules.master.web.models + +import java.time.LocalDate + +data class SearchProdScheduleRequest ( + val id: Long?, + val scheduleAt: String?, + val schedulePeriod: String?, + val schedulePeriodTo: String?, + val totalEstProdCount: Double?, + val type: String?, + val pageSize: Int?, + val pageNum: Int?, +) \ No newline at end of file