소스 검색

auto gen jo in schedule

master
부모
커밋
819a984b41
11개의 변경된 파일500개의 추가작업 그리고 131개의 파일을 삭제
  1. +34
    -0
      deployLocal.bat
  2. +39
    -0
      src/main/java/com/ffii/fpsms/modules/jobOrder/entity/JobOrderRepository.kt
  3. +18
    -0
      src/main/java/com/ffii/fpsms/modules/jobOrder/service/JobOrderService.kt
  4. +18
    -0
      src/main/java/com/ffii/fpsms/modules/master/entity/ProductionScheduleLine.kt
  5. +14
    -1
      src/main/java/com/ffii/fpsms/modules/master/entity/ProductionScheduleRepository.kt
  6. +8
    -0
      src/main/java/com/ffii/fpsms/modules/master/entity/projections/ProdScheduleInfo.kt
  7. +330
    -126
      src/main/java/com/ffii/fpsms/modules/master/service/ProductionScheduleService.kt
  8. +16
    -4
      src/main/java/com/ffii/fpsms/modules/master/web/ProductionScheduleController.kt
  9. +9
    -0
      src/main/java/com/ffii/fpsms/modules/master/web/models/ReleaseProdScheduleRequest.kt
  10. +9
    -0
      src/main/resources/db/changelog/changes/20251211_01_Fai/01_add_fields_to_psl.sql
  11. +5
    -0
      src/main/resources/db/changelog/changes/20251211_01_Fai/02_add_fields_to_psl.sql

+ 34
- 0
deployLocal.bat 파일 보기

@@ -0,0 +1,34 @@
@echo off

set "PROJECT_PATH=C:\dev\FPSMS-backend"
set "SERVICE_NAME=FP-backend"

echo.
echo [1/4] Stopping service %SERVICE_NAME% ...
net stop "%SERVICE_NAME%" >nul 2>&1

echo.
echo [2/4] Waiting 5 seconds...
timeout /t 5 >nul

echo.
echo [3/4] Building new JAR...
cd /d "%PROJECT_PATH%"
call gradlew clean bootJar --no-daemon

if %errorlevel% neq 0 (
echo.
echo Build FAILED! Service not restarted.
timeout /t 10
exit /b 1
)

echo.
echo [4/4] Starting service %SERVICE_NAME% ...
net start "%SERVICE_NAME%"

echo.
echo SUCCESS! %SERVICE_NAME% is running with the new JAR!
echo Deployment completed: %date% %time%
echo Closing in 5 seconds...
timeout /t 5 >nul

+ 39
- 0
src/main/java/com/ffii/fpsms/modules/jobOrder/entity/JobOrderRepository.kt 파일 보기

@@ -133,4 +133,43 @@ interface JobOrderRepository : AbstractRepository<JobOrder, Long> {
planStartTo: LocalDateTime?,
pageable: Pageable
): Page<JobOrderInfo>

@Query(
nativeQuery = true,
value = """
select
jo.id,
jo.code,
b.name,
jo.reqQty,
b.outputQtyUom as unit,
uc2.udfudesc as uom,
json_arrayagg(
json_object(
'id', jobm.id,
'code', i.code,
'name', i.name,
'lotNo', il.lotNo,
'reqQty', jobm.reqQty,
'uom', uc.udfudesc,
'status', jobm.status
)
) as pickLines,
jo.status
from job_order jo
left join bom b on b.id = jo.bomId
left join item_uom iu on b.itemId = iu.itemId and iu.salesUnit = true
left join uom_conversion uc2 on uc2.id = iu.uomId
left join job_order_bom_material jobm on jo.id = jobm.jobOrderId
left join items i on i.id = jobm.itemId
left join uom_conversion uc on uc.id = jobm.uomId
left join stock_out_line sol on sol.id = jobm.stockOutLineId
left join inventory_lot_line ill on ill.id = sol.inventoryLotLineId
left join inventory_lot il on il.id = ill.inventoryLotId
where jo.prodScheduleLineId = :prodScheduleLineId
group by jo.id, uc2.udfudesc
limit 1
"""
)
fun findJobOrderByProdScheduleLineId(prodScheduleLineId: Long): JobOrderDetailWithJsonString?;
}

+ 18
- 0
src/main/java/com/ffii/fpsms/modules/jobOrder/service/JobOrderService.kt 파일 보기

@@ -264,6 +264,24 @@ open class JobOrderService(
)
}

