From 819a984b41d67202712b48502fca3837dc811351 Mon Sep 17 00:00:00 2001 From: "vluk@2fi-solutions.com.hk" Date: Thu, 18 Dec 2025 11:15:02 +0800 Subject: [PATCH 1/3] auto gen jo in schedule --- deployLocal.bat | 34 ++ .../jobOrder/entity/JobOrderRepository.kt | 39 ++ .../jobOrder/service/JobOrderService.kt | 18 + .../master/entity/ProductionScheduleLine.kt | 18 + .../entity/ProductionScheduleRepository.kt | 15 +- .../entity/projections/ProdScheduleInfo.kt | 8 + .../service/ProductionScheduleService.kt | 456 +++++++++++++----- .../web/ProductionScheduleController.kt | 20 +- .../web/models/ReleaseProdScheduleRequest.kt | 9 + .../20251211_01_Fai/01_add_fields_to_psl.sql | 9 + .../20251211_01_Fai/02_add_fields_to_psl.sql | 5 + 11 files changed, 500 insertions(+), 131 deletions(-) create mode 100644 deployLocal.bat create mode 100644 src/main/java/com/ffii/fpsms/modules/master/web/models/ReleaseProdScheduleRequest.kt create mode 100644 src/main/resources/db/changelog/changes/20251211_01_Fai/01_add_fields_to_psl.sql create mode 100644 src/main/resources/db/changelog/changes/20251211_01_Fai/02_add_fields_to_psl.sql diff --git a/deployLocal.bat b/deployLocal.bat new file mode 100644 index 0000000..8b91772 --- /dev/null +++ b/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 \ No newline at end of file diff --git a/src/main/java/com/ffii/fpsms/modules/jobOrder/entity/JobOrderRepository.kt b/src/main/java/com/ffii/fpsms/modules/jobOrder/entity/JobOrderRepository.kt index c2d7afe..e8059b9 100644 --- a/src/main/java/com/ffii/fpsms/modules/jobOrder/entity/JobOrderRepository.kt +++ b/src/main/java/com/ffii/fpsms/modules/jobOrder/entity/JobOrderRepository.kt @@ -133,4 +133,43 @@ interface JobOrderRepository : AbstractRepository { planStartTo: LocalDateTime?, pageable: Pageable ): Page + + @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?; } \ No newline at end of file diff --git a/src/main/java/com/ffii/fpsms/modules/jobOrder/service/JobOrderService.kt b/src/main/java/com/ffii/fpsms/modules/jobOrder/service/JobOrderService.kt index 49826e0..5d8c50d 100644 --- a/src/main/java/com/ffii/fpsms/modules/jobOrder/service/JobOrderService.kt +++ b/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>() {}.type + val jsonResult = sqlResult.pickLines?.let { GsonUtils.stringToJson>(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"); diff --git a/src/main/java/com/ffii/fpsms/modules/master/entity/ProductionScheduleLine.kt b/src/main/java/com/ffii/fpsms/modules/master/entity/ProductionScheduleLine.kt index 12da0b1..afa5180 100644 --- a/src/main/java/com/ffii/fpsms/modules/master/entity/ProductionScheduleLine.kt +++ b/src/main/java/com/ffii/fpsms/modules/master/entity/ProductionScheduleLine.kt @@ -45,6 +45,24 @@ open class ProductionScheduleLine : BaseEntity() { @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) diff --git a/src/main/java/com/ffii/fpsms/modules/master/entity/ProductionScheduleRepository.kt b/src/main/java/com/ffii/fpsms/modules/master/entity/ProductionScheduleRepository.kt index 1532cc4..47f54cb 100644 --- a/src/main/java/com/ffii/fpsms/modules/master/entity/ProductionScheduleRepository.kt +++ b/src/main/java/com/ffii/fpsms/modules/master/entity/ProductionScheduleRepository.kt @@ -202,8 +202,15 @@ interface ProductionScheduleRepository : AbstractRepository> + val data: List> = 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> { + + 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() + open fun getNeedQty(): List { + 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> = 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() + //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 = getProductionScheduleRecord(args) - - //用缺口決定生產順序 - val productionPriorityMap: HashMap = HashMap(); - //TODO: update to use SQL get shop order record for detailed scheduling (real prodQty and openBal) - for (record in scheduledList) { + val productionPriorityMap: HashMap = 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, + sortedEntries: List, 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 diff --git a/src/main/java/com/ffii/fpsms/modules/master/web/ProductionScheduleController.kt b/src/main/java/com/ffii/fpsms/modules/master/web/ProductionScheduleController.kt index cebd7ad..42ac896 100644 --- a/src/main/java/com/ffii/fpsms/modules/master/web/ProductionScheduleController.kt +++ b/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 { + 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) diff --git a/src/main/java/com/ffii/fpsms/modules/master/web/models/ReleaseProdScheduleRequest.kt b/src/main/java/com/ffii/fpsms/modules/master/web/models/ReleaseProdScheduleRequest.kt new file mode 100644 index 0000000..c244f9e --- /dev/null +++ b/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 +) diff --git a/src/main/resources/db/changelog/changes/20251211_01_Fai/01_add_fields_to_psl.sql b/src/main/resources/db/changelog/changes/20251211_01_Fai/01_add_fields_to_psl.sql new file mode 100644 index 0000000..a2ae920 --- /dev/null +++ b/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`; diff --git a/src/main/resources/db/changelog/changes/20251211_01_Fai/02_add_fields_to_psl.sql b/src/main/resources/db/changelog/changes/20251211_01_Fai/02_add_fields_to_psl.sql new file mode 100644 index 0000000..1803c36 --- /dev/null +++ b/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; From a0bf0a79f87cd7009e81453bd5c31ba31ccc3a57 Mon Sep 17 00:00:00 2001 From: "vluk@2fi-solutions.com.hk" Date: Thu, 18 Dec 2025 23:35:54 +0800 Subject: [PATCH 2/3] no message --- .../service/ProductionScheduleService.kt | 58 ++++++++++++++++++- 1 file changed, 55 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/ffii/fpsms/modules/master/service/ProductionScheduleService.kt b/src/main/java/com/ffii/fpsms/modules/master/service/ProductionScheduleService.kt index a8706db..94adeb6 100644 --- a/src/main/java/com/ffii/fpsms/modules/master/service/ProductionScheduleService.kt +++ b/src/main/java/com/ffii/fpsms/modules/master/service/ProductionScheduleService.kt @@ -644,8 +644,8 @@ open class ProductionScheduleService( 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 i.onHandQty is not null + -- and (i.onHandQty -500) * 1.0 / i.avgQtyLastMonth <= 1.9 -- and avgQtyLastMonth - (onHandQty - 500) > 0 """.trimIndent() @@ -716,7 +716,7 @@ open class ProductionScheduleService( updatedScheduleRecord.itemPriority = priority; - if (machineCap - prodDifference >= 0) { + if (updatedScheduleRecord.batchNeed.toInt() > 0) { //have enough quoter for adjustment machineCap -= prodDifference; detailedScheduleOutputList.add(updatedScheduleRecord) @@ -738,6 +738,37 @@ open class ProductionScheduleService( } saveDetailedScheduleOutput(sortedOutputList, accProdCount, fgCount, produceAt) + + //do for 7 days to predict + for (i in 1..6) { + fgCount = 0 + accProdCount = 0.0 + sortedOutputList.forEach { record -> + record.stockQty = record.stockQty + (record.outputQty * record.needNoOfJobOrder.toInt()) - record.avgQtyLastMonth + //compare if less than 1.9 days + record.daysLeft = record.stockQty / record.avgQtyLastMonth + + if(record.daysLeft <= 1.9){ + record.needQty = (record.avgQtyLastMonth * 2) - record.stockQty; + + if(record.needQty > 0.0){ + record.batchNeed = ceil(record.needQty / record.outputQty) + record.needNoOfJobOrder = record.batchNeed + + fgCount += 1 + accProdCount += record.outputQty * record.batchNeed.toInt() + }else{ + record.needNoOfJobOrder = 0 + record.batchNeed = 0 + } + } + + logger.info(record.name + " record.batchNeed: " + record.batchNeed + " record.stockQty:" + record.stockQty + " record.daysLeft:" + record.daysLeft) + + } + + saveDetailedScheduleOutputPredict(sortedOutputList, accProdCount, fgCount, produceAt.plusDays(i.toLong())) + } } open fun saveDetailedScheduleOutput( @@ -761,6 +792,27 @@ open class ProductionScheduleService( } } + open fun saveDetailedScheduleOutputPredict( + sortedEntries: List, + accProdCount: Double, + fgCount: Long, + produceAt: LocalDateTime, + ) { + val tempObj = ProductionSchedule() + tempObj.id = -1; + tempObj.scheduleAt = LocalDateTime.now() + tempObj.produceAt = produceAt; + tempObj.totalFGType = fgCount; + tempObj.totalEstProdCount = accProdCount; + tempObj.type = "detailed" + tempObj.id = saveProductionScheduleToDatabase(tempObj); + + for (detailedScheduleRecord in sortedEntries) { + if(detailedScheduleRecord.batchNeed.toInt() > 0) + saveDetailedScheduleLineToDatabase(tempObj.id ?: -1, detailedScheduleRecord) + } + } + private fun saveDetailedScheduleLineToDatabase(parentId: Long, detailedScheduleObj: NeedQtyRecord) { try { val prodSchedule = productionScheduleRepository.findById(parentId).get() From 99c6e9ccc353d829c536f8868d8aa0a91dfba709 Mon Sep 17 00:00:00 2001 From: "kelvin.yau" Date: Fri, 19 Dec 2025 11:41:20 +0800 Subject: [PATCH 3/3] delete first stock take function, db updates (items, printer, equipment_details, warehouse) --- .../modules/master/entity/EquipmentDetail.kt | 6 +- .../ffii/fpsms/modules/master/entity/Items.kt | 20 +- .../fpsms/modules/master/entity/Printer.kt | 4 + .../fpsms/modules/master/entity/Warehouse.kt | 12 +- .../master/entity/WarehouseRepository.kt | 16 +- .../modules/master/service/ItemsService.kt | 973 +++--------------- .../master/service/WarehouseService.kt | 17 - .../modules/master/web/ItemsController.kt | 15 - .../20251216_01_Enson/01_add_column.sql | 4 +- ..._add_equipmentCode_to_equipment_detail.sql | 5 + .../02_add_type_to_printer.sql | 5 + .../03_modify_items_table.sql | 15 + .../04_modify_warehouse_table.sql | 12 + 13 files changed, 187 insertions(+), 917 deletions(-) create mode 100644 src/main/resources/db/changelog/changes/20251219_01_KelvinY/01_add_equipmentCode_to_equipment_detail.sql create mode 100644 src/main/resources/db/changelog/changes/20251219_01_KelvinY/02_add_type_to_printer.sql create mode 100644 src/main/resources/db/changelog/changes/20251219_01_KelvinY/03_modify_items_table.sql create mode 100644 src/main/resources/db/changelog/changes/20251219_01_KelvinY/04_modify_warehouse_table.sql diff --git a/src/main/java/com/ffii/fpsms/modules/master/entity/EquipmentDetail.kt b/src/main/java/com/ffii/fpsms/modules/master/entity/EquipmentDetail.kt index 5a94e0a..8f86375 100644 --- a/src/main/java/com/ffii/fpsms/modules/master/entity/EquipmentDetail.kt +++ b/src/main/java/com/ffii/fpsms/modules/master/entity/EquipmentDetail.kt @@ -10,6 +10,11 @@ import jakarta.validation.constraints.Size @Table(name = "equipment_detail") @Entity open class EquipmentDetail : BaseEntity() { + + @Size(max = 255) + @Column(name = "equipmentCode", nullable = true, length = 255) + open var equipmentCode: String? = null + @Size(max = 30) @NotNull @Column(name = "code", nullable = false, length = 30) @@ -25,7 +30,6 @@ open class EquipmentDetail : BaseEntity() { @Column(name = "description", nullable = false, length = 500) open var description: String? = null - @Column(name = "equipmentTypeID") open var equipmentTypeId: Long? = null diff --git a/src/main/java/com/ffii/fpsms/modules/master/entity/Items.kt b/src/main/java/com/ffii/fpsms/modules/master/entity/Items.kt index 470ea26..beb70f9 100644 --- a/src/main/java/com/ffii/fpsms/modules/master/entity/Items.kt +++ b/src/main/java/com/ffii/fpsms/modules/master/entity/Items.kt @@ -34,15 +34,9 @@ open class Items : BaseEntity() { @Column(name = "countryOfOrigin") open var countryOfOrigin: String? = null - @Column(name = "inventorySheet") - open var inventorySheet: String? = null - @Column(name = "store_id", nullable = true, length = 255) open var store_id: String? = null - @Column(name = "storeLocation", nullable = true, length = 255) - open var storeLocation: String? = null - @Column(name = "warehouse", nullable = true, length = 255) open var warehouse: String? = null @@ -52,14 +46,17 @@ open class Items : BaseEntity() { @Column(name = "slot", nullable = true, length = 255) open var slot: String? = null - @Column(name = "MTMSPickRoutingID", nullable = true) - open var MTMSPickRoutingID: Int? = null - @Column(name = "LocationCode", nullable = true, length = 255) open var LocationCode: String? = null - @Column(name = "company", nullable = true, length = 30) - open var company: String? = null + @Column(name = "isEgg", nullable = true) + open var isEgg: Boolean? = false + + @Column(name = "isFee", nullable = true) + open var isFee: Boolean? = false + + @Column(name = "isBag", nullable = true) + open var isBag: Boolean? = false @Column(name = "maxQty") open var maxQty: Double? = null @@ -90,4 +87,5 @@ open class Items : BaseEntity() { @JsonManagedReference @JoinColumn(name = "qcCategoryId") open var qcCategory: QcCategory? = null + } \ No newline at end of file diff --git a/src/main/java/com/ffii/fpsms/modules/master/entity/Printer.kt b/src/main/java/com/ffii/fpsms/modules/master/entity/Printer.kt index 82b5d4b..e2b2314 100644 --- a/src/main/java/com/ffii/fpsms/modules/master/entity/Printer.kt +++ b/src/main/java/com/ffii/fpsms/modules/master/entity/Printer.kt @@ -18,6 +18,10 @@ open class Printer : BaseEntity() { @Column(name = "name", length = 500) open var name: String? = null + @Size(max = 255) + @Column(name = "type", nullable = true, length = 255) + open var type: String? = null + @Size(max = 500) @Column(name = "description", length = 500) open var description: String? = null diff --git a/src/main/java/com/ffii/fpsms/modules/master/entity/Warehouse.kt b/src/main/java/com/ffii/fpsms/modules/master/entity/Warehouse.kt index 6f033ef..80f327e 100644 --- a/src/main/java/com/ffii/fpsms/modules/master/entity/Warehouse.kt +++ b/src/main/java/com/ffii/fpsms/modules/master/entity/Warehouse.kt @@ -32,16 +32,16 @@ open class Warehouse : BaseEntity() { @Column(name = "store_id", nullable = false, length = 30) open var store_id: String? = null - @Column(name = "storeLocation", nullable = true, length = 30) - open var storeLocation: String? = null - @Column(name = "stockTakeTable", nullable = true, length = 30) - open var stockTakeTable: String? = null - @Column(name = "company", nullable = true, length = 30) - open var company: String? = null + @Column(name = "warehouse", nullable = true, length = 30) open var warehouse: String? = null + @Column(name = "area", nullable = true, length = 30) open var area: String? = null + @Column(name = "slot", nullable = true, length = 30) open var slot: String? = null + + @Column(name = "stockTakeSection", nullable = true, length = 255) + open var stockTakeSection: String? = null } \ No newline at end of file diff --git a/src/main/java/com/ffii/fpsms/modules/master/entity/WarehouseRepository.kt b/src/main/java/com/ffii/fpsms/modules/master/entity/WarehouseRepository.kt index 0dce39e..5a90967 100644 --- a/src/main/java/com/ffii/fpsms/modules/master/entity/WarehouseRepository.kt +++ b/src/main/java/com/ffii/fpsms/modules/master/entity/WarehouseRepository.kt @@ -13,19 +13,5 @@ interface WarehouseRepository : AbstractRepository { fun findByIdAndDeletedIsFalse(id: Serializable): Warehouse?; fun findByCodeAndDeletedIsFalse(code: String): Warehouse?; - - @Query(""" - SELECT w FROM Warehouse w - WHERE w.code = :code - AND w.stockTakeTable = :stockTakeTable - AND w.company = :company - AND w.storeLocation = :storeLocation - AND w.deleted = false -""") - fun findByCodeAndStockTakeTableAndCompanyAndStoreLocationAndDeletedIsFalse( - code: String, - stockTakeTable: String, - company: String, - storeLocation: String - ): Warehouse? + } \ No newline at end of file diff --git a/src/main/java/com/ffii/fpsms/modules/master/service/ItemsService.kt b/src/main/java/com/ffii/fpsms/modules/master/service/ItemsService.kt index 50c8652..fb01ea7 100644 --- a/src/main/java/com/ffii/fpsms/modules/master/service/ItemsService.kt +++ b/src/main/java/com/ffii/fpsms/modules/master/service/ItemsService.kt @@ -63,7 +63,7 @@ open class ItemsService( private val stockInLineRepository: StockInLineRepository, private val inventoryLotLineRepository: InventoryLotLineRepository, private val inventoryLotRepository: InventoryLotRepository, -): AbstractBaseEntityService(jdbcDao, itemsRepository) { +): AbstractBaseEntityService(jdbcDao, itemsRepository) { private val excelImportPath: String = System.getProperty("user.home") + "/Downloads/StockTakeImport/" @@ -172,7 +172,11 @@ open class ItemsService( logWriter.println("=".repeat(80)) logWriter.println("SUMMARY") logWriter.println("=".repeat(80)) - logWriter.println("End Time: ${LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"))}") + logWriter.println( + "End Time: ${ + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")) + }" + ) logWriter.println() logWriter.println("Items Patched: $successCount") logWriter.println("Items with Errors: $errorCount") @@ -215,7 +219,6 @@ open class ItemsService( } - // do mapping with projection open fun allItems(): List { // TODO: Replace by actual logic @@ -227,39 +230,41 @@ open class ItemsService( val args = mapOf( "type" to "mat" ) - val sql = StringBuilder("select" - + " i.id, " - + " concat(i.code , ' - ' , i.name) as label, " - + " uc.id as uomId, " - + " uc.code as uom, " - + " uc.udfudesc as uomDesc, " - + " COALESCE(inv.availableQty, 0) as currentStockBalance " - + " from items i " - + " left join item_uom iu on iu.itemId = i.id and iu.deleted = false and iu.salesUnit = true " - + " left join uom_conversion uc on uc.id = iu.uomId " - + " left join (" - + " SELECT " - + " il.itemId, " - + " SUM(COALESCE(ill.inQty, 0) - COALESCE(ill.outQty, 0) - COALESCE(ill.holdQty, 0)) as availableQty " - + " FROM inventory_lot_line ill " - + " JOIN inventory_lot il ON il.id = ill.inventoryLotId " - + " LEFT JOIN item_uom iu_ratio ON iu_ratio.itemId = il.itemId AND iu_ratio.deleted = false AND iu_ratio.salesUnit = true " - + " WHERE ill.status = 'available' " - + " AND (COALESCE(ill.inQty, 0) - COALESCE(ill.outQty, 0) - COALESCE(ill.holdQty, 0)) > 0 " - + " AND (il.expiryDate IS NULL OR il.expiryDate >= CURDATE()) " - + " GROUP BY il.itemId " - + " ) inv ON inv.itemId = i.id " - + " where i.deleted = false " - + " and i.type = :type " + val sql = StringBuilder( + "select" + + " i.id, " + + " concat(i.code , ' - ' , i.name) as label, " + + " uc.id as uomId, " + + " uc.code as uom, " + + " uc.udfudesc as uomDesc, " + + " COALESCE(inv.availableQty, 0) as currentStockBalance " + + " from items i " + + " left join item_uom iu on iu.itemId = i.id and iu.deleted = false and iu.salesUnit = true " + + " left join uom_conversion uc on uc.id = iu.uomId " + + " left join (" + + " SELECT " + + " il.itemId, " + + " SUM(COALESCE(ill.inQty, 0) - COALESCE(ill.outQty, 0) - COALESCE(ill.holdQty, 0)) as availableQty " + + " FROM inventory_lot_line ill " + + " JOIN inventory_lot il ON il.id = ill.inventoryLotId " + + " LEFT JOIN item_uom iu_ratio ON iu_ratio.itemId = il.itemId AND iu_ratio.deleted = false AND iu_ratio.salesUnit = true " + + " WHERE ill.status = 'available' " + + " AND (COALESCE(ill.inQty, 0) - COALESCE(ill.outQty, 0) - COALESCE(ill.holdQty, 0)) > 0 " + + " AND (il.expiryDate IS NULL OR il.expiryDate >= CURDATE()) " + + " GROUP BY il.itemId " + + " ) inv ON inv.itemId = i.id " + + " where i.deleted = false " + + " and i.type = :type " ) return jdbcDao.queryForList(sql.toString(), args); } + open fun getPickOrderItemsByPage(args: Map): List> { try { println("=== Debug: getPickOrderItemsByPage in ItemsService ===") println("Args: $args") - + val sql = StringBuilder( """ SELECT @@ -298,33 +303,33 @@ open class ItemsService( AND po.consoCode IS NULL """ ) - + if (args.containsKey("itemCode")) { sql.append(" AND i.code LIKE :itemCode ") } - + if (args.containsKey("itemName")) { sql.append(" AND i.name LIKE :itemName ") } - + if (args.containsKey("status")) { sql.append(" AND po.status = :status ") } - + if (args.containsKey("targetDateFrom")) { sql.append(" AND po.targetDate >= :targetDateFrom ") } - + if (args.containsKey("targetDateTo")) { sql.append(" AND po.targetDate <= :targetDateTo ") } - + sql.append(" ORDER BY po.targetDate DESC, i.name ASC ") - + val finalSql = sql.toString() println("Final SQL: $finalSql") println("SQL args: $args") - + val result = jdbcDao.queryForList(finalSql, args) println("Query result size: ${result.size}") result.forEach { row -> println("Result row: $row") } @@ -335,14 +340,17 @@ open class ItemsService( throw e } } - + open fun getRoughScheduleList(): List> { val now = LocalDateTime.now() - val lastMonthStart = now.minusMonths(1).withDayOfMonth(1).withHour(0).withMinute(0).withSecond(0) // Start of last month - val lastMonthEnd = now.minusDays(now.dayOfMonth.toLong()).withHour(23).withMinute(59).withSecond(59) // End of last month + val lastMonthStart = + now.minusMonths(1).withDayOfMonth(1).withHour(0).withMinute(0).withSecond(0) // Start of last month + val lastMonthEnd = + now.minusDays(now.dayOfMonth.toLong()).withHour(23).withMinute(59).withSecond(59) // End of last month val curMonthStart = now.withDayOfMonth(1) // Start of last month - val curMonthEnd = now.with(TemporalAdjusters.lastDayOfMonth()).withHour(23).withMinute(59).withSecond(59) // End of last month + val curMonthEnd = + now.with(TemporalAdjusters.lastDayOfMonth()).withHour(23).withMinute(59).withSecond(59) // End of last month // Format dates if needed val formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss") @@ -357,43 +365,43 @@ open class ItemsService( val sql = StringBuilder( " SELECT " - + " dol.itemId, " - + " i.name AS itemName, " - + " dol.itemNo, " - + " 0 AS openBalance, " - + " COUNT(DISTINCT dol.deliveryOrderId) AS orderCount, " - + " SUM(IFNULL(dol.qty, 0)) AS totalQty, " - + " SUM(IFNULL(dol.qty, 0)) / COUNT(DISTINCT dol.deliveryOrderId) AS lastMonthAvgSalesCount, " - + " 22000 - IFNULL(i.maxQty , 0) AS fgProductionLimit " - + " FROM delivery_order do " - + " LEFT JOIN delivery_order_line dol ON dol.deliveryOrderId = do.id " - + " LEFT JOIN items i ON i.id = dol.itemId " - + " WHERE do.deleted = false " - + " AND do.estimatedArrivalDate >= :lastMonthStart " - + " AND do.estimatedArrivalDate <= :lastMonthEnd " - + " AND dol.itemId is not null " - + " GROUP BY dol.itemId, dol.itemNo, i.name " + + " dol.itemId, " + + " i.name AS itemName, " + + " dol.itemNo, " + + " 0 AS openBalance, " + + " COUNT(DISTINCT dol.deliveryOrderId) AS orderCount, " + + " SUM(IFNULL(dol.qty, 0)) AS totalQty, " + + " SUM(IFNULL(dol.qty, 0)) / COUNT(DISTINCT dol.deliveryOrderId) AS lastMonthAvgSalesCount, " + + " 22000 - IFNULL(i.maxQty , 0) AS fgProductionLimit " + + " FROM delivery_order do " + + " LEFT JOIN delivery_order_line dol ON dol.deliveryOrderId = do.id " + + " LEFT JOIN items i ON i.id = dol.itemId " + + " WHERE do.deleted = false " + + " AND do.estimatedArrivalDate >= :lastMonthStart " + + " AND do.estimatedArrivalDate <= :lastMonthEnd " + + " AND dol.itemId is not null " + + " GROUP BY dol.itemId, dol.itemNo, i.name " ); return jdbcDao.queryForList(sql.toString(), args); } - open fun getItemsByPage(args: Map): List> { + open fun getItemsByPage(args: Map): List> { // Extract parameters from the DTO val sql = StringBuilder( "SELECT " + - "i.id, " + - "i.code, " + - "i.name, " + - "i.description " + - "FROM items i " + - "WHERE i.deleted = FALSE" + "i.id, " + + "i.code, " + + "i.name, " + + "i.description " + + "FROM items i " + + "WHERE i.deleted = FALSE" ); - if (args.containsKey("name")){ + if (args.containsKey("name")) { sql.append(" AND i.name like :name "); } - if (args.containsKey("code")){ + if (args.containsKey("code")) { sql.append(" AND i.code like :code "); } @@ -430,7 +438,7 @@ open class ItemsService( // QcCheck included item open fun getItem(id: Long): ItemWithQcResponse { - val list = listOf(1,2) + val list = listOf(1, 2) val item = itemsRepository.findByIdAndDeletedFalse(id) ?: Items() val qcItems = qcItemsRepository.findAllByDeletedIsFalse() val qcCheckMap = qcCheckRepository.findAllByItemIdAndDeletedFalse(id).associateBy { it.id } @@ -444,10 +452,10 @@ open class ItemsService( code = it.code!!, description = it.description!!, instruction = check?.description, - lowerLimit = check?.lowerLimit , + lowerLimit = check?.lowerLimit, upperLimit = check?.upperLimit, isActive = check != null - ) // Create combined item + ) // Create combined item } val response = ItemWithQcResponse( item = item, @@ -456,7 +464,8 @@ open class ItemsService( // TODO: Return with QC items return response } -// open fun getItem(id: Long): Items? { + + // open fun getItem(id: Long): Items? { // // TODO: Return with QC items // return itemsRepository.findById(id).get() // } @@ -475,8 +484,8 @@ open class ItemsService( ) } val item = if (request.m18Id != null) findByM18Id(request.m18Id) ?: Items() - else if (request.id != null && request.id > 0) itemsRepository.findByIdAndDeletedFalse(request.id) ?: Items() - else Items() + else if (request.id != null && request.id > 0) itemsRepository.findByIdAndDeletedFalse(request.id) ?: Items() + else Items() logger.info("item: $item") if (item.m18LastModifyDate == request.m18LastModifyDate) { return MessageResponse( @@ -518,65 +527,67 @@ open class ItemsService( } open fun getItemsWithDetailsByPage(args: Map): List> { - val sql = StringBuilder("select" - + " i.id, " - + " i.code, " - + " i.name, " - + " i.type, " - + " i.description, " - + " uc.id as uomId, " - + " uc.code as uom, " - + " uc.udfudesc as uomDesc, " - + " COALESCE(inv.availableQty, 0) as currentStockBalance " - + " from items i " - + " left join item_uom iu on iu.itemId = i.id and iu.deleted = false and iu.salesUnit = true " - + " left join uom_conversion uc on uc.id = iu.uomId " - + " left join (" - + " SELECT " - + " il.itemId, " - + " SUM(COALESCE(ill.inQty, 0) - COALESCE(ill.outQty, 0) - COALESCE(ill.holdQty, 0)) as availableQty " - + " FROM inventory_lot_line ill " - + " JOIN inventory_lot il ON il.id = ill.inventoryLotId " - + " LEFT JOIN item_uom iu_ratio ON iu_ratio.itemId = il.itemId AND iu_ratio.deleted = false AND iu_ratio.salesUnit = true " - + " WHERE ill.status = 'available' " - + " AND (COALESCE(ill.inQty, 0) - COALESCE(ill.outQty, 0) - COALESCE(ill.holdQty, 0)) > 0 " - + " AND (il.expiryDate IS NULL OR il.expiryDate >= CURDATE()) " - + " GROUP BY il.itemId " - + " ) inv ON inv.itemId = i.id " - + " where i.deleted = false " + val sql = StringBuilder( + "select" + + " i.id, " + + " i.code, " + + " i.name, " + + " i.type, " + + " i.description, " + + " uc.id as uomId, " + + " uc.code as uom, " + + " uc.udfudesc as uomDesc, " + + " COALESCE(inv.availableQty, 0) as currentStockBalance " + + " from items i " + + " left join item_uom iu on iu.itemId = i.id and iu.deleted = false and iu.salesUnit = true " + + " left join uom_conversion uc on uc.id = iu.uomId " + + " left join (" + + " SELECT " + + " il.itemId, " + + " SUM(COALESCE(ill.inQty, 0) - COALESCE(ill.outQty, 0) - COALESCE(ill.holdQty, 0)) as availableQty " + + " FROM inventory_lot_line ill " + + " JOIN inventory_lot il ON il.id = ill.inventoryLotId " + + " LEFT JOIN item_uom iu_ratio ON iu_ratio.itemId = il.itemId AND iu_ratio.deleted = false AND iu_ratio.salesUnit = true " + + " WHERE ill.status = 'available' " + + " AND (COALESCE(ill.inQty, 0) - COALESCE(ill.outQty, 0) - COALESCE(ill.holdQty, 0)) > 0 " + + " AND (il.expiryDate IS NULL OR il.expiryDate >= CURDATE()) " + + " GROUP BY il.itemId " + + " ) inv ON inv.itemId = i.id " + + " where i.deleted = false " ) - + // Add search conditions if (args.containsKey("name")) { sql.append(" AND i.name LIKE :name ") } - + if (args.containsKey("code")) { sql.append(" AND i.code LIKE :code ") } - + if (args.containsKey("description")) { sql.append(" AND i.description LIKE :description ") } - + if (args.containsKey("type")) { sql.append(" AND i.type = :type ") } - + sql.append(" ORDER BY i.name ASC ") - + return jdbcDao.queryForList(sql.toString(), args); } open fun getItemsIdWithSameCategoryForQc(args: Map): List { - val sql = StringBuilder("SELECT" - + " i.id " - + " FROM items i " - + " INNER JOIN items_qc_category_mapping iqcm ON iqcm.itemId = i.id AND iqcm.type = :qcType " - + " LEFT JOIN qc_category qcc ON qcc.id = iqcm.qcCategoryId " - + " WHERE i.deleted = false AND qcc.deleted = false" - + " AND LEFT(i.code, 2) = (SELECT LEFT(code, 2) FROM items WHERE id = :itemId)" - + " AND i.id != :itemId " + val sql = StringBuilder( + "SELECT" + + " i.id " + + " FROM items i " + + " INNER JOIN items_qc_category_mapping iqcm ON iqcm.itemId = i.id AND iqcm.type = :qcType " + + " LEFT JOIN qc_category qcc ON qcc.id = iqcm.qcCategoryId " + + " WHERE i.deleted = false AND qcc.deleted = false" + + " AND LEFT(i.code, 2) = (SELECT LEFT(code, 2) FROM items WHERE id = :itemId)" + + " AND i.id != :itemId " ) return jdbcDao.queryForInts(sql.toString(), args); } @@ -593,744 +604,4 @@ open class ItemsService( }?.takeIf { it.isNotBlank() } } - @Throws(IOException::class) - @Transactional - open fun patchItemsFromExcel(workbook: Workbook?): String { - logger.info("--------- Start - Patch Items from Excel -------") - - if (workbook == null) { - logger.error("No Excel Import") - return "Import Excel failure" - } - - // Create log writer - val logWriter = createPatchLogWriter() - - try { - val sheet: Sheet = workbook.getSheetAt(0) - - // Column indices (0-indexed) - val COLUMN_COMPANY_INDEX = 1 // Column B - val COLUMN_INVENTORY_SHEET_INDEX = 4 // Column E - val COLUMN_CODE_INDEX = 7 // Column H - val COLUMN_TYPE_INDEX = 9 // Column J - val COLUMN_STORE_ID_INDEX = 13 // Column N - val COLUMN_STORE_LOCATION_INDEX = 14 // Column O - val COLUMN_WAREHOUSE_INDEX = 15 // Column P - val COLUMN_AREA_INDEX = 16 // Column Q - val COLUMN_SLOT_INDEX = 17 // Column R - val COLUMN_MTMS_PICK_ROUTING_ID_INDEX = 18 // Column S - val COLUMN_ON_HOLD_QTY_INDEX = 21 // Column V - - val START_ROW_INDEX = 3 // Starting from row 4 (0-indexed) - - var successCount = 0 - var errorCount = 0 - var inventoryCreatedCount = 0 - var inventoryFailedCount = 0 - var duplicateSkippedCount = 0 - val errors = mutableListOf() - val missingInventoryItems = mutableListOf() - - logWriter.writeFileInfo(sheet.lastRowNum, START_ROW_INDEX) - logger.info("Last row: ${sheet.lastRowNum}") - - // Create StockTake (one for entire import) - val startTime = LocalDateTime.now() - val stockTakeCode = stockTakeService.assignStockTakeNo() - val saveStockTakeReq = SaveStockTakeRequest( - code = stockTakeCode, - planStart = startTime, - planEnd = startTime, - actualStart = startTime, - actualEnd = null, - status = StockTakeStatus.PENDING.value, - remarks = "Created from Excel import - Patch Items" - ) - val savedStockTake = stockTakeService.saveStockTake(saveStockTakeReq) - logger.info("Created StockTake: ${savedStockTake.code} (ID: ${savedStockTake.id})") - - // Create StockIn (one for entire import, linked to StockTake) - val stockInCode = assignStockInNo() - val saveStockInReq = SaveStockInRequest( - code = stockInCode, // Use generated code - stockTakeId = savedStockTake.id - ) - val savedStockInResponse = stockInService.create(saveStockInReq) - val savedStockIn = savedStockInResponse.entity as StockIn - logger.info("Created StockIn: ${savedStockIn.code} (ID: ${savedStockIn.id})") - - //Tracking duplicates for stock take lines - val processedItemIds = mutableMapOf() - - for (i in START_ROW_INDEX..sheet.lastRowNum) { - val row = sheet.getRow(i) - - if (row == null) { - continue - } - - // Get item code - val itemCode = try { - val cell = row.getCell(COLUMN_CODE_INDEX) - getCellStringValue(cell)?.trim() - } catch (e: Exception) { - logger.error("Row ${i + 1}: Failed to read code - ${e.message}") - logWriter.writeRowError(i, "Failed to read code - ${e.message}") - errorCount++ - errors.add("Row ${i + 1}: Failed to read code - ${e.message}") - continue - } - - if (itemCode.isNullOrBlank()) { - logger.warn("Row ${i + 1}: Empty code, skipping") - logWriter.writeRowSkipped(i, "Empty code") - continue - } - - // Find item by code - val item = try { - itemsRepository.findByCodeAndDeletedFalse(itemCode) - } catch (e: Exception) { - logger.error("Import Error (Row ${i + 1} - Find Item Error): ${e.message}") - logWriter.writeRowError(i, "Failed to find item with code '$itemCode' - ${e.message}") - errorCount++ - errors.add("Row ${i + 1}: Failed to find item with code '$itemCode' - ${e.message}") - continue - } - - if (item == null) { - logger.warn("Row ${i + 1}: Item with code '$itemCode' not found, skipping") - logWriter.writeRowSkipped(i, "Item with code '$itemCode' not found in database") - errorCount++ - errors.add("Row ${i + 1}: Item with code '$itemCode' not found") - continue - } - - if (processedItemIds.containsKey(item.id!!)) { - val previousRow = processedItemIds[item.id!!]!! + 1 - duplicateSkippedCount++ - logger.warn("Row ${i + 1}: Item '$itemCode' was already processed at row $previousRow - Skipping duplicate") - logWriter.writeRowSkipped(i, "Item '$itemCode' already processed at row $previousRow - Skipped") - continue // Skip to next item - } - processedItemIds[item.id!!] = i - - // Create StockTakeLine (for each row/item) - // Read quantity from Column V (same as inventory) - val qty = try { - val cell = row.getCell(COLUMN_ON_HOLD_QTY_INDEX) - val extractedValue = when { - cell == null -> BigDecimal.ZERO - cell.cellType == CellType.NUMERIC -> cell.numericCellValue.toBigDecimal() - cell.cellType == CellType.STRING -> { - val strValue = cell.stringCellValue.trim() - if (strValue.isNotBlank()) strValue.toBigDecimalOrNull() ?: BigDecimal.ZERO - else BigDecimal.ZERO - } - else -> BigDecimal.ZERO - } - // Add 500 to the extracted value - extractedValue + BigDecimal(500) - } catch (e: Exception) { - logger.warn("Row ${i + 1}: Failed to read quantity from column V - ${e.message}") - BigDecimal.ZERO - } - - // Get UOM for the item - val uom = itemUomService.findStockUnitByItemId(item.id!!)?.uom - - // Create StockTakeLine - val saveStockTakeLineReq = SaveStockTakeLineRequest( - stockTakeId = savedStockTake.id, - initialQty = qty, - finalQty = qty, - uomId = uom?.id, - status = StockTakeLineStatus.PENDING.value, - remarks = null, - inventoryLotLineId = null // Will be set later in Step 8 - ) - val savedStockTakeLine = try { - stockTakeLineService.saveStockTakeLine(saveStockTakeLineReq) - } catch (e: Exception) { - logger.error("Row ${i + 1}: Failed to create StockTakeLine for item '$itemCode' - ${e.message}") - logWriter.writeRowError(i, "Failed to create StockTakeLine - ${e.message}") - errorCount++ - errors.add("Row ${i + 1}: Failed to create StockTakeLine - ${e.message}") - continue - } - logger.info("Row ${i + 1}: Created StockTakeLine (ID: ${savedStockTakeLine.id}) for item '$itemCode'") - - try { - // Read Company (Column B) - val company = getCellStringValue(row.getCell(COLUMN_COMPANY_INDEX))?.trim() - if (!company.isNullOrBlank()) { - item.company = company - logger.info("Row ${i + 1}: Company read from Column B: '$company'") - } - - // Read InventorySheet (Column E) - val inventorySheet = getCellStringValue(row.getCell(COLUMN_INVENTORY_SHEET_INDEX))?.trim() - if (!inventorySheet.isNullOrBlank()) { - item.inventorySheet = inventorySheet - } - - // Read Type (Column J) - val type = getCellStringValue(row.getCell(COLUMN_TYPE_INDEX))?.trim() - if (!type.isNullOrBlank()) { - item.type = type - } - - // Read Store ID (Column N) - String type - val storeId = getCellStringValue(row.getCell(COLUMN_STORE_ID_INDEX))?.trim() - if (!storeId.isNullOrBlank()) { - item.store_id = storeId - } - - // Read Store Location (Column O) - val storeLocation = getCellStringValue(row.getCell(COLUMN_STORE_LOCATION_INDEX))?.trim() - if (!storeLocation.isNullOrBlank()) { - item.storeLocation = storeLocation - } - - // Read Warehouse (Column P) - val warehouseFromExcel = getCellStringValue(row.getCell(COLUMN_WAREHOUSE_INDEX))?.trim() - if (!warehouseFromExcel.isNullOrBlank()) { - item.warehouse = warehouseFromExcel - } - - // Read Area (Column Q) - val area = getCellStringValue(row.getCell(COLUMN_AREA_INDEX))?.trim() - if (!area.isNullOrBlank()) { - item.area = area - } - - // Read Slot (Column R) - val slot = getCellStringValue(row.getCell(COLUMN_SLOT_INDEX))?.trim() - if (!slot.isNullOrBlank()) { - item.slot = slot - } - - // Combine Column N (Store ID), Column P (Warehouse), Column Q (Area), and Column R (Slot) to form LocationCode - // Format: "N-P-Q-R" (e.g., "ST01-WH01-A01-S01") - val locationCodeParts = mutableListOf() - if (!storeId.isNullOrBlank()) { - locationCodeParts.add(storeId) - } - if (!warehouseFromExcel.isNullOrBlank()) { - locationCodeParts.add(warehouseFromExcel) - } - if (!area.isNullOrBlank()) { - locationCodeParts.add(area) - } - if (!slot.isNullOrBlank()) { - locationCodeParts.add(slot) - } - - val locationCodeFromExcel = if (locationCodeParts.isNotEmpty()) { - locationCodeParts.joinToString("-") - } else { - null - } - - if (!locationCodeFromExcel.isNullOrBlank()) { - item.LocationCode = locationCodeFromExcel - logger.info("Row ${i + 1}: LocationCode created: '$locationCodeFromExcel' (Format: N-P-Q-R, N='$storeId', P='$warehouseFromExcel', Q='$area', R='$slot')") - } else { - logger.warn("Row ${i + 1}: LocationCode cannot be created - Column N='$storeId', P='$warehouseFromExcel', Q='$area', R='$slot' (all empty or null)") - } - // Read MTMS Pick Routing ID (Column S) - Int type - val mtmsPickRoutingId = try { - val cell = row.getCell(COLUMN_MTMS_PICK_ROUTING_ID_INDEX) - when { - cell == null -> null - cell.cellType == CellType.NUMERIC -> cell.numericCellValue.toInt() - cell.cellType == CellType.STRING -> { - val strValue = cell.stringCellValue.trim() - if (strValue.isNotBlank()) strValue.toIntOrNull() else null - } - else -> null - } - } catch (e: Exception) { - logger.warn("Row ${i + 1}: Failed to read MTMS Pick Routing ID from column S - ${e.message}") - null - } - if (mtmsPickRoutingId != null) { - item.MTMSPickRoutingID = mtmsPickRoutingId - } - - } catch (e: Exception) { - logger.error("Row ${i + 1}: Failed to read Excel columns for item '$itemCode' - ${e.message}") - logWriter.writeRowError(i, "Failed to read Excel columns - ${e.message}") - // Continue processing - don't fail the entire row - } - - - //Create StockInLine (for each row/item) - // Get warehouse from item's LocationCode, inventorySheet, company, and storeLocation - // Match: item.LocationCode -> warehouse.code AND item.inventorySheet -> warehouse.stockTakeTable AND item.company -> warehouse.company AND item.storeLocation -> warehouse.storeLocation - val locationCode = item.LocationCode - val inventorySheet = item.inventorySheet - val company = item.company - val storeLocation = item.storeLocation - logger.info("Row ${i + 1}: Looking up warehouse with LocationCode: '$locationCode', inventorySheet: '$inventorySheet', company: '$company', storeLocation: '$storeLocation'") - - val warehouse = if (locationCode != null && locationCode.isNotBlank() && - inventorySheet != null && inventorySheet.isNotBlank() && - company != null && company.isNotBlank() && - storeLocation != null && storeLocation.isNotBlank()) { - try { - val foundWarehouse = warehouseService.findByCodeAndStockTakeTableAndCompanyAndStoreLocation(locationCode, inventorySheet, company, storeLocation) - if (foundWarehouse != null) { - logger.info("Row ${i + 1}: Found warehouse: code='${foundWarehouse.code}', stockTakeTable='${foundWarehouse.stockTakeTable}', company='${foundWarehouse.company}', storeLocation='${foundWarehouse.storeLocation}', id=${foundWarehouse.id}") - } else { - logger.warn("Row ${i + 1}: Warehouse not found in database for LocationCode: '$locationCode', inventorySheet: '$inventorySheet', company: '$company', storeLocation: '$storeLocation'") - } - foundWarehouse - } catch (e: Exception) { - logger.warn("Row ${i + 1}: Failed to find warehouse with LocationCode '$locationCode', inventorySheet '$inventorySheet', company '$company', storeLocation '$storeLocation' - ${e.message}") - null - } - } else { - if (locationCode.isNullOrBlank()) { - logger.warn("Row ${i + 1}: Item '$itemCode' has no LocationCode (null or blank), warehouse will be null") - } - if (inventorySheet.isNullOrBlank()) { - logger.warn("Row ${i + 1}: Item '$itemCode' has no inventorySheet (null or blank), warehouse will be null") - } - if (company.isNullOrBlank()) { - logger.warn("Row ${i + 1}: Item '$itemCode' has no company (null or blank), warehouse will be null") - } - if (storeLocation.isNullOrBlank()) { - logger.warn("Row ${i + 1}: Item '$itemCode' has no storeLocation (null or blank), warehouse will be null") - } - null - } - - // Calculate expiry date (+30 days) - val expiryDate = LocalDateTime.now().plusDays(30).toLocalDate() - - // Generate productLotNo: LT-YYYYMMDD-itemCode (for productLotNo field) - val dateStr = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMdd")) - val productLotNo = "LT-$dateStr-$itemCode" - - // Generate lotNo: LT-YYYYMMDD-XXXX (sequential number, same for StockInLine and InventoryLot) - val lotNo = stockInLineService.assignLotNo() - - // Generate dnNo: DN-YYYYMMDD-itemCode - val dnNo = "DN-$dateStr-$itemCode" - - // Create StockInLine - val saveStockInLineReq = SaveStockInLineRequest( - stockInId = savedStockIn.id, - itemId = item.id!!, - acceptedQty = qty, - acceptQty = qty, - expiryDate = expiryDate, - warehouseId = warehouse?.id, - stockTakeLineId = savedStockTakeLine.id, - dnNo = dnNo, - productLotNo = productLotNo, // LT-YYYYMMDD-itemCode - qcAccept = true, - status = StockInLineStatus.PENDING.status - ) - - val savedStockInLine = try { - stockInLineService.create(saveStockInLineReq) - } catch (e: Exception) { - logger.error("Row ${i + 1}: Failed to create StockInLine for item '$itemCode' - ${e.message}") - logWriter.writeRowError(i, "Failed to create StockInLine - ${e.message}") - errorCount++ - errors.add("Row ${i + 1}: Failed to create StockInLine - ${e.message}") - continue - } - logger.info("Row ${i + 1}: Created StockInLine (ID: ${savedStockInLine.id}) for item '$itemCode'") - - // Set lotNo on StockInLine (after creation, as it's not in SaveStockInLineRequest) - val stockInLineEntity = savedStockInLine.id?.let { - stockInLineRepository.findById(it).orElse(null) - } - if (stockInLineEntity != null) { - stockInLineEntity.lotNo = lotNo - stockInLineRepository.saveAndFlush(stockInLineEntity) - logger.info("Row ${i + 1}: Set lotNo '$lotNo' on StockInLine (ID: ${stockInLineEntity.id})") - } else { - logger.warn("Row ${i + 1}: Could not find StockInLine entity to set lotNo") - } - - val inventoryLotLines = if (qty > BigDecimal.ZERO && warehouse?.id != null) { - mutableListOf( - SaveInventoryLotLineForSil( - qty = qty, // Quantity from Column V - warehouseId = warehouse.id - ) - ) - } else { - logger.warn("Row ${i + 1}: Cannot create inventoryLotLines - qty: $qty, warehouse: ${warehouse?.id}") - null - } - - // Step 7a: First update to PENDING with qcAccept=true to create InventoryLot - // This will create InventoryLot and automatically set status to RECEIVED - saveStockInLineReq.apply { - id = savedStockInLine.id - status = StockInLineStatus.PENDING.status - this.dnNo = dnNo - this.productLotNo = lotNo - this.qcAccept = true - this.acceptedQty = qty - this.acceptQty = qty - // Don't set inventoryLotLines yet - } - val updatedForLot = try { - stockInLineService.update(saveStockInLineReq) - } catch (e: Exception) { - logger.error("Row ${i + 1}: Failed to update StockInLine to PENDING (for InventoryLot) for item '$itemCode' - ${e.message}") - logWriter.writeRowError(i, "Failed to update StockInLine to PENDING - ${e.message}") - errorCount++ - errors.add("Row ${i + 1}: Failed to update StockInLine to PENDING - ${e.message}") - continue - } - - // Check if update was successful - if (updatedForLot?.id == null) { - logger.error("Row ${i + 1}: StockInLine update returned null ID for item '$itemCode'") - logWriter.writeRowError(i, "StockInLine update returned null") - errorCount++ - errors.add("Row ${i + 1}: StockInLine update returned null") - continue - } - - logger.info("Row ${i + 1}: Updated StockInLine to PENDING (ID: ${updatedForLot.id}) - InventoryLot should be created") - - // Refresh StockInLine entity to get the InventoryLot that was just created - val refreshedStockInLine = stockInLineRepository.findById(updatedForLot.id!!).orElse(null) - if (refreshedStockInLine?.inventoryLot == null) { - logger.warn("Row ${i + 1}: InventoryLot was not created for StockInLine ${updatedForLot.id}") - logWriter.writeRowError(i, "InventoryLot was not created") - } else { - // Update InventoryLot's lotNo to match StockInLine's lotNo (LT-YYYYMMDD-XXXX format) - val inventoryLot = refreshedStockInLine.inventoryLot!! - if (inventoryLot.lotNo != lotNo) { - inventoryLot.lotNo = lotNo - inventoryLotRepository.saveAndFlush(inventoryLot) - logger.info("Row ${i + 1}: Updated InventoryLot lotNo to '$lotNo' to match StockInLine (InventoryLot ID: ${inventoryLot.id})") - } - logger.info("Row ${i + 1}: InventoryLot created (ID: ${inventoryLot.id}, lotNo: ${inventoryLot.lotNo})") - } - - // Step 7b: Second update to RECEIVED with inventoryLotLines to create InventoryLotLine - // Only proceed if warehouse is available (InventoryLotLine requires warehouse) - if (warehouse?.id == null) { - logger.warn("Row ${i + 1}: Warehouse is null (no LocationCode), skipping InventoryLotLine creation for item '$itemCode'") - logWriter.writeRowError(i, "Warehouse is null - InventoryLotLine will not be created") - // Continue without InventoryLotLine - StockTakeLine will not be updated with inventoryLotLineId - } else { - // Warehouse is available, proceed with InventoryLotLine creation - saveStockInLineReq.apply { - id = savedStockInLine.id - status = StockInLineStatus.RECEIVED.status // Explicitly set to RECEIVED - this.dnNo = dnNo - this.productLotNo = productLotNo - this.acceptedQty = qty - this.acceptQty = qty - this.inventoryLotLines = inventoryLotLines // REQUIRED for InventoryLotLine creation - } - val finalStockInLine = try { - stockInLineService.update(saveStockInLineReq) - } catch (e: Exception) { - logger.error("Row ${i + 1}: Failed to update StockInLine to RECEIVED (for InventoryLotLine) for item '$itemCode' - ${e.message}") - logWriter.writeRowError(i, "Failed to update StockInLine to RECEIVED - ${e.message}") - errorCount++ - errors.add("Row ${i + 1}: Failed to update StockInLine to RECEIVED - ${e.message}") - continue - } - - if (finalStockInLine?.id == null) { - logger.error("Row ${i + 1}: StockInLine update to RECEIVED returned null ID for item '$itemCode'") - logWriter.writeRowError(i, "StockInLine update to RECEIVED returned null") - errorCount++ - errors.add("Row ${i + 1}: StockInLine update to RECEIVED returned null") - continue - } - - logger.info("Row ${i + 1}: Updated StockInLine to RECEIVED (ID: ${finalStockInLine.id}) for item '$itemCode' with lotNo: $lotNo, dnNo: $dnNo") - - - // Step 8: Find InventoryLotLine and update StockTakeLine - // Flush first to ensure InventoryLotLine is persisted - stockInLineRepository.flush() - - // Verify InventoryLotLine was created by querying directly - val inventoryLotLine = try { - inventoryLotLineRepository.findByInventoryLotStockInLineIdAndWarehouseId( - inventoryLotStockInLineId = finalStockInLine.id!!, - warehouseId = warehouse.id!! - ) - } catch (e: Exception) { - logger.warn("Row ${i + 1}: Failed to find InventoryLotLine - ${e.message}") - // Try alternative: find by InventoryLot - try { - val refreshedStockInLine = stockInLineRepository.findById(finalStockInLine.id!!).orElse(null) - refreshedStockInLine?.inventoryLot?.id?.let { inventoryLotId -> - inventoryLotLineRepository.findAllByInventoryLotId(inventoryLotId) - .firstOrNull { it.warehouse?.id == warehouse.id } - } - } catch (e2: Exception) { - logger.warn("Row ${i + 1}: Alternative search failed - ${e2.message}") - null - } - } - - if (inventoryLotLine != null) { - logger.info("Row ${i + 1}: InventoryLotLine found (ID: ${inventoryLotLine.id}, qty: ${inventoryLotLine.inQty})") - } else { - logger.warn("Row ${i + 1}: InventoryLotLine not found for StockInLine ${finalStockInLine.id}") - logWriter.writeRowError(i, "InventoryLotLine not found") - } - - if (inventoryLotLine != null) { - val updateStockTakeLineReq = SaveStockTakeLineRequest( - id = savedStockTakeLine.id, - stockTakeId = savedStockTake.id, - initialQty = savedStockTakeLine.initialQty, - finalQty = savedStockTakeLine.finalQty, - uomId = savedStockTakeLine.uom?.id, - status = StockTakeLineStatus.COMPLETED.value, - completeDate = LocalDateTime.now(), - inventoryLotLineId = inventoryLotLine.id, - remarks = savedStockTakeLine.remarks - ) - try { - stockTakeLineService.saveStockTakeLine(updateStockTakeLineReq) - logger.info("Row ${i + 1}: Updated StockTakeLine (ID: ${savedStockTakeLine.id}) to COMPLETED with inventoryLotLineId: ${inventoryLotLine.id}") - } catch (e: Exception) { - logger.error("Row ${i + 1}: Failed to update StockTakeLine for item '$itemCode' - ${e.message}") - logWriter.writeRowError(i, "Failed to update StockTakeLine - ${e.message}") - } - } else { - logger.warn("Row ${i + 1}: InventoryLotLine not found, StockTakeLine will not be updated with inventoryLotLineId") - logWriter.writeRowError(i, "InventoryLotLine not found for StockTakeLine update") - } - } - - // Update fields - Replace existing data with Excel data - try { - - // Save the item - val savedItem = itemsRepository.saveAndFlush(item) - - logger.info("Row ${i + 1}: After save - Saved item ID: ${savedItem.id}") - - successCount++ - logger.info("Row ${i + 1}: Successfully patched item '${item.name}' (code: $itemCode)") - - // Write to log file - logWriter.writeItemPatched( - rowNum = i, - itemCode = itemCode, - itemId = savedItem.id, - itemName = item.name, - inventorySheet = item.inventorySheet, - type = item.type, - storeId = item.store_id, - storeLocation = item.storeLocation, - warehouse = item.warehouse, - area = item.area, - slot = item.slot, - locationCode = item.LocationCode, - mtmsPickRoutingId = item.MTMSPickRoutingID - ) - - // Delete old inventory record if exists, then create new one - try { - val itemId = savedItem.id - if (itemId == null) { - logger.error("Row ${i + 1}: Item ID is null after save, cannot create inventory for item '$itemCode'") - logWriter.writeInventoryIdNull(i) - missingInventoryItems.add("Row ${i + 1}: Item '$itemCode' - Item ID is null") - inventoryFailedCount++ - } else { - // Check if inventory exists - Find ALL inventory records for this item - val existingInventories = try { - inventoryRepository.findAllByItemIdAndDeletedIsFalse(itemId) - } catch (e: Exception) { - logger.warn("Row ${i + 1}: Failed to find existing inventory for item '$itemCode' (ID: $itemId) - ${e.message}") - emptyList() - } - - // Delete ALL existing inventory records if any exist - if (existingInventories.isNotEmpty()) { - try { - val deletedCount = existingInventories.size - existingInventories.forEach { inventory -> - inventoryRepository.delete(inventory) - } - inventoryRepository.flush() - logger.info("Row ${i + 1}: Deleted $deletedCount existing inventory record(s) for item '${item.name}' (code: $itemCode)") - if (deletedCount > 1) { - logger.warn("Row ${i + 1}: Found $deletedCount duplicate inventory records for item '$itemCode' - all deleted") - } - logWriter.writeInventoryDeleted(i, existingInventories.first().id) - } catch (e: Exception) { - logger.warn("Row ${i + 1}: Failed to delete existing inventory for item '$itemCode' - ${e.message}") - logWriter.writeInventoryDeleteError(i, e.message ?: "Unknown error") - } - } - - // Read quantity from Column V (will be used for onHandQty) - val stockTakeCount = try { - val cell = row.getCell(COLUMN_ON_HOLD_QTY_INDEX) - val extractedValue = when { - cell == null -> BigDecimal.ZERO - cell.cellType == CellType.NUMERIC -> cell.numericCellValue.toBigDecimal() - cell.cellType == CellType.STRING -> { - val strValue = cell.stringCellValue.trim() - if (strValue.isNotBlank()) strValue.toBigDecimalOrNull() ?: BigDecimal.ZERO - else strValue.toBigDecimalOrNull() ?: BigDecimal.ZERO - } - else -> BigDecimal.ZERO - } - // Add 500 to the extracted value - extractedValue + BigDecimal(500) - } catch (e: Exception) { - logger.warn("Row ${i + 1}: Failed to read quantity from column V - ${e.message}") - BigDecimal.ZERO - } - - // Create new inventory record - try { - val inventory = Inventory() - inventory.item = savedItem - inventory.onHandQty = stockTakeCount // Column V value goes to onHandQty - inventory.onHoldQty = BigDecimal.ZERO // onHoldQty is always 0 - inventory.unavailableQty = BigDecimal.ZERO - inventory.price = BigDecimal.ZERO - inventory.cpu = BigDecimal.ZERO - inventory.cpm = BigDecimal.ZERO - inventory.status = "active" - - inventoryRepository.saveAndFlush(inventory) - inventoryCreatedCount++ - logger.info("Row ${i + 1}: Created new inventory record for item '${item.name}' (code: $itemCode) with onHandQty: $stockTakeCount") - logWriter.writeInventoryCreated(i, stockTakeCount) - - } catch (e: Exception) { - inventoryFailedCount++ - logger.error("Row ${i + 1}: Failed to create inventory record for item '$itemCode' (ID: $itemId) - ${e.message}", e) - logWriter.writeInventoryError(i, "Failed to create - ${e.message}") - missingInventoryItems.add("Row ${i + 1}: Item '$itemCode' - ${e.message}") - } - } - } catch (e: Exception) { - inventoryFailedCount++ - logger.error("Row ${i + 1}: Unexpected error during inventory creation for item '$itemCode' - ${e.message}", e) - logWriter.writeInventoryError(i, "Unexpected error - ${e.message}") - missingInventoryItems.add("Row ${i + 1}: Item '$itemCode' - ${e.message}") - } - - logWriter.writeEmptyLine() - - } catch (e: Exception) { - logger.error("Import Error (Row ${i + 1} - Save Error): ${e.message}") - logWriter.writeRowError(i, "Failed to save item '$itemCode' - ${e.message}") - errorCount++ - errors.add("Row ${i + 1}: Failed to save item '$itemCode' - ${e.message}") - } - } - - // Update StockTake to COMPLETED - val endTime = LocalDateTime.now() - saveStockTakeReq.apply { - id = savedStockTake.id - actualEnd = endTime - status = StockTakeStatus.COMPLETED.value - } - stockTakeService.saveStockTake(saveStockTakeReq) - logger.info("Updated StockTake to COMPLETED: ${savedStockTake.code}") - - // Write summary - logWriter.writeSummary( - successCount = successCount, - errorCount = errorCount, - inventoryCreatedCount = inventoryCreatedCount, - inventoryFailedCount = inventoryFailedCount, - duplicateSkippedCount = duplicateSkippedCount, - missingInventoryItems = missingInventoryItems, - errors = errors - ) - - logger.info("--------- End - Patch Items from Excel -------") - logger.info("Success: $successCount, Errors: $errorCount, Duplicates Skipped: $duplicateSkippedCount") - logger.info("Inventory Created: $inventoryCreatedCount, Inventory Failed: $inventoryFailedCount") - logger.info("Log file saved to: ${logWriter.getLogFilePath()}") - - if (errors.isNotEmpty()) { - logger.error("Errors encountered: ${errors.joinToString("\n")}") - } - - return "Patch Excel completed. Success: $successCount, Errors: $errorCount. Inventory Created: $inventoryCreatedCount, Failed: $inventoryFailedCount. Log file: ${logWriter.getLogFilePath()}" - } finally { - logWriter.close() - } - } - - - /** - * Patch items from Excel file located in configured folder - * @param fileName The name of the Excel file (e.g., "items_patch.xlsx") - * @return Result message with success/error count - */ - @Throws(IOException::class) - @Transactional - open fun patchItemsFromExcelFileName(fileName: String): String { - val filePath = "$excelImportPath$fileName" - logger.info("Reading Excel file from: $filePath") - - val workbook: Workbook? = try { - val file = File(filePath) - - if (!file.exists()) { - logger.error("File not found: $filePath") - return "File not found: $filePath" - } - - if (!file.isFile) { - logger.error("Path is not a file: $filePath") - return "Path is not a file: $filePath" - } - - if (!file.canRead()) { - logger.error("File is not readable: $filePath") - return "File is not readable: $filePath" - } - - logger.info("Successfully located file: $filePath") - XSSFWorkbook(FileInputStream(file)) - } catch (e: java.io.FileNotFoundException) { - logger.error("File not found: $filePath - ${e.message}") - return "File not found: $filePath" - } catch (e: Exception) { - logger.error("Failed to read Excel file: ${e.message}", e) - return "Failed to read Excel file: ${e.message}" - } - - return try { - if (workbook == null) { - "Failed to read Excel workbook" - } else { - patchItemsFromExcel(workbook) - } - } catch (e: Exception) { - logger.error("Error processing Excel file: ${e.message}", e) - "Error processing Excel file: ${e.message}" - } finally { - // Close workbook to free resources - try { - workbook?.close() - } catch (e: Exception) { - logger.warn("Error closing workbook: ${e.message}") - } - } - } } \ No newline at end of file diff --git a/src/main/java/com/ffii/fpsms/modules/master/service/WarehouseService.kt b/src/main/java/com/ffii/fpsms/modules/master/service/WarehouseService.kt index bd55024..241e630 100644 --- a/src/main/java/com/ffii/fpsms/modules/master/service/WarehouseService.kt +++ b/src/main/java/com/ffii/fpsms/modules/master/service/WarehouseService.kt @@ -244,9 +244,6 @@ open class WarehouseService( this.store_id = store_id this.slot = slot this.order = order - this.storeLocation = storeLocation - this.stockTakeTable = stockTakeTable - this.company = company } warehouseRepository.save(existingWarehouse) updateCount++ @@ -262,9 +259,6 @@ open class WarehouseService( this.store_id = store_id this.slot = slot this.order = order - this.storeLocation = storeLocation - this.stockTakeTable = stockTakeTable - this.company = company } warehouseRepository.save(newWarehouse) createCount++ @@ -342,15 +336,4 @@ open class WarehouseService( // calculate total sort value: floor * 10000 + letter * 100 + slot return floorOrder * 10000L + letterOrder * 100L + slot } - - open fun findByCodeAndStockTakeTableAndCompanyAndStoreLocation( - code: String, - stockTakeTable: String, - company: String, - storeLocation: String - ): Warehouse? { - return warehouseRepository.findByCodeAndStockTakeTableAndCompanyAndStoreLocationAndDeletedIsFalse( - code, stockTakeTable, company, storeLocation - ) - } } \ No newline at end of file diff --git a/src/main/java/com/ffii/fpsms/modules/master/web/ItemsController.kt b/src/main/java/com/ffii/fpsms/modules/master/web/ItemsController.kt index 4d6ee68..6c2d556 100644 --- a/src/main/java/com/ffii/fpsms/modules/master/web/ItemsController.kt +++ b/src/main/java/com/ffii/fpsms/modules/master/web/ItemsController.kt @@ -128,19 +128,4 @@ fun getItemsWithDetailsByPage(request: HttpServletRequest): RecordsRes { - return try { - if (fileName.isBlank()) { - return ResponseEntity.badRequest().body("File name cannot be empty") - } - - logger.info("Request to patch items from file: $fileName") - val result = itemsService.patchItemsFromExcelFileName(fileName) - ResponseEntity.ok(result) - } catch (e: Exception) { - logger.error("Error patching from file: ${e.message}", e) - ResponseEntity.badRequest().body("Error: ${e.message}") - } - } } \ No newline at end of file diff --git a/src/main/resources/db/changelog/changes/20251216_01_Enson/01_add_column.sql b/src/main/resources/db/changelog/changes/20251216_01_Enson/01_add_column.sql index f0fa849..d925d3a 100644 --- a/src/main/resources/db/changelog/changes/20251216_01_Enson/01_add_column.sql +++ b/src/main/resources/db/changelog/changes/20251216_01_Enson/01_add_column.sql @@ -1,5 +1,7 @@ -- liquibase formatted sql -- changeset Enson:add_column +-- preconditions onFail:MARK_RAN +-- precondition-sql-check expectedResult:0 SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = 'fpsmsdb' AND table_name = 'bag' create table `fpsmsdb`.`bag` ( `id` int not null auto_increment, @@ -56,4 +58,4 @@ create table `fpsmsdb`.`jo_bag_consumption` ( `deleted` tinyint(1) NOT NULL DEFAULT '0', primary key (`id`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; \ No newline at end of file diff --git a/src/main/resources/db/changelog/changes/20251219_01_KelvinY/01_add_equipmentCode_to_equipment_detail.sql b/src/main/resources/db/changelog/changes/20251219_01_KelvinY/01_add_equipmentCode_to_equipment_detail.sql new file mode 100644 index 0000000..9dd6d0f --- /dev/null +++ b/src/main/resources/db/changelog/changes/20251219_01_KelvinY/01_add_equipmentCode_to_equipment_detail.sql @@ -0,0 +1,5 @@ +-- liquibase formatted sql +-- changeset KelvinY:add_equipmentCode_to_equipment_detail + +ALTER TABLE `fpsmsdb`.`equipment_detail` +ADD COLUMN `equipmentCode` VARCHAR(255) NULL; \ No newline at end of file diff --git a/src/main/resources/db/changelog/changes/20251219_01_KelvinY/02_add_type_to_printer.sql b/src/main/resources/db/changelog/changes/20251219_01_KelvinY/02_add_type_to_printer.sql new file mode 100644 index 0000000..c5ce91f --- /dev/null +++ b/src/main/resources/db/changelog/changes/20251219_01_KelvinY/02_add_type_to_printer.sql @@ -0,0 +1,5 @@ +-- liquibase formatted sql +-- changeset KelvinY:add_type_to_printer + +ALTER TABLE `fpsmsdb`.`printer` +ADD COLUMN `type` VARCHAR(255) NULL; \ No newline at end of file diff --git a/src/main/resources/db/changelog/changes/20251219_01_KelvinY/03_modify_items_table.sql b/src/main/resources/db/changelog/changes/20251219_01_KelvinY/03_modify_items_table.sql new file mode 100644 index 0000000..2359732 --- /dev/null +++ b/src/main/resources/db/changelog/changes/20251219_01_KelvinY/03_modify_items_table.sql @@ -0,0 +1,15 @@ +-- liquibase formatted sql +-- changeset KelvinY:modify_items_table + +-- Add new columns +ALTER TABLE `fpsmsdb`.`items` +ADD COLUMN `isEgg` TINYINT(1) NULL DEFAULT 0 AFTER `LocationCode`, +ADD COLUMN `isFee` TINYINT(1) NULL DEFAULT 0 AFTER `isEgg`, +ADD COLUMN `isBag` TINYINT(1) NULL DEFAULT 0 AFTER `isFee`; + +-- Remove old columns +ALTER TABLE `fpsmsdb`.`items` +DROP COLUMN `storeLocation`, +DROP COLUMN `MTMSPickRoutingID`, +DROP COLUMN `inventorySheet`, +DROP COLUMN `company`; \ No newline at end of file diff --git a/src/main/resources/db/changelog/changes/20251219_01_KelvinY/04_modify_warehouse_table.sql b/src/main/resources/db/changelog/changes/20251219_01_KelvinY/04_modify_warehouse_table.sql new file mode 100644 index 0000000..69a0778 --- /dev/null +++ b/src/main/resources/db/changelog/changes/20251219_01_KelvinY/04_modify_warehouse_table.sql @@ -0,0 +1,12 @@ +-- liquibase formatted sql +-- changeset KelvinY:modify_warehouse_table + +-- Add new column +ALTER TABLE `fpsmsdb`.`warehouse` +ADD COLUMN `stockTakeSection` VARCHAR(255) NULL; + +-- Remove old columns +ALTER TABLE `fpsmsdb`.`warehouse` +DROP COLUMN `storeLocation`, +DROP COLUMN `stockTakeTable`, +DROP COLUMN `company`; \ No newline at end of file