kelvin.yau 1 month ago
parent
commit
dff945c465
17 changed files with 514 additions and 261 deletions
  1. +6
    -5
      src/main/java/com/ffii/fpsms/config/security/jwt/web/JwtAuthenticationController.java
  2. +31
    -7
      src/main/java/com/ffii/fpsms/modules/master/entity/QcCategory.kt
  3. +23
    -0
      src/main/java/com/ffii/fpsms/modules/master/entity/QcCategoryRepository.kt
  4. +5
    -0
      src/main/java/com/ffii/fpsms/modules/master/entity/QcItemCategory.kt
  5. +3
    -0
      src/main/java/com/ffii/fpsms/modules/master/entity/QcItemCategoryRepository.kt
  6. +13
    -0
      src/main/java/com/ffii/fpsms/modules/master/service/ItemsService.kt
  7. +24
    -1
      src/main/java/com/ffii/fpsms/modules/master/service/QcCategoryService.kt
  8. +12
    -2
      src/main/java/com/ffii/fpsms/modules/master/service/QcItemService.kt
  9. +8
    -3
      src/main/java/com/ffii/fpsms/modules/master/web/QcCategoryController.kt
  10. +16
    -1
      src/main/java/com/ffii/fpsms/modules/master/web/QcItemController.kt
  11. +317
    -242
      src/main/java/com/ffii/fpsms/modules/pickOrder/service/PickOrderService.kt
  12. +26
    -0
      src/main/java/com/ffii/fpsms/modules/qc/entity/projection/QcCategoryInfo.kt
  13. +9
    -0
      src/main/resources/db/changelog/changes/20251009_01_kelvinS/01_create_items_qc_category_mapping.sql
  14. +5
    -0
      src/main/resources/db/changelog/changes/20251009_01_kelvinS/02_alter_qc_item_category.sql
  15. +5
    -0
      src/main/resources/db/changelog/changes/20251009_01_kelvinS/03_alter_qc_category.sql
  16. +5
    -0
      src/main/resources/db/changelog/changes/20251009_01_kelvinS/04_alter_items_qc_category_mapping.sql
  17. +6
    -0
      src/main/resources/db/changelog/changes/20251023_02_kelvinS/01_alter_qc_category.sql

+ 6
- 5
src/main/java/com/ffii/fpsms/config/security/jwt/web/JwtAuthenticationController.java View File

@@ -108,16 +108,17 @@ public class JwtAuthenticationController {
}

