| @@ -1182,25 +1182,34 @@ open fun releaseDeliveryOrderWithoutTicket(request: ReleaseDoRequest): ReleaseDo | |||
| val targetDate = deliveryOrder.estimatedArrivalDate?.toLocalDate() ?: LocalDate.now() | |||
| val itemIds = deliveryOrder.deliveryOrderLines.mapNotNull { it.item?.id }.distinct() | |||
| val inventoryQuery = """ | |||
| val itemsStoreIdQuery = """ | |||
| 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() | |||
| 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" | |||
| } else { | |||
| "2F" | |||
| @@ -94,10 +94,11 @@ class DoPickOrderQueryService( | |||
| .groupBy { it.key.first } | |||
| .mapValues { (_, entries) -> | |||
| entries.map { it.value } | |||
| .filter { it.unassigned > 0 } // filter out lanes with no unassigned orders | |||
| .sortedByDescending { it.unassigned } | |||
| .take(3) | |||
| } | |||
| .filterValues { lanes -> lanes.any { it.unassigned > 0 } } | |||
| .filterValues { lanes -> lanes.isNotEmpty() } | |||
| .toSortedMap(compareBy { it }) | |||
| .entries.take(4) | |||
| .map { (time, lanes) -> | |||
| @@ -47,32 +47,31 @@ class DoReleaseCoordinatorService( | |||
| val updateSql = """ | |||
| UPDATE fpsmsdb.do_pick_order dpo | |||
| 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 | |||
| 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 | |||
| LEFT JOIN DoFloorCounts dfc ON dfc.deliveryOrderId = do.id | |||
| LEFT JOIN DoStoreIdCounts dsc ON dsc.deliveryOrderId = do.id | |||
| WHERE do.deleted = 0 | |||
| GROUP BY do.id | |||
| ), | |||
| @@ -89,7 +88,7 @@ class DoReleaseCoordinatorService( | |||
| WHEN count_4f > count_2f THEN 4 | |||
| ELSE 2 | |||
| END AS preferred_store_id | |||
| FROM DoFloorSummary | |||
| FROM DoStoreIdSummary | |||
| ), | |||
| TruckSelection AS ( | |||
| SELECT | |||
| @@ -161,7 +160,7 @@ class DoReleaseCoordinatorService( | |||
| CONCAT('TI-', | |||
| DATE_FORMAT(dpo2.RequiredDeliveryDate, '%Y%m%d'), | |||
| '-', | |||
| COALESCE(ts.preferred_floor, '2F'), | |||
| REPLACE(COALESCE(dpo2.store_id, ts.preferred_floor, '2F'), '/', ''), | |||
| '-', | |||
| LPAD( | |||
| ROW_NUMBER() OVER ( | |||
| @@ -5,4 +5,6 @@ import org.springframework.stereotype.Repository | |||
| @Repository | |||
| 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.JobTypeRepository | |||
| 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 | |||
| open class JoPickOrderService( | |||
| private val joPickOrderRepository: JoPickOrderRepository, | |||
| @@ -50,7 +55,11 @@ open class JoPickOrderService( | |||
| private val stockOutLineRepository: StockOutLIneRepository, | |||
| private val jobOrderRepository: JobOrderRepository, | |||
| 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) | |||
| } | |||
| @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> { | |||
| 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 findShopComboByTypeAndDeletedIsFalse(type: ShopType): List<ShopCombo> | |||
| fun findByCode(code: String): Shop? | |||
| } | |||
| @@ -1,6 +1,7 @@ | |||
| package com.ffii.fpsms.modules.master.entity.projections | |||
| import org.springframework.beans.factory.annotation.Value | |||
| import java.math.BigDecimal | |||
| interface BomCombo { | |||
| val id: Long; | |||
| @@ -8,4 +9,5 @@ interface BomCombo { | |||
| val value: Long; | |||
| @get:Value("#{target.code} - #{target.name} - #{target.item.itemUoms.^[salesUnit == true && deleted == false]?.uom.udfudesc}") | |||
| val label: String; | |||
| val outputQty: BigDecimal; | |||
| } | |||
| @@ -7,21 +7,26 @@ import org.springframework.stereotype.Repository | |||
| @Repository | |||
| interface TruckRepository : AbstractRepository<Truck, Long> { | |||
| 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") | |||
| fun findFirstByShopIdAndDeletedFalse(@Param("shopId") shopId: Long): List<Truck> | |||
| // 使用新的 TruckLanceCode 字段名 | |||
| @Query("SELECT t FROM Truck t WHERE t.truckLanceCode = :truckLanceCode AND t.deleted = false") | |||
| fun findByTruckLanceCodeAndDeletedFalse(@Param("truckLanceCode") truckLanceCode: String): Truck? | |||
| // 按 ShopCode 查询 | |||
| @Query("SELECT t FROM Truck t WHERE t.shopCode = :shopCode AND t.deleted = false") | |||
| fun findByShopCodeAndDeletedFalse(@Param("shopCode") shopCode: String): List<Truck> | |||
| // 按 Store_id 查询 | |||
| 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 | |||
| 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 holdQtyInBaseUnits = holdQtyMap[lotLine.id] ?: zero | |||
| @@ -157,7 +161,7 @@ open class SuggestedPickLotService( | |||
| return@forEachIndexed | |||
| } | |||
| 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 | |||
| // 修复:在销售单位中计算分配数量 | |||
| @@ -902,7 +906,7 @@ private fun generateOptimalSuggestionsForAllPickOrders( | |||
| val lotEntities = inventoryLotLineRepository.findAllByIdIn(availableLots.mapNotNull { it.id }) | |||
| 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 | |||
| val remainingQtyPerLine = pickOrderLines.associate { pol -> | |||
| val stockOutLines = stockOutLIneRepository.findAllByPickOrderLineIdAndDeletedFalse(pol.id!!) | |||
| @@ -929,7 +933,10 @@ private fun generateOptimalSuggestionsForAllPickOrders( | |||
| lotEntities.forEach { lot -> | |||
| if (remainingPickOrderLines.isEmpty()) return@forEach | |||
| val warehouseStoreId=lot.warehouse?.store_id | |||
| if ( warehouseStoreId == "3F") { | |||
| return@forEach | |||
| } | |||
| val totalQty = lot.inQty ?: zero | |||
| val outQty = lot.outQty ?: zero | |||
| 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) } | |||
| ?: return@forEach | |||
| val warehouseStoreId=lot.warehouse?.store_id | |||
| if ( warehouseStoreId == "3F") { | |||
| return@forEach | |||
| } | |||
| val totalQty = lot.inQty ?: zero | |||
| val outQty = lot.outQty ?: zero | |||
| val holdQty = lot.holdQty ?: zero | |||