ソースを参照

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?, planStartTo: LocalDateTime?,
pageable: Pageable pageable: Pageable
): Page<JobOrderInfo> ): 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 { open fun jobOrderDetailByCode(code: String): JobOrderDetail {
val sqlResult = jobOrderRepository.findJobOrderDetailByCode(code) ?: throw NoSuchElementException("Job Order not found: $code"); 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") @Column(name = "weightingRef")
open var weightingRef: Double = 0.0 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 @JsonBackReference
@ManyToOne @ManyToOne
@JoinColumn(name = "prodScheduleId", nullable = false) @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.scheduleAt,
prod.totalFGType, prod.totalFGType,
prod.totalEstProdCount, prod.totalEstProdCount,
prod.daysLeft,
prod.needNoOfJobOrder,
prod.itemPriority,
prod.batchNeed,
prod.stockQty,
prod.avgQtyLastMonth,
json_arrayagg( 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 ) as prodScheduleLines
from ( from (
select select
@@ -213,6 +220,12 @@ interface ProductionScheduleRepository : AbstractRepository<ProductionSchedule,
ps.totalEstProdCount, ps.totalEstProdCount,
psl.approverId is not null as approved, psl.approverId is not null as approved,
psl.id as pslId, psl.id as pslId,
psl.daysLeft,
psl.needNoOfJobOrder,
psl.avgQtyLastMonth,
psl.itemPriority,
psl.batchNeed,
psl.stockQty,
pm.bomMaterials, pm.bomMaterials,
pm.bomOutputQty, pm.bomOutputQty,
uc.udfudesc as uomName, 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 approved: Boolean?,
val proportion: BigDecimal?, val proportion: BigDecimal?,
val uomName: String?, 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 ( 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.entity.projections.*
import com.ffii.fpsms.modules.master.web.models.MessageResponse 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.ReleaseProdScheduleLineRequest
import com.ffii.fpsms.modules.master.web.models.ReleaseProdScheduleRequest
import com.ffii.fpsms.modules.master.web.models.SearchProdScheduleRequest import com.ffii.fpsms.modules.master.web.models.SearchProdScheduleRequest
import com.ffii.fpsms.modules.settings.service.SettingsService import com.ffii.fpsms.modules.settings.service.SettingsService
import com.ffii.fpsms.modules.stock.entity.Inventory 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.scheduling.support.CronTrigger
import org.springframework.stereotype.Service import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional import org.springframework.transaction.annotation.Transactional
import org.springframework.jdbc.core.BeanPropertyRowMapper
import java.lang.reflect.Type import java.lang.reflect.Type
import java.math.BigDecimal import java.math.BigDecimal
import java.math.RoundingMode import java.math.RoundingMode
@@ -43,6 +45,7 @@ import kotlin.collections.component1
import kotlin.collections.component2 import kotlin.collections.component2
import kotlin.jvm.optionals.getOrNull import kotlin.jvm.optionals.getOrNull
import kotlin.math.ceil import kotlin.math.ceil
import kotlin.comparisons.maxOf


@Service @Service
open class ProductionScheduleService( 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]) @Transactional(rollbackFor = [java.lang.Exception::class])
open fun releaseProdScheduleLine(request: ReleaseProdScheduleLineRequest): MessageResponse { open fun releaseProdScheduleLine(request: ReleaseProdScheduleLineRequest): MessageResponse {
val prodScheduleLine = request.id.let { productionScheduleLineRepository.findById(it).getOrNull() } ?: throw NoSuchElementException() val prodScheduleLine = request.id.let { productionScheduleLineRepository.findById(it).getOrNull() } ?: throw NoSuchElementException()
val bom = prodScheduleLine.item.id?.let { bomService.findByItemId(it) } val bom = prodScheduleLine.item.id?.let { bomService.findByItemId(it) }
val approver = SecurityUtils.getUser().getOrNull() 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) val isSameQty = request.demandQty.equals(prodScheduleLine.prodQty)


// Update Prod Schedule Type // Update Prod Schedule Type
@@ -385,107 +485,71 @@ open class ProductionScheduleService(
} }
productionScheduleLineRepository.save(prodScheduleLine) 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 // Get Latest Data
// val bomMaterials = prodScheduleLine.id?.let { productionScheduleLineRepository.getBomMaterials(it) } // val bomMaterials = prodScheduleLine.id?.let { productionScheduleLineRepository.getBomMaterials(it) }
val latestDetail = prodScheduleLine.productionSchedule.id?.let { detailedProdScheduleDetail(it) } val latestDetail = prodScheduleLine.productionSchedule.id?.let { detailedProdScheduleDetail(it) }


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


//====================細排相關 START====================// //====================細排相關 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 { open fun getDailyProductionCount(assignDate: Int, selectedDate: LocalDateTime): Int {


@@ -507,65 +571,164 @@ open class ProductionScheduleService(
+ " Limit 1" + " Limit 1"
); );
return jdbcDao.queryForInt(sql.toString(), args); 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( val args = mapOf(
"assignDate" to assignDate, "assignDate" to assignDate,
"selectedDate" to selectedDate.format(formatter) "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()); 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 //sort by difference
val sortedEntries = productionPriorityMap.entries.sortedBy { it.value } val sortedEntries = productionPriorityMap.entries.sortedBy { it.value }


var accProdCount = 0.0; var accProdCount = 0.0;
var fgCount = 0L; var fgCount = 0L;


for ((updatedScheduleRecord, totalDifference) in sortedEntries) {
for ((updatedScheduleRecord, priority) in sortedEntries) {
//match id with rough schedule record to create new record //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 //have enough quoter for adjustment
idleProductionCount -= prodDifference;
machineCap -= prodDifference;
detailedScheduleOutputList.add(updatedScheduleRecord) detailedScheduleOutputList.add(updatedScheduleRecord)
accProdCount += updatedScheduleRecord.prodQty
accProdCount += updatedScheduleRecord.outputQty
fgCount++ fgCount++
} else { } else {
println("[INFO] item " + updatedScheduleRecord.name + " have bee skipped"); 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 // Update itemPriority in the sorted list
var tempPriority = 1L var tempPriority = 1L
@@ -576,9 +739,9 @@ open class ProductionScheduleService(


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


for (detailedScheduleRecord in sortedEntries) { 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 { try {
val prodSchedule = productionScheduleRepository.findById(parentId).get() 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() var savedItem = ProductionScheduleLine()
print("###detailedScheduleObj.stockQty:" + detailedScheduleObj.stockQty)
savedItem.id = -1; savedItem.id = -1;
// savedItem.prodScheduleId = parentId; // savedItem.prodScheduleId = parentId;
savedItem.productionSchedule = prodSchedule; savedItem.productionSchedule = prodSchedule;
savedItem.item = item; savedItem.item = item;
savedItem.lastMonthAvgSales = detailedScheduleObj.lastMonthAvgSales ?: 0.0;
savedItem.lastMonthAvgSales = detailedScheduleObj.avgQtyLastMonth ?: 0.0;
savedItem.refScheduleId = detailedScheduleObj.id; savedItem.refScheduleId = detailedScheduleObj.id;
savedItem.approverId = null; savedItem.approverId = null;
savedItem.estCloseBal = detailedScheduleObj.estCloseBal;
savedItem.prodQty = detailedScheduleObj.prodQty
savedItem.estCloseBal = detailedScheduleObj.outputQty;
savedItem.prodQty = detailedScheduleObj.outputQty
savedItem.type = "detailed" 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) productionScheduleLineRepository.saveAndFlush(savedItem)


} catch (e: Exception) { } catch (e: Exception) {
@@ -622,6 +794,38 @@ open class ProductionScheduleService(
} }
} }
//====================細排相關 END====================// //====================細排相關 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() { open class ProductionScheduleRecord : ProductionScheduleLine() {
//SQL record in general with item name //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.MessageResponse
import com.ffii.fpsms.modules.master.web.models.ReleaseProdScheduleLineRequest 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.SearchProdScheduleRequest
import com.ffii.fpsms.modules.master.web.models.ReleaseProdScheduleRequest
import jakarta.servlet.http.HttpServletRequest import jakarta.servlet.http.HttpServletRequest
import jakarta.validation.Valid import jakarta.validation.Valid
import org.springframework.web.bind.annotation.* import org.springframework.web.bind.annotation.*
@@ -65,6 +66,7 @@ class ProductionScheduleController(


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


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


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

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


@@ -97,11 +105,12 @@ class ProductionScheduleController(
fun generateDetailSchedule(request: HttpServletRequest?): Int { fun generateDetailSchedule(request: HttpServletRequest?): Int {
try { try {
// For test // 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 today = LocalDateTime.now()


val latestRoughScheduleAt = productionScheduleService.getLatestScheduleAt("rough");
//val latestRoughScheduleAt = productionScheduleService.getLatestScheduleAt("rough");
// val latestRoughScheduleAt = productionScheduleService.getSecondLatestRoughScheduleAt("rough"); // val latestRoughScheduleAt = productionScheduleService.getSecondLatestRoughScheduleAt("rough");
// val latestRoughScheduleAt = productionScheduleService.getScheduledAtByDate(genDate?.toLocalDate() ?: today.toLocalDate()); // val latestRoughScheduleAt = productionScheduleService.getScheduledAtByDate(genDate?.toLocalDate() ?: today.toLocalDate());
// assume schedule period is monday to sunday // assume schedule period is monday to sunday
@@ -115,8 +124,11 @@ class ProductionScheduleController(
// } else assignDate.toInt() // } else assignDate.toInt()
//TODO: update this to receive selectedDate and assignDate from schedule //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("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 return 200
} catch (e: Exception) { } catch (e: Exception) {
throw RuntimeException("Error generate schedule: ${e.message}", e) 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;

読み込み中…
キャンセル
保存