CANCERYS\kw093 4 недель назад
Родитель
Сommit
bb022e855e
37 измененных файлов: 2574 добавлений и 365 удалений
  1. +3
    -1
      src/main/java/com/ffii/fpsms/modules/bag/entity/joBagConsumptionRepository.kt
  2. +88
    -1
      src/main/java/com/ffii/fpsms/modules/bag/service/bagService.kt
  3. +15
    -0
      src/main/java/com/ffii/fpsms/modules/bag/web/bagController.kt
  4. +56
    -1
      src/main/java/com/ffii/fpsms/modules/bag/web/model/bagReponse.kt
  5. +0
    -1
      src/main/java/com/ffii/fpsms/modules/deliveryOrder/entity/models/DeliveryOrderInfo.kt
  6. +412
    -278
      src/main/java/com/ffii/fpsms/modules/deliveryOrder/service/DeliveryOrderService.kt
  7. +170
    -39
      src/main/java/com/ffii/fpsms/modules/deliveryOrder/service/DoReleaseCoordinatorService.kt
  8. +5
    -0
      src/main/java/com/ffii/fpsms/modules/deliveryOrder/web/DeliveryOrderController.kt
  9. +20
    -0
      src/main/java/com/ffii/fpsms/modules/deliveryOrder/web/models/TicketReleaseTableResponse.kt
  10. +41
    -3
      src/main/java/com/ffii/fpsms/modules/jobOrder/service/JobOrderService.kt
  11. +4
    -0
      src/main/java/com/ffii/fpsms/modules/jobOrder/web/JobOrderController.kt
  12. +4
    -0
      src/main/java/com/ffii/fpsms/modules/jobOrder/web/model/CreateJobOrderRequest.kt
  13. +5
    -0
      src/main/java/com/ffii/fpsms/modules/master/entity/WarehouseRepository.kt
  14. +4
    -4
      src/main/java/com/ffii/fpsms/modules/pickOrder/entity/PickExecutionIssueRepository.kt
  15. +13
    -0
      src/main/java/com/ffii/fpsms/modules/pickOrder/entity/TruckRepository.kt
  16. +4
    -1
      src/main/java/com/ffii/fpsms/modules/productProcess/entity/ProductProcess.kt
  17. +6
    -1
      src/main/java/com/ffii/fpsms/modules/productProcess/entity/ProductProcessLine.kt
  18. +3
    -1
      src/main/java/com/ffii/fpsms/modules/productProcess/entity/projections/ProductProcessInfo.kt
  19. +151
    -31
      src/main/java/com/ffii/fpsms/modules/productProcess/service/ProductProcessService.kt
  20. +8
    -0
      src/main/java/com/ffii/fpsms/modules/productProcess/web/ProductProcessController.kt
  21. +12
    -1
      src/main/java/com/ffii/fpsms/modules/productProcess/web/model/SaveProductProcessRequest.kt
  22. +11
    -0
      src/main/java/com/ffii/fpsms/modules/stock/entity/InventoryLotLineRepository.kt
  23. +1
    -0
      src/main/java/com/ffii/fpsms/modules/stock/entity/InventoryLotRepository.kt
  24. +3
    -0
      src/main/java/com/ffii/fpsms/modules/stock/entity/StockTake.kt
  25. +1
    -0
      src/main/java/com/ffii/fpsms/modules/stock/entity/StockTakeLineRepository.kt
  26. +102
    -0
      src/main/java/com/ffii/fpsms/modules/stock/entity/StockTakeRecord.kt
  27. +24
    -0
      src/main/java/com/ffii/fpsms/modules/stock/entity/StockTakeRecordRepository.kt
  28. +1
    -0
      src/main/java/com/ffii/fpsms/modules/stock/enums/StockTakeEnum.kt
  29. +999
    -0
      src/main/java/com/ffii/fpsms/modules/stock/service/StockTakeRecordService.kt
  30. +66
    -2
      src/main/java/com/ffii/fpsms/modules/stock/service/StockTakeService.kt
  31. +6
    -0
      src/main/java/com/ffii/fpsms/modules/stock/web/StockTakeController.kt
  32. +180
    -0
      src/main/java/com/ffii/fpsms/modules/stock/web/StockTakeRecordController.kt
  33. +1
    -0
      src/main/java/com/ffii/fpsms/modules/stock/web/model/SaveStockTakeRequest.kt
  34. +90
    -0
      src/main/java/com/ffii/fpsms/modules/stock/web/model/StockTakeRecordReponse.kt
  35. +53
    -0
      src/main/resources/db/changelog/changes/20251230_01_Enson/02_alter_table.sql
  36. +5
    -0
      src/main/resources/db/changelog/changes/20251230_01_Enson/04_alter_table.sql
  37. +7
    -0
      src/main/resources/db/changelog/changes/20260105_enson/01_alter_table.sql

+ 3
- 1
src/main/java/com/ffii/fpsms/modules/bag/entity/joBagConsumptionRepository.kt Просмотреть файл

@@ -12,5 +12,7 @@ import java.time.LocalDateTime

@Repository
interface JoBagConsumptionRepository : AbstractRepository<JoBagConsumption, Long> {

fun findAllByDeletedIsFalse(): List<JoBagConsumption>
fun findAllByBagLotLineIdAndDeletedIsFalse(bagLotLineId: Long): List<JoBagConsumption>
fun findAllByBagId(bagId: Long): List<BagLotLine>
}

+ 88
- 1
src/main/java/com/ffii/fpsms/modules/bag/service/bagService.kt Просмотреть файл

@@ -14,13 +14,15 @@ import java.time.LocalTime
import java.time.LocalDateTime
import java.time.LocalDate
import com.ffii.fpsms.modules.jobOrder.entity.JobOrderRepository
import com.ffii.fpsms.modules.productProcess.entity.ProductProcessRepository
@Service
open class BagService(
private val bagRepository: BagRepository,
private val bagLotLineRepository: BagLotLineRepository,
private val joBagConsumptionRepository: JoBagConsumptionRepository,
private val inventoryLotRepository: InventoryLotRepository,
private val jobOrderRepository: JobOrderRepository
private val jobOrderRepository: JobOrderRepository,
private val productProcessRepository: ProductProcessRepository
) {
open fun createBagLotLinesByBagId(request: CreateBagLotLineRequest): MessageResponse {
val bag = bagRepository.findById(request.bagId).orElse(null)
@@ -124,6 +126,14 @@ fun createJoBagConsumption(request: CreateJoBagConsumptionRequest): MessageRespo
this.time = LocalDateTime.now()
}
joBagConsumptionRepository.save(joBagConsumption)
// 更新 ProductProcess 的 submitedBagRecord 为 true
val productProcesses = productProcessRepository.findByJobOrder_Id(request.jobId)
productProcesses.forEach { productProcess ->
productProcess.submitedBagRecord = true
productProcessRepository.save(productProcess)
}
return MessageResponse(
id = null,
code = null,
@@ -153,4 +163,81 @@ open fun getAllBagInfo(): List<BagInfo> {
}
}
}
open fun getAllBagUsageRecords(): List<BagUsageRecordResponse> {
val allRecords = joBagConsumptionRepository.findAllByDeletedIsFalse()
return allRecords.map { record ->
val bag = bagRepository.findById(record.bagId ?: 0L).orElse(null)
val bagLotLine = bagLotLineRepository.findById(record.bagLotLineId ?: 0L).orElse(null)
BagUsageRecordResponse(
id = record.id ?: 0L,
bagId = record.bagId ?: 0L,
bagLotLineId = record.bagLotLineId ?: 0L,
jobId = record.jobId ?: 0L,
jobOrderCode = record.jobOrderCode ?: "",
stockOutLineId = record.stockOutLineId ?: 0L,
startQty = record.startQty ?: 0,
consumedQty = record.consumedQty ?: 0,
scrapQty = record.scrapQty ?: 0,
endQty = record.endQty ?: 0,
date = record.date ?: LocalDate.now(),
time = record.time ?: LocalDateTime.now(),
bagName = bag?.itemName,
bagCode = bag?.itemCode,
lotNo = bagLotLine?.lotNo
)
}
}

open fun getBagSummaries(): List<BagSummaryResponse> {
return bagRepository.findAllByDeletedIsFalse().map { bag ->
BagSummaryResponse(
id = bag.id ?: 0L,
bagName = bag.itemName,
bagCode = bag.itemCode,
takenBagBalance = bag.takenBagBalance,
deleted = bag.deleted
)
}
}

open fun getBagLotLines(bagId: Long): List<BagLotLineResponse> {
val lines = bagLotLineRepository.findAllByBagId(bagId)
return lines.map { line ->
BagLotLineResponse(
id = line.id ?: 0L,
bagId = line.bagId ?: 0L,
lotNo = line.lotNo,
stockOutLineId = line.stockOutLineId,
startQty = line.startQty,
consumedQty = line.consumedQty,
scrapQty = line.scrapQty,
balanceQty = line.balanceQty,
firstUseDate = line.firstUseDate,
lastUseDate = line.lastUseDate
)
}
}


open fun getBagConsumptions(bagLotLineId: Long): List<BagConsumptionResponse> {
val records = joBagConsumptionRepository.findAllByBagLotLineIdAndDeletedIsFalse(bagLotLineId)
return records.map { r ->
BagConsumptionResponse(
id = r.id ?: 0L,
bagId = r.bagId ?: 0L,
bagLotLineId = r.bagLotLineId ?: 0L,
jobId = r.jobId ?: 0L,
jobOrderCode = r.jobOrderCode,
stockOutLineId = r.stockOutLineId,
startQty = r.startQty,
consumedQty = r.consumedQty,
scrapQty = r.scrapQty,
endQty = r.endQty,
date = r.date,
time = r.time
)
}
}
}

+ 15
- 0
src/main/java/com/ffii/fpsms/modules/bag/web/bagController.kt Просмотреть файл

@@ -47,4 +47,19 @@ class BagController(
fun createJoBagConsumption(@RequestBody request: CreateJoBagConsumptionRequest): MessageResponse {
return bagService.createJoBagConsumption(request)
}
@GetMapping("/bagUsageRecords")
fun getBagUsageRecords(): List<BagUsageRecordResponse> {
return bagService.getAllBagUsageRecords()
}
@GetMapping("/bags")
fun getBags(): List<BagSummaryResponse> =
bagService.getBagSummaries()

@GetMapping("/bags/{bagId}/lot-lines")
fun getBagLotLines(@PathVariable bagId: Long): List<BagLotLineResponse> =
bagService.getBagLotLines(bagId)

@GetMapping("/lot-lines/{bagLotLineId}/consumptions")
fun getBagConsumptions(@PathVariable bagLotLineId: Long): List<BagConsumptionResponse> =
bagService.getBagConsumptions(bagLotLineId)
}

+ 56
- 1
src/main/java/com/ffii/fpsms/modules/bag/web/model/bagReponse.kt Просмотреть файл

@@ -1,5 +1,6 @@
package com.ffii.fpsms.modules.bag.web.model

