| @@ -1182,25 +1182,34 @@ open fun releaseDeliveryOrderWithoutTicket(request: ReleaseDoRequest): ReleaseDo | |||||
| val targetDate = deliveryOrder.estimatedArrivalDate?.toLocalDate() ?: LocalDate.now() | val targetDate = deliveryOrder.estimatedArrivalDate?.toLocalDate() ?: LocalDate.now() | ||||
| val itemIds = deliveryOrder.deliveryOrderLines.mapNotNull { it.item?.id }.distinct() | val itemIds = deliveryOrder.deliveryOrderLines.mapNotNull { it.item?.id }.distinct() | ||||
| val inventoryQuery = """ | |||||
| val itemsStoreIdQuery = """ | |||||
| SELECT | SELECT | ||||
| w.store_id as floor, | |||||
| COUNT(DISTINCT il.itemId) as item_count | |||||
| FROM inventory_lot il | |||||
| INNER JOIN inventory i ON i.itemId = il.itemId AND i.deleted = 0 AND i.onHandQty > 0 | |||||
| INNER JOIN inventory_lot_line ill ON ill.inventoryLotId = il.id AND ill.deleted = 0 | |||||
| INNER JOIN warehouse w ON w.id = ill.warehouseId AND w.deleted = 0 AND w.store_id IN ('2F', '4F') | |||||
| WHERE il.itemId IN (${itemIds.joinToString(",")}) AND il.deleted = 0 | |||||
| GROUP BY w.store_id | |||||
| i.store_id, | |||||
| COUNT(DISTINCT i.id) as item_count | |||||
| FROM items i | |||||
| WHERE i.id IN (${itemIds.joinToString(",")}) | |||||
| AND i.deleted = 0 | |||||
| GROUP BY i.store_id | |||||
| """.trimIndent() | """.trimIndent() | ||||
| val inventoryResults = jdbcDao.queryForList(inventoryQuery) | |||||
| val floorItemCount = mutableMapOf<String, Int>() | |||||
| inventoryResults.forEach { row -> | |||||
| floorItemCount[row["floor"] as? String ?: "Other"] = (row["item_count"] as? Number)?.toInt() ?: 0 | |||||
| val itemsStoreIdResults = jdbcDao.queryForList(itemsStoreIdQuery) | |||||
| val storeIdItemCount = mutableMapOf<String, Int>() | |||||
| itemsStoreIdResults.forEach { row -> | |||||
| val rawStoreId = row["store_id"] as? String | |||||
| if (rawStoreId != null) { | |||||
| val normalizedStoreId = when (rawStoreId) { | |||||
| "3F" -> "4F" | |||||
| else -> rawStoreId | |||||
| } | |||||
| storeIdItemCount[normalizedStoreId] = | |||||
| (storeIdItemCount[normalizedStoreId] ?: 0) + | |||||
| ((row["item_count"] as? Number)?.toInt() ?: 0) | |||||
| } | |||||
| } | } | ||||
| val preferredFloor = if ((floorItemCount["4F"] ?: 0) == itemIds.size && (floorItemCount["2F"] ?: 0) == 0) { | |||||
| val preferredFloor = if ((storeIdItemCount["4F"] ?: 0) == itemIds.size && | |||||
| (storeIdItemCount["2F"] ?: 0) == 0) { | |||||
| "4F" | "4F" | ||||
| } else { | } else { | ||||
| "2F" | "2F" | ||||
| @@ -94,10 +94,11 @@ class DoPickOrderQueryService( | |||||
| .groupBy { it.key.first } | .groupBy { it.key.first } | ||||
| .mapValues { (_, entries) -> | .mapValues { (_, entries) -> | ||||
| entries.map { it.value } | entries.map { it.value } | ||||
| .filter { it.unassigned > 0 } // filter out lanes with no unassigned orders | |||||
| .sortedByDescending { it.unassigned } | .sortedByDescending { it.unassigned } | ||||
| .take(3) | .take(3) | ||||
| } | } | ||||
| .filterValues { lanes -> lanes.any { it.unassigned > 0 } } | |||||
| .filterValues { lanes -> lanes.isNotEmpty() } | |||||
| .toSortedMap(compareBy { it }) | .toSortedMap(compareBy { it }) | ||||
| .entries.take(4) | .entries.take(4) | ||||
| .map { (time, lanes) -> | .map { (time, lanes) -> | ||||
| @@ -47,32 +47,31 @@ class DoReleaseCoordinatorService( | |||||
| val updateSql = """ | val updateSql = """ | ||||
| UPDATE fpsmsdb.do_pick_order dpo | UPDATE fpsmsdb.do_pick_order dpo | ||||
| INNER JOIN ( | INNER JOIN ( | ||||
| WITH DoFloorCounts AS ( | |||||
| SELECT | |||||
| dol.deliveryOrderId, | |||||
| w.store_id, | |||||
| COUNT(DISTINCT dol.itemId) AS item_count | |||||
| FROM fpsmsdb.delivery_order_line dol | |||||
| INNER JOIN fpsmsdb.inventory i ON i.itemId = dol.itemId | |||||
| AND i.deleted = 0 | |||||
| AND i.onHandQty > 0 | |||||
| INNER JOIN fpsmsdb.inventory_lot il ON il.itemId = i.itemId | |||||
| AND il.deleted = 0 | |||||
| INNER JOIN fpsmsdb.inventory_lot_line ill ON ill.inventoryLotId = il.id | |||||
| AND ill.deleted = 0 | |||||
| INNER JOIN fpsmsdb.warehouse w ON w.id = ill.warehouseId | |||||
| AND w.deleted = 0 | |||||
| AND w.store_id IN ('2F', '4F') | |||||
| WHERE dol.deleted = 0 | |||||
| GROUP BY dol.deliveryOrderId, w.store_id | |||||
| ), | |||||
| DoFloorSummary AS ( | |||||
| WITH DoStoreIdCounts AS ( | |||||
| SELECT | |||||
| dol.deliveryOrderId, | |||||
| CASE | |||||
| WHEN i.store_id = '3F' THEN '4F' | |||||
| ELSE i.store_id | |||||
| END AS store_id, -- 这里做 3F → 4F | |||||
| COUNT(DISTINCT dol.itemId) AS item_count | |||||
| FROM fpsmsdb.delivery_order_line dol | |||||
| INNER JOIN fpsmsdb.items i ON i.id = dol.itemId | |||||
| AND i.deleted = 0 | |||||
| WHERE dol.deleted = 0 | |||||
| GROUP BY dol.deliveryOrderId, | |||||
| CASE | |||||
| WHEN i.store_id = '3F' THEN '4F' | |||||
| ELSE i.store_id | |||||
| END | |||||
| ), | |||||
| DoStoreIdSummary AS ( | |||||
| SELECT | SELECT | ||||
| do.id AS deliveryOrderId, | do.id AS deliveryOrderId, | ||||
| COALESCE(SUM(CASE WHEN dfc.store_id = '2F' THEN dfc.item_count ELSE 0 END), 0) AS count_2f, | |||||
| COALESCE(SUM(CASE WHEN dfc.store_id = '4F' THEN dfc.item_count ELSE 0 END), 0) AS count_4f | |||||
| COALESCE(SUM(CASE WHEN dsc.store_id = '2F' THEN dsc.item_count ELSE 0 END), 0) AS count_2f, | |||||
| COALESCE(SUM(CASE WHEN dsc.store_id = '4F' THEN dsc.item_count ELSE 0 END), 0) AS count_4f | |||||
| FROM fpsmsdb.delivery_order do | FROM fpsmsdb.delivery_order do | ||||
| LEFT JOIN DoFloorCounts dfc ON dfc.deliveryOrderId = do.id | |||||
| LEFT JOIN DoStoreIdCounts dsc ON dsc.deliveryOrderId = do.id | |||||
| WHERE do.deleted = 0 | WHERE do.deleted = 0 | ||||
| GROUP BY do.id | GROUP BY do.id | ||||
| ), | ), | ||||
| @@ -89,7 +88,7 @@ class DoReleaseCoordinatorService( | |||||
| WHEN count_4f > count_2f THEN 4 | WHEN count_4f > count_2f THEN 4 | ||||
| ELSE 2 | ELSE 2 | ||||
| END AS preferred_store_id | END AS preferred_store_id | ||||
| FROM DoFloorSummary | |||||
| FROM DoStoreIdSummary | |||||
| ), | ), | ||||
| TruckSelection AS ( | TruckSelection AS ( | ||||
| SELECT | SELECT | ||||
| @@ -161,7 +160,7 @@ class DoReleaseCoordinatorService( | |||||
| CONCAT('TI-', | CONCAT('TI-', | ||||
| DATE_FORMAT(dpo2.RequiredDeliveryDate, '%Y%m%d'), | DATE_FORMAT(dpo2.RequiredDeliveryDate, '%Y%m%d'), | ||||
| '-', | '-', | ||||
| COALESCE(ts.preferred_floor, '2F'), | |||||
| REPLACE(COALESCE(dpo2.store_id, ts.preferred_floor, '2F'), '/', ''), | |||||
| '-', | '-', | ||||
| LPAD( | LPAD( | ||||
| ROW_NUMBER() OVER ( | ROW_NUMBER() OVER ( | ||||
| @@ -5,4 +5,6 @@ import org.springframework.stereotype.Repository | |||||
| @Repository | @Repository | ||||
| interface JobOrderProcessRepository : AbstractRepository<JobOrderProcess, Long> { | interface JobOrderProcessRepository : AbstractRepository<JobOrderProcess, Long> { | ||||
| fun findAllByJo_Id(joId: Long): List<JobOrderProcess> | |||||
| } | } | ||||
| @@ -34,6 +34,11 @@ import com.ffii.fpsms.modules.jobOrder.web.model.AllJoPickOrderResponse | |||||
| import com.ffii.fpsms.modules.jobOrder.entity.JobOrderRepository | import com.ffii.fpsms.modules.jobOrder.entity.JobOrderRepository | ||||
| import com.ffii.fpsms.modules.jobOrder.entity.JobTypeRepository | import com.ffii.fpsms.modules.jobOrder.entity.JobTypeRepository | ||||
| import com.ffii.fpsms.modules.master.entity.ItemsRepository | import com.ffii.fpsms.modules.master.entity.ItemsRepository | ||||
| import com.ffii.fpsms.modules.productProcess.entity.ProductProcessRepository | |||||
| import com.ffii.fpsms.modules.jobOrder.enums.JobOrderStatus | |||||
| import com.ffii.fpsms.modules.productProcess.entity.ProductProcessLineRepository | |||||
| import com.ffii.fpsms.modules.stock.entity.StockOutRepository | |||||
| import com.ffii.fpsms.modules.jobOrder.entity.JobOrderProcessRepository | |||||
| @Service | @Service | ||||
| open class JoPickOrderService( | open class JoPickOrderService( | ||||
| private val joPickOrderRepository: JoPickOrderRepository, | private val joPickOrderRepository: JoPickOrderRepository, | ||||
| @@ -50,7 +55,11 @@ open class JoPickOrderService( | |||||
| private val stockOutLineRepository: StockOutLIneRepository, | private val stockOutLineRepository: StockOutLIneRepository, | ||||
| private val jobOrderRepository: JobOrderRepository, | private val jobOrderRepository: JobOrderRepository, | ||||
| private val jobTypeRepository: JobTypeRepository, | private val jobTypeRepository: JobTypeRepository, | ||||
| private val itemsRepository: ItemsRepository | |||||
| private val itemsRepository: ItemsRepository, | |||||
| private val productProcessRepository: ProductProcessRepository, | |||||
| private val productProcessLineRepository: ProductProcessLineRepository, | |||||
| private val stockOutRepository: StockOutRepository, | |||||
| private val jobOrderProcessRepository: JobOrderProcessRepository | |||||
| ) { | ) { | ||||
| @@ -1951,5 +1960,113 @@ open fun updateRecordHandledByForItem(pickOrderId: Long, itemId: Long, userId: L | |||||
| return joPickOrderRecordRepository.save(joPickOrderRecord) | return joPickOrderRecordRepository.save(joPickOrderRecord) | ||||
| } | } | ||||
| @Transactional(rollbackFor = [Exception::class]) | |||||
| open fun deleteJoPickOrderJobOrderProductProcessPickOrder(jobOrderId: Long): MessageResponse { | |||||
| println("=== deleteJoPickOrderJobOrderProductProcessPickOrder ===") | |||||
| println("jobOrderId: $jobOrderId") | |||||
| // 1. 只允许删除 PLANNING 的工单 | |||||
| val jobOrder = jobOrderRepository.findById(jobOrderId) | |||||
| .orElseThrow { NoSuchElementException("JobOrder $jobOrderId not found") } | |||||
| if (jobOrder.status != JobOrderStatus.PLANNING) { | |||||
| return null as MessageResponse; | |||||
| } | |||||
| // 2. Product Process & Product Process Line | |||||
| val productProcesses = productProcessRepository.findByJobOrder_Id(jobOrderId) | |||||
| val productProcessIds = productProcesses.mapNotNull { it.id } | |||||
| if (productProcessIds.isNotEmpty()) { | |||||
| val productProcessLines = | |||||
| productProcessLineRepository.findByProductProcess_IdIn(productProcessIds) | |||||
| if (productProcessLines.isNotEmpty()) { | |||||
| productProcessLineRepository.deleteAll(productProcessLines) | |||||
| } | |||||
| productProcessRepository.deleteAll(productProcesses) | |||||
| } | |||||
| // 3. PickOrder / PickOrderLine / JoPickOrder / JoPickOrderRecord | |||||
| val pickOrders = pickOrderRepository.findAllByJobOrder_Id(jobOrderId) | |||||
| if (pickOrders.isNotEmpty()) { | |||||
| val pickOrderIds = pickOrders.mapNotNull { it.id } | |||||
| // 3.1 所有 pick order line | |||||
| val pickOrderLines = pickOrderIds.flatMap { poId -> | |||||
| pickOrderLineRepository.findAllByPickOrderId(poId) | |||||
| } | |||||
| val pickOrderLineIds = pickOrderLines.mapNotNull { it.id } | |||||
| // 3.2 jo_pick_order & jo_pick_order_record | |||||
| pickOrderIds.forEach { poId -> | |||||
| val joPickOrders = joPickOrderRepository.findByPickOrderId(poId) | |||||
| if (joPickOrders.isNotEmpty()) { | |||||
| joPickOrderRepository.deleteAll(joPickOrders) | |||||
| } | |||||
| val joPickOrderRecords = joPickOrderRecordRepository.findByPickOrderId(poId) | |||||
| if (joPickOrderRecords.isNotEmpty()) { | |||||
| joPickOrderRecordRepository.deleteAll(joPickOrderRecords) | |||||
| } | |||||
| } | |||||
| // 4. SuggestedPickLot & 释放 inventoryLotLine.holdQty | |||||
| if (pickOrderLineIds.isNotEmpty()) { | |||||
| val suggestedPickLots = | |||||
| suggestPickLotRepository.findAllByPickOrderLineIdIn(pickOrderLineIds) | |||||
| if (suggestedPickLots.isNotEmpty()) { | |||||
| val lotLineIds = suggestedPickLots.mapNotNull { it.suggestedLotLine?.id }.distinct() | |||||
| if (lotLineIds.isNotEmpty()) { | |||||
| val lotLines = inventoryLotLineRepository.findAllByIdIn(lotLineIds) | |||||
| // 把当初 hold 住的 qty 减回去(releaseJobOrder 里是直接 + qty,这里反向 - qty) | |||||
| lotLines.forEach { ill -> | |||||
| val totalQtyToRelease = suggestedPickLots | |||||
| .filter { it.suggestedLotLine?.id == ill.id } | |||||
| .mapNotNull { it.qty } | |||||
| .fold(BigDecimal.ZERO) { acc, q -> acc + q } | |||||
| if (totalQtyToRelease > BigDecimal.ZERO) { | |||||
| val current = ill.holdQty ?: BigDecimal.ZERO | |||||
| ill.holdQty = current - totalQtyToRelease | |||||
| } | |||||
| } | |||||
| inventoryLotLineRepository.saveAll(lotLines) | |||||
| } | |||||
| suggestPickLotRepository.deleteAll(suggestedPickLots) | |||||
| } | |||||
| } | |||||
| // 5. StockOut & StockOutLine(按 consoPickOrderCode 关联) | |||||
| pickOrders | |||||
| .mapNotNull { it.consoCode } | |||||
| .mapNotNull { stockOutRepository.findByConsoPickOrderCode(it).orElse(null) } | |||||
| .forEach { stockOut -> | |||||
| val lines = stockOutLineRepository.findAllByStockOutId(stockOut.id!!) | |||||
| if (lines.isNotEmpty()) { | |||||
| stockOutLineRepository.deleteAll(lines) | |||||
| } | |||||
| stockOutRepository.delete(stockOut) | |||||
| } | |||||
| // 6. 最后删 PickOrderLine & PickOrder | |||||
| if (pickOrderLines.isNotEmpty()) { | |||||
| pickOrderLineRepository.deleteAll(pickOrderLines) | |||||
| } | |||||
| pickOrderRepository.deleteAll(pickOrders) | |||||
| } | |||||
| val jobOrderProcesses = jobOrderProcessRepository.findAllByJo_Id(jobOrderId) | |||||
| if (jobOrderProcesses.isNotEmpty()) { | |||||
| jobOrderProcessRepository.deleteAll(jobOrderProcesses) | |||||
| } | |||||
| // 7. 最后删除 JobOrder 本身 | |||||
| jobOrderRepository.delete(jobOrder) | |||||
| return MessageResponse( | |||||
| id = jobOrder.id, | |||||
| code = jobOrder.code, | |||||
| name = jobOrder.bom?.name, | |||||
| type = null, | |||||
| message = "Job Order deleted", | |||||
| errorPosition = null | |||||
| ) | |||||
| } | |||||
| } | } | ||||
| @@ -289,4 +289,10 @@ fun updateJoPickOrderHandledBy(@Valid @RequestBody request: UpdateJoPickOrderHan | |||||
| fun getAllJobTypes(): List<JobTypeResponse> { | fun getAllJobTypes(): List<JobTypeResponse> { | ||||
| return jobOrderService.getAllJobTypes() | return jobOrderService.getAllJobTypes() | ||||
| } | } | ||||
| @PostMapping("/demo/deleteJobOrder/{jobOrderId}") | |||||
| fun deleteJoPickOrderJobOrderProductProcessPickOrder(@PathVariable jobOrderId: Long): MessageResponse { | |||||
| return joPickOrderService.deleteJoPickOrderJobOrderProductProcessPickOrder(jobOrderId) | |||||
| } | } | ||||
| } | |||||
| @@ -25,4 +25,6 @@ interface ShopRepository : AbstractRepository<Shop, Long> { | |||||
| fun findM18IdsByCodeNotRegexpAndTypeAndDeletedIsFalse(codeNotRegexp: String, type: String): List<Long>? | fun findM18IdsByCodeNotRegexpAndTypeAndDeletedIsFalse(codeNotRegexp: String, type: String): List<Long>? | ||||
| fun findShopComboByTypeAndDeletedIsFalse(type: ShopType): List<ShopCombo> | fun findShopComboByTypeAndDeletedIsFalse(type: ShopType): List<ShopCombo> | ||||
| fun findByCode(code: String): Shop? | |||||
| } | } | ||||
| @@ -1,6 +1,7 @@ | |||||
| package com.ffii.fpsms.modules.master.entity.projections | package com.ffii.fpsms.modules.master.entity.projections | ||||
| import org.springframework.beans.factory.annotation.Value | import org.springframework.beans.factory.annotation.Value | ||||
| import java.math.BigDecimal | |||||
| interface BomCombo { | interface BomCombo { | ||||
| val id: Long; | val id: Long; | ||||
| @@ -8,4 +9,5 @@ interface BomCombo { | |||||
| val value: Long; | val value: Long; | ||||
| @get:Value("#{target.code} - #{target.name} - #{target.item.itemUoms.^[salesUnit == true && deleted == false]?.uom.udfudesc}") | @get:Value("#{target.code} - #{target.name} - #{target.item.itemUoms.^[salesUnit == true && deleted == false]?.uom.udfudesc}") | ||||
| val label: String; | val label: String; | ||||
| val outputQty: BigDecimal; | |||||
| } | } | ||||
| @@ -7,21 +7,26 @@ import org.springframework.stereotype.Repository | |||||
| @Repository | @Repository | ||||
| interface TruckRepository : AbstractRepository<Truck, Long> { | interface TruckRepository : AbstractRepository<Truck, Long> { | ||||
| fun findByShopIdAndDeletedFalse(shopId: Long): List<Truck> | fun findByShopIdAndDeletedFalse(shopId: Long): List<Truck> | ||||
| @Query("SELECT t FROM Truck t WHERE t.shop.id = :shopId AND t.deleted = false ORDER BY t.id ASC") | @Query("SELECT t FROM Truck t WHERE t.shop.id = :shopId AND t.deleted = false ORDER BY t.id ASC") | ||||
| fun findFirstByShopIdAndDeletedFalse(@Param("shopId") shopId: Long): List<Truck> | fun findFirstByShopIdAndDeletedFalse(@Param("shopId") shopId: Long): List<Truck> | ||||
| // 使用新的 TruckLanceCode 字段名 | // 使用新的 TruckLanceCode 字段名 | ||||
| @Query("SELECT t FROM Truck t WHERE t.truckLanceCode = :truckLanceCode AND t.deleted = false") | @Query("SELECT t FROM Truck t WHERE t.truckLanceCode = :truckLanceCode AND t.deleted = false") | ||||
| fun findByTruckLanceCodeAndDeletedFalse(@Param("truckLanceCode") truckLanceCode: String): Truck? | fun findByTruckLanceCodeAndDeletedFalse(@Param("truckLanceCode") truckLanceCode: String): Truck? | ||||
| // 按 ShopCode 查询 | // 按 ShopCode 查询 | ||||
| @Query("SELECT t FROM Truck t WHERE t.shopCode = :shopCode AND t.deleted = false") | @Query("SELECT t FROM Truck t WHERE t.shopCode = :shopCode AND t.deleted = false") | ||||
| fun findByShopCodeAndDeletedFalse(@Param("shopCode") shopCode: String): List<Truck> | fun findByShopCodeAndDeletedFalse(@Param("shopCode") shopCode: String): List<Truck> | ||||
| // 按 Store_id 查询 | // 按 Store_id 查询 | ||||
| fun findByStoreIdAndDeletedFalse(storeId: Int): List<Truck> | fun findByStoreIdAndDeletedFalse(storeId: Int): List<Truck> | ||||
| // 按 TruckLanceCode 查询 | |||||
| fun findByTruckLanceCode(truckLanceCode: String): Truck? | |||||
| fun findByShopNameAndStoreIdAndTruckLanceCode(shopName: String, storeId: Int, truckLanceCode: String): Truck? | |||||
| fun findByShopCodeAndStoreId(shopCode: String, storeId: Int): Truck? | |||||
| } | } | ||||
| @@ -0,0 +1,198 @@ | |||||
| package com.ffii.fpsms.modules.pickOrder.service | |||||
| import com.ffii.core.support.AbstractBaseEntityService | |||||
| import com.ffii.core.support.JdbcDao | |||||
| import com.ffii.core.utils.ExcelUtils | |||||
| import com.ffii.core.utils.JwtTokenUtil | |||||
| import com.ffii.fpsms.modules.master.entity.ItemsRepository | |||||
| import com.ffii.fpsms.modules.master.entity.Warehouse | |||||
| import com.ffii.fpsms.modules.master.entity.WarehouseRepository | |||||
| import com.ffii.fpsms.modules.master.entity.projections.WarehouseCombo | |||||
| import com.ffii.fpsms.modules.master.web.models.SaveWarehouseRequest | |||||
| import com.ffii.fpsms.modules.master.web.models.NewWarehouseRequest | |||||
| import com.ffii.fpsms.modules.purchaseOrder.entity.PurchaseOrderLineRepository | |||||
| import com.ffii.fpsms.modules.stock.entity.InventoryLotRepository | |||||
| import com.ffii.fpsms.modules.stock.entity.StockInLine | |||||
| import com.ffii.fpsms.modules.stock.entity.StockInLineRepository | |||||
| import com.ffii.fpsms.modules.stock.entity.StockInRepository | |||||
| import com.ffii.fpsms.modules.stock.service.InventoryLotService | |||||
| import com.ffii.fpsms.modules.stock.service.StockInService | |||||
| import org.apache.poi.ss.usermodel.Sheet | |||||
| import org.apache.poi.ss.usermodel.Workbook | |||||
| import org.slf4j.Logger | |||||
| import org.slf4j.LoggerFactory | |||||
| import org.springframework.stereotype.Service | |||||
| import java.math.BigDecimal | |||||
| import kotlin.jvm.optionals.getOrNull | |||||
| import com.ffii.fpsms.modules.pickOrder.entity.Truck | |||||
| import com.ffii.fpsms.modules.pickOrder.entity.TruckRepository | |||||
| import com.ffii.fpsms.modules.pickOrder.web.models.SaveTruckRequest | |||||
| import java.time.LocalTime | |||||
| import java.time.format.DateTimeFormatter | |||||
| import com.ffii.fpsms.modules.master.entity.ShopRepository | |||||
| @Service | |||||
| open class TruckService( | |||||
| private val jdbcDao: JdbcDao, | |||||
| private val truckRepository: TruckRepository, | |||||
| private val shopRepository: ShopRepository, | |||||
| ) : AbstractBaseEntityService<Truck, Long, TruckRepository>(jdbcDao, truckRepository) { | |||||
| open fun saveTruck(request: SaveTruckRequest): Truck { | |||||
| val truck = request.id?.let { | |||||
| truckRepository.findById(it).orElse(null) | |||||
| } ?: Truck() | |||||
| val shop = shopRepository.findById(request.shopId).orElse(null) | |||||
| if (shop == null) { | |||||
| throw IllegalArgumentException("Shop not found with id: ${request.shopId}") | |||||
| } | |||||
| truck.apply { | |||||
| this.storeId = request.store_id | |||||
| this.truckLanceCode = request.truckLanceCode | |||||
| this.departureTime = request.departureTime | |||||
| this.shop = shop | |||||
| this.shopName = request.shopName | |||||
| this.shopCode = request.shopCode | |||||
| this.loadingSequence = request.loadingSequence | |||||
| } | |||||
| return truckRepository.save(truck); | |||||
| } | |||||
| private fun parseDepartureTime(timeStr: String?): LocalTime? { | |||||
| if (timeStr.isNullOrBlank()) return null | |||||
| return try { | |||||
| val cleaned = timeStr.trim().uppercase().replace(" ", "") | |||||
| // 处理 3:00AM / 5:30PM 这类 12 小时制 | |||||
| if (cleaned.contains("AM") || cleaned.contains("PM")) { | |||||
| val isPM = cleaned.contains("PM") | |||||
| val timePart = cleaned.replace("AM", "").replace("PM", "") | |||||
| val parts = timePart.split(":") | |||||
| if (parts.size == 2) { | |||||
| var hour = parts[0].toInt() | |||||
| val minute = parts[1].toIntOrNull() ?: 0 | |||||
| if (isPM && hour != 12) hour += 12 | |||||
| if (!isPM && hour == 12) hour = 0 | |||||
| LocalTime.of(hour, minute) | |||||
| } else null | |||||
| } else { | |||||
| // 处理 17:30 / 3:00 这类 24 小时制 | |||||
| try { | |||||
| LocalTime.parse(timeStr.trim(), DateTimeFormatter.ofPattern("H:mm")) | |||||
| } catch (_: Exception) { | |||||
| LocalTime.parse(timeStr.trim(), DateTimeFormatter.ofPattern("HH:mm")) | |||||
| } | |||||
| } | |||||
| } catch (e: Exception) { | |||||
| logger.warn("Failed to parse departure time: $timeStr", e) | |||||
| null | |||||
| } | |||||
| } | |||||
| private fun normalizeShopCode(shopCode: String): String { | |||||
| val firstDigitIndex = shopCode.indexOfFirst { it.isDigit() } | |||||
| if (firstDigitIndex == -1) { | |||||
| return shopCode | |||||
| } | |||||
| val letterPart = shopCode.substring(0, firstDigitIndex) | |||||
| val numberPart = shopCode.substring(firstDigitIndex) | |||||
| val normalizedNumber = if (numberPart.startsWith("0") && numberPart.length > 1) { | |||||
| numberPart.substring(1) | |||||
| } else { | |||||
| numberPart | |||||
| } | |||||
| return letterPart + normalizedNumber | |||||
| } | |||||
| open fun importExcel(workbook: Workbook?): String { | |||||
| logger.info("--------- Start - Import Warehouse Excel -------"); | |||||
| if (workbook == null) { | |||||
| logger.error("No Excel Import"); | |||||
| return "Import Excel failure"; | |||||
| } | |||||
| val sheet: Sheet = workbook.getSheetAt(0); | |||||
| // Columns | |||||
| val COLUMN_STORE_ID_INDEX = 0; | |||||
| val COLUMN_REQUIRED_DELIVERY_DATE_INDEX = 1; | |||||
| val COLUMN_DEPARTURE_TIME_INDEX = 2; | |||||
| val COLUMN_TRUCK_LANCE_CODE_INDEX = 3; | |||||
| val COLUMN_SHOP_CODE_INDEX = 4; | |||||
| val COLUMN_SHOP_NAME_INDEX = 5; | |||||
| val COLUMN_LOADING_SEQUENCE_INDEX = 6; | |||||
| val COLUMN_REMARK_INDEX = 7; | |||||
| val START_ROW_INDEX = 3; | |||||
| logger.info("Total rows in sheet: ${sheet.lastRowNum + 1}, Processing from row ${START_ROW_INDEX + 1} to ${sheet.lastRowNum + 1}"); | |||||
| // Start Import | |||||
| for (i in START_ROW_INDEX..<sheet.lastRowNum) { | |||||
| val row = sheet.getRow(i) | |||||
| try { | |||||
| val truckLanceCode = ExcelUtils.getStringValue(row.getCell(COLUMN_TRUCK_LANCE_CODE_INDEX)).trim() | |||||
| val departureTimeStr = ExcelUtils.getStringValue(row.getCell(COLUMN_DEPARTURE_TIME_INDEX)).trim() | |||||
| val shopName = ExcelUtils.getStringValue(row.getCell(COLUMN_SHOP_NAME_INDEX)).trim() | |||||
| val shopCode = ExcelUtils.getStringValue(row.getCell(COLUMN_SHOP_CODE_INDEX)).trim() | |||||
| val loadingSequence = ExcelUtils.getIntValue(row.getCell(COLUMN_LOADING_SEQUENCE_INDEX)) | |||||
| val store_id = ExcelUtils.getStringValue(row.getCell(COLUMN_STORE_ID_INDEX)).trim() | |||||
| //val remark = ExcelUtils.getStringValue(row.getCell(COLUMN_REMARK_INDEX)).trim() | |||||
| val storeIdInt = when (store_id.uppercase()) { | |||||
| "2F" -> 2 | |||||
| "4F" -> 4 | |||||
| "3F" -> 3 | |||||
| else -> { | |||||
| logger.warn("Invalid store_id '${store_id}', defaulting to 2") | |||||
| 2 | |||||
| } | |||||
| } | |||||
| val departureTime = parseDepartureTime(departureTimeStr) | |||||
| if (departureTime == null) { | |||||
| logger.warn("Row ${i + 1}: Invalid departure time '$departureTimeStr', skipping") | |||||
| continue | |||||
| } | |||||
| val normalizedShopCode = normalizeShopCode(shopCode) | |||||
| val shop = shopRepository.findAllByDeletedIsFalse().firstOrNull { it.code == normalizedShopCode } | |||||
| //println("shop: ${shop}") | |||||
| val existingTruck = truckRepository.findByShopCodeAndStoreId(shopCode, storeIdInt) | |||||
| if (existingTruck != null) { | |||||
| val truckRequest = SaveTruckRequest( | |||||
| id = existingTruck.id, | |||||
| store_id = storeIdInt, | |||||
| truckLanceCode = truckLanceCode ?: existingTruck.truckLanceCode ?: "", | |||||
| departureTime = departureTime, | |||||
| shopId = shop?.id!!, | |||||
| shopName = shopName?: "", | |||||
| shopCode = shopCode, | |||||
| loadingSequence = loadingSequence | |||||
| ) | |||||
| saveTruck(truckRequest) | |||||
| } else { | |||||
| // 创建新记录 | |||||
| val truckRequest = SaveTruckRequest( | |||||
| id = null, | |||||
| store_id = storeIdInt, | |||||
| truckLanceCode = truckLanceCode ?: "", | |||||
| departureTime = departureTime, | |||||
| shopId = shop?.id!!, | |||||
| shopName = shopName?: "", | |||||
| shopCode = shopCode, | |||||
| loadingSequence = loadingSequence | |||||
| ) | |||||
| saveTruck(truckRequest) | |||||
| } | |||||
| } catch (e: Exception) { | |||||
| logger.error("Import Error (Warehouse Error): ${e.message}") | |||||
| } | |||||
| } | |||||
| logger.info("--------- End - Import Warehouse Excel -------") | |||||
| return "Import Excel success"; | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,85 @@ | |||||
| package com.ffii.fpsms.modules.pickOrder.web | |||||
| import com.ffii.fpsms.modules.master.web.models.MessageResponse | |||||
| import org.springframework.web.bind.ServletRequestBindingException | |||||
| import jakarta.servlet.http.HttpServletRequest | |||||
| import org.apache.poi.ss.usermodel.Workbook | |||||
| import org.apache.poi.xssf.usermodel.XSSFWorkbook | |||||
| import org.springframework.http.ResponseEntity | |||||
| import org.springframework.web.bind.annotation.* | |||||
| import org.springframework.web.multipart.MultipartHttpServletRequest | |||||
| import com.ffii.fpsms.modules.pickOrder.web.models.SaveTruckRequest | |||||
| import com.ffii.fpsms.modules.pickOrder.service.TruckService | |||||
| import com.ffii.fpsms.modules.pickOrder.entity.TruckRepository | |||||
| @RestController | |||||
| @RequestMapping("/truck") | |||||
| class TruckController( | |||||
| private val truckService: TruckService, | |||||
| private val truckRepository: TruckRepository, | |||||
| ) { | |||||
| @PostMapping("/save") | |||||
| fun saveTruck(@RequestBody request: SaveTruckRequest): MessageResponse { | |||||
| try { | |||||
| val truck = truckService.saveTruck(request) | |||||
| return MessageResponse( | |||||
| id = truck.id, | |||||
| name = truck.shopName, | |||||
| code = truck.truckLanceCode, | |||||
| type = "truck", | |||||
| message = if (truck.id != null) "Truck updated successfully" else "Truck created successfully", | |||||
| errorPosition = null, | |||||
| entity = truck | |||||
| ) | |||||
| } catch (e: Exception) { | |||||
| return MessageResponse( | |||||
| id = null, | |||||
| name = null, | |||||
| code = null, | |||||
| type = "truck", | |||||
| message = "Error: ${e.message}", | |||||
| errorPosition = null, | |||||
| entity = null | |||||
| ) | |||||
| } | |||||
| } | |||||
| @PostMapping("/importExcel") | |||||
| @Throws(ServletRequestBindingException::class) | |||||
| fun importExcel(request: HttpServletRequest): ResponseEntity<*> { | |||||
| var workbook: Workbook? = null | |||||
| try { | |||||
| val multipartFile = (request as MultipartHttpServletRequest).getFile("multipartFileList") | |||||
| workbook = XSSFWorkbook(multipartFile?.inputStream) | |||||
| } catch (e: Exception) { | |||||
| println("Error reading Excel file: ${e.message}") | |||||
| return ResponseEntity.badRequest().body( | |||||
| MessageResponse( | |||||
| id = null, | |||||
| name = null, | |||||
| code = null, | |||||
| type = "truck", | |||||
| message = "Error reading Excel file: ${e.message}", | |||||
| errorPosition = null, | |||||
| entity = null | |||||
| ) | |||||
| ) | |||||
| } | |||||
| val result = truckService.importExcel(workbook) | |||||
| return ResponseEntity.ok( | |||||
| MessageResponse( | |||||
| id = null, | |||||
| name = null, | |||||
| code = null, | |||||
| type = "truck", | |||||
| message = result, | |||||
| errorPosition = null, | |||||
| entity = null | |||||
| ) | |||||
| ) | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,12 @@ | |||||
| package com.ffii.fpsms.modules.pickOrder.web.models | |||||
| import java.time.LocalTime | |||||
| data class SaveTruckRequest( | |||||
| val id: Long? = null, | |||||
| val store_id: Int, | |||||
| val truckLanceCode: String, | |||||
| val departureTime: LocalTime, | |||||
| val shopId: Long, | |||||
| val shopName: String, | |||||
| val shopCode: String, | |||||
| val loadingSequence: Int, | |||||
| ) | |||||
| @@ -142,7 +142,11 @@ open class SuggestedPickLotService( | |||||
| if (remainingQtyToAllocate <= zero) return@forEachIndexed | if (remainingQtyToAllocate <= zero) return@forEachIndexed | ||||
| println("calculateRemainingQtyForInfo(lotLine) ${calculateRemainingQtyForInfo(lotLine)}") | println("calculateRemainingQtyForInfo(lotLine) ${calculateRemainingQtyForInfo(lotLine)}") | ||||
| val inventoryLotLine = lotLine.id?.let { inventoryLotLineService.findById(it).getOrNull() } | |||||
| val warehouseStoreId=inventoryLotLine?.warehouse?.store_id | |||||
| if ( warehouseStoreId == "3F") { | |||||
| return@forEachIndexed | |||||
| } | |||||
| // 修复:计算可用数量,转换为销售单位 | // 修复:计算可用数量,转换为销售单位 | ||||
| val availableQtyInBaseUnits = calculateRemainingQtyForInfo(lotLine) | val availableQtyInBaseUnits = calculateRemainingQtyForInfo(lotLine) | ||||
| val holdQtyInBaseUnits = holdQtyMap[lotLine.id] ?: zero | val holdQtyInBaseUnits = holdQtyMap[lotLine.id] ?: zero | ||||
| @@ -157,7 +161,7 @@ open class SuggestedPickLotService( | |||||
| return@forEachIndexed | return@forEachIndexed | ||||
| } | } | ||||
| println("$index : ${lotLine.id}") | println("$index : ${lotLine.id}") | ||||
| val inventoryLotLine = lotLine.id?.let { inventoryLotLineService.findById(it).getOrNull() } | |||||
| // val inventoryLotLine = lotLine.id?.let { inventoryLotLineService.findById(it).getOrNull() } | |||||
| val originalHoldQty = inventoryLotLine?.holdQty | val originalHoldQty = inventoryLotLine?.holdQty | ||||
| // 修复:在销售单位中计算分配数量 | // 修复:在销售单位中计算分配数量 | ||||
| @@ -902,7 +906,7 @@ private fun generateOptimalSuggestionsForAllPickOrders( | |||||
| val lotEntities = inventoryLotLineRepository.findAllByIdIn(availableLots.mapNotNull { it.id }) | val lotEntities = inventoryLotLineRepository.findAllByIdIn(availableLots.mapNotNull { it.id }) | ||||
| lotEntities.forEach { lot -> lot.holdQty = BigDecimal.ZERO } | lotEntities.forEach { lot -> lot.holdQty = BigDecimal.ZERO } | ||||
| // FIX: Calculate remaining quantity for each pick order line | |||||
| // FIX: Calculate remaining quantity for each pick or der line | |||||
| // FIX: Calculate remaining quantity for each pick order line | // FIX: Calculate remaining quantity for each pick order line | ||||
| val remainingQtyPerLine = pickOrderLines.associate { pol -> | val remainingQtyPerLine = pickOrderLines.associate { pol -> | ||||
| val stockOutLines = stockOutLIneRepository.findAllByPickOrderLineIdAndDeletedFalse(pol.id!!) | val stockOutLines = stockOutLIneRepository.findAllByPickOrderLineIdAndDeletedFalse(pol.id!!) | ||||
| @@ -929,7 +933,10 @@ private fun generateOptimalSuggestionsForAllPickOrders( | |||||
| lotEntities.forEach { lot -> | lotEntities.forEach { lot -> | ||||
| if (remainingPickOrderLines.isEmpty()) return@forEach | if (remainingPickOrderLines.isEmpty()) return@forEach | ||||
| val warehouseStoreId=lot.warehouse?.store_id | |||||
| if ( warehouseStoreId == "3F") { | |||||
| return@forEach | |||||
| } | |||||
| val totalQty = lot.inQty ?: zero | val totalQty = lot.inQty ?: zero | ||||
| val outQty = lot.outQty ?: zero | val outQty = lot.outQty ?: zero | ||||
| val holdQty = lot.holdQty ?: zero // This should be 0 now | val holdQty = lot.holdQty ?: zero // This should be 0 now | ||||
| @@ -1204,7 +1211,10 @@ private fun generateCorrectSuggestionsWithExistingHolds(pickOrder: PickOrder): L | |||||
| val lot = lotInfo.id?.let { inventoryLotLineRepository.findById(it).orElse(null) } | val lot = lotInfo.id?.let { inventoryLotLineRepository.findById(it).orElse(null) } | ||||
| ?: return@forEach | ?: return@forEach | ||||
| val warehouseStoreId=lot.warehouse?.store_id | |||||
| if ( warehouseStoreId == "3F") { | |||||
| return@forEach | |||||
| } | |||||
| val totalQty = lot.inQty ?: zero | val totalQty = lot.inQty ?: zero | ||||
| val outQty = lot.outQty ?: zero | val outQty = lot.outQty ?: zero | ||||
| val holdQty = lot.holdQty ?: zero | val holdQty = lot.holdQty ?: zero | ||||