From 2074a4fa0cd0cd1de4e70fb48243fd9826275aaf Mon Sep 17 00:00:00 2001 From: "jason.lam" Date: Wed, 28 May 2025 17:12:41 +0800 Subject: [PATCH] update save rough production schedule logic --- .../master/entity/ProductionSchedule.kt | 20 ++ .../master/entity/ProductionScheduleLine.kt | 40 +++ .../ProductionScheduleLineRepository.kt | 8 + .../entity/ProductionScheduleRepository.kt | 8 + .../service/ProductionScheduleService.kt | 294 ++++++++++++++++++ .../web/ProductionScheduleController.kt | 119 +++++++ .../01_create_rough_schedule_table.sql | 4 +- 7 files changed, 492 insertions(+), 1 deletion(-) create mode 100644 src/main/java/com/ffii/fpsms/modules/master/entity/ProductionSchedule.kt create mode 100644 src/main/java/com/ffii/fpsms/modules/master/entity/ProductionScheduleLine.kt create mode 100644 src/main/java/com/ffii/fpsms/modules/master/entity/ProductionScheduleLineRepository.kt create mode 100644 src/main/java/com/ffii/fpsms/modules/master/entity/ProductionScheduleRepository.kt create mode 100644 src/main/java/com/ffii/fpsms/modules/master/service/ProductionScheduleService.kt create mode 100644 src/main/java/com/ffii/fpsms/modules/master/web/ProductionScheduleController.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 new file mode 100644 index 0000000..b960238 --- /dev/null +++ b/src/main/java/com/ffii/fpsms/modules/master/entity/ProductionSchedule.kt @@ -0,0 +1,20 @@ +package com.ffii.fpsms.modules.master.entity; + +import com.ffii.core.entity.BaseEntity +import jakarta.persistence.* +import java.time.LocalDateTime + +@Entity +@Table(name = "production_schedule") +open class ProductionSchedule : BaseEntity() { + @Column(name = "scheduleAt") + open var scheduleAt: LocalDateTime = LocalDateTime.now() + + @Column(name = "totalEstProdCount") + open var totalEstProdCount: Double = 0.0 + + @Column(name = "totalFGType") + open var totalFGType: Long = 0L + + +} 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 new file mode 100644 index 0000000..c5fe86c --- /dev/null +++ b/src/main/java/com/ffii/fpsms/modules/master/entity/ProductionScheduleLine.kt @@ -0,0 +1,40 @@ +package com.ffii.fpsms.modules.master.entity; + +import com.ffii.core.entity.BaseEntity +import jakarta.persistence.* +import java.time.LocalDateTime + +@Entity +@Table(name = "production_schedule_line") +open class ProductionScheduleLine : BaseEntity() { + @Column(name = "prodScheduleId") + open var prodScheduleId: Long = 0L + + @Column(name = "itemId") + open var itemId: Long = 0L + + @Column(name = "lastMonthAvgSales") + open var lastMonthAvgSales: Double = 0.0 + + @Column(name = "prodQty") + open var prodQty: Double = 0.0 + + @Column(name = "estCloseBal") + open var estCloseBal: Double = 0.0 + + @Column(name = "type") + open var type: String = "rough" + + @Column(name = "approverId") + open var approverId: Long? = 0 + + @Column(name = "refScheduleId") + open var refScheduleId: Long? = 0 + + //in Integer (i.e.: Day1, Day2, Day3) + @Column(name = "assignDate") + open var assignDate: Long = 0L + + @Column(name = "itemPriority") + open var itemPriority: Long = 0L +} 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 new file mode 100644 index 0000000..d734dce --- /dev/null +++ b/src/main/java/com/ffii/fpsms/modules/master/entity/ProductionScheduleLineRepository.kt @@ -0,0 +1,8 @@ +package com.ffii.fpsms.modules.master.entity + +import com.ffii.core.support.AbstractRepository +import org.springframework.stereotype.Repository + +@Repository +interface ProductionScheduleLineRepository : AbstractRepository { +} \ No newline at end of file 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 new file mode 100644 index 0000000..91c32a4 --- /dev/null +++ b/src/main/java/com/ffii/fpsms/modules/master/entity/ProductionScheduleRepository.kt @@ -0,0 +1,8 @@ +package com.ffii.fpsms.modules.master.entity + +import com.ffii.core.support.AbstractRepository +import org.springframework.stereotype.Repository + +@Repository +interface ProductionScheduleRepository : AbstractRepository { +} \ 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 new file mode 100644 index 0000000..8ccde4f --- /dev/null +++ b/src/main/java/com/ffii/fpsms/modules/master/service/ProductionScheduleService.kt @@ -0,0 +1,294 @@ +package com.ffii.fpsms.modules.master.service + +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 org.springframework.stereotype.Service +import java.time.LocalDateTime +import java.util.* +import kotlin.collections.component1 +import kotlin.collections.component2 +import kotlin.math.ceil + +@Service +open class ProductionScheduleService( + private val jdbcDao: JdbcDao, + private val itemService: ItemsService, + private val productionScheduleRepository: ProductionScheduleRepository, + private val productionScheduleLineRepository: ProductionScheduleLineRepository, +): AbstractBaseEntityService(jdbcDao, productionScheduleRepository) { + // do mapping with projection + + open fun convertToFinishedGoodList(): ArrayList { + val roughScheduleList = itemService.getRoughScheduleList() // Retrieve the list from your existing method + val finishedGoodList = ArrayList() + + for (item in roughScheduleList) { + val finishedGood = FinishedGood( + id = item["itemId"].toString().toLong(), // Cast to Long + name = item["itemName"] as String?, // Cast to String + receipeId = 0, // Assuming itemNo is a Long + openBalance = 0.0, // Set default value or calculate if needed + lastMonthAvgSalesCount = item["lastMonthAvgSalesCount"].toString().toDouble(), // Cast to Double + fgProductionLimit = item["fgProductionLimit"].toString().toDouble() // Default production limit + ) + finishedGoodList.add(finishedGood) + } + + return finishedGoodList + } + + open fun generateRoughScheduleByWeek(fgList : ArrayList): HashMap { + val roughScheduleOutput: HashMap = HashMap(); + + var itemCount = 1 + for(fg:FinishedGood in fgList) { + val roughScheduleRecord: RoughScheduleObj = roughScheduleByItem(fg); + itemCount++ + roughScheduleOutput.put(roughScheduleRecord, roughScheduleRecord.totalDifference); + } + + //完成初步fg粗排後檢查調整 + for (i in 0 until 12) { + var accProductionCount = 0L; + + // Sort by totalDifference before processing + val sortedEntries = roughScheduleOutput.entries.sortedBy { it.value } + + //原排產 + var itemCount = 1L; + for ((roughScheduleRecord, totalDifference) in sortedEntries) { + //記錄粗排優先順序(以最少庫存缺口最大優先) + roughScheduleRecord.itemPriority[i] = itemCount; + itemCount++; + + if (accProductionCount + ceil(roughScheduleRecord.productionSchedule[i]).toLong() <= 22000) { //沒有超過每日產量 + accProductionCount += ceil(roughScheduleRecord.productionSchedule[i]).toLong() //分開日子算 + + 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; + 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(); + } + + // Update totalDifference in roughScheduleRecord + roughScheduleRecord.totalDifference += (roughScheduleRecord.productionSchedule[i] - maxExtraProduction.toDouble()); + roughScheduleOutput[roughScheduleRecord] = roughScheduleRecord.totalDifference + } + + } + + //解決拖欠的數量 + 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){ + //可以全做 + roughScheduleRecord.productionSchedule[i] = roughScheduleRecord.productionSchedule[i] + ceil(roughScheduleRecord.totalForgoneCount).toLong(); + + accProductionCount += ceil(roughScheduleRecord.totalForgoneCount).toLong(); + + roughScheduleRecord.totalDifference += roughScheduleRecord.totalForgoneCount; + roughScheduleOutput[roughScheduleRecord] = roughScheduleRecord.totalDifference + + //update close balance + for (j in i until 12){ + roughScheduleRecord.closeBalance[j] = roughScheduleRecord.closeBalance[j] + roughScheduleRecord.totalForgoneCount; + } + roughScheduleRecord.totalForgoneCount = 0.0; + } + else{ + //只能做部分 + var maxExtraProduction = Math.max((22000L - accProductionCount), 0L); + roughScheduleRecord.totalForgoneCount = Math.max((roughScheduleRecord.totalForgoneCount - maxExtraProduction), 0.0); + + 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; + } + } + } + + } + } + + //debug use + for ((roughScheduleRecord, totalDifference) in sortedEntries){ + println("[RUN" + i + "][index:" + totalDifference + "] - " + roughScheduleRecord.fgDetail?.name + " - " + roughScheduleRecord.productionSchedule[i]); + } + println("Total Production Count $accProductionCount"); + println("========================================"); + + } + + return roughScheduleOutput + } + + open fun roughScheduleByItem(fg: FinishedGood): RoughScheduleObj{ + val minStockCount: Double = fg.lastMonthAvgSalesCount * 2 + 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 + )), + 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){ + //最少庫存-closeBalance 有可能小於0,所以必須要確保輸出>=0 + 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/*)*/; + } + + var totalProductionCount: Double = 0.0 + + for (i in 5 until 12) { + totalProductionCount = totalProductionCount + productionSchedule[i]; + } + + val totalDifference: Double = -(totalProductionCount - fg.openBalance); + + + return RoughScheduleObj( + fg, + productionSchedule, + closeBalance, + totalDifference, + totalProductionCount, + 0.0 + ); + } + + open fun saveRoughScheduleOutput(sortedEntries: List>, accProdCount: Double, fgCount: Long) { + val tempObj = ProductionSchedule() + tempObj.id = -1; + tempObj.scheduleAt = LocalDateTime.now() + tempObj.totalFGType = fgCount; + tempObj.totalEstProdCount = accProdCount; + tempObj.id = saveRoughScheduleToDatabase(tempObj); + + for ((roughScheduleRecord, totalDifference) in sortedEntries) { + saveRoughScheduleLineToDatabase(tempObj.id ?: -1, roughScheduleRecord) + } + } + + private fun saveRoughScheduleToDatabase(newRecord: ProductionSchedule): Long?{ + try { + val savedItem = productionScheduleRepository.saveAndFlush(newRecord) + return savedItem.id + } catch (e: Exception) { + throw RuntimeException("Error saving production schedule: ${e.message}", e) + } + } + + private fun saveRoughScheduleLineToDatabase(parentId: Long, roughScheduleObj: RoughScheduleObj) { + for (i in 5 until 12){ + try { + var savedItem = ProductionScheduleLine() + savedItem.id = -1; + savedItem.prodScheduleId = parentId; + savedItem.itemId = 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.itemPriority = roughScheduleObj.itemPriority[i] + productionScheduleLineRepository.saveAndFlush(savedItem) + + } catch (e: Exception) { + throw RuntimeException("Error saving production schedule line: ${e.message}", e) + } + } + + } + + open class FinishedGood { + //var fgDetail: FinishedGood? = null + var id: Long? = null + var name: String? = null + var receipeId: Long? = null + var openBalance: Double = 0.0 + var lastMonthAvgSalesCount: Double = 0.0 + var fgProductionLimit: Double = 22000.0 + + constructor( + id: Long?, + name: String?, + receipeId: Long?, + openBalance: Double, + lastMonthAvgSalesCount: Double, + fgProductionLimit: Double + ) { + this.id = id + this.name = name + this.receipeId = receipeId + this.openBalance = openBalance + this.lastMonthAvgSalesCount = lastMonthAvgSalesCount + this.fgProductionLimit = fgProductionLimit + } + } + + open class RoughScheduleObj { + var fgDetail: FinishedGood? = null + var productionSchedule: Array = Array(12, { 0.0 }) + var closeBalance: Array = Array(12) { 0.0 }; + var itemPriority: Array = Array(12) { 0 }; + var totalDifference: Double = 0.0 + var totalProductionCount: Double = 0.0 + var totalForgoneCount: Double = 0.0 + + + constructor( + fgDetail: FinishedGood?, + productionSchedule: Array, + closeBalance: Array, + totalDifference: Double, + totalProductionCount: Double, + totalForgoneCount: Double + ) { + this.fgDetail = fgDetail + this.productionSchedule = productionSchedule + this.closeBalance = closeBalance + this.totalDifference = totalDifference + this.totalProductionCount = totalProductionCount + this.totalForgoneCount = totalForgoneCount + } + + override fun toString(): String { + return "RoughScheduleObj(fgDetail=$fgDetail, productionSchedule=${productionSchedule.contentToString()}, closeBalance=${closeBalance.contentToString()}, totalDifference=$totalDifference, totalProductionCount=$totalProductionCount, totalForgoneCount=$totalForgoneCount)" + } + + } + +} \ 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 new file mode 100644 index 0000000..f8295e9 --- /dev/null +++ b/src/main/java/com/ffii/fpsms/modules/master/web/ProductionScheduleController.kt @@ -0,0 +1,119 @@ +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.fpsms.modules.master.service.ProductionScheduleService +import com.ffii.fpsms.modules.master.service.ProductionScheduleService.FinishedGood +import com.ffii.fpsms.modules.master.service.ProductionScheduleService.RoughScheduleObj +import jakarta.servlet.http.HttpServletRequest +import org.springframework.web.bind.annotation.* +import java.util.ArrayList +import java.util.HashMap +import kotlin.collections.component1 +import kotlin.collections.component2 + + +@RestController +@RequestMapping("/productionSchedule") +class ProductionScheduleController( + private val productionScheduleService: ProductionScheduleService, +) { +// @GetMapping +// fun allItems(): List { +// return uomConversionService.allItems() +// } + + @RequestMapping(value = ["/testRoughSchedule"], method = [RequestMethod.GET]) + fun generateRoughSchedule(request: HttpServletRequest?): List> { + try { + val demoFGList_old = arrayListOf( + FinishedGood( + 1, + "豆腐花 - Tofu pudding", + 3, + 91.0, + 31.0, + 3000.0 + ), + FinishedGood( + 2, + "中湯包 - Chinese Soup", + 4, + 100.0, + 72.5, + 3000.0 + ), + FinishedGood( + 3, + "苦瓜炒蛋 - Fried Egg", + 3, + 700.0, + 300.5, + 22000.0 + ), + FinishedGood( + 4, + "酸菜魚湯包 - Pickled fish soup", + 4, + 3000.0, + 600.5, + 22000.0 + ), + FinishedGood( + 5, + "咖哩雞球 - Curry chicken balls", + 5, + 3000.0, + 2150.5, + 22000.0 + ), + FinishedGood( + 6, + "水煮牛 - Spicy Beef", + 6, + 1000.0, + 2000.5, + 22000.0 + ), + FinishedGood( + 6, + "海鮮濃湯 - Sea Food Soup", + 6, + 0.0, + 3500.5, + 22000.0 + ), + FinishedGood( + 7, + "薯條 - Fries", + 7, + 0.0, + 3000.5, + 22000.0 + ), + ); + + 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); + return sortedEntries.map { entry -> + hashMapOf(entry.key to entry.value) + } + } catch (e: Exception) { + throw RuntimeException("Error generate schedule: ${e.message}", e) + } + } +} \ No newline at end of file diff --git a/src/main/resources/db/changelog/changes/20250528_01_jason_lam/01_create_rough_schedule_table.sql b/src/main/resources/db/changelog/changes/20250528_01_jason_lam/01_create_rough_schedule_table.sql index d70ada4..c0405fe 100644 --- a/src/main/resources/db/changelog/changes/20250528_01_jason_lam/01_create_rough_schedule_table.sql +++ b/src/main/resources/db/changelog/changes/20250528_01_jason_lam/01_create_rough_schedule_table.sql @@ -31,8 +31,10 @@ CREATE TABLE production_schedule_line lastMonthAvgSales DECIMAL(16, 2) NULL DEFAULT 0.0, prodQty DECIMAL(16, 2) NOT NULL, estCloseBal DECIMAL(16, 2) NULL DEFAULT 0.0, - type VARCHAR(30) NOT NULL DEFAULT "rough", + type VARCHAR(10) NOT NULL DEFAULT "rough", approverId INT(11) NULL, refScheduleId INT(11) NULL, + assignDate INT(11) NULL, + itemPriority INT(11) NULL, CONSTRAINT pk_production_schedule_line PRIMARY KEY (id) ); \ No newline at end of file