import java.time.LocalDate
import java.time.LocalDateTime
data class GetAllBagInfoResponse(
val bagId: Long,
val bagName: String,
@@ -16,4 +17,58 @@ data class BagInfo(
val code: String,
val balanceQty: Int,

)
data class BagUsageRecordResponse(
val id: Long,
val bagId: Long,
val bagLotLineId: Long,
val jobId: Long,
val jobOrderCode: String,
val stockOutLineId: Long,
val startQty: Int,
val consumedQty: Int,
val scrapQty: Int,
val endQty: Int,
val date: LocalDate,
val time: LocalDateTime,
val bagName: String?,
val bagCode: String?,
val lotNo: String?
)


data class BagSummaryResponse(
val id: Long, // bag.id
val bagName: String?,
val bagCode: String?,
val takenBagBalance: Int?, // 如有
val deleted: Boolean?
)

data class BagLotLineResponse(
val id: Long,
val bagId: Long,
val lotNo: String?,
val stockOutLineId: Long?,
val startQty: Int?,
val consumedQty: Int?,
val scrapQty: Int?,
val balanceQty: Int?,
val firstUseDate: LocalDate?,
val lastUseDate: LocalDate?
)

data class BagConsumptionResponse(
val id: Long,
val bagId: Long,
val bagLotLineId: Long,
val jobId: Long,
val jobOrderCode: String?,
val stockOutLineId: Long?,
val startQty: Int?,
val consumedQty: Int?,
val scrapQty: Int?,
val endQty: Int?,
val date: LocalDate?,
val time: LocalDateTime?
)

+ 0
- 1
src/main/java/com/ffii/fpsms/modules/deliveryOrder/entity/models/DeliveryOrderInfo.kt Просмотреть файл

@@ -28,5 +28,4 @@ interface DeliveryOrderInfo{

val deliveryOrderLines: List<DeliveryOrderLineInfo>


}

+ 412
- 278
src/main/java/com/ffii/fpsms/modules/deliveryOrder/service/DeliveryOrderService.kt Просмотреть файл

@@ -421,19 +421,19 @@ open class DeliveryOrderService(
}
deliveryOrderRepository.save(deliveryOrder)

val pols = deliveryOrder.deliveryOrderLines.map {
SavePickOrderLineRequest(
itemId = it.item?.id,
qty = it.qty ?: BigDecimal.ZERO,
uomId = it.uom?.id,
val pols = deliveryOrder.deliveryOrderLines.map {
SavePickOrderLineRequest(
itemId = it.item?.id,
qty = it.qty ?: BigDecimal.ZERO,
uomId = it.uom?.id,
)
}
val po = SavePickOrderRequest(
doId = deliveryOrder.id,
type = PickOrderType.DELIVERY_ORDER,
targetDate = deliveryOrder.estimatedArrivalDate?.toLocalDate() ?: LocalDate.now(),
pickOrderLine = pols
)
}
val po = SavePickOrderRequest(
doId = deliveryOrder.id,
type = PickOrderType.DELIVERY_ORDER,
targetDate = deliveryOrder.estimatedArrivalDate?.toLocalDate() ?: LocalDate.now(),
pickOrderLine = pols
)

val createdPickOrder = pickOrderService.create(po)
println("🔍 DEBUG: Created pick order - ID: ${createdPickOrder.id}")
@@ -608,64 +608,64 @@ open class DeliveryOrderService(

println(" DEBUG: Truck matches preferred floor - Truck Store: $truckStoreId, Preferred: $preferredFloor")

// storeId 基于 items 的 preferredFloor
val storeId = "$preferredFloor/F"
val loadingSequence = truck.loadingSequence ?: 999

println("🔍 DEBUG: Creating DoPickOrder - Floor: $preferredFloor, Store: $storeId, Truck: ${truck.id}")

val doPickOrder = DoPickOrder(
storeId = storeId,
ticketNo = "TEMP-${System.currentTimeMillis()}",
ticketStatus = DoPickOrderStatus.pending,
truckId = truck.id,
doOrderId = deliveryOrder.id,
pickOrderId = createdPickOrder.id,
truckDepartureTime = truck.departureTime,
shopId = deliveryOrder.shop?.id,
handledBy = null,
pickOrderCode = createdPickOrder.code,
deliveryOrderCode = deliveryOrder.code,
loadingSequence = loadingSequence,
ticketReleaseTime = null,
truckLanceCode = truck.truckLanceCode,
shopCode = deliveryOrder.shop?.code,
shopName = deliveryOrder.shop?.name,
requiredDeliveryDate = targetDate
)

val savedDoPickOrder = doPickOrderService.save(doPickOrder)
println("🔍 DEBUG: Saved DoPickOrder - ID: ${savedDoPickOrder.id}")
truck
}
return MessageResponse(
id = deliveryOrder.id,
code = deliveryOrder.code,
name = deliveryOrder.shop?.name,
type = null,
message = null,
errorPosition = null,
entity = mapOf("status" to deliveryOrder.status?.value)
// storeId 基于 items 的 preferredFloor
val storeId = "$preferredFloor/F"
val loadingSequence = truck.loadingSequence ?: 999

println("🔍 DEBUG: Creating DoPickOrder - Floor: $preferredFloor, Store: $storeId, Truck: ${truck.id}")

val doPickOrder = DoPickOrder(
storeId = storeId,
ticketNo = "TEMP-${System.currentTimeMillis()}",
ticketStatus = DoPickOrderStatus.pending,
truckId = truck.id,
doOrderId = deliveryOrder.id,
pickOrderId = createdPickOrder.id,
truckDepartureTime = truck.departureTime,
shopId = deliveryOrder.shop?.id,
handledBy = null,
pickOrderCode = createdPickOrder.code,
deliveryOrderCode = deliveryOrder.code,
loadingSequence = loadingSequence,
ticketReleaseTime = null,
truckLanceCode = truck.truckLanceCode,
shopCode = deliveryOrder.shop?.code,
shopName = deliveryOrder.shop?.name,
requiredDeliveryDate = targetDate
)

val savedDoPickOrder = doPickOrderService.save(doPickOrder)
println("🔍 DEBUG: Saved DoPickOrder - ID: ${savedDoPickOrder.id}")
truck
}
return MessageResponse(
id = deliveryOrder.id,
code = deliveryOrder.code,
name = deliveryOrder.shop?.name,
type = null,
message = null,
errorPosition = null,
entity = mapOf("status" to deliveryOrder.status?.value)
)
}

open fun getLotNumbersForPickOrderByItemId(itemId: Long, pickOrderId: Long): String {
try {
val pickOrderLines = pickOrderLineRepository.findAllByPickOrderId(pickOrderId)
val pickOrderLineIds = pickOrderLines.mapNotNull { it.id }
val suggestedPickLots = suggestedPickLotRepository.findAllByPickOrderLineIdIn(pickOrderLineIds)

val lotNumbers = suggestedPickLots
.filter { it.pickOrderLine?.item?.id == itemId }
.mapNotNull { it.suggestedLotLine?.inventoryLot?.lotNo }
.distinct()

return lotNumbers.joinToString(", ")
} catch (e: Exception) {
println("Error getting lot numbers for item $itemId in pick order $pickOrderId: ${e.message}")
return ""
}
open fun getLotNumbersForPickOrderByItemId(itemId: Long, pickOrderId: Long): String {
try {
val pickOrderLines = pickOrderLineRepository.findAllByPickOrderId(pickOrderId)
val pickOrderLineIds = pickOrderLines.mapNotNull { it.id }
val suggestedPickLots = suggestedPickLotRepository.findAllByPickOrderLineIdIn(pickOrderLineIds)

val lotNumbers = suggestedPickLots
.filter { it.pickOrderLine?.item?.id == itemId }
.mapNotNull { it.suggestedLotLine?.inventoryLot?.lotNo }
.distinct()

return lotNumbers.joinToString(", ")
} catch (e: Exception) {
println("Error getting lot numbers for item $itemId in pick order $pickOrderId: ${e.message}")
return ""
}
}


// Delivery Note
@@ -794,7 +794,8 @@ open class DeliveryOrderService(

params["shopName"] = doPickOrder.shopName ?: deliveryNoteInfo[0].shopName ?: ""
params["shopAddress"] = deliveryNoteInfo[0].shopAddress ?: ""
params["deliveryDate"] = deliveryNoteInfo[0].estimatedArrivalDate?.format(DateTimeFormatter.ISO_LOCAL_DATE) ?: ""
params["deliveryDate"] =
deliveryNoteInfo[0].estimatedArrivalDate?.format(DateTimeFormatter.ISO_LOCAL_DATE) ?: ""
params["truckNo"] = truckNo
params["ShopPurchaseOrderNo"] = doPickOrder.deliveryOrderCode ?: deliveryNoteInfo.joinToString(", ") { it.code }
params["FGPickOrderNo"] = doPickOrder.pickOrderCode ?: selectedPickOrder?.code ?: ""
@@ -810,7 +811,7 @@ open class DeliveryOrderService(
deliveryNote: net.sf.jasperreports.engine.JasperReport,
fields: MutableList<MutableMap<String, Any>>,
params: MutableMap<String, Any>
) : Map<String, Any> {
): Map<String, Any> {

val doPickOrderRecord = doPickOrderRecordRepository.findById(request.doPickOrderId).orElseThrow {
NoSuchElementException("DoPickOrderRecord not found with ID: ${request.doPickOrderId}")
@@ -819,12 +820,12 @@ open class DeliveryOrderService(
val doPickOrderLineRecords = doPickOrderLineRecordRepository.findByDoPickOrderId(doPickOrderRecord.recordId)

val pickOrderIds = doPickOrderLineRecords.mapNotNull { it.pickOrderId }.distinct()
if(pickOrderIds.isEmpty()){
if (pickOrderIds.isEmpty()) {
throw IllegalStateException("DoPickOrderRecord ${request.doPickOrderId} has no associated pick orders")
}

val deliveryOrderIds = doPickOrderLineRecords.mapNotNull { it.doOrderId }.distinct()
if(deliveryOrderIds.isEmpty()){
if (deliveryOrderIds.isEmpty()) {
throw IllegalStateException("DoPickOrderRecord ${request.doPickOrderId} has no associated delivery orders")
}

@@ -920,9 +921,11 @@ open class DeliveryOrderService(

params["shopName"] = doPickOrderRecord.shopName ?: deliveryNoteInfo[0].shopName ?: ""
params["shopAddress"] = deliveryNoteInfo[0].shopAddress ?: ""
params["deliveryDate"] = deliveryNoteInfo[0].estimatedArrivalDate?.format(DateTimeFormatter.ISO_LOCAL_DATE) ?: ""
params["deliveryDate"] =
deliveryNoteInfo[0].estimatedArrivalDate?.format(DateTimeFormatter.ISO_LOCAL_DATE) ?: ""
params["truckNo"] = truckNo
params["ShopPurchaseOrderNo"] = doPickOrderRecord.deliveryOrderCode ?: deliveryNoteInfo.joinToString(", ") { it.code }
params["ShopPurchaseOrderNo"] =
doPickOrderRecord.deliveryOrderCode ?: deliveryNoteInfo.joinToString(", ") { it.code }
params["FGPickOrderNo"] = doPickOrderRecord.pickOrderCode ?: selectedPickOrder?.code ?: ""

return mapOf(
@@ -934,7 +937,8 @@ open class DeliveryOrderService(
//Print Delivery Note
@Transactional
open fun printDeliveryNote(request: PrintDeliveryNoteRequest) {
val printer = printerService.findById(request.printerId) ?: throw java.util.NoSuchElementException("No such printer")
val printer =
printerService.findById(request.printerId) ?: throw java.util.NoSuchElementException("No such printer")

val pdf = exportDeliveryNote(
ExportDeliveryNoteRequest(
@@ -993,7 +997,7 @@ open class DeliveryOrderService(
val doPickOrderLineRecords = doPickOrderLineRecordRepository.findByDoPickOrderId(doPickOrderRecord.recordId)

val deliveryOrderIds = doPickOrderLineRecords.mapNotNull { it.doOrderId }.distinct()
if(deliveryOrderIds.isEmpty()){
if (deliveryOrderIds.isEmpty()) {
throw IllegalStateException("DoPickOrderRecord ${request.doPickOrderId} has no associated delivery orders")
}

@@ -1010,7 +1014,7 @@ open class DeliveryOrderService(
val field = mutableMapOf<String, Any>()
}

if(cartonLabelInfo.size > 1){
if (cartonLabelInfo.size > 1) {

}

@@ -1036,245 +1040,375 @@ open class DeliveryOrderService(
}


//Print Carton Labels
@Transactional
open fun printDNLabels(request: PrintDNLabelsRequest) {
val printer =
printerService.findById(request.printerId) ?: throw java.util.NoSuchElementException("No such printer")
//Print Carton Labels
@Transactional
open fun printDNLabels(request: PrintDNLabelsRequest) {
val printer =
printerService.findById(request.printerId) ?: throw java.util.NoSuchElementException("No such printer")

val pdf = exportDNLabels(
ExportDNLabelsRequest(
doPickOrderId = request.doPickOrderId,
numOfCarton = request.numOfCarton
)
val pdf = exportDNLabels(
ExportDNLabelsRequest(
doPickOrderId = request.doPickOrderId,
numOfCarton = request.numOfCarton
)
)

val jasperPrint = pdf["report"] as JasperPrint
val jasperPrint = pdf["report"] as JasperPrint

val tempPdfFile = File.createTempFile("print_job_", ".pdf")
val tempPdfFile = File.createTempFile("print_job_", ".pdf")

try {
JasperExportManager.exportReportToPdfFile(jasperPrint, tempPdfFile.absolutePath)
try {
JasperExportManager.exportReportToPdfFile(jasperPrint, tempPdfFile.absolutePath)


val printQty = if (request.printQty == null || request.printQty <= 0) 1 else request.printQty
printer.ip?.let { ip ->
printer.port?.let { port ->
ZebraPrinterUtil.printPdfToZebra(
tempPdfFile,
ip,
port,
printQty,
ZebraPrinterUtil.PrintDirection.ROTATED
)
}
val printQty = if (request.printQty == null || request.printQty <= 0) 1 else request.printQty
printer.ip?.let { ip ->
printer.port?.let { port ->
ZebraPrinterUtil.printPdfToZebra(
tempPdfFile,
ip,
port,
printQty,
ZebraPrinterUtil.PrintDirection.ROTATED
)
}

println("Test PDF saved to: ${tempPdfFile.absolutePath}")

} finally {
//tempPdfFile.delete()
}

}
@Transactional(rollbackFor = [Exception::class])
open fun releaseDeliveryOrderWithoutTicket(request: ReleaseDoRequest): ReleaseDoResult {
println("🔍 DEBUG: Starting releaseDeliveryOrderWithoutTicket for DO ID: ${request.id}")
println("Test PDF saved to: ${tempPdfFile.absolutePath}")

val deliveryOrder = deliveryOrderRepository.findByIdAndDeletedIsFalse(request.id)
?: throw NoSuchElementException("Delivery Order not found")
} finally {
//tempPdfFile.delete()
}

// 检查状态,跳过已完成或已发布的DO
if (deliveryOrder.status == DeliveryOrderStatus.COMPLETED || deliveryOrder.status == DeliveryOrderStatus.RECEIVING) {
throw IllegalStateException("Delivery Order ${deliveryOrder.id} is already ${deliveryOrder.status?.value}, skipping release")
}

// 更新状态为released (使用RECEIVING表示已发布)
deliveryOrder.apply {
status = DeliveryOrderStatus.RECEIVING // 使用RECEIVING表示已发布状态
}
deliveryOrderRepository.save(deliveryOrder)

// 创建 pick order
val pols = deliveryOrder.deliveryOrderLines.map {
SavePickOrderLineRequest(
itemId = it.item?.id,
qty = it.qty ?: BigDecimal.ZERO,
uomId = it.uom?.id,
)
}
val po = SavePickOrderRequest(
doId = deliveryOrder.id,
type = PickOrderType.DELIVERY_ORDER,
targetDate = deliveryOrder.estimatedArrivalDate?.toLocalDate() ?: LocalDate.now(),
pickOrderLine = pols
)

val createdPickOrder = pickOrderService.create(po)
val consoCode = pickOrderService.assignConsoCode()
val pickOrderEntity = pickOrderRepository.findById(createdPickOrder.id!!).orElse(null)

if (pickOrderEntity != null) {
pickOrderEntity.consoCode = consoCode
pickOrderEntity.status = com.ffii.fpsms.modules.pickOrder.enums.PickOrderStatus.RELEASED
pickOrderRepository.saveAndFlush(pickOrderEntity)

// 创建 suggestions 和 hold inventory
val lines = pickOrderLineRepository.findAllByPickOrderId(pickOrderEntity.id!!)
val suggestions = suggestedPickLotService.suggestionForPickOrderLines(
SuggestedPickLotForPolRequest(pickOrderLines = lines)
)
val saveSuggestedPickLots = suggestedPickLotService.saveAll(suggestions.suggestedList)
val insufficientCount = suggestions.suggestedList.count { it.suggestedLotLine == null }
if (insufficientCount > 0) {
println("⚠️ WARNING: $insufficientCount items have insufficient stock (issues auto-created)")
@Transactional(rollbackFor = [Exception::class])
open fun releaseDeliveryOrderWithoutTicket(request: ReleaseDoRequest): ReleaseDoResult {
println("🔍 DEBUG: Starting releaseDeliveryOrderWithoutTicket for DO ID: ${request.id}")

val deliveryOrder = deliveryOrderRepository.findByIdAndDeletedIsFalse(request.id)
?: throw NoSuchElementException("Delivery Order not found")

// 检查状态,跳过已完成或已发布的DO
if (deliveryOrder.status == DeliveryOrderStatus.COMPLETED || deliveryOrder.status == DeliveryOrderStatus.RECEIVING) {
throw IllegalStateException("Delivery Order ${deliveryOrder.id} is already ${deliveryOrder.status?.value}, skipping release")
}

// 更新状态为released (使用RECEIVING表示已发布)
deliveryOrder.apply {
status = DeliveryOrderStatus.RECEIVING // 使用RECEIVING表示已发布状态
}
deliveryOrderRepository.save(deliveryOrder)

// 创建 pick order
val pols = deliveryOrder.deliveryOrderLines.map {
SavePickOrderLineRequest(
itemId = it.item?.id,
qty = it.qty ?: BigDecimal.ZERO,
uomId = it.uom?.id,
)
}
val inventoryLotLines = inventoryLotLineRepository.findAllByIdIn(
saveSuggestedPickLots.mapNotNull { it.suggestedLotLine?.id }
val po = SavePickOrderRequest(
doId = deliveryOrder.id,
type = PickOrderType.DELIVERY_ORDER,
targetDate = deliveryOrder.estimatedArrivalDate?.toLocalDate() ?: LocalDate.now(),
pickOrderLine = pols
)

saveSuggestedPickLots.forEach { lot ->
if (lot.suggestedLotLine != null && lot.suggestedLotLine?.id != null && lot.suggestedLotLine!!.id!! > 0) {
val lineIndex = inventoryLotLines.indexOf(lot.suggestedLotLine)
if (lineIndex >= 0) {
inventoryLotLines[lineIndex].holdQty =
(inventoryLotLines[lineIndex].holdQty ?: BigDecimal.ZERO).plus(lot.qty ?: BigDecimal.ZERO)
val createdPickOrder = pickOrderService.create(po)
val consoCode = pickOrderService.assignConsoCode()
val pickOrderEntity = pickOrderRepository.findById(createdPickOrder.id!!).orElse(null)

if (pickOrderEntity != null) {
pickOrderEntity.consoCode = consoCode
pickOrderEntity.status = com.ffii.fpsms.modules.pickOrder.enums.PickOrderStatus.RELEASED
pickOrderRepository.saveAndFlush(pickOrderEntity)

// 创建 suggestions 和 hold inventory
val lines = pickOrderLineRepository.findAllByPickOrderId(pickOrderEntity.id!!)
val suggestions = suggestedPickLotService.suggestionForPickOrderLines(
SuggestedPickLotForPolRequest(pickOrderLines = lines)
)
val saveSuggestedPickLots = suggestedPickLotService.saveAll(suggestions.suggestedList)
val insufficientCount = suggestions.suggestedList.count { it.suggestedLotLine == null }
if (insufficientCount > 0) {
println("⚠️ WARNING: $insufficientCount items have insufficient stock (issues auto-created)")
}
val inventoryLotLines = inventoryLotLineRepository.findAllByIdIn(
saveSuggestedPickLots.mapNotNull { it.suggestedLotLine?.id }
)

saveSuggestedPickLots.forEach { lot ->
if (lot.suggestedLotLine != null && lot.suggestedLotLine?.id != null && lot.suggestedLotLine!!.id!! > 0) {
val lineIndex = inventoryLotLines.indexOf(lot.suggestedLotLine)
if (lineIndex >= 0) {
inventoryLotLines[lineIndex].holdQty =
(inventoryLotLines[lineIndex].holdQty ?: BigDecimal.ZERO).plus(lot.qty ?: BigDecimal.ZERO)
}
}
}
inventoryLotLineRepository.saveAll(inventoryLotLines)

// 创建 stock out
val stockOut = StockOut().apply {
this.type = "do"
this.consoPickOrderCode = consoCode
this.status = StockOutStatus.PENDING.status
this.handler = request.userId
}
val savedStockOut = stockOutRepository.saveAndFlush(stockOut)

saveSuggestedPickLots.forEach { lot ->
val polId = lot.pickOrderLine?.id
val illId = lot.suggestedLotLine?.id
if (polId != null) {
val pickOrderLine = pickOrderLineRepository.findById(polId).orElse(null)
val inventoryLotLine = illId?.let { inventoryLotLineRepository.findById(it).orElse(null) }

if (pickOrderLine != null) {
val line = StockOutLine().apply {
this.stockOut = savedStockOut
this.pickOrderLine = pickOrderLine
this.inventoryLotLine = inventoryLotLine // 可能为 null
this.item = pickOrderLine.item
// 修复:根据是否有 inventoryLotLine 设置状态
this.status = if (inventoryLotLine == null) {
StockOutLineStatus.PARTIALLY_COMPLETE.status // 没有库存批次时使用 PARTIALLY_COMPLETE
} else {
StockOutLineStatus.PENDING.status // 有正常库存批次时使用 PENDING
}
this.qty = 0.0
}
stockOutLineRepository.save(line)
}
}
}
}

// 分析楼层分布
val targetDate = deliveryOrder.estimatedArrivalDate?.toLocalDate() ?: LocalDate.now()
val itemIds = deliveryOrder.deliveryOrderLines.mapNotNull { it.item?.id }.distinct()

val itemsStoreIdQuery = """
SELECT
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 itemsStoreIdResults = jdbcDao.queryForList(itemsStoreIdQuery)
val storeIdItemCount = mutableMapOf<String, Int>()
var totalItemsWithStoreId = 0 // 统计有 store_id 的商品总数

itemsStoreIdResults.forEach { row ->
val rawStoreId = row["store_id"] as? String
if (rawStoreId != null) { // 只统计非 NULL 的 store_id
val normalizedStoreId = when (rawStoreId) {
"3F" -> "4F"
else -> rawStoreId
}
val count = (row["item_count"] as? Number)?.toInt() ?: 0
storeIdItemCount[normalizedStoreId] =
(storeIdItemCount[normalizedStoreId] ?: 0) + count
totalItemsWithStoreId += count
}
}
inventoryLotLineRepository.saveAll(inventoryLotLines)

// 创建 stock out
val stockOut = StockOut().apply {
this.type = "do"
this.consoPickOrderCode = consoCode
this.status = StockOutStatus.PENDING.status
this.handler = request.userId

val count2F = storeIdItemCount["2F"] ?: 0
val count4F = storeIdItemCount["4F"] ?: 0


val preferredFloor = when {
totalItemsWithStoreId == 0 -> {
println("⚠️ WARNING: All items have NULL store_id, defaulting to 2F")
"2F"
}

count4F > count2F -> "4F"
count2F > count4F -> "2F"
count4F == totalItemsWithStoreId && count2F == 0 -> "4F"
else -> "2F" // 默认 2F
}
val savedStockOut = stockOutRepository.saveAndFlush(stockOut)

saveSuggestedPickLots.forEach { lot ->
val polId = lot.pickOrderLine?.id
val illId = lot.suggestedLotLine?.id
if (polId != null) {
val pickOrderLine = pickOrderLineRepository.findById(polId).orElse(null)
val inventoryLotLine = illId?.let { inventoryLotLineRepository.findById(it).orElse(null) }

if (pickOrderLine != null) {
val line = StockOutLine().apply {
this.stockOut = savedStockOut
this.pickOrderLine = pickOrderLine
this.inventoryLotLine = inventoryLotLine // 可能为 null
this.item = pickOrderLine.item
// 修复:根据是否有 inventoryLotLine 设置状态
this.status = if (inventoryLotLine == null) {
StockOutLineStatus.PARTIALLY_COMPLETE.status // 没有库存批次时使用 PARTIALLY_COMPLETE

println("🔍 DEBUG: Floor calculation for DO ${deliveryOrder.id}")
println(" - Total items: ${itemIds.size}")
println(" - Items with store_id: $totalItemsWithStoreId")
println(" - Items without store_id: ${itemIds.size - totalItemsWithStoreId}")
println(" - 2F items: $count2F")
println(" - 4F items: $count4F")
println(" - Preferred floor: $preferredFloor")


val truck = deliveryOrder.shop?.id?.let { shopId ->
val trucks = truckRepository.findByShopIdAndDeletedFalse(shopId)
val preferredStoreId = when (preferredFloor) {
"2F" -> "2F"
"4F" -> "4F"
"3F" -> "3F"
else -> "2F"
}


val matchedTrucks = trucks.filter { it.storeId == preferredStoreId }

if (matchedTrucks.isEmpty()) {
null
} else {

if (preferredStoreId == "4F" && matchedTrucks.size > 1) {
deliveryOrder.estimatedArrivalDate?.let { estimatedArrivalDate ->
val targetDate = estimatedArrivalDate.toLocalDate()
val dayOfWeek = targetDate.dayOfWeek


val dayAbbr = when (dayOfWeek) {
java.time.DayOfWeek.MONDAY -> "Mon"
java.time.DayOfWeek.TUESDAY -> "Tue"
java.time.DayOfWeek.WEDNESDAY -> "Wed"
java.time.DayOfWeek.THURSDAY -> "Thu"
java.time.DayOfWeek.FRIDAY -> "Fri"
java.time.DayOfWeek.SATURDAY -> "Sat"
java.time.DayOfWeek.SUNDAY -> "Sun"
}

println("🔍 DEBUG: DO ${deliveryOrder.id} - Target date: $targetDate ($dayAbbr), Shop: $shopId")
println("🔍 DEBUG: Found ${matchedTrucks.size} matched 4F trucks")


val dayMatchedTrucks = matchedTrucks.filter {
it.truckLanceCode?.contains(dayAbbr, ignoreCase = true) == true
}

println("🔍 DEBUG: Found ${dayMatchedTrucks.size} trucks matching $dayAbbr")
dayMatchedTrucks.forEach { t ->
println(" - Truck ID=${t.id}, Code=${t.truckLanceCode}, Time=${t.departureTime}")
}

if (dayMatchedTrucks.isNotEmpty()) {

val selected = dayMatchedTrucks.minByOrNull { it.departureTime ?: LocalTime.of(23, 59, 59) }
println("✅ DEBUG: Selected truck matching $dayAbbr - ID=${selected?.id}, Code=${selected?.truckLanceCode}")
selected
} else {
StockOutLineStatus.PENDING.status // 有正常库存批次时使用 PENDING

println("⚠️ WARNING: No truck matching $dayAbbr, using first available truck")
matchedTrucks.minByOrNull { it.departureTime ?: LocalTime.of(23, 59, 59) }
}
this.qty = 0.0
} ?: run {

matchedTrucks.minByOrNull { it.departureTime ?: LocalTime.of(23, 59, 59) }
}
stockOutLineRepository.save(line)
} else {

matchedTrucks.minByOrNull { it.departureTime ?: LocalTime.of(23, 59, 59) }
}
}
}
}

// 分析楼层分布
val targetDate = deliveryOrder.estimatedArrivalDate?.toLocalDate() ?: LocalDate.now()
val itemIds = deliveryOrder.deliveryOrderLines.mapNotNull { it.item?.id }.distinct()
val itemsStoreIdQuery = """
SELECT
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 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)
// 如果没有匹配的 truck,抛出异常跳过
if (truck == null) {
val errorMsg =
"No matching truck found for preferredFloor ($preferredFloor). Skipping DO ${deliveryOrder.id}."
println("⚠️ $errorMsg")
throw IllegalStateException(errorMsg)
}
}

val preferredFloor = if ((storeIdItemCount["4F"] ?: 0) == itemIds.size &&
(storeIdItemCount["2F"] ?: 0) == 0) {
"4F"
} else {
"2F"
println(" DEBUG: Matched truck - ID=${truck.id}, Store=${truck.storeId}, Floor=$preferredFloor")

return ReleaseDoResult(
deliveryOrderId = deliveryOrder.id!!,
deliveryOrderCode = deliveryOrder.code,
pickOrderId = createdPickOrder.id!!,
pickOrderCode = pickOrderEntity?.code,
shopId = deliveryOrder.shop?.id,
shopCode = deliveryOrder.shop?.code,
shopName = deliveryOrder.shop?.name,
estimatedArrivalDate = targetDate,
preferredFloor = preferredFloor,
truckId = truck.id,
truckDepartureTime = truck.departureTime,
truckLanceCode = truck.truckLanceCode,
loadingSequence = truck.loadingSequence // 直接使用 truck 的值
)
}

// 查找匹配 preferred floor 的 truck
val truck = deliveryOrder.shop?.id?.let { shopId ->
val trucks = truckRepository.findByShopIdAndDeletedFalse(shopId)
val preferredStoreId = when (preferredFloor) {
"2F" -> "2F"
"4F" -> "4F"
"3F" -> "3F"
else -> "2F"
private fun getDayOfWeekAbbr(date: LocalDate): String =
when (date.dayOfWeek) {
java.time.DayOfWeek.MONDAY -> "Mon"
java.time.DayOfWeek.TUESDAY -> "Tue"
java.time.DayOfWeek.WEDNESDAY -> "Wed"
java.time.DayOfWeek.THURSDAY -> "Thu"
java.time.DayOfWeek.FRIDAY -> "Fri"
java.time.DayOfWeek.SATURDAY -> "Sat"
java.time.DayOfWeek.SUNDAY -> "Sun"
}
// 只选择 store_id 匹配的 truck
val matchedTrucks = trucks.filter { it.storeId == preferredStoreId }
if (matchedTrucks.isEmpty()) {
null // 没有匹配的 truck
} else {
matchedTrucks.minByOrNull { it.departureTime ?: LocalTime.of(23, 59, 59) }
}
}

// 如果没有匹配的 truck,抛出异常跳过
if (truck == null) {
val errorMsg = "No matching truck found for preferredFloor ($preferredFloor). Skipping DO ${deliveryOrder.id}."
println("⚠️ $errorMsg")
throw IllegalStateException(errorMsg)
}
open fun check4FTruckAvailabilityForDoList(doIds: List<Long>): Check4FTruckBatchResponse {
val problems = mutableListOf<ProblemDoDto>()

println(" DEBUG: Matched truck - ID=${truck.id}, Store=${truck.storeId}, Floor=$preferredFloor")

return ReleaseDoResult(
deliveryOrderId = deliveryOrder.id!!,
deliveryOrderCode = deliveryOrder.code,
pickOrderId = createdPickOrder.id!!,
pickOrderCode = pickOrderEntity?.code,
shopId = deliveryOrder.shop?.id,
shopCode = deliveryOrder.shop?.code,
shopName = deliveryOrder.shop?.name,
estimatedArrivalDate = targetDate,
preferredFloor = preferredFloor,
truckId = truck.id,
truckDepartureTime = truck.departureTime,
truckLanceCode = truck.truckLanceCode,
loadingSequence = truck.loadingSequence // 直接使用 truck 的值
)
}
doIds.forEach { doId ->
val deliveryOrder = deliveryOrderRepository.findByIdAndDeletedIsFalse(doId) ?: return@forEach
val shopId = deliveryOrder.shop?.id ?: return@forEach
val eta = deliveryOrder.estimatedArrivalDate?.toLocalDate() ?: return@forEach

@Transactional
open fun syncDeliveryOrderStatusFromDoPickOrder(): Int {
// 1) Check if this DO is a 4F order (your “all items on 4F, no 2F” rule)
val sql = """
UPDATE fpsmsdb.delivery_order do
INNER JOIN fpsmsdb.do_pick_order dpo ON dpo.do_order_id = do.id
SET do.status = 'completed'
WHERE dpo.ticket_status = 'completed'
AND do.status != 'completed'
AND do.deleted = 0
AND dpo.deleted = 0
""".trimIndent()
return jdbcDao.executeUpdate(sql, emptyMap<String, Any>())
SELECT
COUNT(DISTINCT CASE WHEN i.store_id = '4F' THEN dol.itemId END) AS count_4f,
COUNT(DISTINCT CASE WHEN i.store_id = '2F' THEN dol.itemId END) AS count_2f,
COUNT(DISTINCT dol.itemId) AS total_items
FROM fpsmsdb.delivery_order_line dol
INNER JOIN fpsmsdb.items i ON i.id = dol.itemId AND i.deleted = 0
WHERE dol.deliveryOrderId = :doId AND dol.deleted = 0
""".trimIndent()

val res = jdbcDao.queryForList(sql, mapOf("doId" to doId)).firstOrNull() ?: return@forEach
val count4F = (res["count_4f"] as? Number)?.toInt() ?: 0
val count2F = (res["count_2f"] as? Number)?.toInt() ?: 0
val totalItems = (res["total_items"] as? Number)?.toInt() ?: 0

val is4FOrder = (totalItems > 0 && count4F == totalItems && count2F == 0)
if (!is4FOrder) {
// Not a 4F order → no problem, skip
return@forEach
}

// 2) Find 4F trucks for that shop on that weekday
val dayAbbr = getDayOfWeekAbbr(eta)
val trucks = truckRepository.findByShopIdAndStoreIdAndDayOfWeek(shopId, "4F", dayAbbr)

if (trucks.isEmpty()) {
// Problem DO: 4F order but no matching 4F truck that day
val availableTrucksAll4F = truckRepository.findByShopIdAndDeletedFalse(shopId)
.filter { it.storeId == "4F" }

val truckDtos = availableTrucksAll4F.map { t ->
TruckInfoDto(
id = t.id,
truckLanceCode = t.truckLanceCode,
departureTime = t.departureTime?.toString(),
storeId = t.storeId,
shopCode = t.shopCode,
shopName = t.shopName
)
}

problems += ProblemDoDto(
deliveryOrderId = deliveryOrder.id!!,
deliveryOrderCode = deliveryOrder.code,
targetDate = eta,
availableTrucks = truckDtos
)
}
}

return Check4FTruckBatchResponse(
hasProblem = problems.isNotEmpty(),
problems = problems
)
}
}

+ 170
- 39
src/main/java/com/ffii/fpsms/modules/deliveryOrder/service/DoReleaseCoordinatorService.kt Просмотреть файл

@@ -19,6 +19,8 @@ import com.ffii.fpsms.modules.deliveryOrder.entity.DeliveryOrderRepository
import com.ffii.fpsms.modules.deliveryOrder.enums.DeliveryOrderStatus
import com.ffii.fpsms.modules.deliveryOrder.entity.DoPickOrderRepository
import com.ffii.fpsms.modules.pickOrder.service.PickExecutionIssueService
import java.time.LocalDate
import java.time.DayOfWeek
data class BatchReleaseJobStatus(
val jobId: String,
val total: Int,
@@ -41,9 +43,37 @@ class DoReleaseCoordinatorService(
private val poolSize = Runtime.getRuntime().availableProcessors()
private val executor = Executors.newFixedThreadPool(min(poolSize, 4))
private val jobs = ConcurrentHashMap<String, BatchReleaseJobStatus>()
private fun getDayOfWeekAbbr(date: LocalDate?): String? {
if (date == null) return null
return when (date.dayOfWeek) {
DayOfWeek.MONDAY -> "Mon"
DayOfWeek.TUESDAY -> "Tue"
DayOfWeek.WEDNESDAY -> "Wed"
DayOfWeek.THURSDAY -> "Thu"
DayOfWeek.FRIDAY -> "Fri"
DayOfWeek.SATURDAY -> "Sat"
DayOfWeek.SUNDAY -> "Sun"
}
}

// 辅助方法:在 SQL 中获取星期几的缩写(MySQL)
private fun getDayOfWeekAbbrSql(dateColumn: String): String {
return """
CASE DAYNAME($dateColumn)
WHEN 'Monday' THEN 'Mon'
WHEN 'Tuesday' THEN 'Tue'
WHEN 'Wednesday' THEN 'Wed'
WHEN 'Thursday' THEN 'Thu'
WHEN 'Friday' THEN 'Fri'
WHEN 'Saturday' THEN 'Sat'
WHEN 'Sunday' THEN 'Sun'
ELSE ''
END
""".trimIndent()
}
private fun updateBatchTicketNumbers() {
try {
val dayOfWeekSql = getDayOfWeekAbbrSql("do.estimatedArrivalDate")
val updateSql = """
UPDATE fpsmsdb.do_pick_order dpo
INNER JOIN (
@@ -53,7 +83,7 @@ class DoReleaseCoordinatorService(
CASE
WHEN i.store_id = '3F' THEN '4F'
ELSE i.store_id
END AS store_id, -- 这里做 3F → 4F
END AS store_id,
COUNT(DISTINCT dol.itemId) AS item_count
FROM fpsmsdb.delivery_order_line dol
INNER JOIN fpsmsdb.items i ON i.id = dol.itemId
@@ -95,7 +125,26 @@ class DoReleaseCoordinatorService(
do.id AS delivery_order_id,
do.estimatedArrivalDate,
pf.preferred_floor,
$dayOfWeekSql AS day_of_week_abbr,
CASE
WHEN (SELECT COUNT(*) FROM fpsmsdb.truck t WHERE t.shopId = do.shopId AND t.deleted = 0 AND t.Store_id = pf.preferred_store_id) > 1
AND pf.preferred_store_id = 4
AND do.estimatedArrivalDate IS NOT NULL THEN
COALESCE(
(SELECT t.DepartureTime FROM fpsmsdb.truck t
WHERE t.shopId = do.shopId AND t.deleted = 0
AND t.Store_id = pf.preferred_store_id
AND t.TruckLanceCode LIKE CONCAT('%', $dayOfWeekSql, '%')
ORDER BY t.DepartureTime ASC LIMIT 1),
(SELECT t.DepartureTime FROM fpsmsdb.truck t
WHERE t.shopId = do.shopId AND t.deleted = 0
AND t.Store_id = pf.preferred_store_id
ORDER BY t.DepartureTime ASC LIMIT 1),
(SELECT t.DepartureTime FROM fpsmsdb.truck t
WHERE t.shopId = do.shopId AND t.deleted = 0
ORDER BY t.DepartureTime ASC LIMIT 1),
'23:59:59'
)
WHEN (SELECT COUNT(*) FROM fpsmsdb.truck t WHERE t.shopId = do.shopId AND t.deleted = 0) > 1 THEN
COALESCE(
(SELECT t.DepartureTime FROM fpsmsdb.truck t
@@ -116,6 +165,24 @@ class DoReleaseCoordinatorService(
)
END AS selected_departure_time,
CASE
WHEN (SELECT COUNT(*) FROM fpsmsdb.truck t WHERE t.shopId = do.shopId AND t.deleted = 0 AND t.Store_id = pf.preferred_store_id) > 1
AND pf.preferred_store_id = 4
AND do.estimatedArrivalDate IS NOT NULL THEN
COALESCE(
(SELECT t.TruckLanceCode FROM fpsmsdb.truck t
WHERE t.shopId = do.shopId AND t.deleted = 0
AND t.Store_id = pf.preferred_store_id
AND t.TruckLanceCode LIKE CONCAT('%', $dayOfWeekSql, '%')
ORDER BY t.DepartureTime ASC LIMIT 1),
(SELECT t.TruckLanceCode FROM fpsmsdb.truck t
WHERE t.shopId = do.shopId AND t.deleted = 0
AND t.Store_id = pf.preferred_store_id
ORDER BY t.DepartureTime ASC LIMIT 1),
(SELECT t.TruckLanceCode FROM fpsmsdb.truck t
WHERE t.shopId = do.shopId AND t.deleted = 0
ORDER BY t.DepartureTime ASC LIMIT 1),
'ZZ'
)
WHEN (SELECT COUNT(*) FROM fpsmsdb.truck t WHERE t.shopId = do.shopId AND t.deleted = 0) > 1 THEN
COALESCE(
(SELECT t.TruckLanceCode FROM fpsmsdb.truck t
@@ -136,6 +203,12 @@ class DoReleaseCoordinatorService(
)
END AS selected_truck_lance,
COALESCE(
(SELECT t.LoadingSequence FROM fpsmsdb.truck t
WHERE t.shopId = do.shopId AND t.deleted = 0
AND t.Store_id = pf.preferred_store_id
AND (pf.preferred_store_id != 4 OR do.estimatedArrivalDate IS NULL OR
t.TruckLanceCode LIKE CONCAT('%', $dayOfWeekSql, '%'))
ORDER BY t.DepartureTime ASC LIMIT 1),
(SELECT t.LoadingSequence FROM fpsmsdb.truck t
WHERE t.shopId = do.shopId AND t.deleted = 0
AND t.Store_id = pf.preferred_store_id
@@ -193,6 +266,7 @@ class DoReleaseCoordinatorService(
try {
println("🔍 DEBUG: Getting ordered IDs for ${ids.size} orders")
println("🔍 DEBUG: First 5 IDs: ${ids.take(5)}")
val dayOfWeekSql = getDayOfWeekAbbrSql("do.estimatedArrivalDate")
val sql = """
WITH DoFloorCounts AS (
SELECT
@@ -251,13 +325,39 @@ class DoReleaseCoordinatorService(
FROM DoFloorSummary
),
TruckSelection AS (
SELECT
SELECT
do.id AS delivery_order_id,
do.shopId,
do.estimatedArrivalDate,
pf.preferred_floor,
pf.preferred_store_id,
$dayOfWeekSql AS day_of_week_abbr,
CASE
WHEN (SELECT COUNT(*) FROM fpsmsdb.truck t WHERE t.shopId = do.shopId AND t.deleted = 0 AND t.Store_id = pf.preferred_store_id) > 1
AND pf.preferred_store_id = 4
AND do.estimatedArrivalDate IS NOT NULL THEN
COALESCE(
(SELECT t.DepartureTime
FROM fpsmsdb.truck t
WHERE t.shopId = do.shopId
AND t.deleted = 0
AND t.Store_id = pf.preferred_store_id
AND t.TruckLanceCode LIKE CONCAT('%', $dayOfWeekSql, '%')
ORDER BY t.DepartureTime ASC
LIMIT 1),
(SELECT t.DepartureTime
FROM fpsmsdb.truck t
WHERE t.shopId = do.shopId AND t.deleted = 0
AND t.Store_id = pf.preferred_store_id
ORDER BY t.DepartureTime ASC
LIMIT 1),
(SELECT t.DepartureTime
FROM fpsmsdb.truck t
WHERE t.shopId = do.shopId AND t.deleted = 0
ORDER BY t.DepartureTime ASC
LIMIT 1),
'23:59:59'
)
WHEN (SELECT COUNT(*) FROM fpsmsdb.truck t WHERE t.shopId = do.shopId AND t.deleted = 0) > 1 THEN
COALESCE(
(SELECT t.DepartureTime
@@ -284,6 +384,31 @@ class DoReleaseCoordinatorService(
)
END AS selected_departure_time,
CASE
WHEN (SELECT COUNT(*) FROM fpsmsdb.truck t WHERE t.shopId = do.shopId AND t.deleted = 0 AND t.Store_id = pf.preferred_store_id) > 1
AND pf.preferred_store_id = 4
AND do.estimatedArrivalDate IS NOT NULL THEN
COALESCE(
(SELECT t.TruckLanceCode
FROM fpsmsdb.truck t
WHERE t.shopId = do.shopId
AND t.deleted = 0
AND t.Store_id = pf.preferred_store_id
AND t.TruckLanceCode LIKE CONCAT('%', $dayOfWeekSql, '%')
ORDER BY t.DepartureTime ASC
LIMIT 1),
(SELECT t.TruckLanceCode
FROM fpsmsdb.truck t
WHERE t.shopId = do.shopId AND t.deleted = 0
AND t.Store_id = pf.preferred_store_id
ORDER BY t.DepartureTime ASC
LIMIT 1),
(SELECT t.TruckLanceCode
FROM fpsmsdb.truck t
WHERE t.shopId = do.shopId AND t.deleted = 0
ORDER BY t.DepartureTime ASC
LIMIT 1),
'ZZ'
)
WHEN (SELECT COUNT(*) FROM fpsmsdb.truck t WHERE t.shopId = do.shopId AND t.deleted = 0) > 1 THEN
COALESCE(
(SELECT t.TruckLanceCode
@@ -310,17 +435,24 @@ class DoReleaseCoordinatorService(
)
END AS selected_truck_lance,
COALESCE(
(SELECT t.LoadingSequence
FROM fpsmsdb.truck t
WHERE t.shopId = do.shopId AND t.deleted = 0
AND t.Store_id = pf.preferred_store_id
ORDER BY t.DepartureTime ASC LIMIT 1),
(SELECT t.LoadingSequence
FROM fpsmsdb.truck t
WHERE t.shopId = do.shopId AND t.deleted = 0
ORDER BY t.DepartureTime ASC LIMIT 1),
999
) AS loading_sequence
(SELECT t.LoadingSequence
FROM fpsmsdb.truck t
WHERE t.shopId = do.shopId AND t.deleted = 0
AND t.Store_id = pf.preferred_store_id
AND (pf.preferred_store_id != 4 OR do.estimatedArrivalDate IS NULL OR
t.TruckLanceCode LIKE CONCAT('%', $dayOfWeekSql, '%'))
ORDER BY t.DepartureTime ASC LIMIT 1),
(SELECT t.LoadingSequence
FROM fpsmsdb.truck t
WHERE t.shopId = do.shopId AND t.deleted = 0
AND t.Store_id = pf.preferred_store_id
ORDER BY t.DepartureTime ASC LIMIT 1),
(SELECT t.LoadingSequence
FROM fpsmsdb.truck t
WHERE t.shopId = do.shopId AND t.deleted = 0
ORDER BY t.DepartureTime ASC LIMIT 1),
999
) AS loading_sequence
FROM fpsmsdb.delivery_order do
LEFT JOIN PreferredFloor pf ON pf.deliveryOrderId = do.id
WHERE do.id IN (${ids.joinToString(",")})
@@ -338,35 +470,34 @@ class DoReleaseCoordinatorService(
""".trimIndent()
println("🔍 DEBUG: SQL length: ${sql.length} characters") // 添加这行
println("🔍 DEBUG: SQL first 500 chars: ${sql.take(500)}") // 添加这行
println("🔍 DEBUG: SQL length: ${sql.length} characters")
println("🔍 DEBUG: SQL first 500 chars: ${sql.take(500)}")
val results = jdbcDao.queryForList(sql)
println("🔍 DEBUG: Results type: ${results.javaClass.name}") // 添加这行
println("🔍 DEBUG: Results size: ${results.size}") // 添加这行
if (results.isNotEmpty()) {
println("🔍 DEBUG: First result keys: ${results[0].keys}") // 添加这行
println("🔍 DEBUG: First result: ${results[0]}") // 添加这行
}
val sortedIds = results.mapNotNull { row ->
(row["id"] as? Number)?.toLong()
}
println("🔍 DEBUG: Query returned ${sortedIds.size} sorted IDs")
println("🔍 DEBUG: First 10 sorted IDs: ${sortedIds.take(10)}")
return if (sortedIds.isEmpty()) {
println("⚠️ WARNING: No sorted IDs, using original order")
ids
} else {
sortedIds
}
println("🔍 DEBUG: Results type: ${results.javaClass.name}")
println("🔍 DEBUG: Results size: ${results.size}")
if (results.isNotEmpty()) {
println("🔍 DEBUG: First result keys: ${results[0].keys}")
println("🔍 DEBUG: First result: ${results[0]}")
}
val sortedIds = results.mapNotNull { row ->
(row["id"] as? Number)?.toLong()
}
println("🔍 DEBUG: Query returned ${sortedIds.size} sorted IDs")
println("🔍 DEBUG: First 10 sorted IDs: ${sortedIds.take(10)}")
return if (sortedIds.isEmpty()) {
println("⚠️ WARNING: No sorted IDs, using original order")
ids
} else {
sortedIds
}
} catch (e: Exception) {
println("❌ ERROR: ${e.message}")
println("❌ ERROR Stack Trace:") // 添加这行
println("❌ ERROR: ${e.message}")
println("❌ ERROR Stack Trace:")
e.printStackTrace()
return ids
}


+ 5
- 0
src/main/java/com/ffii/fpsms/modules/deliveryOrder/web/DeliveryOrderController.kt Просмотреть файл

@@ -38,6 +38,7 @@ import com.ffii.fpsms.modules.deliveryOrder.service.DoPickOrderService
import com.ffii.core.response.RecordsRes
import com.ffii.fpsms.modules.deliveryOrder.web.models.PrintQrCodeForDoRequest
import com.ffii.fpsms.modules.stock.service.StockInLineService
import com.ffii.fpsms.modules.deliveryOrder.web.models.Check4FTruckBatchResponse

@RequestMapping("/do")
@RestController
@@ -228,4 +229,8 @@ class DeliveryOrderController(
fun printQrCodeForDeliveryOrder(@ModelAttribute request: PrintQrCodeForDoRequest) {
stockInLineService.printQrCodeForDeliveryOrder(request)
}
@PostMapping("/check-4f-trucks-batch")
fun check4FTrucksBatch(@RequestBody doIds: List<Long>): Check4FTruckBatchResponse {
return deliveryOrderService.check4FTruckAvailabilityForDoList(doIds)
}
}

+ 20
- 0
src/main/java/com/ffii/fpsms/modules/deliveryOrder/web/models/TicketReleaseTableResponse.kt Просмотреть файл

@@ -26,4 +26,24 @@ data class TicketReleaseTableResponse(
val requiredDeliveryDate: LocalDate?,
val handlerName: String?,
val numberOfFGItems: Int = 0
)
data class TruckInfoDto(
val id: Long?,
val truckLanceCode: String?,
val departureTime: String?, // or LocalTime?
val storeId: String?,
val shopCode: String?,
val shopName: String?
)

data class ProblemDoDto(
val deliveryOrderId: Long,
val deliveryOrderCode: String?,
val targetDate: LocalDate,
val availableTrucks: List<TruckInfoDto>
)

data class Check4FTruckBatchResponse(
val hasProblem: Boolean,
val problems: List<ProblemDoDto>
)

+ 41
- 3
src/main/java/com/ffii/fpsms/modules/jobOrder/service/JobOrderService.kt Просмотреть файл

@@ -26,8 +26,9 @@ import com.google.gson.reflect.TypeToken
import org.springframework.data.domain.PageRequest
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional
import java.math.BigDecimal

import java.math.BigDecimal
import com.ffii.fpsms.modules.jobOrder.entity.JobOrderBomMaterialRepository
import java.time.format.DateTimeFormatter
import kotlin.jvm.optionals.getOrNull
import com.ffii.fpsms.modules.jobOrder.entity.JobTypeRepository
@@ -62,7 +63,7 @@ import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import kotlin.math.exp
import com.ffii.fpsms.modules.productProcess.entity.ProductProcessRepository
import java.math.RoundingMode
import java.time.LocalDate
import java.time.LocalDateTime
@Service
@@ -83,7 +84,8 @@ open class JobOrderService(
val jobTypeRepository: JobTypeRepository,
val inventoryRepository: InventoryRepository,
val stockInLineRepository: StockInLineRepository,
val productProcessRepository: ProductProcessRepository
val productProcessRepository: ProductProcessRepository,
val jobOrderBomMaterialRepository: JobOrderBomMaterialRepository
) {

open fun allJobOrdersByPage(request: SearchJobOrderInfoRequest): RecordsRes<JobOrderInfo> {
@@ -803,4 +805,40 @@ open class JobOrderService(
)
}
*/
open fun updateJoReqQty(request: UpdateJoReqQtyRequest): MessageResponse {
val jobOrder = jobOrderRepository.findById(request.id).orElse(null)
?: throw NoSuchElementException("Job Order not found with id: ${request.id}")
val newReqQty = BigDecimal.valueOf(request.reqQty.toLong())
// 更新 JobOrder 的 reqQty
jobOrder.reqQty = newReqQty
jobOrderRepository.save(jobOrder)
// 更新相关的 JobOrderBomMaterial 的 reqQty(根据新的比例重新计算)
val bom = jobOrder.bom
if (bom != null && bom.outputQty != null && bom.outputQty!! > BigDecimal.ZERO) {
val proportion = newReqQty.divide(bom.outputQty!!, 5, RoundingMode.HALF_UP)
val jobOrderBomMaterials = jobOrderBomMaterialRepository.findAllByJobOrderId(jobOrder.id)
jobOrderBomMaterials.forEach { jobm ->
// 找到对应的 BOM Material
val bomMaterial = bom.bomMaterials?.find { it.item?.id == jobm.item?.id }
if (bomMaterial != null && bomMaterial.qty != null) {
jobm.reqQty = (bomMaterial.qty!!.times(proportion)).setScale(0, RoundingMode.CEILING)
jobOrderBomMaterialRepository.save(jobm)
}
}
}
return MessageResponse(
id = jobOrder.id,
code = jobOrder.code,
name = jobOrder.bom?.name,
type = "success",
message = "Job Order ReqQty Updated",
errorPosition = null,
entity = mapOf("reqQty" to request.reqQty)
)
}
}

+ 4
- 0
src/main/java/com/ffii/fpsms/modules/jobOrder/web/JobOrderController.kt Просмотреть файл

@@ -316,4 +316,8 @@ fun checkJobOrderCreated(@Valid @RequestBody request: CheckJobOrderCreatedReques
return jobOrderService.checkJobOrderCreated(request)
}
*/
@PostMapping("/updateReqQty")
fun updateJoReqQty(@Valid @RequestBody request: UpdateJoReqQtyRequest): MessageResponse {
return jobOrderService.updateJoReqQty(request)
}
}

+ 4
- 0
src/main/java/com/ffii/fpsms/modules/jobOrder/web/model/CreateJobOrderRequest.kt Просмотреть файл

@@ -149,4 +149,8 @@ data class JobOrderInfoResponse(
val code: String,
val itemName: String,
val reqQty: BigDecimal,
)
data class UpdateJoReqQtyRequest(
val id: Long,
val reqQty: Int
)

+ 5
- 0
src/main/java/com/ffii/fpsms/modules/master/entity/WarehouseRepository.kt Просмотреть файл

@@ -14,4 +14,9 @@ interface WarehouseRepository : AbstractRepository<Warehouse, Long> {

fun findByCodeAndDeletedIsFalse(code: String): Warehouse?;

fun findAllByDeletedIsFalse(): List<Warehouse>;
fun findAllByStockTakeSectionAndDeletedIsFalse(stockTakeSection: String): List<Warehouse>;
fun findDistinctStockTakeSectionsByDeletedIsFalse(): List<String>;
fun findAllByIdIn(ids: List<Long>): List<Warehouse>;
fun findAllByCodeAndDeletedIsFalse(code: String): List<Warehouse>
}

+ 4
- 4
src/main/java/com/ffii/fpsms/modules/pickOrder/entity/PickExecutionIssueRepository.kt Просмотреть файл

@@ -67,10 +67,10 @@ fun findMaterialIssues(): List<PickExecutionIssue>
fun getBadItemList_statusIn(@Param("statuses") statuses: List<PickExecutionIssueEnum>): List<PickExecutionIssue>

@Query("""
SELECT p FROM PickExecutionIssue p
WHERE p.badItemQty IS NOT NULL
AND p.badItemQty > 0
AND p.deleted = false
SELECT p FROM PickExecutionIssue p
WHERE (p.badItemQty IS NOT NULL AND p.badItemQty > 0)
OR (p.missQty IS NOT NULL AND p.missQty > 0)
AND p.deleted = false
ORDER BY p.created DESC
""")
fun getBadItemList(): List<PickExecutionIssue>


+ 13
- 0
src/main/java/com/ffii/fpsms/modules/pickOrder/entity/TruckRepository.kt Просмотреть файл

@@ -38,4 +38,17 @@ interface TruckRepository : AbstractRepository<Truck, Long> {
fun findAllByShopId(shopId :Long):List<Truck>
//fun findByTruckLanceCode(truckLanceCode: String):List<Truck>
//fun deleteByTruckLanceCodeAndDepartureTimeAndLoadingSequenceAndDistrictReferenceAndStoreId(truckLanceCode: String, departureTime: LocalTime, loadingSequence: Long, districtReference: Long, storeId: Long): String
@Query("""
SELECT t FROM Truck t
WHERE t.shop.id = :shopId
AND t.storeId = :storeId
AND t.deleted = false
AND t.truckLanceCode LIKE CONCAT('%', :dayOfWeekAbbr, '%')
ORDER BY t.departureTime ASC
""")
fun findByShopIdAndStoreIdAndDayOfWeek(
@Param("shopId") shopId: Long,
@Param("storeId") storeId: String,
@Param("dayOfWeekAbbr") dayOfWeekAbbr: String
): List<Truck>
}

+ 4
- 1
src/main/java/com/ffii/fpsms/modules/productProcess/entity/ProductProcess.kt Просмотреть файл

@@ -53,5 +53,8 @@ open class ProductProcess : BaseEntity<Long>() {

@Column(name = "productionPriority")
open var productionPriority: Int? = 50

@Column(name = "submitedBagRecord")
open var submitedBagRecord: Boolean? = false
}

+ 6
- 1
src/main/java/com/ffii/fpsms/modules/productProcess/entity/ProductProcessLine.kt Просмотреть файл

@@ -39,7 +39,12 @@ open class ProductProcessLine : BaseEntity<Long>() {
@Column(name = "equipment_name", length = 100)
open var equipmentType: String? = null
// 添加 @ManyToOne
@Column(name = "processingTime")
open var processingTime: Int? = null
@Column(name = "setupTime")
open var setupTime: Int? = null
@Column(name = "changeoverTime")
open var changeoverTime: Int? = null
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "byproductId")
open var byproduct: Items? = null


+ 3
- 1
src/main/java/com/ffii/fpsms/modules/productProcess/entity/projections/ProductProcessInfo.kt Просмотреть файл

@@ -5,7 +5,7 @@ import java.time.LocalDate
import com.ffii.fpsms.modules.productProcess.enums.ProductProcessStatus
import com.fasterxml.jackson.annotation.JsonFormat
import com.ffii.fpsms.modules.jobOrder.enums.JobOrderStatus
import java.math.BigDecimal
data class ProductProcessInfo(
val id: Long?,
val productProcessCode: String?,
@@ -33,11 +33,13 @@ data class ProductProcessInfo(
val itemId: Long?,
val itemCode: String?,
val itemName: String?,
val bomBaseQty: BigDecimal?,
val outputQtyUom: String?,
val outputQty: Int?,
val timeSequence: Int?,
val complexity: Int?,
val productionPriority: Int?,
val submitedBagRecord: Boolean?,
val productProcessLines: List<ProductProcessLineInfo>?,
val totalStockQty: Int?,
val insufficientStockQty: Int?,


+ 151
- 31
src/main/java/com/ffii/fpsms/modules/productProcess/service/ProductProcessService.kt Просмотреть файл

@@ -76,7 +76,7 @@ open class ProductProcessService(
) {
open fun findAll(pageable: Pageable): Page<ProductProcess> {
println("📋 Service: Finding all ProductProcess with page: ${pageable.pageNumber}, size: ${pageable.pageSize}")
println(" Service: Finding all ProductProcess with page: ${pageable.pageNumber}, size: ${pageable.pageSize}")
val result = productProcessRepository.findAll(pageable)
println(" Service: Found ${result.totalElements} records")
return result
@@ -161,7 +161,7 @@ open class ProductProcessService(

// 添加:查询工序的所有步骤
open fun getLines(productProcessId: Long): List<ProductProcessLine> {
println("📋 Service: Getting lines for process ID: $productProcessId")
println(" Service: Getting lines for process ID: $productProcessId")
val lines = productProcessLineRepository.findByProductProcess_Id(productProcessId)
println(" Service: Found ${lines.size} lines")
return lines
@@ -268,7 +268,7 @@ open class ProductProcessService(
}
open fun getIssues(productProcessId: Long): List<ProductionProcessIssue> {
println("📋 Service: Getting issues for ProductProcess ID: $productProcessId")
println(" Service: Getting issues for ProductProcess ID: $productProcessId")
val issues = productionProcessIssueRepository.findByProductProcess_Id(productProcessId)
println(" Service: Found ${issues.size} issues")
return issues
@@ -317,7 +317,7 @@ open class ProductProcessService(
return result
}
open fun findAllAsDto(pageable: Pageable): Page<ProductProcessSimpleResponse> {
println("📋 Service: Finding all ProductProcess as DTO with page: ${pageable.pageNumber}, size: ${pageable.pageSize}")
println(" Service: Finding all ProductProcess as DTO with page: ${pageable.pageNumber}, size: ${pageable.pageSize}")
val entityPage = productProcessRepository.findAll(pageable)
val dtoList = entityPage.content.map { entity ->
@@ -580,6 +580,7 @@ open class ProductProcessService(
itemName = bom?.item?.name?:"",
timeSequence = bom?.timeSequence?:0,
complexity = bom?.complexity?:0,
bomBaseQty = bom.outputQty ?: BigDecimal.ZERO,
isDark = calculateColourScore(bom?.isDark?:0),
isDense = bom?.isDense?:0,
isFloat = calculateFloatScore(bom?.isFloat?:0),
@@ -594,7 +595,8 @@ open class ProductProcessService(
startTime = process.startTime?:LocalDateTime.now(),
endTime = process.endTime?:LocalDateTime.now(),
date = process.date?:LocalDate.now(),
productionPriority = process.productionPriority?:50,
productionPriority = process.productionPriority?:50,
submitedBagRecord = process.submitedBagRecord?:false,
totalStockQty = totalStockQty,
insufficientStockQty = insufficientStockQty,
sufficientStockQty = sufficientStockQty,
@@ -617,9 +619,9 @@ open class ProductProcessService(
equipment_name = line.equipmentType?:"",
equipmentDetailCode = equipmentDetail?.code?:"",
status = line.status?:"",
durationInMinutes = line.bomProcess?.durationInMinute?:0,
prepTimeInMinutes = line.bomProcess?.prepTimeInMinute?:0,
postProdTimeInMinutes = line.bomProcess?.postProdTimeInMinute?:0,
durationInMinutes = line.processingTime?:0,
prepTimeInMinutes = line.setupTime?:0,
postProdTimeInMinutes = line.changeoverTime?:0,
byproductId = line.byproduct?.id?:0,
byproductName = line.byproduct?.name?:"",
byproductQty = line.byproductQty?:0,
@@ -640,7 +642,7 @@ open class ProductProcessService(
endTime = line.endTime
)
},
}.sortedBy { it.seqNo },
jobOrderLines = bomMaterials.map { line ->
val itemId = line.item?.id ?: 0L
val stockQty = stockQtyMap[itemId]?.toInt() ?: 0
@@ -719,6 +721,9 @@ open class ProductProcessService(
this.description = bomProcess.description?:""
this.equipmentType = equipment?.code?:""
this.status = "Pending"
this.processingTime = bomProcess.durationInMinute
this.setupTime = bomProcess.prepTimeInMinute
this.changeoverTime = bomProcess.postProdTimeInMinute
}
productProcessLineRepository.save(productProcessLine)
}
@@ -921,7 +926,7 @@ open class ProductProcessService(
}
open fun getJobOrderProcessLineDetail(productProcessLineId: Long): JobOrderProcessLineDetailResponse {
val productProcessLine = productProcessLineRepository.findById(productProcessLineId).orElse(null)
val productProcess = productProcessRepository.findById(productProcessLine?.productProcess?.id?:0L).orElse(null)
val bomProcessId = productProcessLine?.bomProcess?.id
println("bomProcessId ${bomProcessId}")
val bomProcess: BomProcess? = bomProcessId?.let {
@@ -961,6 +966,7 @@ open class ProductProcessService(
startTime = productProcessLine.startTime?:LocalDateTime.now(),
endTime = productProcessLine.endTime?:LocalDateTime.now(),
stopTime = productProcessIssue?.stopTime,
submitedBagRecord = productProcess.submitedBagRecord?:false,
// ✅ 添加总暂停时间(毫秒)
totalPausedTimeMs = totalPausedTimeMs.toLong(),
status = productProcessLine.status?:"",
@@ -985,13 +991,13 @@ open class ProductProcessService(
}

open fun updateProductProcessLineStatus(productProcessLineId: Long, status: String): MessageResponse {
println("📋 Service: Updating ProductProcessLine Status: $productProcessLineId")
println(" Service: Updating ProductProcessLine Status: $productProcessLineId")
val productProcessLine = productProcessLineRepository.findById(productProcessLineId).orElse(null)
println("📋 Service: ProductProcessLine: $productProcessLine")
println(" Service: ProductProcessLine: $productProcessLine")
productProcessLine.status = status
// productProcessLine.endTime = LocalDateTime.now()
productProcessLineRepository.save(productProcessLine)
println("📋 Service: ProductProcessLine Status Updated: ${productProcessLine.status}")
println(" Service: ProductProcessLine Status Updated: ${productProcessLine.status}")
CompleteProductProcessStatusIfAllLinesCompleted(productProcessLine.productProcess?.id?:0)
@@ -1008,8 +1014,8 @@ open class ProductProcessService(
val productProcessLine = productProcessLineRepository.findById(productProcessLineId).orElse(null)
val productProcess = productProcessRepository.findById(productProcessLine?.productProcess?.id?:0L).orElse(null)
// 如果 startTime 为空,才设置它(保留已存在的 startTime)
println("📋 Service: ProductProcessLine StartTime: ${productProcessLine.startTime}")
println("📋 Service: ProductProcess StartTime: ${productProcess.startTime}")
println(" Service: ProductProcessLine StartTime: ${productProcessLine.startTime}")
println(" Service: ProductProcess StartTime: ${productProcess.startTime}")
if (productProcessLine.startTime == null) {
productProcessLine.startTime = LocalDateTime.now()
productProcessLineRepository.save(productProcessLine)
@@ -1022,7 +1028,7 @@ open class ProductProcessService(
// 总是设置 endTime(因为 Pass 意味着完成)
productProcessLine.endTime = LocalDateTime.now()
productProcessLineRepository.save(productProcessLine)
println("📋 Service: ProductProcessLine EndTime: ${productProcessLine.endTime}")
println(" Service: ProductProcessLine EndTime: ${productProcessLine.endTime}")
// 更新状态为 "Pass"
updateProductProcessLineStatus(productProcessLineId, "Pass")

@@ -1042,20 +1048,20 @@ open class ProductProcessService(
}
open fun CompleteProductProcessStatusIfAllLinesCompleted(productProcessId: Long): MessageResponse {
val productProcess = productProcessRepository.findById(productProcessId).orElse(null)
println("📋 Service: ProductProcess: $productProcess")
println(" Service: ProductProcess: $productProcess")
val productProcessLines = productProcessLineRepository.findByProductProcess_Id(productProcessId)

println("📋 Service: ProductProcessLines: $productProcessLines")
println(" Service: ProductProcessLines: $productProcessLines")
if(productProcessLines.all { it.status == "Completed" || it.status == "Pass" }) {
productProcess.status = ProductProcessStatus.COMPLETED
if (productProcess.endTime == null) {
productProcess.endTime = LocalDateTime.now()
}
productProcessRepository.save(productProcess)
println("📋 Service: ProductProcess Status Updated: ${productProcess.status}")
println(" Service: ProductProcess Status Updated: ${productProcess.status}")
}
else {
println("📋 Service: ProductProcess Lines are not completed")
println(" Service: ProductProcess Lines are not completed")
}
return MessageResponse(
id = null,
@@ -1296,9 +1302,9 @@ open class ProductProcessService(
}
else{
updateProductProcessLineStatus(productProcessLineId, "InProgress")
println("📋 Service: ProductProcess Lines are not Pending")
println("📋 Service: ProductProcess Lines: ${allproductProcessLines.map { it.status }}")
println("📋 Service: ProductProcess Line: ${productProcessLine.status}")
println(" Service: ProductProcess Lines are not Pending")
println(" Service: ProductProcess Lines: ${allproductProcessLines.map { it.status }}")
println(" Service: ProductProcess Line: ${productProcessLine.status}")
}
return MessageResponse(
id = productProcessLine.id,
@@ -1390,16 +1396,16 @@ open class ProductProcessService(
}

open fun SaveProductProcessIssueTime(request: SaveProductProcessIssueTimeRequest): MessageResponse {
println("📋 Service: Saving ProductProcess Issue Time: ${request.productProcessLineId}")
val productProcessLine = productProcessLineRepository.findById(request.productProcessLineId).orElse(null)
val productProcess = productProcessRepository.findById(productProcessLine.productProcess?.id?:0L).orElse(null)
//val productProcessLines=productProcessLineRepository.findByProductProcess_Id(productProcessId)
val startTime=productProcessLine?.startTime
println("📋 Service: Start Time: $startTime")
val stopTime=LocalDateTime.now()
println("📋 Service: Stop Time: $stopTime")
val operatorId=productProcessLine.operator?.id
val Operator=userRepository.findById(operatorId).orElse(null)
val reason = request.reason
@@ -1429,13 +1435,13 @@ open class ProductProcessService(


open fun SaveProductProcessResumeTime(productProcessIssueId: Long): MessageResponse {
println("📋 Service: Saving ProductProcess Resume Time: $productProcessIssueId")
println(" Service: Saving ProductProcess Resume Time: $productProcessIssueId")
val productProcessLineIssue = productionProcessIssueRepository.findById(productProcessIssueId).orElse(null)
val productProcessLine = productProcessLineRepository.findById(productProcessLineIssue.productProcessLineId?:0L).orElse(null)
val resumeTime = LocalDateTime.now()
println("📋 Service: Resume Time: $resumeTime")
println(" Service: Resume Time: $resumeTime")
productProcessLineIssue?.resumeTime = resumeTime
println("📋 Service: Resume Time: $resumeTime")
println(" Service: Resume Time: $resumeTime")
val totalTime = resumeTime.toEpochSecond(ZoneOffset.UTC) - (productProcessLineIssue.stopTime?.toEpochSecond(ZoneOffset.UTC) ?: 0L)
productProcessLineIssue?.totalTime = totalTime.toInt()
productProcessLineIssue?.status = "Resumed"
@@ -1453,7 +1459,32 @@ open class ProductProcessService(
)

}

open fun UpdateProductProcessLineProcessingTimeSetupTimeChangeoverTime(request: UpdateProductProcessLineProcessingTimeSetupTimeChangeoverTimeRequest): MessageResponse {
val productProcessLine = productProcessLineRepository.findById(request.productProcessLineId).orElse(null)
val processingTime = request.processingTime
val setupTime = request.setupTime
val changeoverTime = request.changeoverTime
if(processingTime != null) {
productProcessLine.processingTime = processingTime
productProcessLineRepository.save(productProcessLine)
}
if(setupTime != null) {
productProcessLine.setupTime = setupTime
productProcessLineRepository.save(productProcessLine)
}
if(changeoverTime != null) {
productProcessLine.changeoverTime = changeoverTime
productProcessLineRepository.save(productProcessLine)
}
return MessageResponse(
id = request.productProcessLineId,
code = "200",
name = "ProductProcess ProcessingTimeSetupTimeChangeoverTime Updated",
type = "success",
message = "ProductProcess ProcessingTimeSetupTimeChangeoverTime Updated",
errorPosition = null,
)
}
open fun UpdateProductProcessPriority(productProcessId: Long, productionPriority: Int): MessageResponse {
val productProcess = productProcessRepository.findById(productProcessId).orElse(null)
productProcess.productionPriority = productionPriority
@@ -1467,5 +1498,94 @@ open class ProductProcessService(
errorPosition = null,
)
}
}
open fun createNewProductProcessLine(productProcessLineId: Long): MessageResponse {
val sourceLine = productProcessLineRepository.findById(productProcessLineId).orElse(null)
?: return MessageResponse(
id = productProcessLineId,
code = "404",
name = "ProductProcess Line Not Found",
type = "error",
message = "ProductProcess Line with ID $productProcessLineId not found",
errorPosition = null,
)
val productProcessId = sourceLine.productProcess?.id ?: return MessageResponse(
id = productProcessLineId,
code = "400",
name = "Invalid ProductProcess",
type = "error",
message = "ProductProcess Line has no associated ProductProcess",
errorPosition = null,
)
val originalSeqNo = sourceLine.seqNo ?: return MessageResponse(
id = productProcessLineId,
code = "400",
name = "Invalid SeqNo",
type = "error",
message = "ProductProcess Line has no seqNo",
errorPosition = null,
)
// 先获取同一 productProcess 的所有 lines(在更新前)
val allLines = productProcessLineRepository.findByProductProcess_Id(productProcessId)
// 更新所有其他 seqNo >= originalSeqNo + 1 的 lines(不包括原 line)
allLines.filter {
it.id != sourceLine.id &&
it.seqNo != null &&
it.seqNo!! >= originalSeqNo + 1
}.forEach { line ->
line.seqNo = (line.seqNo ?: 0) + 1
productProcessLineRepository.save(line)
}
// 创建新的 line,复制所有字段
val newLine = ProductProcessLine().apply {
// 复制基本字段
this.productProcess = sourceLine.productProcess
this.bomProcess = sourceLine.bomProcess
this.operator = sourceLine.operator
this.equipment = sourceLine.equipment
this.equipmentDetailId = sourceLine.equipmentDetailId
this.handler = sourceLine.handler
this.seqNo = originalSeqNo + 1 // 新 line 的 seqNo = originalSeqNo + 1
this.name = sourceLine.name
this.description = sourceLine.description
this.equipmentType = sourceLine.equipmentType
this.status = "Pending" // 新创建的 line 状态总是设为 Pending
this.byproduct = sourceLine.byproduct
this.byproductName = sourceLine.byproductName
this.byproductQty = sourceLine.byproductQty
this.byproductUom = sourceLine.byproductUom
this.scrapQty = sourceLine.scrapQty
this.scrapUom = sourceLine.scrapUom
this.defectQty = sourceLine.defectQty
this.defectUom = sourceLine.defectUom
this.defectDescription = sourceLine.defectDescription
this.defectQty2 = sourceLine.defectQty2
this.defectUom2 = sourceLine.defectUom2
this.defectDescription2 = sourceLine.defectDescription2
this.defectQty3 = sourceLine.defectQty3
this.defectUom3 = sourceLine.defectUom3
this.defectDescription3 = sourceLine.defectDescription3
this.outputFromProcessQty = sourceLine.outputFromProcessQty
this.outputFromProcessUom = sourceLine.outputFromProcessUom
// 不复制时间字段,新 line 应该没有开始和结束时间
this.startTime = null
this.endTime = null
}
// 保存新 line(原 line 的 seqNo 保持不变,不需要更新)
val savedNewLine = productProcessLineRepository.save(newLine)
return MessageResponse(
id = savedNewLine.id ?: productProcessLineId,
code = "200",
name = "ProductProcess Line Created",
type = "success",
message = "ProductProcess Line Created successfully with seqNo ${originalSeqNo + 1}",
errorPosition = null,
)
}}


+ 8
- 0
src/main/java/com/ffii/fpsms/modules/productProcess/web/ProductProcessController.kt Просмотреть файл

@@ -217,4 +217,12 @@ class ProductProcessController(
fun passProductProcessLine(@PathVariable lineId: Long): MessageResponse {
return productProcessService.passProductProcessLine(lineId)
}
@PostMapping("/Demo/ProcessLine/new/{lineId}")
fun newProductProcessLine(@PathVariable lineId: Long): MessageResponse {
return productProcessService.createNewProductProcessLine(lineId)
}
@PostMapping("/Demo/ProcessLine/update/processingTimeSetupTimeChangeoverTime/{lineId}")
fun updateProductProcessLineProcessingTimeSetupTimeChangeoverTime(@PathVariable lineId: Long, @RequestBody request: UpdateProductProcessLineProcessingTimeSetupTimeChangeoverTimeRequest): MessageResponse {
return productProcessService.UpdateProductProcessLineProcessingTimeSetupTimeChangeoverTime(request)
}
}

+ 12
- 1
src/main/java/com/ffii/fpsms/modules/productProcess/web/model/SaveProductProcessRequest.kt Просмотреть файл

@@ -152,7 +152,8 @@ data class JobOrderProcessLineDetailResponse(
val byproductId: Long?,
val byproductName: String?,
val byproductQty: Int?,
val byproductUom: String?
val byproductUom: String?,
val submitedBagRecord: Boolean?
)

data class AllJoborderProductProcessInfoResponse(
@@ -207,4 +208,14 @@ data class SaveProductProcessIssueTimeRequest(
data class SaveProductProcessResumeTimeRequest(
val productProcessLineId: Long,
val resumeTime: LocalDateTime?
)
data class UpdateJoReqQtyRequest(
val id: Long,
val reqQty: Int
)
data class UpdateProductProcessLineProcessingTimeSetupTimeChangeoverTimeRequest(
val productProcessLineId: Long,
val processingTime: Int?,
val setupTime: Int?,
val changeoverTime: Int?
)

+ 11
- 0
src/main/java/com/ffii/fpsms/modules/stock/entity/InventoryLotLineRepository.kt Просмотреть файл

@@ -52,4 +52,15 @@ interface InventoryLotLineRepository : AbstractRepository<InventoryLotLine, Long
AND ill.deleted = false
""")
fun findByLotNoAndItemId(lotNo: String, itemId: Long): InventoryLotLine?
// InventoryLotLineRepository.kt 中添加
@Query("SELECT ill FROM InventoryLotLine ill WHERE ill.warehouse.id IN :warehouseIds AND ill.deleted = false")
fun findAllByWarehouseIdInAndDeletedIsFalse(@Param("warehouseIds") warehouseIds: List<Long>): List<InventoryLotLine>

@Query("""
SELECT ill FROM InventoryLotLine ill
WHERE ill.warehouse.code = :warehouseCode
AND ill.deleted = false
ORDER BY ill.inventoryLot.item.code, ill.inventoryLot.lotNo
""")
fun findAllByWarehouseCodeAndDeletedIsFalse(@Param("warehouseCode") warehouseCode: String): List<InventoryLotLine>
}

+ 1
- 0
src/main/java/com/ffii/fpsms/modules/stock/entity/InventoryLotRepository.kt Просмотреть файл

@@ -23,4 +23,5 @@ interface InventoryLotRepository: AbstractRepository<InventoryLot, Long> {
AND il.deleted = false
""")
fun findByLotNoAndItemId(lotNo: String, itemId: Long): InventoryLot?
fun findByIdAndDeletedFalse(id: Serializable): InventoryLot?
}

+ 3
- 0
src/main/java/com/ffii/fpsms/modules/stock/entity/StockTake.kt Просмотреть файл

@@ -39,4 +39,7 @@ open class StockTake: BaseEntity<Long>() {
@Size(max = 500)
@Column(name = "remarks", length = 500)
open var remarks: String? = null
@Size(max = 255)
@Column(name = "stockTakeSection", length = 255)
open var stockTakeSection: String? = null
}

+ 1
- 0
src/main/java/com/ffii/fpsms/modules/stock/entity/StockTakeLineRepository.kt Просмотреть файл

@@ -7,4 +7,5 @@ import java.io.Serializable
@Repository
interface StockTakeLineRepository : AbstractRepository<StockTakeLine, Long> {
fun findByIdAndDeletedIsFalse(id: Serializable): StockTakeLine?;
fun findByInventoryLotLineIdAndStockTakeIdAndDeletedIsFalse(inventoryLotLineId: Serializable, stockTakeId: Serializable): StockTakeLine?;
}

+ 102
- 0
src/main/java/com/ffii/fpsms/modules/stock/entity/StockTakeRecord.kt Просмотреть файл

@@ -0,0 +1,102 @@
// StockTakeRecord.kt
package com.ffii.fpsms.modules.stock.entity

import com.ffii.core.entity.BaseEntity
import com.ffii.fpsms.modules.master.entity.Warehouse
import jakarta.persistence.*
import java.math.BigDecimal
import java.time.LocalDate
import java.time.LocalDateTime

@Entity
@Table(name = "stocktakerecord")
open class StockTakeRecord : BaseEntity<Long>() {
@Column(name = "itemId", nullable = false)
open var itemId: Long? = null
@Column(name = "lotId", nullable = false)
open var lotId: Long? = null
@ManyToOne
@JoinColumn(name = "warehouseId", nullable = false)
open var warehouse: Warehouse? = null
@Column(name = "stockTakeSection", length = 255)
open var stockTakeSection: String? = null
@ManyToOne
@JoinColumn(name = "stockTakeId", nullable = false)
open var stockTake: StockTake? = null
@Column(name = "approverId")
open var approverId: Long? = null
@Column(name = "approverName", length = 100)
open var approverName: String? = null
@Column(name = "stockTakerId", nullable = false)
open var stockTakerId: Long? = null
@Column(name = "stockTakerName", length = 100)
open var stockTakerName: String? = null
@Column(name = "pickerFirstStockTakeQty", precision = 14, scale = 2)
open var pickerFirstStockTakeQty: BigDecimal? = null
@Column(name = "pickerSecondStockTakeQty", precision = 14, scale = 2)
open var pickerSecondStockTakeQty: BigDecimal? = null
@Column(name = "approverStockTakeQty", precision = 14, scale = 2)
open var approverStockTakeQty: BigDecimal? = null
@Column(name = "approverSecondStockTakeQty", precision = 14, scale = 2)
open var approverSecondStockTakeQty: BigDecimal? = null
@Column(name = "bookQty", nullable = false, precision = 14, scale = 2)
open var bookQty: BigDecimal? = null
@Column(name = "badQty", precision = 14, scale = 2)
open var badQty: BigDecimal? = null
@Column(name = "pickerFirstBadQty", precision = 14, scale = 2)
open var pickerFirstBadQty: BigDecimal? = null
@Column(name = "pickerSecondBadQty", precision = 14, scale = 2)
open var pickerSecondBadQty: BigDecimal? = null
@Column(name = "approverBadQty", precision = 14, scale = 2)
open var approverBadQty: BigDecimal? = null
@Column(name = "varianceQty", precision = 14, scale = 2)
open var varianceQty: BigDecimal? = null
@Column(name = "uom", length = 30)
open var uom: String? = null
@Column(name = "stockTakeStartTime")
open var stockTakeStartTime: LocalDateTime? = null
@Column(name = "stockTakeEndTime")
open var stockTakeEndTime: LocalDateTime? = null
@Column(name = "date", nullable = false)
open var date: LocalDate? = null
@Column(name = "status", nullable = false, length = 30)
open var status: String? = null
@Column(name = "remarks", length = 500)
open var remarks: String? = null
@Column(name = "itemCode", length = 50)
open var itemCode: String? = null
@Column(name = "itemName", length = 200)
open var itemName: String? = null

@Column(name = "inventoryLotId")
open var inventoryLotId: Long? = null
@Column(name = "lotNo", length = 512)
open var lotNo: String? = null
@Column(name = "expiredDate")
open var expiredDate: LocalDate? = null
}

+ 24
- 0
src/main/java/com/ffii/fpsms/modules/stock/entity/StockTakeRecordRepository.kt Просмотреть файл

@@ -0,0 +1,24 @@
// 创建 StockTakeRecordRepository.kt
package com.ffii.fpsms.modules.stock.entity

import com.ffii.core.support.AbstractRepository
import org.springframework.stereotype.Repository
import java.io.Serializable
import org.springframework.data.jpa.repository.Query
@Repository
interface StockTakeRecordRepository : AbstractRepository<StockTakeRecord, Long> {
fun findAllByStockTakeIdAndDeletedIsFalse(stockTakeId: Long): List<StockTakeRecord>;
fun findByIdAndDeletedIsFalse(id: Serializable): StockTakeRecord?;
@Query("""
SELECT w.stockTakeSection, str.stockTake.id, COUNT(str.id) as count
FROM StockTakeRecord str
INNER JOIN Warehouse w ON str.warehouse.id = w.id
WHERE str.deleted = false
AND w.deleted = false
AND w.stockTakeSection IS NOT NULL
AND w.stockTakeSection != ''
AND str.stockTake.id IS NOT NULL
GROUP BY w.stockTakeSection, str.stockTake.id
""")
fun countStockTakeRecordsBySectionAndStockTakeId(): List<Array<Any>>
}

+ 1
- 0
src/main/java/com/ffii/fpsms/modules/stock/enums/StockTakeEnum.kt Просмотреть файл

@@ -3,5 +3,6 @@ package com.ffii.fpsms.modules.stock.enums
enum class StockTakeStatus(val value: String) {
PENDING("pending"),
STOCKTAKING("stockTaking"),
APPROVING("approving"),
COMPLETED("completed"),
}

+ 999
- 0
src/main/java/com/ffii/fpsms/modules/stock/service/StockTakeRecordService.kt
Разница между файлами не показана из-за своего большого размера
Просмотреть файл


+ 66
- 2
src/main/java/com/ffii/fpsms/modules/stock/service/StockTakeService.kt Просмотреть файл

@@ -48,7 +48,7 @@ class StockTakeService(
fun saveStockTake(request: SaveStockTakeRequest): StockTake {
val stockTake = request.id?.let { stockTakeRepository.findByIdAndDeletedIsFalse(it) } ?: StockTake();
val status = request.status?.let { _status -> StockTakeStatus.entries.find { it.value == _status } }
request.code?.let { stockTake.code = it }
if (stockTake.code == null) {
stockTake.code = assignStockTakeNo()
@@ -59,7 +59,8 @@ class StockTakeService(
request.actualEnd?.let { stockTake.actualEnd = it }
status?.let { stockTake.status = it }
request.remarks?.let { stockTake.remarks = it }

request.stockTakeSection?.let { stockTake.stockTakeSection = it } // 添加此行
return stockTakeRepository.save(stockTake);
}

@@ -230,4 +231,67 @@ class StockTakeService(
logger.info("--------- End - Import Stock Take Excel -------")
return "Import Excel success";
}




fun createStockTakeForSections(): Map<String, String> {
logger.info("--------- Start - Create Stock Take for Sections -------")
val result = mutableMapOf<String, String>()
// 1. 获取所有不同的 stockTakeSection(从 warehouse 表)
val allWarehouses = warehouseRepository.findAllByDeletedIsFalse()
val distinctSections = allWarehouses
.mapNotNull { it.stockTakeSection }
.distinct()
.filter { !it.isBlank() }
// 2. 获取所有 stock_take 记录(按 stockTakeSection 分组)
val allStockTakes = stockTakeRepository.findAll()
.filter { !it.deleted }
.groupBy { it.stockTakeSection }
// 3. 为每个 stockTakeSection 检查并创建
distinctSections.forEach { section ->
val stockTakesForSection = allStockTakes[section] ?: emptyList()
// 检查:如果该 section 的所有记录都是 COMPLETED,才创建新的
val allCompleted = stockTakesForSection.isEmpty() ||
stockTakesForSection.all { it.status == StockTakeStatus.COMPLETED }
if (allCompleted) {
try {
val now = LocalDateTime.now()
val code = assignStockTakeNo()
val saveStockTakeReq = SaveStockTakeRequest(
code = code,
planStart = now,
planEnd = now.plusDays(1),
actualStart = null,
actualEnd = null,
status = StockTakeStatus.PENDING.value,
remarks = null,
stockTakeSection = section
)
val savedStockTake = saveStockTake(saveStockTakeReq)
result[section] = "Created: ${savedStockTake.code}"
logger.info("Created stock take for section $section: ${savedStockTake.code}")
} catch (e: Exception) {
result[section] = "Error: ${e.message}"
logger.error("Error creating stock take for section $section: ${e.message}")
}
} else {
result[section] = "Skipped: Has non-completed records"
logger.info("Skipped section $section: Has non-completed records")
}
}
// 移除 null section 处理逻辑,因为 warehouse 表中没有 null 的 stockTakeSection
logger.info("--------- End - Create Stock Take for Sections -------")
return result
}
}

+ 6
- 0
src/main/java/com/ffii/fpsms/modules/stock/web/StockTakeController.kt Просмотреть файл

@@ -40,4 +40,10 @@ class StockTakeController(

return ResponseEntity.ok(stockTakeService.importExcel(workbook))
}
@PostMapping("/createForSections")
fun createStockTakeForSections(): ResponseEntity<Map<String, String>> {
val result = stockTakeService.createStockTakeForSections()
return ResponseEntity.ok(result)
}

}

+ 180
- 0
src/main/java/com/ffii/fpsms/modules/stock/web/StockTakeRecordController.kt Просмотреть файл

@@ -0,0 +1,180 @@
package com.ffii.fpsms.modules.stock.web

import com.ffii.fpsms.modules.stock.service.StockTakeRecordService
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.PostMapping
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RestController
import org.springframework.web.bind.annotation.RequestParam
import org.springframework.web.bind.annotation.RequestBody
import org.springframework.http.ResponseEntity
import com.ffii.fpsms.modules.stock.web.model.*
import org.slf4j.LoggerFactory

@RestController
@RequestMapping("/stockTakeRecord")
class StockTakeRecordController(
private val stockOutRecordService: StockTakeRecordService
) {
private val logger = LoggerFactory.getLogger(StockTakeRecordController::class.java)
@GetMapping("/AllPickedStockOutRecordList")
fun AllPickedStockOutRecordList(): List<AllPickedStockTakeListReponse> {
return stockOutRecordService.AllPickedStockTakeList()
}
@GetMapping("/AllApproverStockTakeList")
fun AllApproverStockTakeList(): List<AllPickedStockTakeListReponse> {
return stockOutRecordService.AllApproverStockTakeList()
}
@GetMapping("/inventoryLotDetailsBySectionNotMatch")
fun getInventoryLotDetailsByStockTakeSectionNotMatch(
@RequestParam stockTakeSection: String,
@RequestParam(required = false) stockTakeId: Long?
): List<InventoryLotDetailResponse> {
return stockOutRecordService.getInventoryLotDetailsByStockTakeSectionNotMatch(stockTakeSection, stockTakeId)
}
@GetMapping("/inventoryLotDetails")
fun getInventoryLotDetailsByWarehouseCode(
@RequestParam warehouseCode: String
): List<InventoryLotDetailResponse> {
return stockOutRecordService.getInventoryLotDetailsByWarehouseCode(warehouseCode)
}
@GetMapping("/inventoryLotDetailsBySection")
fun getInventoryLotDetailsByStockTakeSection(
@RequestParam stockTakeSection: String,
@RequestParam(required = false) stockTakeId: Long?
): List<InventoryLotDetailResponse> {
return stockOutRecordService.getInventoryLotDetailsByStockTakeSection(stockTakeSection, stockTakeId)
}
@PostMapping("/saveStockTakeRecord")
fun saveStockTakeRecord(
@RequestBody request: SaveStockTakeRecordRequest,
@RequestParam stockTakeId: Long,
@RequestParam stockTakerId: Long
): ResponseEntity<Any> {
return try {
val savedRecord = stockOutRecordService.saveStockTakeRecord(request, stockTakeId, stockTakerId)
logger.info("Successfully saved stock take record: ${savedRecord.id}")
ResponseEntity.ok(savedRecord)
} catch (e: IllegalArgumentException) {
logger.warn("Validation error: ${e.message}")
ResponseEntity.badRequest().body(mapOf(
"error" to "VALIDATION_ERROR",
"message" to (e.message ?: "Validation failed")
))
} catch (e: Exception) {
logger.error("Error saving stock take record", e)
ResponseEntity.status(500).body(mapOf(
"error" to "INTERNAL_ERROR",
"message" to (e.message ?: "Failed to save stock take record")
))
}
}
@PostMapping("/batchSaveStockTakeRecords")
fun batchSaveStockTakeRecords(
@RequestBody request: BatchSaveStockTakeRecordRequest
): ResponseEntity<Any> {
return try {
val result = stockOutRecordService.batchSaveStockTakeRecords(request)
logger.info("Batch save completed: success=${result.successCount}, errors=${result.errorCount}")
ResponseEntity.ok(result)
} catch (e: IllegalArgumentException) {
logger.warn("Validation error: ${e.message}")
ResponseEntity.badRequest().body(mapOf(
"error" to "VALIDATION_ERROR",
"message" to (e.message ?: "Validation failed")
))
} catch (e: Exception) {
logger.error("Error batch saving stock take records", e)
ResponseEntity.status(500).body(mapOf(
"error" to "INTERNAL_ERROR",
"message" to (e.message ?: "Failed to batch save stock take records")
))
}
}
@GetMapping("/checkAndUpdateStockTakeStatus")
fun checkAndUpdateStockTakeStatus(
@RequestParam stockTakeId: Long,
@RequestParam stockTakeSection: String
): ResponseEntity<Any> {
return try {
val result = stockOutRecordService.checkAndUpdateStockTakeStatus(stockTakeId, stockTakeSection)
ResponseEntity.ok(result)
} catch (e: Exception) {
logger.error("Error checking and updating stock take status", e)
ResponseEntity.status(500).body(
mapOf(
"error" to "INTERNAL_ERROR",
"message" to (e.message ?: "Failed to check and update stock take status")
)
)
}
}
@PostMapping("/saveApproverStockTakeRecord")
fun saveApproverStockTakeRecord(
@RequestBody request: SaveApproverStockTakeRecordRequest,
@RequestParam stockTakeId: Long
): ResponseEntity<Any> {
return try {
val savedRecord = stockOutRecordService.saveApproverStockTakeRecord(request, stockTakeId)
logger.info("Successfully saved approver stock take record: ${savedRecord.id}")
ResponseEntity.ok(savedRecord)
} catch (e: IllegalArgumentException) {
logger.warn("Validation error: ${e.message}")
ResponseEntity.badRequest().body(mapOf(
"error" to "VALIDATION_ERROR",
"message" to (e.message ?: "Validation failed")
))
} catch (e: Exception) {
logger.error("Error saving approver stock take record", e)
ResponseEntity.status(500).body(mapOf(
"error" to "INTERNAL_ERROR",
"message" to (e.message ?: "Failed to save approver stock take record")
))
}
}

@PostMapping("/batchSaveApproverStockTakeRecords")
fun batchSaveApproverStockTakeRecords(
@RequestBody request: BatchSaveApproverStockTakeRecordRequest
): ResponseEntity<Any> {
return try {
val result = stockOutRecordService.batchSaveApproverStockTakeRecords(request)
logger.info("Batch approver save completed: success=${result.successCount}, errors=${result.errorCount}")
ResponseEntity.ok(result)
} catch (e: IllegalArgumentException) {
logger.warn("Validation error: ${e.message}")
ResponseEntity.badRequest().body(mapOf(
"error" to "VALIDATION_ERROR",
"message" to (e.message ?: "Validation failed")
))
} catch (e: Exception) {
logger.error("Error batch saving approver stock take records", e)
ResponseEntity.status(500).body(mapOf(
"error" to "INTERNAL_ERROR",
"message" to (e.message ?: "Failed to batch save approver stock take records")
))
}
}
@PostMapping("/updateStockTakeRecordStatusToNotMatch")
fun updateStockTakeRecordStatusToNotMatch(
@RequestParam stockTakeRecordId: Long
): ResponseEntity<Any> {
return try {
val result = stockOutRecordService.updateStockTakeRecordStatusToNotMatch(stockTakeRecordId)
ResponseEntity.ok(result)
} catch (e: Exception) {
logger.error("Error updating stock take record status to not match", e)
ResponseEntity.status(500).body(mapOf(
"error" to "INTERNAL_ERROR",
"message" to (e.message ?: "Failed to update stock take record status to not match")
))
}
}
}

+ 1
- 0
src/main/java/com/ffii/fpsms/modules/stock/web/model/SaveStockTakeRequest.kt Просмотреть файл

@@ -11,4 +11,5 @@ data class SaveStockTakeRequest(
var actualEnd: LocalDateTime?,
var status: String?,
val remarks: String?,
val stockTakeSection: String?=null,
)

+ 90
- 0
src/main/java/com/ffii/fpsms/modules/stock/web/model/StockTakeRecordReponse.kt Просмотреть файл

@@ -0,0 +1,90 @@
// StockTakeRecordReponse.kt
package com.ffii.fpsms.modules.stock.web.model

import java.time.LocalDate
import java.math.BigDecimal
data class AllPickedStockTakeListReponse(
val id: Long,
val stockTakeSession: String,
val lastStockTakeDate: LocalDate?,
val status: String,
val currentStockTakeItemNumber: Int,
val totalInventoryLotNumber: Int,
val stockTakeId: Long,
)
data class InventoryLotDetailResponse(
val id: Long,
val inventoryLotId: Long,
val itemId: Long,
val itemCode: String?,
val itemName: String?,
val lotNo: String?,
val expiryDate: LocalDate?,
val productionDate: java.time.LocalDateTime?,
val stockInDate: java.time.LocalDateTime?,
val inQty: BigDecimal?,
val outQty: BigDecimal?,
val holdQty: BigDecimal?,
val availableQty: BigDecimal?,
val uom: String?,
val warehouseCode: String?,
val warehouse: String?,
val warehouseSlot: String?,
val warehouseArea: String?,
val warehouseName: String?,
val varianceQty: BigDecimal? = null,
val status: String?,
val remarks: String?,
val stockTakeRecordStatus: String?,
val stockTakeRecordId: Long? = null,
val firstStockTakeQty: BigDecimal? = null,
val secondStockTakeQty: BigDecimal? = null,
val firstBadQty: BigDecimal? = null,
val secondBadQty: BigDecimal? = null,
val approverQty: BigDecimal? = null,
val approverBadQty: BigDecimal? = null,
val finalQty: BigDecimal? = null,
)
data class InventoryLotLineListRequest(
val warehouseCode: String
)
data class SaveStockTakeRecordRequest(
val stockTakeRecordId: Long? = null, // null = 创建,非 null = 更新
val inventoryLotLineId: Long, // 创建时需要,用于识别 inventory lot line
val qty: BigDecimal, // QTY(第一次或第二次,后端判断)
val badQty: BigDecimal,
// val stockTakerName: String,
val remark: String? = null
)
data class SaveApproverStockTakeRecordRequest(
val stockTakeRecordId: Long? = null,
val qty: BigDecimal,
val badQty: BigDecimal,
val approverId: Long? = null,
val approverQty: BigDecimal? = null,
val approverBadQty: BigDecimal? = null,
//val remark: String? = null
)
data class BatchSaveStockTakeRecordRequest(
val stockTakeId: Long,
val stockTakeSection: String,
val stockTakerId: Long,
//val stockTakerName: String
)

data class BatchSaveStockTakeRecordResponse(
val successCount: Int,
val errorCount: Int,
val errors: List<String>
)
data class BatchSaveApproverStockTakeRecordRequest(
val stockTakeId: Long,
val stockTakeSection: String,
val approverId: Long,
)

data class BatchSaveApproverStockTakeRecordResponse(
val successCount: Int,
val errorCount: Int,
val errors: List<String>
)

+ 53
- 0
src/main/resources/db/changelog/changes/20251230_01_Enson/02_alter_table.sql Просмотреть файл

@@ -0,0 +1,53 @@
-- liquibase formatted sql
-- changeset Enson:add_column

CREATE TABLE IF NOT EXISTS stockTakeRecord (
id INT AUTO_INCREMENT PRIMARY KEY,
created DATETIME,
createdBy VARCHAR(30),
version INT,
modified DATETIME,
modifiedBy VARCHAR(30),
deleted TINYINT(1),

itemId INT NOT NULL,
lotId INT NOT NULL,
warehouseId INT NOT NULL,
stockTakeSection varchar(255),
stockTakeId INT NOT NULL,
stockTakerName VARCHAR(100),
approverId INT,
approverName VARCHAR(100),
stockTakerId INT NOT NULL,

pickerFirstStockTakeQty DECIMAL(14,2),
pickerSecondStockTakeQty DECIMAL(14,2),
approverStockTakeQty DECIMAL(14,2),
approverSecondStockTakeQty DECIMAL(14,2),
bookQty DECIMAL(14,2) NOT NULL,
badQty DECIMAL(14,2),
pickerFirstBadQty DECIMAL(14,2),
pickerSecondBadQty DECIMAL(14,2),
approverBadQty DECIMAL(14,2),
varianceQty DECIMAL(14,2),
uom VARCHAR(30),

stockTakeStartTime DATETIME,
stockTakeEndTime DATETIME,
date DATE NOT NULL,

status VARCHAR(30) NOT NULL,
remarks VARCHAR(500),

itemCode VARCHAR(50),
itemName VARCHAR(200),
inventoryLotId INT,
lotNo VARCHAR(512),
expiredDate DATE

);

+ 5
- 0
src/main/resources/db/changelog/changes/20251230_01_Enson/04_alter_table.sql Просмотреть файл

@@ -0,0 +1,5 @@
-- liquibase formatted sql
-- changeset Enson:add_column

ALTER TABLE `fpsmsdb`.`stock_take`
ADD COLUMN `stockTakeSection` varchar(255) NULL DEFAULT '' AFTER `remarks`;

+ 7
- 0
src/main/resources/db/changelog/changes/20260105_enson/01_alter_table.sql Просмотреть файл

@@ -0,0 +1,7 @@
--liquibase formatted sql
--changeset author:add_time_fields_to_productprocessline

ALTER TABLE `productprocessline`
ADD COLUMN `processingTime` INT(11) NULL AFTER `endTime`,
ADD COLUMN `setupTime` INT(11) NULL AFTER `processingTime`,
ADD COLUMN `changeoverTime` INT(11) NULL AFTER `setupTime`;

Загрузка…
Отмена
Сохранить