open fun jobOrderDetailByPsId(prodScheduleLineId: Long): JobOrderDetail {
val sqlResult = jobOrderRepository.findJobOrderByProdScheduleLineId(prodScheduleLineId) ?: throw NoSuchElementException("Job Order not found: $prodScheduleLineId");

val type = object : TypeToken<List<JobOrderDetailPickLine>>() {}.type
val jsonResult = sqlResult.pickLines?.let { GsonUtils.stringToJson<List<JobOrderDetailPickLine>>(it, type) }
return JobOrderDetail(
id = sqlResult.id,
code = sqlResult.code,
itemCode = sqlResult.itemCode,
name = sqlResult.name,
reqQty = sqlResult.reqQty,
uom = sqlResult.uom,
pickLines = jsonResult,
status = sqlResult.status,
shortUom = sqlResult.shortUom
)
}

open fun jobOrderDetailByCode(code: String): JobOrderDetail {
val sqlResult = jobOrderRepository.findJobOrderDetailByCode(code) ?: throw NoSuchElementException("Job Order not found: $code");



+ 18
- 0
src/main/java/com/ffii/fpsms/modules/master/entity/ProductionScheduleLine.kt 파일 보기

@@ -45,6 +45,24 @@ open class ProductionScheduleLine : BaseEntity<Long>() {
@Column(name = "weightingRef")
open var weightingRef: Double = 0.0

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

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

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

@Column(name = "batchNeed")
open var batchNeed: Int = 0

@Column(name = "needNoOfJobOrder")
open var needNoOfJobOrder: Int = 0

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

@JsonBackReference
@ManyToOne
@JoinColumn(name = "prodScheduleId", nullable = false)


+ 14
- 1
src/main/java/com/ffii/fpsms/modules/master/entity/ProductionScheduleRepository.kt 파일 보기

@@ -202,8 +202,15 @@ interface ProductionScheduleRepository : AbstractRepository<ProductionSchedule,
prod.scheduleAt,
prod.totalFGType,
prod.totalEstProdCount,
prod.daysLeft,
prod.needNoOfJobOrder,
prod.itemPriority,
prod.batchNeed,
prod.stockQty,
prod.avgQtyLastMonth,
json_arrayagg(
json_object('id', prod.pslId, 'bomMaterials', prod.bomMaterials, 'jobNo', prod.jobNo, 'code', prod.code, 'name', prod.name, 'type', prod.type, 'demandQty', prod.demandQty, 'bomOutputQty', prod.bomOutputQty, 'uomName', prod.uomName, 'prodTimeInMinute', prod.prodTimeInMinute, 'priority', prod.priority, 'approved', prod.approved, 'proportion', prod.proportion)
json_object('id', prod.pslId, 'bomMaterials', prod.bomMaterials, 'jobNo', prod.jobNo, 'code', prod.code, 'name', prod.name, 'type', prod.type, 'demandQty', prod.demandQty, 'bomOutputQty', prod.bomOutputQty, 'uomName', prod.uomName, 'prodTimeInMinute', prod.prodTimeInMinute, 'priority', prod.priority, 'approved', prod.approved, 'proportion', prod.proportion, 'daysLeft', prod.daysLeft, 'needNoOfJobOrder', prod.needNoOfJobOrder, 'batchNeed', prod.batchNeed, 'avgQtyLastMonth', prod.avgQtyLastMonth, 'stockQty', prod.stockQty)
) as prodScheduleLines
from (
select
@@ -213,6 +220,12 @@ interface ProductionScheduleRepository : AbstractRepository<ProductionSchedule,
ps.totalEstProdCount,
psl.approverId is not null as approved,
psl.id as pslId,
psl.daysLeft,
psl.needNoOfJobOrder,
psl.avgQtyLastMonth,
psl.itemPriority,
psl.batchNeed,
psl.stockQty,
pm.bomMaterials,
pm.bomOutputQty,
uc.udfudesc as uomName,


+ 8
- 0
src/main/java/com/ffii/fpsms/modules/master/entity/projections/ProdScheduleInfo.kt 파일 보기

@@ -47,6 +47,14 @@ data class DetailedProdScheduleLineInfo(
val approved: Boolean?,
val proportion: BigDecimal?,
val uomName: String?,
val daysLeft: BigDecimal?,
val onHandQty: BigDecimal?,
val stockQty: BigDecimal?,
val lastMonthAvgSales: BigDecimal?,
val avgQtyLastMonth: BigDecimal?,
val needNoOfJobOrder: BigDecimal?,
val batchNeed: BigDecimal?,
val itemPriority: BigDecimal?,
)

data class DetailedProdScheduleLineBomMaterial (


+ 330
- 126
src/main/java/com/ffii/fpsms/modules/master/service/ProductionScheduleService.kt 파일 보기

@@ -15,6 +15,7 @@ import com.ffii.fpsms.modules.master.entity.*
import com.ffii.fpsms.modules.master.entity.projections.*
import com.ffii.fpsms.modules.master.web.models.MessageResponse
import com.ffii.fpsms.modules.master.web.models.ReleaseProdScheduleLineRequest
import com.ffii.fpsms.modules.master.web.models.ReleaseProdScheduleRequest
import com.ffii.fpsms.modules.master.web.models.SearchProdScheduleRequest
import com.ffii.fpsms.modules.settings.service.SettingsService
import com.ffii.fpsms.modules.stock.entity.Inventory
@@ -31,6 +32,7 @@ import org.springframework.data.domain.PageRequest
import org.springframework.scheduling.support.CronTrigger
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional
import org.springframework.jdbc.core.BeanPropertyRowMapper
import java.lang.reflect.Type
import java.math.BigDecimal
import java.math.RoundingMode
@@ -43,6 +45,7 @@ import kotlin.collections.component1
import kotlin.collections.component2
import kotlin.jvm.optionals.getOrNull
import kotlin.math.ceil
import kotlin.comparisons.maxOf

@Service
open class ProductionScheduleService(
@@ -360,12 +363,109 @@ open class ProductionScheduleService(
)
}

@Transactional(rollbackFor = [java.lang.Exception::class])
open fun releaseProdSchedule(request: ReleaseProdScheduleRequest): MessageResponse {
// 1. Fetch data safely. Assuming searchProdScheduleLine returns a List<Map<String, Any>>
val data: List<Map<String, Any>> = searchProdScheduleLine(request.id)
if (data.isEmpty()) {
logger.warn("No production schedule lines found for ID: ${request.id}")
return MessageResponse(
id = request.id.toLong(),
message = "No lines to release. Parent ID: ${request.id}",
errorPosition = "data",
name = "",
code = "",
type = ""
)
}

val approver = SecurityUtils.getUser().getOrNull() // Get approver once

for (d in data) {
// 2. Safely cast the ID (Int) and convert it to Long for repository lookup
val prodScheduleLineIdInt = d["id"] as? Int
?: throw IllegalStateException("Production Schedule Line ID is missing or not an Int in search result map.")
val prodScheduleLineId = prodScheduleLineIdInt.toLong()
val prodScheduleLine = productionScheduleLineRepository.findById(prodScheduleLineId).getOrNull()
?: throw NoSuchElementException("Production Schedule Line with ID $prodScheduleLineId not found.")

try {
jobOrderService.jobOrderDetailByPsId(prodScheduleLineId)
} catch (e: NoSuchElementException) {
// 3. Fetch BOM, handling nullability safely
val item = prodScheduleLine.item
?: throw IllegalStateException("Item object is missing for Production Schedule Line $prodScheduleLineId.")

val itemId = item.id
?: throw IllegalStateException("Item ID is missing for Production Schedule Line $prodScheduleLineId.")

val bom = bomService.findByItemId(itemId)
?: throw NoSuchElementException("BOM not found for Item ID $itemId.")

// 4. Update Prod Schedule Line fields
prodScheduleLine.apply {
// Use bom.outputQty, ensuring it's treated as Double for prodQty
prodQty = bom.outputQty?.toDouble()
?: throw IllegalStateException("BOM output quantity is null for Item ID $itemId.")
approverId = approver?.id
}
productionScheduleLineRepository.save(prodScheduleLine)

// 5. Logging (optional but kept)
logger.info("prodScheduleLine.prodQty: ${prodScheduleLine.prodQty}")
logger.info("bom?.outputQty: ${bom.outputQty} ${bom.outputQtyUom}")

logger.info("[releaseProdSchedule] prodScheduleLine.needNoOfJobOrder:" + prodScheduleLine.needNoOfJobOrder)
repeat(prodScheduleLine.needNoOfJobOrder) {
// 6. Create Job Order
val joRequest = CreateJobOrderRequest(
bomId = bom.id, // bom is guaranteed non-null here
reqQty = bom.outputQty, // bom is guaranteed non-null here
approverId = approver?.id,
// CRUCIAL FIX: Use the line ID, not the parent schedule ID
prodScheduleLineId = prodScheduleLine.id!!
)

// Assuming createJobOrder returns the created Job Order (jo)
val jo = jobOrderService.createJobOrder(joRequest)
val createdJobOrderId = jo.id
?: throw IllegalStateException("Job Order creation failed: returned object ID is null.")

// 7. Create related job order data
jobOrderBomMaterialService.createJobOrderBomMaterialsByJoId(createdJobOrderId)
jobOrderProcessService.createJobOrderProcessesByJoId(createdJobOrderId)
productProcessService.createProductProcessByJobOrderId(createdJobOrderId)
}
}

// No need to fetch latest detail inside the loop
}

return MessageResponse(
id = request.id.toLong(),
message = "Successfully created Job Orders for all production schedule lines (Parent ID: ${request.id}).",
name = "",
code = "",
type = "",
errorPosition = null
// entity = mapOf("prodScheduleLines" to latestDetail?.prodScheduleLines)
)
}

@Transactional(rollbackFor = [java.lang.Exception::class])
open fun releaseProdScheduleLine(request: ReleaseProdScheduleLineRequest): MessageResponse {
val prodScheduleLine = request.id.let { productionScheduleLineRepository.findById(it).getOrNull() } ?: throw NoSuchElementException()
val bom = prodScheduleLine.item.id?.let { bomService.findByItemId(it) }
val approver = SecurityUtils.getUser().getOrNull()
val proportion = BigDecimal.ONE // request.demandQty.divide(bom?.outputQty ?: BigDecimal.ONE, 5, RoundingMode.HALF_UP)
val isSameQty = request.demandQty.equals(prodScheduleLine.prodQty)

// Update Prod Schedule Type
@@ -385,107 +485,71 @@ open class ProductionScheduleService(
}
productionScheduleLineRepository.save(prodScheduleLine)

// Create Job Order
val joRequest = CreateJobOrderRequest(
bomId = bom?.id,
reqQty = request.demandQty,
approverId = approver?.id,
prodScheduleLineId = request.id,
//jobType = null,
)

val jo = jobOrderService.createJobOrder(joRequest)

// Create Job Order Bom Materials
if (bom?.bomMaterials != null) {
// Job Order Bom Material
val jobmRequests = bom.bomMaterials.map { bm ->
val demandQty = bm.qty?.times(proportion) ?: BigDecimal.ZERO
val saleUnit = bm.item?.id?.let { itemUomService.findSalesUnitByItemId(it) }
println("itemId: ${bm.item?.id} | itemNo: ${bm.item?.code} | itemName: ${bm.item?.name} | saleUnit: ${saleUnit?.uom?.udfudesc}")

val jobm = CreateJobOrderBomMaterialRequest(
joId = jo.id,
itemId = bm.item?.id,
reqQty = bm.qty?.times(proportion) ?: BigDecimal.ZERO,
uomId = saleUnit?.uom?.id
)

jobm
}

if (jobmRequests != null) {
jobOrderBomMaterialService.createJobOrderBomMaterials(jobmRequests)
}

// Inventory
/* val inventories = bom.bomMaterials.map { bm ->
val demandQty = bm.qty?.times(proportion) ?: BigDecimal.ZERO

var inventory = bm.item?.id?.let { inventoryRepository.findByItemId(it).getOrNull() }
if (inventory != null) {
inventory.apply {
this.onHoldQty = (this.onHoldQty ?: BigDecimal.ZERO).plus(demandQty)
}
} else {
if (bm.item != null) {
val itemUom = bm.item?.id?.let { itemUomService.findSalesUnitByItemId(it) }
inventory = Inventory().apply {
item = bm.item
onHandQty = BigDecimal.ZERO
unavailableQty = BigDecimal.ZERO
this.onHoldQty = demandQty
uom = itemUom?.uom
status = "unavailable"
}
}
}

inventory
}.groupBy { it?.item } // Group by item
.mapNotNull { (item, invList) ->
if (invList.isNotEmpty()) {
invList[0]?.apply {
onHoldQty = invList.sumOf { it?.onHoldQty ?: BigDecimal.ZERO }
}
} else {
null
}
}

inventoryRepository.saveAllAndFlush(inventories) */
}

// Create Job Order Process
val jopRequests = bom?.bomProcesses?.map { bp ->
CreateJobOrderProcessRequest(
joId = jo.id,
processId = bp.process?.id,
seqNo = bp.seqNo,
//compare prodQty to bom outputQty
logger.info("request.demandQty:" + request.demandQty);
logger.info("prodScheduleLine.prodQty:" + prodScheduleLine.prodQty);
logger.info("bom?.outputQty:" + bom?.outputQty + "" + bom?.outputQtyUom);

logger.info("prodScheduleLine.needNoOfJobOrder:" + prodScheduleLine.needNoOfJobOrder)
repeat(prodScheduleLine.needNoOfJobOrder) {
// Create Job Order
val joRequest = CreateJobOrderRequest(
bomId = bom?.id,
reqQty = bom?.outputQty,
approverId = approver?.id,
prodScheduleLineId = request.id
)
}

if (jopRequests != null) {
jobOrderProcessService.createJobOrderProcesses(jopRequests)
}
val jo = jobOrderService.createJobOrder(joRequest)

productProcessService.createProductProcessByJobOrderId(jo.id!!)
logger.info("jo created:" + jo.id!!)
jobOrderBomMaterialService.createJobOrderBomMaterialsByJoId(jo.id!!)
jobOrderProcessService.createJobOrderProcessesByJoId(jo.id!!)
productProcessService.createProductProcessByJobOrderId(jo.id!!)
}
// Get Latest Data
// val bomMaterials = prodScheduleLine.id?.let { productionScheduleLineRepository.getBomMaterials(it) }
val latestDetail = prodScheduleLine.productionSchedule.id?.let { detailedProdScheduleDetail(it) }

return MessageResponse(
id = request.id,
name = null,
code = jo.code,
//code = "",
//entity = null,
type = null,
message = "Success",
code = "",
entity = mapOf("prodScheduleLines" to latestDetail?.prodScheduleLines),
errorPosition = null
)
}

//====================細排相關 START====================//
open fun searchProdScheduleLine(prodScheduleId: Int): List<Map<String, Any>> {

val args = mapOf(
"prodScheduleId" to prodScheduleId,
)

val sql = """
SELECT
psl.*
FROM production_schedule ps
LEFT JOIN production_schedule_line psl ON psl.prodScheduleId = ps.id
WHERE ps.deleted = FALSE
AND psl.deleted = FALSE
AND psl.prodScheduleId = :prodScheduleId
""";

return jdbcDao.queryForList(sql, args);
}

open fun getDailyProductionCount(assignDate: Int, selectedDate: LocalDateTime): Int {

@@ -507,65 +571,164 @@ open class ProductionScheduleService(
+ " Limit 1"
);
return jdbcDao.queryForInt(sql.toString(), args);
}

open fun generateDetailedScheduleByDay(assignDate: Int, selectedDate: LocalDateTime, produceAt: LocalDateTime) {
val detailedScheduleOutputList = ArrayList<ProductionScheduleRecord>()
open fun getNeedQty(): List<NeedQtyRecord> {
val sql = """
SELECT
i.outputQty,
i.avgQtyLastMonth,
(i.onHandQty -500),
(i.onHandQty -500) * 1.0 / i.avgQtyLastMonth as daysLeft,
i.avgQtyLastMonth * 2 - stockQty as needQty,
i.stockQty,
CASE
WHEN stockQty * 1.0 / i.avgQtyLastMonth <= 1.9 THEN
CEIL((i.avgQtyLastMonth * 1.9 - stockQty) / i.outputQty)
ELSE 0
END AS needNoOfJobOrder,
CASE
WHEN stockQty * 1.0 / i.avgQtyLastMonth <= 1.9 THEN
CEIL((i.avgQtyLastMonth * 1.9 - stockQty) / i.outputQty)
ELSE 0
END AS batchNeed,
markDark + markFloat + markDense + markAS as priority,
i.*
FROM
(SELECT
(SELECT
ROUND(AVG(d.dailyQty) * 1.5)
FROM
(SELECT
SUM(dol.qty) AS dailyQty
FROM
delivery_order_line dol
LEFT JOIN delivery_order do ON dol.deliveryOrderId = do.id
WHERE
dol.itemId = items.id
-- AND MONTH(do.estimatedArrivalDate) = MONTH(DATE_SUB(NOW(), INTERVAL 1 MONTH))
AND do.estimatedArrivalDate >= '2025-12-01' AND do.estimatedArrivalDate < '2025-12-11'
GROUP BY do.estimatedArrivalDate) AS d) AS avgQtyLastMonth,
inventory.onHandQty - 500 AS stockQty,
bom.outputQty,
bom.outputQtyUom,
(SELECT
udfudesc
FROM
delivery_order_line
LEFT JOIN uom_conversion ON delivery_order_line.uomId = uom_conversion.id
WHERE
delivery_order_line.itemId = bom.itemId
LIMIT 1) AS doUom,
items.code,
items.name,
bom.description,
inventory.onHandQty,
bom.itemId,
bom.id AS bomId,
CASE WHEN bom.isDark = 5 THEN 11
ELSE 0 END as markDark,
CASE WHEN bom.isFloat = 3 THEN 11
ELSE 0 END as markFloat,
CASE WHEN bom.isDense = 5 THEN 11
ELSE 0 END as markDense,
CASE WHEN bom.allergicSubstances = 5 THEN 11
ELSE 0 END as markAS,
inventory.id AS inventoryId
FROM
bom
LEFT JOIN items ON bom.itemId = items.id
LEFT JOIN inventory ON items.id = inventory.itemId
WHERE
bom.itemId != 16771) AS i
WHERE 1
and i.avgQtyLastMonth is not null
and i.onHandQty is not null
and (i.onHandQty -500) * 1.0 / i.avgQtyLastMonth <= 1.9
-- and avgQtyLastMonth - (onHandQty - 500) > 0

""".trimIndent()

val rows: List<Map<String, Any>> = jdbcDao.queryForList(sql)

return rows.map { row ->
NeedQtyRecord().apply {
name = row["name"] as String
id = (row["itemId"] as Number).toLong()
needQty = (row["needQty"] as Number).toDouble()
outputQty = (row["outputQty"] as Number).toDouble()
stockQty = (row["stockQty"] as Number).toDouble()
avgQtyLastMonth = (row["avgQtyLastMonth"] as Number).toDouble()
needNoOfJobOrder = (row["needNoOfJobOrder"] as Number).toLong()
daysLeft = (row["daysLeft"] as Number).toDouble()
batchNeed = row["batchNeed"] as Number

// check the produce date have manual changes.
val manualChange = productionScheduleRepository.findByTypeAndProduceAtAndDeletedIsFalse("detailed", produceAt)
if (manualChange != null) {
return;
}
}
}

//increasement available
var idleProductionCount = 22000.0 - getDailyProductionCount(assignDate, selectedDate);

println("idleProductionCount - " + idleProductionCount);
open fun generateDetailedScheduleByDay(assignDate: Int, selectedDate: LocalDateTime, produceAt: LocalDateTime) {
val detailedScheduleOutputList = ArrayList<NeedQtyRecord>()

//increasement available
//var warehouseCap = 22000.0
var machineCap = 10000.0
var needQtyList = getNeedQty()
println("needQtyList - " + needQtyList);
//##### The 22000, 10000 machine cap just means the max warehouse storage qty, not production qty cap
//##### The total production qty of the date is 10000 due to machine cap
//##### search all items with bom to consider need or no need production
val args = mapOf(
"assignDate" to assignDate,
"selectedDate" to selectedDate.format(formatter)
)

val scheduledList: List<ProductionScheduleRecord> = getProductionScheduleRecord(args)

//用缺口決定生產順序
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) {
val productionPriorityMap: HashMap<NeedQtyRecord, Double> = HashMap();
for (record in needQtyList) {
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)
//val tempRecord = record;
//val difference =
// -tempRecord.outputQty
val isDark = maxOf(record.isDark.toDouble(), 0.0)
val isFloat = maxOf(record.isFloat.toDouble(), 0.0)
val isDense = maxOf(record.isDense.toDouble(), 0.0)
val allergicSubstances = maxOf(record.allergicSubstances.toDouble(), 0.0)

val priority = isDark + isFloat + isDense + allergicSubstances
record.itemPriority = priority
productionPriorityMap.put(record, priority)
}

//##### all qty should be 包, FG qty/bom outputQty
//sort by difference
val sortedEntries = productionPriorityMap.entries.sortedBy { it.value }

var accProdCount = 0.0;
var fgCount = 0L;

for ((updatedScheduleRecord, totalDifference) in sortedEntries) {
for ((updatedScheduleRecord, priority) in sortedEntries) {
//match id with rough schedule record to create new record
val targetRecord = scheduledList.find { it.item.id == updatedScheduleRecord.item.id }
val prodDifference = updatedScheduleRecord.prodQty - (targetRecord?.prodQty ?: 0.0)

if (idleProductionCount - prodDifference >= 0) {
val targetRecord = needQtyList.find { it.id == updatedScheduleRecord.id }
//??? this should be the bom output Qty * no. of job order needed
val prodDifference = updatedScheduleRecord.outputQty - (targetRecord?.outputQty ?: 0.0)
updatedScheduleRecord.itemPriority = priority;
if (machineCap - prodDifference >= 0) {
//have enough quoter for adjustment
idleProductionCount -= prodDifference;
machineCap -= prodDifference;
detailedScheduleOutputList.add(updatedScheduleRecord)
accProdCount += updatedScheduleRecord.prodQty
accProdCount += updatedScheduleRecord.outputQty
fgCount++
} else {
println("[INFO] item " + updatedScheduleRecord.name + " have bee skipped");
}
}

// Sort detailedScheduleOutputList by item priority
val sortedOutputList = detailedScheduleOutputList.sortedBy { it.weightingRef }.toMutableList()
// Sort detailedScheduleOutputList by item priority: needQty
val sortedOutputList = detailedScheduleOutputList.sortedBy { it.needQty }.toMutableList()

// Update itemPriority in the sorted list
var tempPriority = 1L
@@ -576,9 +739,9 @@ open class ProductionScheduleService(

saveDetailedScheduleOutput(sortedOutputList, accProdCount, fgCount, produceAt)
}
open fun saveDetailedScheduleOutput(
sortedEntries: List<ProductionScheduleRecord>,
sortedEntries: List<NeedQtyRecord>,
accProdCount: Double,
fgCount: Long,
produceAt: LocalDateTime,
@@ -593,28 +756,37 @@ open class ProductionScheduleService(
tempObj.id = saveProductionScheduleToDatabase(tempObj);

for (detailedScheduleRecord in sortedEntries) {
saveDetailedScheduleLineToDatabase(tempObj.id ?: -1, detailedScheduleRecord)
if(detailedScheduleRecord.batchNeed.toInt() > 0)
saveDetailedScheduleLineToDatabase(tempObj.id ?: -1, detailedScheduleRecord)
}
}

private fun saveDetailedScheduleLineToDatabase(parentId: Long, detailedScheduleObj: ProductionScheduleRecord) {
private fun saveDetailedScheduleLineToDatabase(parentId: Long, detailedScheduleObj: NeedQtyRecord) {
try {
val prodSchedule = productionScheduleRepository.findById(parentId).get()
val item = detailedScheduleObj.item.id?.let { itemService.findById(it) } ?: Items()
val item = detailedScheduleObj.id?.let { itemService.findById(it) } ?: Items()
var savedItem = ProductionScheduleLine()
print("###detailedScheduleObj.stockQty:" + detailedScheduleObj.stockQty)
savedItem.id = -1;
// savedItem.prodScheduleId = parentId;
savedItem.productionSchedule = prodSchedule;
savedItem.item = item;
savedItem.lastMonthAvgSales = detailedScheduleObj.lastMonthAvgSales ?: 0.0;
savedItem.lastMonthAvgSales = detailedScheduleObj.avgQtyLastMonth ?: 0.0;
savedItem.refScheduleId = detailedScheduleObj.id;
savedItem.approverId = null;
savedItem.estCloseBal = detailedScheduleObj.estCloseBal;
savedItem.prodQty = detailedScheduleObj.prodQty
savedItem.estCloseBal = detailedScheduleObj.outputQty;
savedItem.prodQty = detailedScheduleObj.outputQty
savedItem.type = "detailed"
savedItem.assignDate = detailedScheduleObj.assignDate;
savedItem.weightingRef = detailedScheduleObj.weightingRef
savedItem.itemPriority = detailedScheduleObj.itemPriority
savedItem.assignDate = LocalDateTime.now().dayOfMonth.toLong();
savedItem.weightingRef = detailedScheduleObj.needQty
savedItem.itemPriority = detailedScheduleObj.needQty.toLong()
savedItem.outputQty = detailedScheduleObj.outputQty
savedItem.avgQtyLastMonth = detailedScheduleObj.avgQtyLastMonth
savedItem.stockQty = detailedScheduleObj.stockQty
savedItem.daysLeft = detailedScheduleObj.daysLeft
savedItem.batchNeed = detailedScheduleObj.batchNeed.toInt()
savedItem.needNoOfJobOrder = detailedScheduleObj.needNoOfJobOrder.toInt()

productionScheduleLineRepository.saveAndFlush(savedItem)

} catch (e: Exception) {
@@ -622,6 +794,38 @@ open class ProductionScheduleService(
}
}
//====================細排相關 END====================//
open class NeedQtyRecord {
//SQL record in general with item name
open var name: String = "" //item name
open var avgQtyLastMonth: Double = 0.0
open var needNoOfJobOrder: Number = 0
open var id: Long = 0
open var needQty: Double = 0.0
open var outputQty: Double = 0.0
open var itemPriority: Number = 0
open var isDark: Number = 0
open var isFloat: Number = 0
open var isDense: Number = 0
open var allergicSubstances: Number = 0
open var stockQty: Double = 0.0
open var daysLeft: Double = 0.0
open var batchNeed: Number = 0

override fun toString(): String {
return "NeedQtyRecord(name=${name}," +
" avgQtyLastMonth=$avgQtyLastMonth" +
" stockQty=$stockQty" +
" itemId=$id" +
" needNoOfJobOrder=$needNoOfJobOrder" +
" outputQty=$outputQty" +
" needQty=$needQty" +
" itemPriority=$itemPriority"+
" isDark=$isDark" +
" isFloat=$isFloat" +
" isDense=$isDense" +
" allergicSubstances=$allergicSubstances"
}
}

open class ProductionScheduleRecord : ProductionScheduleLine() {
//SQL record in general with item name


+ 16
- 4
src/main/java/com/ffii/fpsms/modules/master/web/ProductionScheduleController.kt 파일 보기

@@ -13,6 +13,7 @@ import com.ffii.fpsms.modules.master.service.ProductionScheduleService.RoughSche
import com.ffii.fpsms.modules.master.web.models.MessageResponse
import com.ffii.fpsms.modules.master.web.models.ReleaseProdScheduleLineRequest
import com.ffii.fpsms.modules.master.web.models.SearchProdScheduleRequest
import com.ffii.fpsms.modules.master.web.models.ReleaseProdScheduleRequest
import jakarta.servlet.http.HttpServletRequest
import jakarta.validation.Valid
import org.springframework.web.bind.annotation.*
@@ -65,6 +66,7 @@ class ProductionScheduleController(

@GetMapping("/detail/detailed/{id}")
fun getDetailedProdScheduleDetail(@PathVariable id: Long): DetailedProdScheduleWithLine {
print("getDetailedProdScheduleDetail################")
return productionScheduleService.detailedProdScheduleDetail(id)
}

@@ -78,8 +80,14 @@ class ProductionScheduleController(
return productionScheduleService.releaseProdScheduleLine(request)
}

@PostMapping("/detail/detailed/release")
fun releaseProdSchedule(@Valid @RequestBody request: ReleaseProdScheduleRequest): MessageResponse {
return productionScheduleService.releaseProdSchedule(request)
}

@GetMapping("/getRecordByPage")
fun allProdSchedulesByPage(@ModelAttribute request: SearchProdScheduleRequest): RecordsRes<ProdScheduleInfo> {
print("allProdSchedulesByPage############")
return productionScheduleService.allProdSchedulesByPage(request);
}

@@ -97,11 +105,12 @@ class ProductionScheduleController(
fun generateDetailSchedule(request: HttpServletRequest?): Int {
try {
// For test
val genDate = request?.getParameter("genDate")?.let { LocalDate.parse(it).atStartOfDay() }
//val genDate = request?.getParameter("genDate")?.let { LocalDate.parse(it).atStartOfDay() }

val genDate = LocalDateTime.now()
val today = LocalDateTime.now()

val latestRoughScheduleAt = productionScheduleService.getLatestScheduleAt("rough");
//val latestRoughScheduleAt = productionScheduleService.getLatestScheduleAt("rough");
// val latestRoughScheduleAt = productionScheduleService.getSecondLatestRoughScheduleAt("rough");
// val latestRoughScheduleAt = productionScheduleService.getScheduledAtByDate(genDate?.toLocalDate() ?: today.toLocalDate());
// assume schedule period is monday to sunday
@@ -115,8 +124,11 @@ class ProductionScheduleController(
// } else assignDate.toInt()
//TODO: update this to receive selectedDate and assignDate from schedule
// productionScheduleService.generateDetailedScheduleByDay(1, LocalDateTime.of(2025,6,25,0,0,0))
println("genDate: $genDate | today: $today | latestRoughScheduleAt: $latestRoughScheduleAt | assignDate: $assignDate ")
productionScheduleService.generateDetailedScheduleByDay(assignDate, latestRoughScheduleAt ?: LocalDateTime.now(), genDate ?: today)
//println("genDate: $genDate | today: $today | latestRoughScheduleAt: $latestRoughScheduleAt | assignDate: $assignDate ")
//productionScheduleService.generateDetailedScheduleByDay(assignDate, latestRoughScheduleAt ?: LocalDateTime.now(), genDate ?: today)
println("Start generate detailed Schedule")
productionScheduleService.generateDetailedScheduleByDay(assignDate, today, today)
return 200
} catch (e: Exception) {
throw RuntimeException("Error generate schedule: ${e.message}", e)


+ 9
- 0
src/main/java/com/ffii/fpsms/modules/master/web/models/ReleaseProdScheduleRequest.kt 파일 보기

@@ -0,0 +1,9 @@
package com.ffii.fpsms.modules.master.web.models

import jakarta.validation.constraints.NotNull
import java.math.BigDecimal

data class ReleaseProdScheduleRequest(
@field:NotNull(message = "Id cannot be null")
val id: Int
)

+ 9
- 0
src/main/resources/db/changelog/changes/20251211_01_Fai/01_add_fields_to_psl.sql 파일 보기

@@ -0,0 +1,9 @@
-- liquibase formatted sql
-- changeset Fai:add_fields_to_psl

ALTER TABLE `fpsmsdb`.`production_schedule_line`
ADD COLUMN `outputQty` DECIMAL(16,2) NULL AFTER `weightingRef`,
ADD COLUMN `stockQty` DECIMAL(16,2) NULL AFTER `outputQty`,
ADD COLUMN `daysLeft` DECIMAL(16,2) NULL AFTER `stockQty`,
ADD COLUMN `batchNeed` INT NULL AFTER `daysLeft`,
ADD COLUMN `needNoOfJobOrder` INT NULL AFTER `batchNeed`;

+ 5
- 0
src/main/resources/db/changelog/changes/20251211_01_Fai/02_add_fields_to_psl.sql 파일 보기

@@ -0,0 +1,5 @@
-- liquibase formatted sql
-- changeset Fai:add_avgQtyLastMonth_to_psl

ALTER TABLE `fpsmsdb`.`production_schedule_line`
ADD COLUMN `avgQtyLastMonth` DECIMAL(16,2) NULL;

불러오는 중...
취소
저장