| @@ -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.LaneRow | |||
| import com.ffii.fpsms.modules.deliveryOrder.web.models.StoreLaneSummary | |||
| import com.ffii.fpsms.modules.deliveryOrder.web.models.DoPickOrderSummaryItem | |||
| import org.springframework.stereotype.Service | |||
| import java.time.LocalDate | |||
| import com.ffii.fpsms.modules.deliveryOrder.entity.DoPickOrderRecordRepository | |||
| @Service | |||
| class DoPickOrderQueryService( | |||
| private val doPickOrderRepository: DoPickOrderRepository, | |||
| private val jdbcDao: JdbcDao | |||
| private val jdbcDao: JdbcDao, | |||
| private val doPickOrderRecordRepository: DoPickOrderRecordRepository | |||
| ) { | |||
| fun getSummaryByStore(storeId: String, requiredDate: LocalDate?): StoreLaneSummary { | |||
| @@ -25,15 +28,25 @@ class DoPickOrderQueryService( | |||
| 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, | |||
| targetDate, | |||
| 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!!) | |||
| if (!hasNonIssueLines) { | |||
| println("🔍 DEBUG: Filtering out DoPickOrder ${doPickOrder.id} - all lines are issues") | |||
| @@ -41,9 +54,34 @@ class DoPickOrderQueryService( | |||
| 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) -> | |||
| LaneBtn( | |||
| truckLanceCode = list.first().truckLanceCode ?: "", | |||
| @@ -72,6 +110,7 @@ class DoPickOrderQueryService( | |||
| return StoreLaneSummary(storeId = storeId, rows = timeGroups) | |||
| } | |||
| private fun checkDoPickOrderHasNonIssueLines(doPickOrderId: Long): Boolean { | |||
| return try { | |||
| val totalLinesSql = """ | |||
| @@ -105,4 +144,54 @@ class DoPickOrderQueryService( | |||
| 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 doPickOrderLineRepository: DoPickOrderLineRepository, | |||
| @Lazy private val deliveryOrderRepository: DeliveryOrderRepository, | |||
| private val doPickOrderLineRecordRepository: DoPickOrderLineRecordRepository | |||
| ) { | |||
| open fun findReleasedDoPickOrders(): List<DoPickOrder> { | |||
| @@ -52,4 +52,9 @@ data class AssignByLaneRequest( | |||
| val truckDepartureTime: String?, // 可选:限定出车时间 | |||
| val truckLanceCode: String , | |||
| 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.assignTo?.id == userId && | |||
| 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()) { | |||
| @@ -3867,6 +3870,8 @@ ORDER BY | |||
| mapOf( | |||
| "id" to lineId, | |||
| "pickOrderLineId" to lineId, | |||
| "pickOrderId" to po.id, | |||
| "requiredQty" to pol.qty, | |||
| "status" to pol.status?.value, | |||
| "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.stock.entity.projection.StockOutLineInfo | |||
| import com.ffii.fpsms.modules.stock.entity.StockOutLIneRepository | |||
| import com.ffii.fpsms.modules.stock.web.model.StockOutStatus | |||
| @Service | |||
| open class SuggestedPickLotService( | |||
| val suggestedPickLotRepository: SuggestPickLotRepository, | |||
| @@ -282,71 +283,118 @@ open class SuggestedPickLotService( | |||
| return suggestedPickLotRepository.saveAllAndFlush(request) | |||
| } | |||
| 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 | |||
| 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 item = pickOrderLine.item ?: itemRepository.findById(pickOrderLine.item!!.id!!).orElseThrow() | |||
| // Create stock out line with inventoryLotLineId = null | |||
| val stockOutLine = StockOutLine().apply { | |||
| this.item = item | |||
| this.qty = 0.0 | |||
| 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.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 | |||
| } 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]) | |||
| open fun resuggestPickOrder(pickOrderId: Long): MessageResponse { | |||
| try { | |||
| @@ -379,7 +427,7 @@ open class SuggestedPickLotService( | |||
| // Only resuggest if the pick order has rejected stock out lines | |||
| pickOrderToCheck.pickOrderLines.any { pol -> | |||
| 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) { | |||
| 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`; | |||