| @@ -6,13 +6,16 @@ import com.ffii.fpsms.modules.deliveryOrder.enums.DoPickOrderStatus | |||||
| import com.ffii.fpsms.modules.deliveryOrder.web.models.LaneBtn | import com.ffii.fpsms.modules.deliveryOrder.web.models.LaneBtn | ||||
| import com.ffii.fpsms.modules.deliveryOrder.web.models.LaneRow | import com.ffii.fpsms.modules.deliveryOrder.web.models.LaneRow | ||||
| import com.ffii.fpsms.modules.deliveryOrder.web.models.StoreLaneSummary | import com.ffii.fpsms.modules.deliveryOrder.web.models.StoreLaneSummary | ||||
| import com.ffii.fpsms.modules.deliveryOrder.web.models.DoPickOrderSummaryItem | |||||
| import org.springframework.stereotype.Service | import org.springframework.stereotype.Service | ||||
| import java.time.LocalDate | import java.time.LocalDate | ||||
| import com.ffii.fpsms.modules.deliveryOrder.entity.DoPickOrderRecordRepository | |||||
| @Service | @Service | ||||
| class DoPickOrderQueryService( | class DoPickOrderQueryService( | ||||
| private val doPickOrderRepository: DoPickOrderRepository, | private val doPickOrderRepository: DoPickOrderRepository, | ||||
| private val jdbcDao: JdbcDao | |||||
| private val jdbcDao: JdbcDao, | |||||
| private val doPickOrderRecordRepository: DoPickOrderRecordRepository | |||||
| ) { | ) { | ||||
| fun getSummaryByStore(storeId: String, requiredDate: LocalDate?): StoreLaneSummary { | fun getSummaryByStore(storeId: String, requiredDate: LocalDate?): StoreLaneSummary { | ||||
| @@ -25,15 +28,25 @@ class DoPickOrderQueryService( | |||||
| else -> storeId | else -> storeId | ||||
| } | } | ||||
| val allRecords = doPickOrderRepository.findByStoreIdAndRequiredDeliveryDateAndTicketStatusIn( | |||||
| // Query active do_pick_order records (pending, released, completed that haven't been moved yet) | |||||
| val activeRecords = doPickOrderRepository.findByStoreIdAndRequiredDeliveryDateAndTicketStatusIn( | |||||
| actualStoreId, | actualStoreId, | ||||
| targetDate, | targetDate, | ||||
| listOf(DoPickOrderStatus.pending, DoPickOrderStatus.released, DoPickOrderStatus.completed) | listOf(DoPickOrderStatus.pending, DoPickOrderStatus.released, DoPickOrderStatus.completed) | ||||
| ) | ) | ||||
| println("🔍 DEBUG: Found ${allRecords.size} records for date $targetDate") | |||||
| // Query completed records from do_pick_order_record table | |||||
| val completedRecords = doPickOrderRecordRepository.findByStoreIdAndRequiredDeliveryDateAndTicketStatusIn( | |||||
| actualStoreId, | |||||
| targetDate, | |||||
| listOf(DoPickOrderStatus.completed) | |||||
| ) | |||||
| val filteredRecords = allRecords.filter { doPickOrder -> | |||||
| println("🔍 DEBUG: Found ${activeRecords.size} active records for date $targetDate") | |||||
| println("🔍 DEBUG: Found ${completedRecords.size} completed records for date $targetDate") | |||||
| // Filter active records (check for non-issue lines) | |||||
| val filteredActiveRecords = activeRecords.filter { doPickOrder -> | |||||
| val hasNonIssueLines = checkDoPickOrderHasNonIssueLines(doPickOrder.id!!) | val hasNonIssueLines = checkDoPickOrderHasNonIssueLines(doPickOrder.id!!) | ||||
| if (!hasNonIssueLines) { | if (!hasNonIssueLines) { | ||||
| println("🔍 DEBUG: Filtering out DoPickOrder ${doPickOrder.id} - all lines are issues") | println("🔍 DEBUG: Filtering out DoPickOrder ${doPickOrder.id} - all lines are issues") | ||||
| @@ -41,9 +54,34 @@ class DoPickOrderQueryService( | |||||
| hasNonIssueLines | hasNonIssueLines | ||||
| } | } | ||||
| println("🔍 DEBUG: After filtering, ${filteredRecords.size} records remain") | |||||
| // For completed records, check if they have non-issue lines in the record table | |||||
| val filteredCompletedRecords = completedRecords.filter { record -> | |||||
| val hasNonIssueLines = checkDoPickOrderRecordHasNonIssueLines(record.id!!) | |||||
| if (!hasNonIssueLines) { | |||||
| println("🔍 DEBUG: Filtering out DoPickOrderRecord ${record.id} - all lines are issues") | |||||
| } | |||||
| hasNonIssueLines | |||||
| } | |||||
| // Combine both lists - need to create a common interface or convert to a common type | |||||
| // Since both have the same fields we need, we can create a wrapper or use a data class | |||||
| val allRecords = filteredActiveRecords.map { | |||||
| DoPickOrderSummaryItem( | |||||
| truckDepartureTime = it.truckDepartureTime, | |||||
| truckLanceCode = it.truckLanceCode, | |||||
| handledBy = it.handledBy | |||||
| ) | |||||
| } + filteredCompletedRecords.map { | |||||
| DoPickOrderSummaryItem( | |||||
| truckDepartureTime = it.truckDepartureTime, | |||||
| truckLanceCode = it.truckLanceCode, | |||||
| handledBy = it.handledBy | |||||
| ) | |||||
| } | |||||
| println("🔍 DEBUG: After filtering, ${allRecords.size} records remain (${filteredActiveRecords.size} active + ${filteredCompletedRecords.size} completed)") | |||||
| val grouped = filteredRecords.groupBy { it.truckDepartureTime to it.truckLanceCode } | |||||
| val grouped = allRecords.groupBy { it.truckDepartureTime to it.truckLanceCode } | |||||
| .mapValues { (_, list) -> | .mapValues { (_, list) -> | ||||
| LaneBtn( | LaneBtn( | ||||
| truckLanceCode = list.first().truckLanceCode ?: "", | truckLanceCode = list.first().truckLanceCode ?: "", | ||||
| @@ -72,6 +110,7 @@ class DoPickOrderQueryService( | |||||
| return StoreLaneSummary(storeId = storeId, rows = timeGroups) | return StoreLaneSummary(storeId = storeId, rows = timeGroups) | ||||
| } | } | ||||
| private fun checkDoPickOrderHasNonIssueLines(doPickOrderId: Long): Boolean { | private fun checkDoPickOrderHasNonIssueLines(doPickOrderId: Long): Boolean { | ||||
| return try { | return try { | ||||
| val totalLinesSql = """ | val totalLinesSql = """ | ||||
| @@ -105,4 +144,54 @@ class DoPickOrderQueryService( | |||||
| true | true | ||||
| } | } | ||||
| } | } | ||||
| // Add new method to check non-issue lines for records | |||||
| private fun checkDoPickOrderRecordHasNonIssueLines(recordId: Long): Boolean { | |||||
| return try { | |||||
| // First get the record_id from do_pick_order_record | |||||
| val recordSql = """ | |||||
| SELECT record_id | |||||
| FROM fpsmsdb.do_pick_order_record dpor | |||||
| WHERE dpor.id = :recordId | |||||
| """.trimIndent() | |||||
| val recordResult = jdbcDao.queryForList(recordSql, mapOf("recordId" to recordId)) | |||||
| val recordIdValue = (recordResult.firstOrNull()?.get("record_id") as? Number)?.toLong() | |||||
| if (recordIdValue == null) { | |||||
| return true // If no record_id, assume it's valid | |||||
| } | |||||
| // Check do_pick_order_line_record table | |||||
| val totalLinesSql = """ | |||||
| SELECT COUNT(*) as total_lines | |||||
| FROM fpsmsdb.do_pick_order_line_record dpolr | |||||
| WHERE dpolr.record_id = :recordId | |||||
| AND dpolr.deleted = 0 | |||||
| """.trimIndent() | |||||
| val totalLinesResult = jdbcDao.queryForList(totalLinesSql, mapOf("recordId" to recordIdValue)) | |||||
| val totalLines = (totalLinesResult.firstOrNull()?.get("total_lines") as? Number)?.toInt() ?: 0 | |||||
| if (totalLines == 0) { | |||||
| return true | |||||
| } | |||||
| val nonIssueLinesSql = """ | |||||
| SELECT COUNT(*) as non_issue_lines | |||||
| FROM fpsmsdb.do_pick_order_line_record dpolr | |||||
| WHERE dpolr.record_id = :recordId | |||||
| AND dpolr.deleted = 0 | |||||
| AND (dpolr.status IS NULL OR dpolr.status != 'issue') | |||||
| """.trimIndent() | |||||
| val nonIssueLinesResult = jdbcDao.queryForList(nonIssueLinesSql, mapOf("recordId" to recordIdValue)) | |||||
| val nonIssueLines = (nonIssueLinesResult.firstOrNull()?.get("non_issue_lines") as? Number)?.toInt() ?: 0 | |||||
| nonIssueLines > 0 | |||||
| } catch (e: Exception) { | |||||
| println("❌ Error checking non-issue lines for record: ${e.message}") | |||||
| true | |||||
| } | |||||
| } | |||||
| } | } | ||||
| @@ -56,6 +56,7 @@ open class DoPickOrderService( | |||||
| private val truckRepository: TruckRepository, | private val truckRepository: TruckRepository, | ||||
| private val doPickOrderLineRepository: DoPickOrderLineRepository, | private val doPickOrderLineRepository: DoPickOrderLineRepository, | ||||
| @Lazy private val deliveryOrderRepository: DeliveryOrderRepository, | @Lazy private val deliveryOrderRepository: DeliveryOrderRepository, | ||||
| private val doPickOrderLineRecordRepository: DoPickOrderLineRecordRepository | private val doPickOrderLineRecordRepository: DoPickOrderLineRecordRepository | ||||
| ) { | ) { | ||||
| open fun findReleasedDoPickOrders(): List<DoPickOrder> { | open fun findReleasedDoPickOrders(): List<DoPickOrder> { | ||||
| @@ -52,4 +52,9 @@ data class AssignByLaneRequest( | |||||
| val truckDepartureTime: String?, // 可选:限定出车时间 | val truckDepartureTime: String?, // 可选:限定出车时间 | ||||
| val truckLanceCode: String , | val truckLanceCode: String , | ||||
| val requiredDate: LocalDate? // 必填:车道编号 | val requiredDate: LocalDate? // 必填:车道编号 | ||||
| ) | |||||
| ) | |||||
| data class DoPickOrderSummaryItem( | |||||
| val truckDepartureTime: java.time.LocalTime?, | |||||
| val truckLanceCode: String?, | |||||
| val handledBy: Long? | |||||
| ) | |||||
| @@ -3570,7 +3570,10 @@ ORDER BY | |||||
| it.deleted == false && | it.deleted == false && | ||||
| it.assignTo?.id == userId && | it.assignTo?.id == userId && | ||||
| it.type?.value == "do" && | it.type?.value == "do" && | ||||
| (it.status == PickOrderStatus.RELEASED || it.status == PickOrderStatus.PENDING) | |||||
| (it.status == PickOrderStatus.RELEASED || | |||||
| it.status == PickOrderStatus.PENDING || | |||||
| it.status == PickOrderStatus.PICKING || | |||||
| it.status == PickOrderStatus.ASSIGNED) | |||||
| } | } | ||||
| if (userPickOrders.isEmpty()) { | if (userPickOrders.isEmpty()) { | ||||
| @@ -3867,6 +3870,8 @@ ORDER BY | |||||
| mapOf( | mapOf( | ||||
| "id" to lineId, | "id" to lineId, | ||||
| "pickOrderLineId" to lineId, | |||||
| "pickOrderId" to po.id, | |||||
| "requiredQty" to pol.qty, | "requiredQty" to pol.qty, | ||||
| "status" to pol.status?.value, | "status" to pol.status?.value, | ||||
| "item" to mapOf( | "item" to mapOf( | ||||
| @@ -38,6 +38,7 @@ import com.ffii.fpsms.modules.master.entity.ItemsRepository | |||||
| import com.ffii.fpsms.modules.pickOrder.enums.PickOrderLineStatus | import com.ffii.fpsms.modules.pickOrder.enums.PickOrderLineStatus | ||||
| import com.ffii.fpsms.modules.stock.entity.projection.StockOutLineInfo | import com.ffii.fpsms.modules.stock.entity.projection.StockOutLineInfo | ||||
| import com.ffii.fpsms.modules.stock.entity.StockOutLIneRepository | import com.ffii.fpsms.modules.stock.entity.StockOutLIneRepository | ||||
| import com.ffii.fpsms.modules.stock.web.model.StockOutStatus | |||||
| @Service | @Service | ||||
| open class SuggestedPickLotService( | open class SuggestedPickLotService( | ||||
| val suggestedPickLotRepository: SuggestPickLotRepository, | val suggestedPickLotRepository: SuggestPickLotRepository, | ||||
| @@ -282,71 +283,118 @@ open class SuggestedPickLotService( | |||||
| return suggestedPickLotRepository.saveAllAndFlush(request) | return suggestedPickLotRepository.saveAllAndFlush(request) | ||||
| } | } | ||||
| private fun createStockOutLineForSuggestion( | private fun createStockOutLineForSuggestion( | ||||
| suggestion: SuggestedPickLot, | |||||
| pickOrder: PickOrder | |||||
| ): StockOutLine? { | |||||
| try { | |||||
| val suggestedLotLine = suggestion.suggestedLotLine | |||||
| val pickOrderLine = suggestion.pickOrderLine | |||||
| if (suggestedLotLine == null || pickOrderLine == null) { | |||||
| println("Cannot create stock out line: missing suggestedLotLine or pickOrderLine") | |||||
| return null | |||||
| } | |||||
| suggestion: SuggestedPickLot, | |||||
| pickOrder: PickOrder | |||||
| ): StockOutLine? { | |||||
| try { | |||||
| val suggestedLotLine = suggestion.suggestedLotLine | |||||
| val pickOrderLine = suggestion.pickOrderLine | |||||
| // FIX: Allow creating stock out line even when suggestedLotLine is null (insufficient stock case) | |||||
| if (pickOrderLine == null) { | |||||
| println("Cannot create stock out line: missing pickOrderLine") | |||||
| return null | |||||
| } | |||||
| // Get pick order type value (do, job, material, etc.) | |||||
| val pickOrderTypeValue = pickOrder.type?.value ?: "do" | |||||
| // If suggestedLotLine is null, create stock out line with inventoryLotLineId = null | |||||
| if (suggestedLotLine == null) { | |||||
| println("Creating stock out line with no lot (insufficient stock) for pickOrderLineId: ${pickOrderLine.id}, type: $pickOrderTypeValue") | |||||
| // Check if stock out line already exists | |||||
| val existingStockOutLine = stockOutLIneRepository.findByPickOrderLineIdAndInventoryLotLineIdAndDeletedFalse( | |||||
| pickOrderLine.id!!, | |||||
| suggestedLotLine.id!! | |||||
| ) | |||||
| // Check if stock out line already exists for this pick order line with null lot | |||||
| val existingStockOutLineInfo = stockOutLIneRepository | |||||
| .findAllByPickOrderLineIdAndDeletedFalse(pickOrderLine.id!!) | |||||
| .find { it.inventoryLotLineId == null } | |||||
| if (existingStockOutLine.isNotEmpty()) { | |||||
| println("Stock out line already exists for pickOrderLineId: ${pickOrderLine.id}, inventoryLotLineId: ${suggestedLotLine.id}") | |||||
| return existingStockOutLine.first() | |||||
| if (existingStockOutLineInfo != null) { | |||||
| println("Stock out line already exists for pickOrderLineId: ${pickOrderLine.id} with no lot") | |||||
| // Get the actual entity by ID | |||||
| return stockOutLIneRepository.findById(existingStockOutLineInfo.id!!).orElse(null) | |||||
| } | } | ||||
| // Get or create StockOut | // Get or create StockOut | ||||
| val stockOut = stockOutRepository.findByConsoPickOrderCode(pickOrder.consoCode ?: "") | val stockOut = stockOutRepository.findByConsoPickOrderCode(pickOrder.consoCode ?: "") | ||||
| .orElseGet { | .orElseGet { | ||||
| // Create new StockOut if it doesn't exist | |||||
| val newStockOut = StockOut().apply { | val newStockOut = StockOut().apply { | ||||
| this.consoPickOrderCode = pickOrder.consoCode ?: "" | this.consoPickOrderCode = pickOrder.consoCode ?: "" | ||||
| this.type = pickOrderTypeValue // Use pick order type (do, job, material, etc.) | |||||
| this.status = StockOutStatus.PENDING.status | |||||
| } | } | ||||
| stockOutRepository.save(newStockOut) | stockOutRepository.save(newStockOut) | ||||
| } | } | ||||
| // Update pick order line status to PICKING | |||||
| val updatedPickOrderLine = pickOrderLineRepository.saveAndFlush( | |||||
| pickOrderLine.apply { | |||||
| this.status = PickOrderLineStatus.PICKING | |||||
| } | |||||
| ) | |||||
| // Get item | |||||
| val item = itemRepository.findById(updatedPickOrderLine.item!!.id!!).orElseThrow() | |||||
| // Create stock out line | |||||
| val item = pickOrderLine.item ?: itemRepository.findById(pickOrderLine.item!!.id!!).orElseThrow() | |||||
| // Create stock out line with inventoryLotLineId = null | |||||
| val stockOutLine = StockOutLine().apply { | val stockOutLine = StockOutLine().apply { | ||||
| this.item = item | |||||
| this.qty = 0.0 | |||||
| this.stockOut = stockOut | this.stockOut = stockOut | ||||
| this.inventoryLotLine = suggestedLotLine | |||||
| this.pickOrderLine = updatedPickOrderLine | |||||
| this.pickOrderLine = pickOrderLine | |||||
| this.inventoryLotLine = null // No lot available | |||||
| this.qty = (suggestion.qty ?: BigDecimal.ZERO).toDouble() | |||||
| this.status = StockOutLineStatus.PENDING.status | this.status = StockOutLineStatus.PENDING.status | ||||
| this.deleted = false | |||||
| } | } | ||||
| val savedStockOutLine = stockOutLIneRepository.saveAndFlush(stockOutLine) | |||||
| println(" Created stock out line ID: ${savedStockOutLine.id} for suggestion ID: ${suggestion.id}") | |||||
| val savedStockOutLine = stockOutLIneRepository.save(stockOutLine) | |||||
| println("Created stock out line ${savedStockOutLine.id} with no lot for pickOrderLineId: ${pickOrderLine.id}") | |||||
| return savedStockOutLine | return savedStockOutLine | ||||
| } catch (e: Exception) { | |||||
| println("❌ Error creating stock out line for suggestion: ${e.message}") | |||||
| e.printStackTrace() | |||||
| return null | |||||
| } | } | ||||
| // Original logic for when suggestedLotLine is not null | |||||
| // Check if stock out line already exists | |||||
| val existingStockOutLine = stockOutLIneRepository.findByPickOrderLineIdAndInventoryLotLineIdAndDeletedFalse( | |||||
| pickOrderLine.id!!, | |||||
| suggestedLotLine.id!! | |||||
| ) | |||||
| if (existingStockOutLine.isNotEmpty()) { | |||||
| println("Stock out line already exists for pickOrderLineId: ${pickOrderLine.id}, inventoryLotLineId: ${suggestedLotLine.id}") | |||||
| return existingStockOutLine.first() | |||||
| } | |||||
| // Get or create StockOut | |||||
| val stockOut = stockOutRepository.findByConsoPickOrderCode(pickOrder.consoCode ?: "") | |||||
| .orElseGet { | |||||
| // Create new StockOut if it doesn't exist | |||||
| val newStockOut = StockOut().apply { | |||||
| this.consoPickOrderCode = pickOrder.consoCode ?: "" | |||||
| this.type = pickOrderTypeValue // Use pick order type (do, job, material, etc.) | |||||
| this.status = StockOutStatus.PENDING.status | |||||
| } | |||||
| stockOutRepository.save(newStockOut) | |||||
| } | |||||
| // Update pick order line status to PICKING | |||||
| val updatedPickOrderLine = pickOrderLineRepository.saveAndFlush( | |||||
| pickOrderLine.apply { | |||||
| this.status = PickOrderLineStatus.PICKING | |||||
| } | |||||
| ) | |||||
| // Get item | |||||
| val item = itemRepository.findById(updatedPickOrderLine.item!!.id!!).orElseThrow() | |||||
| // Create stock out line | |||||
| val stockOutLine = StockOutLine().apply { | |||||
| this.item = item | |||||
| this.qty = 0.0 | |||||
| this.stockOut = stockOut | |||||
| this.inventoryLotLine = suggestedLotLine | |||||
| this.pickOrderLine = updatedPickOrderLine | |||||
| this.status = StockOutLineStatus.PENDING.status | |||||
| } | |||||
| val savedStockOutLine = stockOutLIneRepository.saveAndFlush(stockOutLine) | |||||
| println(" Created stock out line ID: ${savedStockOutLine.id} for suggestion ID: ${suggestion.id}") | |||||
| return savedStockOutLine | |||||
| } catch (e: Exception) { | |||||
| println("❌ Error creating stock out line for suggestion: ${e.message}") | |||||
| e.printStackTrace() | |||||
| return null | |||||
| } | } | ||||
| } | |||||
| @Transactional(rollbackFor = [Exception::class]) | @Transactional(rollbackFor = [Exception::class]) | ||||
| open fun resuggestPickOrder(pickOrderId: Long): MessageResponse { | open fun resuggestPickOrder(pickOrderId: Long): MessageResponse { | ||||
| try { | try { | ||||
| @@ -379,7 +427,7 @@ open class SuggestedPickLotService( | |||||
| // Only resuggest if the pick order has rejected stock out lines | // Only resuggest if the pick order has rejected stock out lines | ||||
| pickOrderToCheck.pickOrderLines.any { pol -> | pickOrderToCheck.pickOrderLines.any { pol -> | ||||
| val stockOutLines = stockOutLIneRepository.findAllByPickOrderLineIdAndDeletedFalse(pol.id!!) | val stockOutLines = stockOutLIneRepository.findAllByPickOrderLineIdAndDeletedFalse(pol.id!!) | ||||
| val hasRejectedStockOutLine = stockOutLines.any { it.status == "rejected" } | |||||
| val hasRejectedStockOutLine = stockOutLines.any { it.status.equals("rejected", ignoreCase = true)} | |||||
| if (hasRejectedStockOutLine) { | if (hasRejectedStockOutLine) { | ||||
| println("Pick Order ${pickOrderToCheck.code} has rejected stock out lines - will resuggest") | println("Pick Order ${pickOrderToCheck.code} has rejected stock out lines - will resuggest") | ||||
| @@ -0,0 +1,9 @@ | |||||
| -- liquibase formatted sql | |||||
| -- changeset enson:altertable_enson | |||||
| ALTER TABLE `fpsmsdb`.`items` | |||||
| ADD COLUMN `store_id` VARCHAR(255) After `type`, | |||||
| ADD COLUMN `MTMSPickRoutingID` INT After `store_id`; | |||||
| ALTER TABLE `fpsmsdb`.`truck` | |||||
| ADD COLUMN `districtReference` INT After `LoadingSequence`; | |||||