Переглянути джерело

[Prod Schedule] Update Prod Schedule Entity & Rough Schedule Search + detail

master
cyril.tsui 1 місяць тому
джерело
коміт
7a29cda3a5
8 змінених файлів з 550 додано та 92 видалено
  1. +5
    -0
      src/main/java/com/ffii/fpsms/modules/master/entity/ProductionSchedule.kt
  2. +13
    -4
      src/main/java/com/ffii/fpsms/modules/master/entity/ProductionScheduleLine.kt
  3. +2
    -0
      src/main/java/com/ffii/fpsms/modules/master/entity/ProductionScheduleLineRepository.kt
  4. +74
    -0
      src/main/java/com/ffii/fpsms/modules/master/entity/ProductionScheduleRepository.kt
  5. +84
    -0
      src/main/java/com/ffii/fpsms/modules/master/entity/projections/ProdScheduleInfo.kt
  6. +319
    -84
      src/main/java/com/ffii/fpsms/modules/master/service/ProductionScheduleService.kt
  7. +39
    -4
      src/main/java/com/ffii/fpsms/modules/master/web/ProductionScheduleController.kt
  8. +14
    -0
      src/main/java/com/ffii/fpsms/modules/master/web/models/SearchProdScheduleRequest.kt

+ 5
- 0
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<Long>() {

@Column(name = "type")
open var type: String = "manual"

@JsonManagedReference
@OneToMany(mappedBy = "productionSchedule", cascade = [CascadeType.ALL], orphanRemoval = true)
open var productionScheduleLines: MutableList<ProductionScheduleLine> = mutableListOf()
}

+ 13
- 4
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<Long>() {
@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<Long>() {

@Column(name = "weightingRef")
open var weightingRef: Double = 0.0

@JsonBackReference
@ManyToOne
@JoinColumn(name = "prodScheduleId", nullable = false)
open var productionSchedule: ProductionSchedule = ProductionSchedule()
}

+ 2
- 0
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<ProductionScheduleLine, Long> {

+ 74
- 0
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<ProductionSchedule, Long> {
@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<ProdScheduleInfo>
}

+ 84
- 0
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<ProdScheduleLineInfoByFg>,
val prodScheduleLinesByFgByDate: Map<Long?, List<ProdScheduleLineInfoByFg>>,
val prodScheduleLinesByBom: List<ProdScheduleLineInfoByBom>,
val prodScheduleLinesByBomByDate: Map<Long?, List<ProdScheduleLineInfoByBomByDate>>
)

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<ProdScheduleLineBomMaterialInfoByFg>?,
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?,
)

+ 319
- 84
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<ProductionSchedule, Long, ProductionScheduleRepository>(jdbcDao, productionScheduleRepository) {
private val bomMaterialService: BomMaterialService, private val bomMaterialRepository: BomMaterialRepository,
) : AbstractBaseEntityService<ProductionSchedule, Long, ProductionScheduleRepository>(
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<ProdScheduleInfo> {
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<ProdScheduleInfo>(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<ProductionScheduleRecord, Double> = 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<ProductionScheduleRecord>, accProdCount: Double, fgCount: Long) {
open fun saveDetailedScheduleOutput(
sortedEntries: List<ProductionScheduleRecord>,
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<FinishedGood>): HashMap<RoughScheduleObj, Double> {
open fun generateRoughScheduleByWeek(fgList: ArrayList<FinishedGood>): HashMap<RoughScheduleObj, Double> {
val roughScheduleOutput: HashMap<RoughScheduleObj, Double> = 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<Double> = Array<Double>(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<Double> = Array<Double>(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<Map.Entry<RoughScheduleObj, Double>>, accProdCount: Double, fgCount: Long) {
open fun saveRoughScheduleOutput(
sortedEntries: List<Map.Entry<RoughScheduleObj, Double>>,
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(
}

}

}

+ 39
- 4
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<Items> {
// 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<ProdScheduleInfo> {
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)


+ 14
- 0
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?,
)

Завантаження…
Відмінити
Зберегти