private ResponseEntity<?> createAuthTokenResponse(JwtRequest authenticationRequest) {
final UserDetails userDetails = userDetailsService.loadUserByUsername(authenticationRequest.getUsername());
if (userDetails == null) {
// final UserDetails userDetails = userDetailsService.loadUserByUsername(authenticationRequest.getUsername());
final User user = userDetailsService.loadUserByUsername(authenticationRequest.getUsername());
if (user == null) {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED)
.body(new ExceptionResponse(authenticationRequest.getUsername() + " not yet register in the system.", null));
}

final String accessToken = jwtTokenUtil.generateToken(userDetails);
final String refreshToken = jwtTokenUtil.createRefreshToken(userDetails.getUsername()).getToken();
final String accessToken = jwtTokenUtil.generateToken(user);
final String refreshToken = jwtTokenUtil.createRefreshToken(user.getUsername()).getToken();

User user = userRepository.findByName(authenticationRequest.getUsername()).get(0);
// User user = userRepository.findByName(authenticationRequest.getUsername()).get(0);

List<String> abilities = new ArrayList<>();
final Set<SimpleGrantedAuthority> userAuthority = userAuthorityService.getUserAuthority(user);


+ 31
- 7
src/main/java/com/ffii/fpsms/modules/master/entity/QcCategory.kt View File

@@ -1,14 +1,10 @@
package com.ffii.fpsms.modules.master.entity

import com.fasterxml.jackson.annotation.JsonBackReference
import com.fasterxml.jackson.annotation.JsonManagedReference
import com.ffii.core.entity.BaseEntity
import jakarta.persistence.CascadeType
import jakarta.persistence.Column
import jakarta.persistence.Entity
import jakarta.persistence.JoinColumn
import jakarta.persistence.JoinTable
import jakarta.persistence.OneToMany
import jakarta.persistence.Table
import com.ffii.core.entity.IdEntity
import jakarta.persistence.*
import jakarta.validation.constraints.NotNull

@Entity
@@ -22,6 +18,14 @@ open class QcCategory : BaseEntity<Long>() {
@Column(name = "name")
open var name: String? = null

@NotNull
@Column(name = "description")
open var description: String? = null

@NotNull
@Column(name = "isDefault")
open var isDefault: Boolean = false

// @OneToMany(cascade = [CascadeType.ALL])
// @JoinTable(
// name = "qc_item_category",
@@ -33,4 +37,24 @@ open class QcCategory : BaseEntity<Long>() {
@JsonManagedReference
@OneToMany(mappedBy = "qcCategory", cascade = [CascadeType.ALL], orphanRemoval = true)
open var qcItemCategory: MutableList<QcItemCategory> = mutableListOf()
}

@Entity
@Table(name = "items_qc_category_mapping")
open class ItemsQcCategoryMapping : IdEntity<Long>() {
@NotNull
@Column(name = "itemId")
open var itemId: Long? = null

@NotNull
@Column(name = "qcCategoryId")
open var qcCategoryId: Long? = null

@NotNull
@Column(name = "type")
open var type: String? = null
//
// @ManyToOne(fetch = FetchType.LAZY)
// @JoinColumn(name = "qcCategoryId")
// val qcCategory: QcCategory? = null
}

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

@@ -2,6 +2,8 @@ package com.ffii.fpsms.modules.master.entity

import com.ffii.core.support.AbstractRepository
import com.ffii.fpsms.modules.master.entity.projections.QcCategoryCombo
import com.ffii.fpsms.modules.qc.entity.projection.QcCategoryInfo
import org.springframework.data.jpa.repository.Query
import org.springframework.stereotype.Repository

@Repository
@@ -9,4 +11,25 @@ interface QcCategoryRepository : AbstractRepository<QcCategory, Long> {
fun findAllByDeletedIsFalse(): List<QcCategory>;

fun findQcCategoryComboByDeletedIsFalse(): List<QcCategoryCombo>;

// @Query(
// value = """
// SELECT qcc.* FROM qc_category qcc
// LEFT JOIN items_qc_category_mapping map ON map.qcCategoryId = qcc.id
// WHERE map.type = :type AND map.itemId = :itemId AND qcc.deleted = false
// ORDER BY qcc.id
// LIMIT 1
// """,
// nativeQuery = true
// )
@Query(
"""
SELECT qcc FROM QcCategory qcc
JOIN ItemsQcCategoryMapping map ON qcc.id = map.qcCategoryId
WHERE map.itemId = :itemId AND map.type = :type AND qcc.deleted = false
"""
)
fun findQcCategoryInfoByItemIdAndType(itemId: Long, type: String): QcCategoryInfo?;
fun findQcCategoryInfoByIsDefault(isDefault: Boolean): QcCategoryInfo?;
// fun findByItemIdAndType(itemId: Long, type: String): QcCategory?;
}

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

@@ -22,7 +22,12 @@ open class QcItemCategory : IdEntity<Long>() {
@JoinColumn(name = "qcItemId")
open var qcItem: QcItem? = null

@NotNull
@Column(name = "order")
open var order: Int = 0

@Size(max = 1000)
@Column(name = "description", length = 1000)
open var description: String? = null

}

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

@@ -1,8 +1,11 @@
package com.ffii.fpsms.modules.master.entity

import com.ffii.core.support.AbstractRepository
import com.ffii.fpsms.modules.qc.entity.projection.QcCategoryInfo
import com.ffii.fpsms.modules.qc.entity.projection.QcItemInfo
import org.springframework.stereotype.Repository

@Repository
interface QcItemCategoryRepository : AbstractRepository<QcItemCategory, Long> {
fun findAllByQcCategoryId(qcCategoryId : Long): List<QcItemInfo>
}

+ 13
- 0
src/main/java/com/ffii/fpsms/modules/master/service/ItemsService.kt View File

@@ -373,4 +373,17 @@ open class ItemsService(
return jdbcDao.queryForList(sql.toString(), args);
}

open fun getItemsIdWithSameCategoryForQc(args: Map<String, Any>): List<Int> {
val sql = StringBuilder("SELECT"
+ " i.id "
+ " FROM items i "
+ " INNER JOIN items_qc_category_mapping iqcm ON iqcm.itemId = i.id AND iqcm.type = :qcType "
+ " LEFT JOIN qc_category qcc ON qcc.id = iqcm.qcCategoryId "
+ " WHERE i.deleted = false AND qcc.deleted = false"
+ " AND LEFT(i.code, 2) = (SELECT LEFT(code, 2) FROM items WHERE id = :itemId)"
+ " AND i.id != :itemId "
)
return jdbcDao.queryForInts(sql.toString(), args);
}
}

+ 24
- 1
src/main/java/com/ffii/fpsms/modules/master/service/QcCategoryService.kt View File

@@ -4,11 +4,13 @@ import com.ffii.core.support.AbstractBaseEntityService
import com.ffii.fpsms.modules.master.entity.QcCategory
import com.ffii.fpsms.modules.master.entity.QcCategoryRepository
import com.ffii.fpsms.modules.master.entity.projections.QcCategoryCombo
import com.ffii.fpsms.modules.qc.entity.projection.QcCategoryInfo
import org.springframework.stereotype.Service

@Service
open class QcCategoryService(
private val qcCategoryRepository: QcCategoryRepository
private val qcCategoryRepository: QcCategoryRepository,
private val itemsService: ItemsService
) {
open fun allQcCategories(): List<QcCategory> {
return qcCategoryRepository.findAllByDeletedIsFalse()
@@ -17,4 +19,25 @@ open class QcCategoryService(
open fun getQcCategoryCombo(): List<QcCategoryCombo> {
return qcCategoryRepository.findQcCategoryComboByDeletedIsFalse();
}

open fun getQcCategoryInfoByMapping(itemId : Long, type: String): QcCategoryInfo? {
var result = qcCategoryRepository.findQcCategoryInfoByItemIdAndType(itemId, type)
if (result == null) { // Temporarily fix for missing QC template (Auto find template from similar items)
val args = mapOf(
"itemId" to itemId,
"qcType" to type
)
val similarItemIds = itemsService.getItemsIdWithSameCategoryForQc(args);

// Comment the lines below to disable auto matching QC from similar items
result = if (similarItemIds.isNotEmpty())
qcCategoryRepository.findQcCategoryInfoByItemIdAndType(similarItemIds[0].toLong(), type)
else null
//
if (result == null) { // Use Default Template
result = qcCategoryRepository.findQcCategoryInfoByIsDefault(true)
}
}
return result;
}
}

+ 12
- 2
src/main/java/com/ffii/fpsms/modules/master/service/QcItemService.kt View File

@@ -1,11 +1,14 @@
package com.ffii.fpsms.modules.master.service

import com.ffii.core.support.AbstractBaseEntityService
import com.ffii.core.support.JdbcDao
import com.ffii.core.exception.NotFoundException
import com.ffii.fpsms.modules.master.entity.QcCategoryRepository
import com.ffii.fpsms.modules.master.entity.QcItem
import com.ffii.fpsms.modules.master.entity.QcItemCategoryRepository
import com.ffii.fpsms.modules.master.entity.QcItemRepository
import com.ffii.fpsms.modules.master.web.models.SaveQcItemRequest
import com.ffii.fpsms.modules.master.web.models.SaveQcItemResponse
import com.ffii.fpsms.modules.qc.entity.projection.QcCategoryInfo
import com.ffii.fpsms.modules.qc.entity.projection.QcItemInfo
import jakarta.validation.Valid
import org.springframework.stereotype.Service
import org.springframework.web.bind.annotation.RequestBody
@@ -13,6 +16,8 @@ import org.springframework.web.bind.annotation.RequestBody
@Service
open class QcItemService(
private val qcItemRepository: QcItemRepository,
private val qcCategoryRepository: QcCategoryRepository,
private val qcItemCategoryRepository: QcItemCategoryRepository,
) {
open fun allQcItems(): List<QcItem> {
return qcItemRepository.findAllByDeletedIsFalse()
@@ -26,6 +31,11 @@ open class QcItemService(
return qcItemRepository.findByIdAndDeletedIsFalse(id)
}

open fun findQcItemsByTemplate(itemId: Long, type: String): List<QcItemInfo>? {
val qcCategoryInfo = qcCategoryRepository.findQcCategoryInfoByItemIdAndType(itemId, type) ?: throw NotFoundException()
return qcItemCategoryRepository.findAllByQcCategoryId(qcCategoryInfo.id)
}

open fun markDeleted(id: Long): List<QcItem> {
val qcItem = qcItemRepository.findById(id).orElseThrow().apply {
deleted = true


+ 8
- 3
src/main/java/com/ffii/fpsms/modules/master/web/QcCategoryController.kt View File

@@ -1,11 +1,11 @@
package com.ffii.fpsms.modules.master.web

import com.ffii.core.exception.NotFoundException
import com.ffii.fpsms.modules.master.entity.QcCategory
import com.ffii.fpsms.modules.master.entity.projections.QcCategoryCombo
import com.ffii.fpsms.modules.master.service.QcCategoryService
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RestController
import com.ffii.fpsms.modules.qc.entity.projection.QcCategoryInfo
import org.springframework.web.bind.annotation.*

@RestController
@RequestMapping("/qcCategories")
@@ -21,4 +21,9 @@ class QcCategoryController(
fun getQcCategoryCombo(): List<QcCategoryCombo> {
return qcCategoryService.getQcCategoryCombo();
}

@GetMapping("/items")
fun getQcCategoryByTemplate(@RequestParam itemId: Long, @RequestParam(defaultValue = "IQC") type: String): QcCategoryInfo {
return qcCategoryService.getQcCategoryInfoByMapping(itemId, type) ?: throw NotFoundException()
}
}

+ 16
- 1
src/main/java/com/ffii/fpsms/modules/master/web/QcItemController.kt View File

@@ -7,7 +7,10 @@ import com.ffii.fpsms.modules.master.entity.QcItemRepository
import com.ffii.fpsms.modules.master.service.QcItemService
import com.ffii.fpsms.modules.master.web.models.SaveQcItemRequest
import com.ffii.fpsms.modules.master.web.models.SaveQcItemResponse
import com.ffii.fpsms.modules.qc.entity.projection.QcCategoryInfo
import com.ffii.fpsms.modules.qc.entity.projection.QcItemInfo
import jakarta.validation.Valid
import jakarta.validation.constraints.NotBlank
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import org.springframework.web.bind.annotation.*
@@ -37,8 +40,20 @@ class QcItemController(
return qcItemService.findQcItemById(id) ?: throw NotFoundException()
}

@GetMapping("/template")
fun qcItemsByTemplate(@RequestParam itemId: Long, @RequestParam(defaultValue = "IQC") type: String): List<QcItemInfo> {
return qcItemService.findQcItemsByTemplate(itemId, type) ?: throw NotFoundException()
}

@PostMapping("/save")
fun saveQcItem(@Valid @RequestBody request: SaveQcItemRequest): SaveQcItemResponse {
return qcItemService.saveQcItem(request)
}
}
}

data class GetQcItemRequest(
@field:NotBlank(message = "Item Id cannot be empty")
val itemId: Long,
@field:NotBlank(message = "Type cannot be empty")
val type: String,
)

+ 317
- 242
src/main/java/com/ffii/fpsms/modules/pickOrder/service/PickOrderService.kt View File

@@ -1250,8 +1250,37 @@ open class PickOrderService(
println("⚠️ WARNING: do_pick_order $doPickOrderId not found, skipping")
return@forEach
}
println("🔍 Processing do_pick_order ID: ${dpo.id}, ticket: ${dpo.ticketNo}")
// ✅ 新增:检查这个 do_pick_order 下是否还有其他未完成的 pick orders
val allDoPickOrderLines = doPickOrderLineRepository.findByDoPickOrderIdAndDeletedFalse(dpo.id!!)
val allPickOrderIdsInDpo = allDoPickOrderLines.mapNotNull { it.pickOrderId }.distinct()
println("🔍 DEBUG: This do_pick_order has ${allPickOrderIdsInDpo.size} pick orders")
// ✅ 检查每个 pick order 的状态
val pickOrderStatuses = allPickOrderIdsInDpo.map { poId ->
val po = pickOrderRepository.findById(poId).orElse(null)
poId to po?.status
}
pickOrderStatuses.forEach { (poId, status) ->
println("🔍 DEBUG: Pick order $poId status: $status")
}
// ✅ 只有当所有 pick orders 都是 COMPLETED 状态时,才移动到 record 表
val allPickOrdersCompleted = pickOrderStatuses.all { (_, status) ->
status == PickOrderStatus.COMPLETED
}
if (!allPickOrdersCompleted) {
println("⏳ Not all pick orders in this do_pick_order are completed, keeping do_pick_order alive")
println("🔍 DEBUG: Completed pick orders: ${pickOrderStatuses.count { it.second == PickOrderStatus.COMPLETED }}/${pickOrderStatuses.size}")
return@forEach // ✅ 跳过这个 do_pick_order,不删除它
}
println("✅ All pick orders in this do_pick_order are completed, moving to record table")

// 2) 先复制 do_pick_order -> do_pick_order_record
val dpoRecord = DoPickOrderRecord(
@@ -3516,261 +3545,307 @@ ORDER BY
val enrichedResults = filteredResults
return enrichedResults
}
open fun getAllPickOrderLotsWithDetailsHierarchical(userId: Long): Map<String, Any?> {
println("=== Debug: getAllPickOrderLotsWithDetailsHierarchical (NEW STRUCTURE) ===")
println("userId filter: $userId")
val user = userService.find(userId).orElse(null)
if (user == null) {
println("❌ User not found: $userId")
return emptyMap()
}
// ✅ Step 1: 获取 do_pick_order 基本信息
val doPickOrderSql = """
SELECT DISTINCT
dpo.id as do_pick_order_id,
dpo.ticket_no,
dpo.store_id,
dpo.TruckLanceCode,
dpo.truck_departure_time,
dpo.ShopCode,
dpo.ShopName
FROM fpsmsdb.do_pick_order dpo
INNER JOIN fpsmsdb.do_pick_order_line dpol ON dpol.do_pick_order_id = dpo.id AND dpol.deleted = 0
INNER JOIN fpsmsdb.pick_order po ON po.id = dpol.pick_order_id
WHERE po.assignTo = :userId
AND po.type = 'do'
AND po.status IN ('assigned', 'released', 'picking')
AND po.deleted = false
AND dpo.deleted = false
LIMIT 1
""".trimIndent()
val doPickOrderInfo = jdbcDao.queryForMap(doPickOrderSql, mapOf("userId" to userId)).orElse(null)
if (doPickOrderInfo == null) {
println("❌ No do_pick_order found for user $userId")
return mapOf(
"fgInfo" to null,
"pickOrders" to emptyList<Any>()
)
}
val doPickOrderId = (doPickOrderInfo["do_pick_order_id"] as? Number)?.toLong()
println("🔍 Found do_pick_order ID: $doPickOrderId")
// ✅ Step 2: 获取该 do_pick_order 下的所有 pick orders
val pickOrdersSql = """
SELECT DISTINCT
dpol.pick_order_id,
dpol.pick_order_code,
dpol.do_order_id,
dpol.delivery_order_code,
po.consoCode,
po.status,
DATE_FORMAT(po.targetDate, '%Y-%m-%d') as targetDate
FROM fpsmsdb.do_pick_order_line dpol
INNER JOIN fpsmsdb.pick_order po ON po.id = dpol.pick_order_id
WHERE dpol.do_pick_order_id = :doPickOrderId
AND dpol.deleted = 0
AND po.deleted = false
ORDER BY dpol.pick_order_id
""".trimIndent()
// ... existing code ...

open fun getAllPickOrderLotsWithDetailsHierarchical(userId: Long): Map<String, Any?> {
println("=== Debug: getAllPickOrderLotsWithDetailsHierarchical (NEW STRUCTURE) ===")
println("userId filter: $userId")
val user = userService.find(userId).orElse(null)
if (user == null) {
println("❌ User not found: $userId")
return emptyMap()
}
// ✅ Step 1: 获取 do_pick_order 基本信息
val doPickOrderSql = """
SELECT DISTINCT
dpo.id as do_pick_order_id,
dpo.ticket_no,
dpo.store_id,
dpo.TruckLanceCode,
dpo.truck_departure_time,
dpo.ShopCode,
dpo.ShopName
FROM fpsmsdb.do_pick_order dpo
INNER JOIN fpsmsdb.do_pick_order_line dpol ON dpol.do_pick_order_id = dpo.id AND dpol.deleted = 0
INNER JOIN fpsmsdb.pick_order po ON po.id = dpol.pick_order_id
WHERE po.assignTo = :userId
AND po.type = 'do'
AND EXISTS (
SELECT 1
FROM fpsmsdb.do_pick_order_line dpol2
INNER JOIN fpsmsdb.pick_order po2 ON po2.id = dpol2.pick_order_id
WHERE dpol2.do_pick_order_id = dpo.id
AND dpol2.deleted = 0
AND po2.status IN ('assigned', 'released', 'picking')
AND po2.deleted = false
)
AND po.deleted = false
AND dpo.deleted = false
LIMIT 1
""".trimIndent()
val doPickOrderInfo = jdbcDao.queryForMap(doPickOrderSql, mapOf("userId" to userId)).orElse(null)
if (doPickOrderInfo == null) {
println("❌ No do_pick_order found for user $userId")
return mapOf(
"fgInfo" to null,
"pickOrders" to emptyList<Any>()
)
}
val doPickOrderId = (doPickOrderInfo["do_pick_order_id"] as? Number)?.toLong()
println("🔍 Found do_pick_order ID: $doPickOrderId")
// ✅ Step 2: 获取该 do_pick_order 下的所有 pick orders
val pickOrdersSql = """
SELECT DISTINCT
dpol.pick_order_id,
dpol.pick_order_code,
dpol.do_order_id,
dpol.delivery_order_code,
po.consoCode,
po.status,
DATE_FORMAT(po.targetDate, '%Y-%m-%d') as targetDate
FROM fpsmsdb.do_pick_order_line dpol
INNER JOIN fpsmsdb.pick_order po ON po.id = dpol.pick_order_id
WHERE dpol.do_pick_order_id = :doPickOrderId
AND dpol.deleted = 0
AND po.deleted = false
ORDER BY dpol.pick_order_id
""".trimIndent()
val pickOrdersInfo = jdbcDao.queryForList(pickOrdersSql, mapOf("doPickOrderId" to doPickOrderId))
println("🔍 Found ${pickOrdersInfo.size} pick orders")
// ✅ Step 3: 为每个 pick order 获取 lines 和 lots
val allPickOrderLines = mutableListOf<Map<String, Any?>>()
val lineCountsPerPickOrder = mutableListOf<Int>()
val pickOrderIds = mutableListOf<Long>()
val pickOrderCodes = mutableListOf<String>()
val doOrderIds = mutableListOf<Long>()
val deliveryOrderCodes = mutableListOf<String>()
pickOrdersInfo.forEach { poInfo ->
val pickOrderId = (poInfo["pick_order_id"] as? Number)?.toLong()
val pickOrderCode = poInfo["pick_order_code"] as? String
val doOrderId = (poInfo["do_order_id"] as? Number)?.toLong()
val deliveryOrderCode = poInfo["delivery_order_code"] as? String
val pickOrdersInfo = jdbcDao.queryForList(pickOrdersSql, mapOf("doPickOrderId" to doPickOrderId))
println("🔍 Found ${pickOrdersInfo.size} pick orders")
// 收集 IDs 和 codes
pickOrderId?.let { pickOrderIds.add(it) }
pickOrderCode?.let { pickOrderCodes.add(it) }
doOrderId?.let { doOrderIds.add(it) }
deliveryOrderCode?.let { deliveryOrderCodes.add(it) }
// ✅ Step 3: 为每个 pick order 获取 lines 和 lots(包括 null stock 的)
val pickOrders = pickOrdersInfo.map { poInfo ->
val pickOrderId = (poInfo["pick_order_id"] as? Number)?.toLong()
// ✅ 查询该 pick order 的所有 lines 和 lots
val linesSql = """
SELECT
po.id as pickOrderId,
po.code as pickOrderCode,
po.consoCode as pickOrderConsoCode,
DATE_FORMAT(po.targetDate, '%Y-%m-%d') as pickOrderTargetDate,
po.type as pickOrderType,
po.status as pickOrderStatus,
po.assignTo as pickOrderAssignTo,
pol.id as pickOrderLineId,
pol.qty as pickOrderLineRequiredQty,
pol.status as pickOrderLineStatus,
i.id as itemId,
i.code as itemCode,
i.name as itemName,
uc.code as uomCode,
uc.udfudesc as uomDesc,
uc.udfShortDesc as uomShortDesc,
ill.id as lotId,
il.lotNo,
DATE_FORMAT(il.expiryDate, '%Y-%m-%d') as expiryDate,
w.name as location,
COALESCE(uc.udfudesc, 'N/A') as stockUnit,
w.`order` as routerIndex,
w.code as routerRoute,
CASE
WHEN sol.status = 'rejected' THEN NULL
ELSE (COALESCE(ill.inQty, 0) - COALESCE(ill.outQty, 0) - COALESCE(ill.holdQty, 0))
END as availableQty,
COALESCE(spl.qty, 0) as requiredQty,
COALESCE(sol.qty, 0) as actualPickQty,
spl.id as suggestedPickLotId,
ill.status as lotStatus,
sol.id as stockOutLineId,
sol.status as stockOutLineStatus,
COALESCE(sol.qty, 0) as stockOutLineQty,
COALESCE(ill.inQty, 0) as inQty,
COALESCE(ill.outQty, 0) as outQty,
COALESCE(ill.holdQty, 0) as holdQty,
CASE
WHEN (il.expiryDate IS NOT NULL AND il.expiryDate < CURDATE()) THEN 'expired'
WHEN sol.status = 'rejected' THEN 'rejected'
WHEN (COALESCE(ill.inQty, 0) - COALESCE(ill.outQty, 0) - COALESCE(ill.holdQty, 0)) <= 0 THEN 'insufficient_stock'
WHEN ill.status = 'unavailable' THEN 'status_unavailable'
ELSE 'available'
END as lotAvailability,
CASE
WHEN sol.status = 'completed' THEN 'completed'
WHEN sol.status = 'rejected' THEN 'rejected'
WHEN sol.status = 'created' THEN 'pending'
ELSE 'pending'
END as processingStatus
FROM fpsmsdb.pick_order po
JOIN fpsmsdb.pick_order_line pol ON pol.poId = po.id AND pol.deleted = false
JOIN fpsmsdb.items i ON i.id = pol.itemId
LEFT JOIN fpsmsdb.uom_conversion uc ON uc.id = pol.uomId
// ✅ 查询该 pick order 的所有 lines 和 lots
val linesSql = """
SELECT
po.id as pickOrderId,
po.code as pickOrderCode,
po.consoCode as pickOrderConsoCode,
DATE_FORMAT(po.targetDate, '%Y-%m-%d') as pickOrderTargetDate,
po.type as pickOrderType,
po.status as pickOrderStatus,
po.assignTo as pickOrderAssignTo,
-- ✅ 关键修改:使用 LEFT JOIN 来包含没有 lot 的 pick order lines
LEFT JOIN (
SELECT spl.pickOrderLineId, spl.suggestedLotLineId AS lotLineId
FROM fpsmsdb.suggested_pick_lot spl
UNION
SELECT sol.pickOrderLineId, sol.inventoryLotLineId
FROM fpsmsdb.stock_out_line sol
WHERE sol.deleted = false
) ll ON ll.pickOrderLineId = pol.id
pol.id as pickOrderLineId,
pol.qty as pickOrderLineRequiredQty,
pol.status as pickOrderLineStatus,
LEFT JOIN fpsmsdb.suggested_pick_lot spl
ON spl.pickOrderLineId = pol.id AND spl.suggestedLotLineId = ll.lotLineId
LEFT JOIN fpsmsdb.stock_out_line sol
ON sol.pickOrderLineId = pol.id AND sol.inventoryLotLineId = ll.lotLineId AND sol.deleted = false
LEFT JOIN fpsmsdb.inventory_lot_line ill ON ill.id = ll.lotLineId AND ill.deleted = false
LEFT JOIN fpsmsdb.inventory_lot il ON il.id = ill.inventoryLotId AND il.deleted = false
LEFT JOIN fpsmsdb.warehouse w ON w.id = ill.warehouseId
i.id as itemId,
i.code as itemCode,
i.name as itemName,
uc.code as uomCode,
uc.udfudesc as uomDesc,
uc.udfShortDesc as uomShortDesc,
WHERE po.id = :pickOrderId
AND po.deleted = false
ORDER BY
COALESCE(w.`order`, 999999) ASC,
pol.id ASC,
il.lotNo ASC
""".trimIndent()
ill.id as lotId,
il.lotNo,
DATE_FORMAT(il.expiryDate, '%Y-%m-%d') as expiryDate,
w.name as location,
COALESCE(uc.udfudesc, 'N/A') as stockUnit,
w.`order` as routerIndex,
w.code as routerRoute,
CASE
WHEN sol.status = 'rejected' THEN NULL
ELSE (COALESCE(ill.inQty, 0) - COALESCE(ill.outQty, 0) - COALESCE(ill.holdQty, 0))
END as availableQty,
COALESCE(spl.qty, 0) as requiredQty,
COALESCE(sol.qty, 0) as actualPickQty,
spl.id as suggestedPickLotId,
ill.status as lotStatus,
sol.id as stockOutLineId,
sol.status as stockOutLineStatus,
COALESCE(sol.qty, 0) as stockOutLineQty,
COALESCE(ill.inQty, 0) as inQty,
COALESCE(ill.outQty, 0) as outQty,
COALESCE(ill.holdQty, 0) as holdQty,
CASE
WHEN (il.expiryDate IS NOT NULL AND il.expiryDate < CURDATE()) THEN 'expired'
WHEN sol.status = 'rejected' THEN 'rejected'
WHEN (COALESCE(ill.inQty, 0) - COALESCE(ill.outQty, 0) - COALESCE(ill.holdQty, 0)) <= 0 THEN 'insufficient_stock'
WHEN ill.status = 'unavailable' THEN 'status_unavailable'
ELSE 'available'
END as lotAvailability,
CASE
WHEN sol.status = 'completed' THEN 'completed'
WHEN sol.status = 'rejected' THEN 'rejected'
WHEN sol.status = 'created' THEN 'pending'
ELSE 'pending'
END as processingStatus
FROM fpsmsdb.pick_order po
JOIN fpsmsdb.pick_order_line pol ON pol.poId = po.id AND pol.deleted = false
JOIN fpsmsdb.items i ON i.id = pol.itemId
LEFT JOIN fpsmsdb.uom_conversion uc ON uc.id = pol.uomId
val linesResults = jdbcDao.queryForList(linesSql, mapOf("pickOrderId" to pickOrderId))
println("🔍 Pick order $pickOrderId has ${linesResults.size} line-lot records")
LEFT JOIN (
SELECT spl.pickOrderLineId, spl.suggestedLotLineId AS lotLineId
FROM fpsmsdb.suggested_pick_lot spl
UNION
SELECT sol.pickOrderLineId, sol.inventoryLotLineId
FROM fpsmsdb.stock_out_line sol
WHERE sol.deleted = false
) ll ON ll.pickOrderLineId = pol.id
// ✅ 按 pickOrderLineId 分组
val lineGroups = linesResults.groupBy { (it["pickOrderLineId"] as? Number)?.toLong() }
LEFT JOIN fpsmsdb.suggested_pick_lot spl
ON spl.pickOrderLineId = pol.id AND spl.suggestedLotLineId = ll.lotLineId
LEFT JOIN fpsmsdb.stock_out_line sol
ON sol.pickOrderLineId = pol.id AND sol.inventoryLotLineId = ll.lotLineId AND sol.deleted = false
LEFT JOIN fpsmsdb.inventory_lot_line ill ON ill.id = ll.lotLineId AND ill.deleted = false
LEFT JOIN fpsmsdb.inventory_lot il ON il.id = ill.inventoryLotId AND il.deleted = false
LEFT JOIN fpsmsdb.warehouse w ON w.id = ill.warehouseId
val pickOrderLines = lineGroups.map { (lineId, lineRows) ->
val firstLineRow = lineRows.firstOrNull()
if (firstLineRow == null) {
null
} else {
// ✅ 构建 lots 列表(如果没有 lot,返回空数组)
val lots = if (lineRows.any { it["lotId"] != null }) {
lineRows.filter { it["lotId"] != null }.map { lotRow ->
mapOf(
"id" to lotRow["lotId"],
"lotNo" to lotRow["lotNo"],
"expiryDate" to lotRow["expiryDate"],
"location" to lotRow["location"],
"stockUnit" to lotRow["stockUnit"],
"availableQty" to lotRow["availableQty"],
"requiredQty" to lotRow["requiredQty"],
"actualPickQty" to lotRow["actualPickQty"],
"inQty" to lotRow["inQty"],
"outQty" to lotRow["outQty"],
"holdQty" to lotRow["holdQty"],
"lotStatus" to lotRow["lotStatus"],
"lotAvailability" to lotRow["lotAvailability"],
"processingStatus" to lotRow["processingStatus"],
"suggestedPickLotId" to lotRow["suggestedPickLotId"],
"stockOutLineId" to lotRow["stockOutLineId"],
"stockOutLineStatus" to lotRow["stockOutLineStatus"],
"stockOutLineQty" to lotRow["stockOutLineQty"],
"router" to mapOf(
"id" to null,
"index" to lotRow["routerIndex"],
"route" to lotRow["routerRoute"],
"area" to lotRow["routerRoute"],
"itemCode" to lotRow["itemId"],
"itemName" to lotRow["itemName"],
"uomId" to lotRow["uomCode"],
"noofCarton" to lotRow["requiredQty"]
)
WHERE po.id = :pickOrderId
AND po.deleted = false
ORDER BY
COALESCE(w.`order`, 999999) ASC,
pol.id ASC,
il.lotNo ASC
""".trimIndent()
val linesResults = jdbcDao.queryForList(linesSql, mapOf("pickOrderId" to pickOrderId))
println("🔍 Pick order $pickOrderId has ${linesResults.size} line-lot records")
// ✅ 按 pickOrderLineId 分组
val lineGroups = linesResults.groupBy { (it["pickOrderLineId"] as? Number)?.toLong() }
val pickOrderLines = lineGroups.map { (lineId, lineRows) ->
val firstLineRow = lineRows.firstOrNull()
if (firstLineRow == null) {
null
} else {
// ✅ 构建 lots 列表
val lots = if (lineRows.any { it["lotId"] != null }) {
lineRows.filter { it["lotId"] != null }.map { lotRow ->
mapOf(
"id" to lotRow["lotId"],
"lotNo" to lotRow["lotNo"],
"expiryDate" to lotRow["expiryDate"],
"location" to lotRow["location"],
"stockUnit" to lotRow["stockUnit"],
"availableQty" to lotRow["availableQty"],
"requiredQty" to lotRow["requiredQty"],
"actualPickQty" to lotRow["actualPickQty"],
"inQty" to lotRow["inQty"],
"outQty" to lotRow["outQty"],
"holdQty" to lotRow["holdQty"],
"lotStatus" to lotRow["lotStatus"],
"lotAvailability" to lotRow["lotAvailability"],
"processingStatus" to lotRow["processingStatus"],
"suggestedPickLotId" to lotRow["suggestedPickLotId"],
"stockOutLineId" to lotRow["stockOutLineId"],
"stockOutLineStatus" to lotRow["stockOutLineStatus"],
"stockOutLineQty" to lotRow["stockOutLineQty"],
"router" to mapOf(
"id" to null,
"index" to lotRow["routerIndex"],
"route" to lotRow["routerRoute"],
"area" to lotRow["routerRoute"],
"itemCode" to lotRow["itemId"],
"itemName" to lotRow["itemName"],
"uomId" to lotRow["uomCode"],
"noofCarton" to lotRow["requiredQty"]
)
}
} else {
emptyList() // ✅ 返回空数组而不是 null
)
}
mapOf(
"id" to lineId,
"requiredQty" to firstLineRow["pickOrderLineRequiredQty"],
"status" to firstLineRow["pickOrderLineStatus"],
"item" to mapOf(
"id" to firstLineRow["itemId"],
"code" to firstLineRow["itemCode"],
"name" to firstLineRow["itemName"],
"uomCode" to firstLineRow["uomCode"],
"uomDesc" to firstLineRow["uomDesc"],
"uomShortDesc" to firstLineRow["uomShortDesc"],
),
"lots" to lots // ✅ 即使是空数组也返回
)
} else {
emptyList()
}
}.filterNotNull()
mapOf(
"pickOrderId" to pickOrderId,
"pickOrderCode" to poInfo["pick_order_code"],
"doOrderId" to poInfo["do_order_id"],
"deliveryOrderCode" to poInfo["delivery_order_code"],
"consoCode" to poInfo["consoCode"],
"status" to poInfo["status"],
"targetDate" to poInfo["targetDate"],
"pickOrderLines" to pickOrderLines
)
}
// ✅ 构建 FG 信息
val fgInfo = mapOf(
"doPickOrderId" to doPickOrderId,
"ticketNo" to doPickOrderInfo["ticket_no"],
"storeId" to doPickOrderInfo["store_id"],
"shopCode" to doPickOrderInfo["ShopCode"],
"shopName" to doPickOrderInfo["ShopName"],
"truckLanceCode" to doPickOrderInfo["TruckLanceCode"],
"departureTime" to doPickOrderInfo["truck_departure_time"]
)
mapOf(
"id" to lineId,
"requiredQty" to firstLineRow["pickOrderLineRequiredQty"],
"status" to firstLineRow["pickOrderLineStatus"],
"item" to mapOf(
"id" to firstLineRow["itemId"],
"code" to firstLineRow["itemCode"],
"name" to firstLineRow["itemName"],
"uomCode" to firstLineRow["uomCode"],
"uomDesc" to firstLineRow["uomDesc"],
"uomShortDesc" to firstLineRow["uomShortDesc"],
),
"lots" to lots
)
}
}.filterNotNull()
return mapOf(
"fgInfo" to fgInfo,
"pickOrders" to pickOrders
// 记录该 pick order 的行数
lineCountsPerPickOrder.add(pickOrderLines.size)
allPickOrderLines.addAll(pickOrderLines)
}
// 合并到总列表
allPickOrderLines.sortWith(compareBy(
{ line ->
val lots = line["lots"] as? List<Map<String, Any?>>
val firstLot = lots?.firstOrNull()
val router = firstLot?.get("router") as? Map<String, Any?>
(router?.get("index") as? Number)?.toInt() ?: 999999
}
))
// ✅ 构建 FG 信息
val fgInfo = mapOf(
"doPickOrderId" to doPickOrderId,
"ticketNo" to doPickOrderInfo["ticket_no"],
"storeId" to doPickOrderInfo["store_id"],
"shopCode" to doPickOrderInfo["ShopCode"],
"shopName" to doPickOrderInfo["ShopName"],
"truckLanceCode" to doPickOrderInfo["TruckLanceCode"],
"departureTime" to doPickOrderInfo["truck_departure_time"]
)
// ✅ 构建合并后的 pick order 对象
val mergedPickOrder = if (pickOrdersInfo.isNotEmpty()) {
val firstPickOrderInfo = pickOrdersInfo.first()
mapOf(
"pickOrderIds" to pickOrderIds,
"pickOrderCodes" to pickOrderCodes,
"doOrderIds" to doOrderIds,
"deliveryOrderCodes" to deliveryOrderCodes,
"lineCountsPerPickOrder" to lineCountsPerPickOrder,
"consoCode" to firstPickOrderInfo["consoCode"],
"status" to firstPickOrderInfo["status"],
"targetDate" to firstPickOrderInfo["targetDate"],
"pickOrderLines" to allPickOrderLines
)
} else {
null
}
return mapOf(
"fgInfo" to fgInfo,
"pickOrders" to listOfNotNull(mergedPickOrder)
)
}
// Fix the type issues in the getPickOrdersByDateAndStore method
open fun getPickOrdersByDateAndStore(storeId: String): Map<String, Any?> {
println("=== Debug: getPickOrdersByDateAndStore ===")


+ 26
- 0
src/main/java/com/ffii/fpsms/modules/qc/entity/projection/QcCategoryInfo.kt View File

@@ -0,0 +1,26 @@
package com.ffii.fpsms.modules.qc.entity.projection

import com.ffii.fpsms.modules.master.entity.QcItemCategory
import org.springframework.beans.factory.annotation.Value

interface QcCategoryInfo {
val id: Long
val code: String
val name: String?
val description: String?
@get:Value("#{target.qcItemCategory}")
var qcItems: List<QcItemInfo>
}

interface QcItemInfo {
val id: Long
val order: Int
@get:Value("#{target.qcItem.id}")
val qcItemId: Long
@get:Value("#{target.qcItem.code}")
val code: String
@get:Value("#{target.qcItem.name}")
val name: String?
@get:Value("#{target.qcItem.description}")
val description: String?
}

+ 9
- 0
src/main/resources/db/changelog/changes/20251009_01_kelvinS/01_create_items_qc_category_mapping.sql View File

@@ -0,0 +1,9 @@
-- liquibase formatted sql
-- changeset kelvinS:create items qc category mapping table

CREATE TABLE `items_qc_category_mapping` (
`id` INT NOT NULL,
`itemId` INT NOT NULL,
`qcCategoryId` INT NOT NULL,
`type` VARCHAR(45) NOT NULL,
PRIMARY KEY (`id`));

+ 5
- 0
src/main/resources/db/changelog/changes/20251009_01_kelvinS/02_alter_qc_item_category.sql View File

@@ -0,0 +1,5 @@
-- liquibase formatted sql
-- changeset kelvinS:alter qc item category table

ALTER TABLE `qc_item_category`
ADD COLUMN `order` INT NOT NULL AFTER `qcCategoryId`;

+ 5
- 0
src/main/resources/db/changelog/changes/20251009_01_kelvinS/03_alter_qc_category.sql View File

@@ -0,0 +1,5 @@
-- liquibase formatted sql
-- changeset kelvinS:alter qc category table

ALTER TABLE `qc_category`
ADD COLUMN `description` VARCHAR(255) NULL AFTER `name`;

+ 5
- 0
src/main/resources/db/changelog/changes/20251009_01_kelvinS/04_alter_items_qc_category_mapping.sql View File

@@ -0,0 +1,5 @@
-- liquibase formatted sql
-- changeset kelvinS:alter item qc category mapping table

ALTER TABLE `items_qc_category_mapping`
CHANGE COLUMN `id` `id` INT NOT NULL AUTO_INCREMENT ;

+ 6
- 0
src/main/resources/db/changelog/changes/20251023_02_kelvinS/01_alter_qc_category.sql View File

@@ -0,0 +1,6 @@
-- liquibase formatted sql
-- changeset kelvinS:alter qc category table

ALTER TABLE `qc_category`
ADD COLUMN `isDefault` TINYINT(1) NOT NULL DEFAULT '0' AFTER `deleted`,
CHANGE COLUMN `name` `name` VARCHAR(255) NOT NULL ;

Loading…
Cancel
Save