CANCERYS\kw093 преди 2 месеца
родител
ревизия
e11f32dd9a
променени са 9 файла, в които са добавени 280 реда и са изтрити 104 реда
  1. +103
    -29
      src/main/java/com/ffii/fpsms/modules/jobOrder/service/JoPickOrderService.kt
  2. +20
    -16
      src/main/java/com/ffii/fpsms/modules/jobOrder/web/JobOrderController.kt
  3. +5
    -2
      src/main/java/com/ffii/fpsms/modules/jobOrder/web/model/SecondScanIssueRequest.kt
  4. +10
    -6
      src/main/java/com/ffii/fpsms/modules/pickOrder/entity/PickExecutionIssue.kt
  5. +11
    -1
      src/main/java/com/ffii/fpsms/modules/pickOrder/entity/PickExecutionIssueRepository.kt
  6. +27
    -0
      src/main/java/com/ffii/fpsms/modules/pickOrder/service/PickExecutionIssueService.kt
  7. +87
    -50
      src/main/java/com/ffii/fpsms/modules/pickOrder/service/PickOrderService.kt
  8. +2
    -0
      src/main/java/com/ffii/fpsms/modules/pickOrder/web/models/PickExecutionIssueRequest.kt
  9. +15
    -0
      src/main/resources/db/changelog/changes/20251009_02_enson/02_altertable_enson.sql

+ 103
- 29
src/main/java/com/ffii/fpsms/modules/jobOrder/service/JoPickOrderService.kt Целия файл

