@@ -46,6 +46,10 @@ 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
import kotlin.comparisons.maxOf
import org.apache.poi.xssf.usermodel.XSSFWorkbook
import org.apache.poi.ss.usermodel.FillPatternType
import org.apache.poi.ss.usermodel.IndexedColors
import java.io.ByteArrayOutputStream
@Service
@Service
open class ProductionScheduleService(
open class ProductionScheduleService(
@@ -336,7 +340,7 @@ open class ProductionScheduleService(
open fun saveProdScheduleLine(request: ReleaseProdScheduleLineRequest): MessageResponse {
open fun saveProdScheduleLine(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 prodSchedule = prodScheduleLine.productionSchedule
val prodSchedule = prodScheduleLine.productionSchedule
// Update Prod Schedule Type
// Update Prod Schedule Type
prodSchedule.apply {
prodSchedule.apply {
type = "manual"
type = "manual"
@@ -351,7 +355,7 @@ open class ProductionScheduleService(
productionScheduleLineRepository.saveAndFlush(prodScheduleLine)
productionScheduleLineRepository.saveAndFlush(prodScheduleLine)
val bomMaterials = prodScheduleLine.id?.let { productionScheduleLineRepository.getBomMaterials(it) }
val bomMaterials = prodScheduleLine.id?.let { productionScheduleLineRepository.getBomMaterials(it) }
return MessageResponse(
return MessageResponse(
id = request.id,
id = request.id,
name = null,
name = null,
@@ -422,11 +426,11 @@ open class ProductionScheduleService(
logger.info("bom?.outputQty: ${bom.outputQty} ${bom.outputQtyUom}")
logger.info("bom?.outputQty: ${bom.outputQty} ${bom.outputQtyUom}")
logger.info("[releaseProdSchedule] prodScheduleLine.needNoOfJobOrder:" + prodScheduleLine.needNoOfJobOrder)
logger.info("[releaseProdSchedule] prodScheduleLine.needNoOfJobOrder:" + prodScheduleLine.needNoOfJobOrder)
repeat(prodScheduleLine.needNoOfJobOrder) {
// repeat(prodScheduleLine.needNoOfJobOrder) {
// 6. Create Job Order
// 6. Create Job Order
val joRequest = CreateJobOrderRequest(
val joRequest = CreateJobOrderRequest(
bomId = bom.id, // bom is guaranteed non-null here
bomId = bom.id, // bom is guaranteed non-null here
reqQty = bom.outputQty, // bom is guaranteed non-null here
reqQty = bom.outputQty?.multiply(BigDecimal.valueOf(prodScheduleLine.batchNeed.toLong())),
approverId = approver?.id,
approverId = approver?.id,
// CRUCIAL FIX: Use the line ID, not the parent schedule ID
// CRUCIAL FIX: Use the line ID, not the parent schedule ID
@@ -442,8 +446,8 @@ open class ProductionScheduleService(
// 7. Create related job order data
// 7. Create related job order data
jobOrderBomMaterialService.createJobOrderBomMaterialsByJoId(createdJobOrderId)
jobOrderBomMaterialService.createJobOrderBomMaterialsByJoId(createdJobOrderId)
jobOrderProcessService.createJobOrderProcessesByJoId(createdJobOrderId)
jobOrderProcessService.createJobOrderProcessesByJoId(createdJobOrderId)
productProcessService.createProductProcessByJobOrderId(createdJobOrderId)
}
productProcessService.createProductProcessByJobOrderId(createdJobOrderId, prodScheduleLine.itemPriority.toInt() )
// }
}
}
@@ -491,22 +495,45 @@ open class ProductionScheduleService(
logger.info("bom?.outputQty:" + bom?.outputQty + "" + bom?.outputQtyUom);
logger.info("bom?.outputQty:" + bom?.outputQty + "" + bom?.outputQtyUom);
logger.info("prodScheduleLine.needNoOfJobOrder:" + prodScheduleLine.needNoOfJobOrder)
logger.info("prodScheduleLine.needNoOfJobOrder:" + prodScheduleLine.needNoOfJobOrder)
repeat(prodScheduleLine.needNoOfJobOrder) {
// Create Job Order
val prodScheduleLineId = prodScheduleLine.id!!
try {
jobOrderService.jobOrderDetailByPsId(prodScheduleLineId)
} catch (e: NoSuchElementException) {
// 3. Fetch BOM, handling nullability safely
val item = prodScheduleLine.item
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.")
val approver = SecurityUtils.getUser().getOrNull() // Get approver once
val joRequest = CreateJobOrderRequest(
val joRequest = CreateJobOrderRequest(
bomId = bom?.id,
reqQty = bom?.outputQty,
bomId = bom.id, // bom is guaranteed non-null here
reqQty = bom.outputQty?.multiply(BigDecimal.valueOf(prodScheduleLine.batchNeed.toLong())) ,
approverId = approver?.id,
approverId = approver?.id,
prodScheduleLineId = request.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 jo = jobOrderService.createJobOrder(joRequest)
logger.info("jo created:" + jo.id!!)
jobOrderBomMaterialService.createJobOrderBomMaterialsByJoId(jo.id!!)
jobOrderProcessService.createJobOrderProcessesByJoId(jo.id!!)
productProcessService.createProductProcessByJobOrderId(jo.id!!)
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, prodScheduleLine.itemPriority.toInt())
}
}
// Get Latest Data
// Get Latest Data
@@ -593,7 +620,7 @@ open class ProductionScheduleService(
CEIL((i.avgQtyLastMonth * 1.9 - stockQty) / i.outputQty)
CEIL((i.avgQtyLastMonth * 1.9 - stockQty) / i.outputQty)
ELSE 0
ELSE 0
END AS batchNeed,
END AS batchNeed,
markDark + markFloat + markDense + markAS as priority,
25 + 25 + markDark + markFloat + markDense + markAS + markTimeSequence + markComplexity as priority,
i.*
i.*
FROM
FROM
(SELECT
(SELECT
@@ -627,14 +654,29 @@ open class ProductionScheduleService(
inventory.onHandQty,
inventory.onHandQty,
bom.itemId,
bom.itemId,
bom.id AS bomId,
bom.id AS bomId,
CASE WHEN bom.isDark = 5 THEN 11
CASE WHEN bom.isDark = 5 THEN 11
WHEN bom.isDark = 3 THEN 6
WHEN bom.isDark = 1 THEN 2
ELSE 0 END as markDark,
ELSE 0 END as markDark,
CASE WHEN bom.isFloat = 3 THEN 11
CASE WHEN bom.isFloat = 5 THEN 11
WHEN bom.isFloat = 3 THEN 6
WHEN bom.isFloat = 1 THEN 2
ELSE 0 END as markFloat,
ELSE 0 END as markFloat,
CASE WHEN bom.isDense = 5 THEN 11
CASE WHEN bom.isDense = 5 THEN 11
WHEN bom.isDense = 3 THEN 6
WHEN bom.isDense = 1 THEN 2
ELSE 0 END as markDense,
ELSE 0 END as markDense,
bom.timeSequence as markTimeSequence,
bom.complexity as markComplexity,
CASE WHEN bom.allergicSubstances = 5 THEN 11
CASE WHEN bom.allergicSubstances = 5 THEN 11
ELSE 0 END as markAS,
ELSE 0 END as markAS,
inventory.id AS inventoryId
inventory.id AS inventoryId
FROM
FROM
bom
bom
@@ -663,6 +705,7 @@ open class ProductionScheduleService(
needNoOfJobOrder = (row["needNoOfJobOrder"] as Number).toLong()
needNoOfJobOrder = (row["needNoOfJobOrder"] as Number).toLong()
daysLeft = (row["daysLeft"] as Number).toDouble()
daysLeft = (row["daysLeft"] as Number).toDouble()
batchNeed = row["batchNeed"] as Number
batchNeed = row["batchNeed"] as Number
priority = row["priority"] as Number
}
}
}
}
@@ -696,7 +739,7 @@ open class ProductionScheduleService(
val allergicSubstances = maxOf(record.allergicSubstances.toDouble(), 0.0)
val allergicSubstances = maxOf(record.allergicSubstances.toDouble(), 0.0)
val priority = isDark + isFloat + isDense + allergicSubstances
val priority = isDark + isFloat + isDense + allergicSubstances
record.itemPriority = priority
record.itemPriority = record. priority
productionPriorityMap.put(record, priority)
productionPriorityMap.put(record, priority)
}
}
@@ -761,6 +804,10 @@ open class ProductionScheduleService(
record.needNoOfJobOrder = 0
record.needNoOfJobOrder = 0
record.batchNeed = 0
record.batchNeed = 0
}
}
}else{
record.needQty = 0.0
record.needNoOfJobOrder = 0
record.batchNeed = 0
}
}
logger.info(record.name + " record.batchNeed: " + record.batchNeed + " record.stockQty:" + record.stockQty + " record.daysLeft:" + record.daysLeft)
logger.info(record.name + " record.batchNeed: " + record.batchNeed + " record.stockQty:" + record.stockQty + " record.daysLeft:" + record.daysLeft)
@@ -831,7 +878,7 @@ open class ProductionScheduleService(
savedItem.type = "detailed"
savedItem.type = "detailed"
savedItem.assignDate = LocalDateTime.now().dayOfMonth.toLong();
savedItem.assignDate = LocalDateTime.now().dayOfMonth.toLong();
savedItem.weightingRef = detailedScheduleObj.needQty
savedItem.weightingRef = detailedScheduleObj.needQty
savedItem.itemPriority = detailedScheduleObj.needQ ty.toLong()
savedItem.itemPriority = detailedScheduleObj.priori ty.toLong()
savedItem.outputQty = detailedScheduleObj.outputQty
savedItem.outputQty = detailedScheduleObj.outputQty
savedItem.avgQtyLastMonth = detailedScheduleObj.avgQtyLastMonth
savedItem.avgQtyLastMonth = detailedScheduleObj.avgQtyLastMonth
savedItem.stockQty = detailedScheduleObj.stockQty
savedItem.stockQty = detailedScheduleObj.stockQty
@@ -862,11 +909,15 @@ open class ProductionScheduleService(
open var stockQty: Double = 0.0
open var stockQty: Double = 0.0
open var daysLeft: Double = 0.0
open var daysLeft: Double = 0.0
open var batchNeed: Number = 0
open var batchNeed: Number = 0
open var priority: Number = 0
override fun toString(): String {
override fun toString(): String {
return "NeedQtyRecord(name=${name}," +
return "NeedQtyRecord(name=${name}," +
" avgQtyLastMonth=$avgQtyLastMonth" +
" avgQtyLastMonth=$avgQtyLastMonth" +
" stockQty=$stockQty" +
" stockQty=$stockQty" +
" daysLeft=$daysLeft" +
" batchNeed=$batchNeed" +
" priority=$priority" +
" itemId=$id" +
" itemId=$id" +
" needNoOfJobOrder=$needNoOfJobOrder" +
" needNoOfJobOrder=$needNoOfJobOrder" +
" outputQty=$outputQty" +
" outputQty=$outputQty" +
@@ -1286,4 +1337,182 @@ open class ProductionScheduleService(
}
}
}
}
fun exportProdScheduleToExcel(lines: List<Map<String, Any>>, lineMats: List<Map<String, Any>>): ByteArray {
val workbook = XSSFWorkbook()
// 1. Group Production Lines by Date
val groupedData = lines.groupBy {
val produceAt = it["produceAt"]
when (produceAt) {
is LocalDateTime -> produceAt.toLocalDate().toString()
is java.sql.Timestamp -> produceAt.toLocalDateTime().toLocalDate().toString()
else -> produceAt?.toString()?.substring(0, 10) ?: "Unknown_Date"
}
}
// 2. Define Header Style
val headerStyle = workbook.createCellStyle().apply {
fillForegroundColor = IndexedColors.GREY_25_PERCENT.index
fillPattern = FillPatternType.SOLID_FOREGROUND
val font = workbook.createFont()
font.bold = true
setFont(font)
}
// 3. Create Production Worksheets
groupedData.forEach { (dateKey, dailyLines) ->
val sheetName = dateKey.replace("[/\\\\?*:\\[\\]]".toRegex(), "-")
val sheet = workbook.createSheet(sheetName)
val headers = listOf("Item Name", "Avg Qty Last Month", "Stock Qty", "Days Left", "Output Qty", "Batch Need", "Priority")
val headerRow = sheet.createRow(0)
headers.forEachIndexed { i, title ->
val cell = headerRow.createCell(i)
cell.setCellValue(title)
cell.setCellStyle(headerStyle)
}
dailyLines.forEachIndexed { index, line ->
val row = sheet.createRow(index + 1)
row.createCell(0).setCellValue(line["itemName"]?.toString() ?: "")
row.createCell(1).setCellValue(asDouble(line["avgQtyLastMonth"]))
row.createCell(2).setCellValue(asDouble(line["stockQty"]))
row.createCell(3).setCellValue(asDouble(line["daysLeft"]))
row.createCell(4).setCellValue(asDouble(line["outputdQty"])) // Note: Matching your snippet's "outputdQty" key
row.createCell(5).setCellValue(asDouble(line["batchNeed"]))
row.createCell(6).setCellValue(asDouble(line["itemPriority"]))
}
for (i in headers.indices) { sheet.autoSizeColumn(i) }
}
// 4. Create Material Summary Worksheet
val matSheet = workbook.createSheet("Material Summary")
val matHeaders = listOf(
"Mat Code", "Mat Name", "Required Qty", "Total Qty Need",
"UoM", "Purchased Qty", "On Hand Qty", "Unavailable Qty",
"Related Item Code", "Related Item Name"
)
val matHeaderRow = matSheet.createRow(0)
matHeaders.forEachIndexed { i, title ->
matHeaderRow.createCell(i).apply {
setCellValue(title)
setCellStyle(headerStyle)
}
}
lineMats.forEachIndexed { index, rowData ->
val row = matSheet.createRow(index + 1)
val totalNeed = asDouble(rowData["totalMatQtyNeed"])
val purchased = asDouble(rowData["purchasedQty"])
val onHand = asDouble(rowData["onHandQty"])
// Calculation: Required Qty = totalMatQtyNeed - purchasedQty - onHandQty (minimum 0)
val requiredQty = (totalNeed - purchased - onHand).coerceAtLeast(0.0)
row.createCell(0).setCellValue(rowData["matCode"]?.toString() ?: "")
row.createCell(1).setCellValue(rowData["matName"]?.toString() ?: "")
row.createCell(2).setCellValue(requiredQty)
row.createCell(3).setCellValue(totalNeed)
row.createCell(4).setCellValue(rowData["uomName"]?.toString() ?: "")
row.createCell(5).setCellValue(purchased)
row.createCell(6).setCellValue(onHand)
row.createCell(7).setCellValue(asDouble(rowData["unavailableQty"]))
row.createCell(8).setCellValue(rowData["itemCode"]?.toString() ?: "")
row.createCell(9).setCellValue(rowData["itemName"]?.toString() ?: "")
}
for (i in matHeaders.indices) { matSheet.autoSizeColumn(i) }
// 5. Finalize and Return
val out = ByteArrayOutputStream()
workbook.use { it.write(out) }
return out.toByteArray()
}
private fun asDouble(value: Any?): Double {
return when (value) {
is Number -> value.toDouble()
is String -> value.toDoubleOrNull() ?: 0.0
else -> 0.0
}
}
//====================細排相關 START====================//
open fun searchExportProdSchedule(fromDate: LocalDate): List<Map<String, Any>> {
val args = mapOf(
"fromDate" to fromDate,
)
val sql = """
select
it.code as itemCode,
it.name as itemName,
ps.produceAt,
psl.*
from production_schedule_line psl
left join production_schedule ps on psl.prodScheduleId = ps.id
left join items it on psl.itemId = it.id
where ps.produceAt >= :fromDate
and ps.id = (select id from production_schedule where produceAt = ps.produceAt order by id desc limit 1)
order by ps.produceAt asc, psl.itemPriority desc, itemCode asc
""";
return jdbcDao.queryForList(sql, args);
}
open fun searchExportProdScheduleMaterial(fromDate: LocalDate): List<Map<String, Any>> {
val args = mapOf(
"fromDate" to fromDate,
)
val sql = """
select
group_concat(distinct it.code) as itemCode,
group_concat(distinct it.name) as itemName,
itm.code as matCode,
itm.name as matName,
sum(bm.qty * psl.batchNeed) as totalMatQtyNeed,
iv.onHandQty,
iv.unavailableQty,
(select sum(qty) from purchase_order_line
left join purchase_order on purchase_order_line.purchaseOrderId = purchase_order.id
where purchase_order_line.itemId = itm.id and date(purchase_order.estimatedArrivalDate) >= date(now()) and purchase_order.completeDate is null) as purchasedQty,
bm.uomName,
min(ps.produceAt) as toProdcueFrom,
max(ps.produceAt) as toProdcueAt,
psl.*
from production_schedule_line psl
left join production_schedule ps on psl.prodScheduleId = ps.id
left join items it on psl.itemId = it.id
left join bom on it.id = bom.itemId
left join bom_material bm on bom.id = bm.bomId
left join items itm on bm.itemId = itm.id
left join inventory iv on itm.id = iv.itemId
where ps.produceAt >= :fromDate
and ps.id = (select id from production_schedule where produceAt = ps.produceAt order by id desc limit 1)
-- and it.code = 'PP2236'
-- order by ps.produceAt asc, psl.itemPriority desc, itemCode asc
group by matCode
""";
return jdbcDao.queryForList(sql, args);
}
}
}