Browse Source

update

master
CANCERYS\kw093 1 week ago
parent
commit
88cdd85d94
13 changed files with 499 additions and 51 deletions
  1. +23
    -14
      src/main/java/com/ffii/fpsms/modules/deliveryOrder/service/DeliveryOrderService.kt
  2. +2
    -1
      src/main/java/com/ffii/fpsms/modules/deliveryOrder/service/DoPickOrderQueryService.kt
  3. +24
    -25
      src/main/java/com/ffii/fpsms/modules/deliveryOrder/service/DoReleaseCoordinatorService.kt
  4. +2
    -0
      src/main/java/com/ffii/fpsms/modules/jobOrder/entity/JobOrderProcessRepository.kt
  5. +118
    -1
      src/main/java/com/ffii/fpsms/modules/jobOrder/service/JoPickOrderService.kt
  6. +6
    -0
      src/main/java/com/ffii/fpsms/modules/jobOrder/web/JobOrderController.kt
  7. +2
    -0
      src/main/java/com/ffii/fpsms/modules/master/entity/ShopRepository.kt
  8. +2
    -0
      src/main/java/com/ffii/fpsms/modules/master/entity/projections/BomCombo.kt
  9. +10
    -5
      src/main/java/com/ffii/fpsms/modules/pickOrder/entity/TruckRepository.kt
  10. +198
    -0
      src/main/java/com/ffii/fpsms/modules/pickOrder/service/TruckService.kt
  11. +85
    -0
      src/main/java/com/ffii/fpsms/modules/pickOrder/web/TruckController.kt
  12. +12
    -0
      src/main/java/com/ffii/fpsms/modules/pickOrder/web/models/SaveTruckRequest.kt
  13. +15
    -5
      src/main/java/com/ffii/fpsms/modules/stock/service/SuggestedPickLotService.kt

+ 23
- 14
src/main/java/com/ffii/fpsms/modules/deliveryOrder/service/DeliveryOrderService.kt View File

@@ -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"


+ 2
- 1
src/main/java/com/ffii/fpsms/modules/deliveryOrder/service/DoPickOrderQueryService.kt View File

@@ -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) ->


+ 24
- 25
src/main/java/com/ffii/fpsms/modules/deliveryOrder/service/DoReleaseCoordinatorService.kt View File

@@ -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 (


+ 2
- 0
src/main/java/com/ffii/fpsms/modules/jobOrder/entity/JobOrderProcessRepository.kt View File

@@ -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>
} }

+ 118
- 1
src/main/java/com/ffii/fpsms/modules/jobOrder/service/JoPickOrderService.kt View File

@@ -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
)
}


} }

+ 6
- 0
src/main/java/com/ffii/fpsms/modules/jobOrder/web/JobOrderController.kt View File

@@ -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)
} }
}

+ 2
- 0
src/main/java/com/ffii/fpsms/modules/master/entity/ShopRepository.kt View File

@@ -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?
} }

+ 2
- 0
src/main/java/com/ffii/fpsms/modules/master/entity/projections/BomCombo.kt View File

@@ -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;
} }

+ 10
- 5
src/main/java/com/ffii/fpsms/modules/pickOrder/entity/TruckRepository.kt View File

@@ -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?
} }

+ 198
- 0
src/main/java/com/ffii/fpsms/modules/pickOrder/service/TruckService.kt View File

@@ -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";
}

}

+ 85
- 0
src/main/java/com/ffii/fpsms/modules/pickOrder/web/TruckController.kt View File

@@ -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
)
)
}

}

+ 12
- 0
src/main/java/com/ffii/fpsms/modules/pickOrder/web/models/SaveTruckRequest.kt View File

@@ -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,
)

+ 15
- 5
src/main/java/com/ffii/fpsms/modules/stock/service/SuggestedPickLotService.kt View File

@@ -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


Loading…
Cancel
Save