@@ -1032,73 +1032,125 @@ open fun recordSecondScanIssue(request: SecondScanIssueRequest): MessageResponse
)
}
// ✅ Get pick order line details with item information
val pickOrderLineResults = jdbcDao.queryForList(
// ✅ Get pick order and pick order line details with item and lot information
val pickOrderDetails = jdbcDao.queryForList(
"""
SELECT
po.code as pickOrderCode,
DATE_FORMAT(po.created, '%Y-%m-%d') as pickOrderCreateDate,
pol.id as pickOrderLineId,
i.code as itemCode,
i.name as itemName,
pol.qty as requiredQty
FROM fpsmsdb.pick_order_line pol
pol.qty as requiredQty,
u.name as pickerName
FROM fpsmsdb.pick_order po
JOIN fpsmsdb.pick_order_line pol ON pol.poId = po.id
JOIN fpsmsdb.items i ON i.id = pol.itemId
WHERE pol.poId = :pickOrderId AND pol.itemId = :itemId AND pol.deleted = false
LEFT JOIN fpsmsdb.user u ON u.id = :createdBy
WHERE po.id = :pickOrderId
AND pol.itemId = :itemId
AND pol.deleted = false
AND po.deleted = false
""",
mapOf(
"pickOrderId" to request.pickOrderId,
"itemId" to request.itemId
"itemId" to request.itemId,
"createdBy" to request.createdBy
)
)
if (pickOrderLineResults.isEmpty()) {
// ✅ 修复:使用正确的变量名 pickOrderDetails
if (pickOrderDetails.isEmpty()) {
return MessageResponse(
id = null,
name = null,
code = "ERROR",
type = "NOT_FOUND",
message = "Pick order line not found for pickOrderId: ${request.pickOrderId}, itemId: ${request.itemId}",
message = "Pick order not found for pickOrderId: ${request.pickOrderId}, itemId: ${request.itemId}",
errorPosition = null
)
}
val lineData = pickOrderLineResults.first()
val pickOrderLineId = (lineData["pickOrderLineId"] as Number).toLong()
val itemCode = lineData["itemCode"] as String?
val itemName = lineData["itemName"] as String?
val requiredQty = lineData["requiredQty"]?.let {
// ✅ 修复:使用正确的变量名 pickOrderDetails
val orderData = pickOrderDetails.first()
val pickOrderCode = orderData["pickOrderCode"] as String?
val pickOrderCreateDate = orderData["pickOrderCreateDate"]?.let {
LocalDate.parse(it.toString())
}
val pickOrderLineId = (orderData["pickOrderLineId"] as Number).toLong()
val itemCode = orderData["itemCode"] as String?
val itemName = orderData["itemName"] as String?
val pickerName = orderData["pickerName"] as String?
val requiredQty = orderData["requiredQty"]?.let {
BigDecimal(it.toString())
}
// ✅ 获取 suggested lot 信息(用于 lot_id, lot_no, store_location)
val lotResults = jdbcDao.queryForList(
"""
SELECT
ill.id as lotId,
il.lotNo as lotNo,
w.name as storeLocation
FROM fpsmsdb.suggested_pick_lot spl
JOIN fpsmsdb.inventory_lot_line ill ON spl.suggestedLotLineId = ill.id
JOIN fpsmsdb.inventory_lot il ON ill.inventoryLotId = il.id
LEFT JOIN fpsmsdb.warehouse w ON w.id = ill.warehouseId
WHERE spl.pickOrderLineId = :pickOrderLineId
AND spl.deleted = false
AND ill.deleted = false
LIMIT 1
""",
mapOf("pickOrderLineId" to pickOrderLineId)
)
val lotId = if (lotResults.isNotEmpty()) {
(lotResults.first()["lotId"] as Number?)?.toLong()
} else null
val lotNo = if (lotResults.isNotEmpty()) {
lotResults.first()["lotNo"] as String?
} else null
val storeLocation = if (lotResults.isNotEmpty()) {
lotResults.first()["storeLocation"] as String?
} else null

val joPickOrderEntity = joPickOrder.get()
joPickOrderEntity.matchStatus = JoPickOrderStatus.completed
joPickOrderEntity.matchQty = request.qty.toInt()

// ✅ 添加:如果 ticketCompleteTime 还没设置,现在设置
if (joPickOrderEntity.ticketCompleteTime == null) {
joPickOrderEntity.ticketCompleteTime = LocalDateTime.now()
}

joPickOrderRepository.save(joPickOrderEntity)
// ✅ 生成 issueNo
val issueNo = generateIssueNoForJo()
// ✅ Create pick execution issue with complete data
val pickExecutionIssue = PickExecutionIssue(
issueNo = issueNo,
type = request.type ?: "match",
pickOrderId = request.pickOrderId,
pickOrderCode = "JO-${request.pickOrderId}",
pickOrderCode = pickOrderCode ?: "P-${request.pickOrderId}",
pickOrderCreateDate = pickOrderCreateDate, // ✅ 现在已经定义了
pickExecutionDate = LocalDate.now(),
pickOrderLineId = pickOrderLineId,
itemId = request.itemId,
itemCode = itemCode, // ✅ Include item code
itemDescription = itemName, // ✅ Include item name
lotId = null,
lotNo = null,
storeLocation = null,
requiredQty = requiredQty, // ✅ Include required quantity
actualPickQty = request.qty.toBigDecimal(), // ✅ Include actual pick quantity
missQty = if (request.isMissing) request.qty.toBigDecimal() else BigDecimal.ZERO,
badItemQty = if (request.isBad) request.qty.toBigDecimal() else BigDecimal.ZERO,
itemCode = itemCode,
itemDescription = itemName,
lotId = lotId,
lotNo = lotNo,
storeLocation = storeLocation,
requiredQty = requiredQty,
actualPickQty = request.qty.toBigDecimal(),
missQty = request.missQty.toBigDecimal(), // ✅ 使用实际的 missQty
badItemQty = request.badItemQty.toBigDecimal(), // ✅ 使用实际的 badItemQty
issueRemark = request.reason,
pickerName = null,
handleStatus = HandleStatus.jopending,
pickerName = pickerName,
handleStatus = HandleStatus.pending,
handleDate = null,
handledBy = null,
created = LocalDateTime.now(),
@@ -1118,19 +1170,41 @@ open fun recordSecondScanIssue(request: SecondScanIssueRequest): MessageResponse
message = "Second scan issue recorded successfully",
errorPosition = null
)
} catch (e: Exception) {
println("❌ Error recording second scan issue: ${e.message}")
println("=== ERROR in recordSecondScanIssue ===")
e.printStackTrace()
return MessageResponse(
id = null,
name = null,
code = "ERROR",
type = "EXCEPTION",
message = "Error recording second scan issue: ${e.message}",
message = "Error: ${e.message}",
errorPosition = null
)
}
}
private fun generateIssueNoForJo(): String {
val now = LocalDateTime.now()
val yearMonth = now.format(java.time.format.DateTimeFormatter.ofPattern("yyMM"))
// 查询当月最新的 issueNo
val latestIssueNo = pickExecutionIssueRepository.findLatestIssueNoByYearMonth(yearMonth)
// 计算下一个序列号
val nextSequence = if (latestIssueNo != null) {
val parts = latestIssueNo.split("-")
if (parts.size == 3) {
val currentSequence = parts[2].toIntOrNull() ?: 0
currentSequence + 1
} else {
1
}
} else {
1
}
return "SKO-${yearMonth}-${nextSequence.toString().padStart(3, '0')}"
}
open fun getCompletedJobOrderPickOrdersWithCompletedSecondScan(userId: Long): List<Map<String, Any?>> {
println("=== getCompletedJobOrderPickOrdersWithCompletedSecondScan ===")
println("userId: $userId")


+ 20
- 16
src/main/java/com/ffii/fpsms/modules/jobOrder/web/JobOrderController.kt Целия файл

@@ -144,22 +144,26 @@ class JobOrderController(
}
@PostMapping("/second-scan-issue/{pickOrderId}/{itemId}")
fun recordSecondScanIssue(
@PathVariable pickOrderId: Long,
@PathVariable itemId: Long,
@RequestBody data: Map<String, Any>
): MessageResponse {
val request = SecondScanIssueRequest(
pickOrderId = pickOrderId, // ✅ Use path variable
itemId = itemId, // ✅ Use path variable
qty = (data["qty"] as Number).toDouble(),
isMissing = data["isMissing"] as Boolean,
isBad = data["isBad"] as Boolean,
reason = data["reason"] as String,
createdBy = (data["createdBy"] as Number).toLong()
)
return joPickOrderService.recordSecondScanIssue(request)
}
fun recordSecondScanIssue(
@PathVariable pickOrderId: Long,
@PathVariable itemId: Long,
@RequestBody data: Map<String, Any>
): MessageResponse {
val request = SecondScanIssueRequest(
pickOrderId = pickOrderId, // ✅ path 变量
itemId = itemId, // ✅ path 变量
qty = (data["qty"] as Number).toDouble(),
// ✅ 新增:安全读取 missQty/badItemQty/type,默认 0/"match"
missQty = (data["missQty"] as? Number)?.toDouble() ?: 0.0,
badItemQty = (data["badItemQty"] as? Number)?.toDouble() ?: 0.0,
isMissing = data["isMissing"] as? Boolean ?: false,
isBad = data["isBad"] as? Boolean ?: false,
reason = data["reason"] as? String ?: "",
createdBy = (data["createdBy"] as Number).toLong(),
type = (data["type"] as? String) ?: "match"
)
return joPickOrderService.recordSecondScanIssue(request)
}
@GetMapping("/completed-job-order-pick-orders-with-completed-second-scan/{userId}")
fun getCompletedJobOrderPickOrdersWithCompletedSecondScan(@PathVariable userId: Long): List<Map<String, Any?>> {
return joPickOrderService.getCompletedJobOrderPickOrdersWithCompletedSecondScan(userId)


+ 5
- 2
src/main/java/com/ffii/fpsms/modules/jobOrder/web/model/SecondScanIssueRequest.kt Целия файл

@@ -3,9 +3,12 @@ package com.ffii.fpsms.modules.jobOrder.web.model
data class SecondScanIssueRequest(
val pickOrderId: Long,
val itemId: Long,
val qty: Double,
val qty: Double, // 这是 actual pick qty (verified qty)
val missQty: Double = 0.0, // ✅ 添加:单独的 miss qty
val badItemQty: Double = 0.0, // ✅ 添加:单独的 bad item qty
val isMissing: Boolean,
val isBad: Boolean,
val reason: String,
val createdBy: Long
val createdBy: Long,
val type: String? = "match"
)

+ 10
- 6
src/main/java/com/ffii/fpsms/modules/pickOrder/entity/PickExecutionIssue.kt Целия файл

@@ -27,7 +27,12 @@ class PickExecutionIssue(

@Column(name = "pick_order_line_id", nullable = false)
val pickOrderLineId: Long,
@Column(name = "issue_no", length = 50)
val issueNo: String? = null,

// ✅ 新增字段:type
@Column(name = "type", length = 50)
val type: String? = null,
@Column(name = "item_id", nullable = false)
val itemId: Long,

@@ -104,6 +109,8 @@ class PickExecutionIssue(
itemCode = null,
itemDescription = null,
lotId = null,
issueNo = null,
type = null,
lotNo = null,
storeLocation = null,
requiredQty = null,
@@ -125,10 +132,7 @@ class PickExecutionIssue(
}

enum class HandleStatus {
pending, // ✅ Change to lowercase to match database
handled, // ✅ Change to lowercase to match database
resolved,
jopending,
johandled,
joresolved // ✅ Change to lowercase to match database
pending,
sort_and_repair,
dispose
}

+ 11
- 1
src/main/java/com/ffii/fpsms/modules/pickOrder/entity/PickExecutionIssueRepository.kt Целия файл

@@ -3,7 +3,8 @@ package com.ffii.fpsms.modules.pickOrder.entity

import org.springframework.data.jpa.repository.JpaRepository
import org.springframework.stereotype.Repository

import org.springframework.data.jpa.repository.Query
import org.springframework.data.repository.query.Param
@Repository
interface PickExecutionIssueRepository : JpaRepository<PickExecutionIssue, Long> {
fun findByPickOrderIdAndDeletedFalse(pickOrderId: Long): List<PickExecutionIssue>
@@ -13,4 +14,13 @@ interface PickExecutionIssueRepository : JpaRepository<PickExecutionIssue, Long>
pickOrderLineId: Long,
lotId: Long
): List<PickExecutionIssue>
@Query("""
SELECT p.issueNo
FROM PickExecutionIssue p
WHERE p.issueNo LIKE CONCAT('SKO-', :yearMonth, '-%')
AND p.deleted = false
ORDER BY p.issueNo DESC
LIMIT 1
""")
fun findLatestIssueNoByYearMonth(@Param("yearMonth") yearMonth: String): String?
}

+ 27
- 0
src/main/java/com/ffii/fpsms/modules/pickOrder/service/PickExecutionIssueService.kt Целия файл

@@ -50,6 +50,8 @@ open class PickExecutionIssueService(

// 2. 创建 pick execution issue 记录
val pickExecutionIssue = PickExecutionIssue(
issueNo = generateIssueNo(),
type = request.type,
pickOrderId = request.pickOrderId,
pickOrderCode = request.pickOrderCode,
pickOrderCreateDate = request.pickOrderCreateDate,
@@ -146,6 +148,31 @@ open class PickExecutionIssueService(
)
}
}
private fun generateIssueNo(): String {
val now = LocalDateTime.now()
val yearMonth = now.format(java.time.format.DateTimeFormatter.ofPattern("yyMM"))
// 查询当月最新的 issueNo
val latestIssueNo = pickExecutionIssueRepository.findLatestIssueNoByYearMonth(yearMonth)
// 计算下一个序列号
val nextSequence = if (latestIssueNo != null) {
// 从最新的 issueNo 中提取序列号
// 例如: SKO-2510-001 -> 001
val parts = latestIssueNo.split("-")
if (parts.size == 3) {
val currentSequence = parts[2].toIntOrNull() ?: 0
currentSequence + 1
} else {
1
}
} else {
1
}
// 格式化为 SKO-YYMM-001
return "SKO-${yearMonth}-${nextSequence.toString().padStart(3, '0')}"
}
// FPSMS-backend/src/main/java/com/ffii/fpsms/modules/pickOrder/service/PickExecutionIssueService.kt
// ✅ 修复:处理有部分拣货但有 miss item 的情况
@Transactional(propagation = Propagation.REQUIRES_NEW, rollbackFor = [Exception::class])


+ 87
- 50
src/main/java/com/ffii/fpsms/modules/pickOrder/service/PickOrderService.kt Целия файл

@@ -1843,57 +1843,93 @@ open fun autoAssignAndReleasePickOrderByStoreAndTicket(storeId: String, ticketNo
val pickOrderIdsStr = pickOrderIds.joinToString(",")
val sql = """
SELECT
-- Pick Order Information
po.id as pickOrderId,
po.code as pickOrderCode,
po.consoCode as pickOrderConsoCode, -- ✅ 添加 consoCode
po.targetDate as pickOrderTargetDate,
po.type as pickOrderType,
po.status as pickOrderStatus,
po.assignTo as pickOrderAssignTo,
-- Pick Order Line Information
pol.id as pickOrderLineId,
pol.qty as pickOrderLineRequiredQty,
-- Item Information
i.id as itemId,
i.code as itemCode,
i.name as itemName,
uc.code as uomCode,
uc.udfudesc as uomDesc,
-- ✅ Calculate total picked quantity from stock_out_line table
COALESCE((
SELECT SUM(sol_picked.qty)
FROM fpsmsdb.stock_out_line sol_picked
WHERE sol_picked.pickOrderLineId = pol.id
AND sol_picked.deleted = false
AND sol_picked.status IN ('completed', 'COMPLETE', 'partially_completed','rejected')
), 0) as totalPickedQty,
-- ✅ Calculate available quantity from inventory
COALESCE((
SELECT inv.onHandQty - inv.onHoldQty - inv.unavailableQty
FROM fpsmsdb.inventory inv
WHERE inv.itemId = i.id
AND inv.deleted = false
), 0) as availableQty
FROM fpsmsdb.pick_order po
JOIN fpsmsdb.pick_order_line pol ON pol.poId = po.id
JOIN fpsmsdb.items i ON i.id = pol.itemId
LEFT JOIN fpsmsdb.uom_conversion uc ON uc.id = pol.uomId
WHERE po.deleted = false
AND po.id IN ($pickOrderIdsStr)
AND pol.deleted = false
AND po.status = 'RELEASED'
SELECT
-- Pick Order Information
po.id as pickOrderId,
po.code as pickOrderCode,
po.consoCode as pickOrderConsoCode,
po.targetDate as pickOrderTargetDate,
po.type as pickOrderType,
po.status as pickOrderStatus,
po.assignTo as pickOrderAssignTo,
ORDER BY
po.code ASC,
i.code ASC
""".trimIndent()
-- Pick Order Line Information
pol.id as pickOrderLineId,
pol.qty as pickOrderLineRequiredQty,
pol.status as pickOrderLineStatus,
-- Item Information
i.id as itemId,
i.code as itemCode,
i.name as itemName,
uc.code as uomCode,
uc.udfudesc as uomDesc,
-- ✅ Calculate total picked quantity from stock_out_line table
COALESCE((
SELECT SUM(sol_picked.qty)
FROM fpsmsdb.stock_out_line sol_picked
WHERE sol_picked.pickOrderLineId = pol.id
AND sol_picked.deleted = false
AND sol_picked.status IN ('completed', 'COMPLETE', 'partially_completed','rejected')
), 0) as totalPickedQty,
-- ✅ Calculate available quantity from inventory
COALESCE((
SELECT inv.onHandQty - inv.onHoldQty - inv.unavailableQty
FROM fpsmsdb.inventory inv
WHERE inv.itemId = i.id
AND inv.deleted = false
), 0) as availableQty,
-- ✅ Check if all stock out lines for this pick order line are completed
CASE
WHEN EXISTS (
SELECT 1
FROM fpsmsdb.stock_out_line sol_check
WHERE sol_check.pickOrderLineId = pol.id
AND sol_check.deleted = false
AND sol_check.status NOT IN ('completed', 'COMPLETE')
) THEN false
WHEN EXISTS (
SELECT 1
FROM fpsmsdb.stock_out_line sol_check
WHERE sol_check.pickOrderLineId = pol.id
AND sol_check.deleted = false
) THEN true
ELSE false
END as allLotsCompleted
FROM fpsmsdb.pick_order po
JOIN fpsmsdb.pick_order_line pol ON pol.poId = po.id
JOIN fpsmsdb.items i ON i.id = pol.itemId
LEFT JOIN fpsmsdb.uom_conversion uc ON uc.id = pol.uomId
WHERE po.deleted = false
AND po.id IN ($pickOrderIdsStr)
AND pol.deleted = false
AND po.status = 'RELEASED'
AND po.type NOT IN ('do', 'job') -- ✅ 排除 do 和 job 类型
-- ✅ Only include lines that have incomplete stock out lines
AND (
NOT EXISTS (
SELECT 1
FROM fpsmsdb.stock_out_line sol_all_complete
WHERE sol_all_complete.pickOrderLineId = pol.id
AND sol_all_complete.deleted = false
)
OR EXISTS (
SELECT 1
FROM fpsmsdb.stock_out_line sol_incomplete
WHERE sol_incomplete.pickOrderLineId = pol.id
AND sol_incomplete.deleted = false
AND sol_incomplete.status NOT IN ('completed', 'COMPLETE')
)
)
ORDER BY
po.code ASC,
i.code ASC
""".trimIndent()
println("🔍 Executing optimized SQL: $sql")
@@ -1966,6 +2002,7 @@ open fun autoAssignAndReleasePickOrderByStoreAndTicket(storeId: String, ticketNo
val releasedPickOrderIds = allPickOrders
.filter { it.status == PickOrderStatus.RELEASED }
.filter { it.assignTo?.id == userId }
.filter { it.type?.value != "do" && it.type?.value != "jo" } // ✅ 排除 do 和 job 类型
.map { it.id!! }
if (releasedPickOrderIds.isEmpty()) {


+ 2
- 0
src/main/java/com/ffii/fpsms/modules/pickOrder/web/models/PickExecutionIssueRequest.kt Целия файл

@@ -15,6 +15,8 @@ data class PickExecutionIssueRequest(
val itemDescription: String? = null,
val lotId: Long? = null,
val lotNo: String? = null,
val issueNo: String? = null,
val type: String? = null,
val storeLocation: String? = null,
val requiredQty: BigDecimal? = null,
val actualPickQty: BigDecimal? = null,


+ 15
- 0
src/main/resources/db/changelog/changes/20251009_02_enson/02_altertable_enson.sql Целия файл

@@ -0,0 +1,15 @@
--liquibase formatted sql

--changeset [enson]:update_pick_execution_issue_add_columns_and_modify_status

-- 1. 添加新列 issueNo
ALTER TABLE pick_execution_issue
ADD COLUMN issue_no VARCHAR(50) NULL AFTER id;

-- 2. 添加新列 Type
ALTER TABLE pick_execution_issue
ADD COLUMN type VARCHAR(50) NULL AFTER issue_no;

-- 3. 修改 handle_status 枚举值
ALTER TABLE pick_execution_issue
MODIFY COLUMN handle_status ENUM('pending', 'Sort and Repair', 'Dispose') NULL DEFAULT 'pending';

Зареждане…
Отказ
Запис