瀏覽代碼

update

master
CANCERYS\kw093 2 週之前
父節點
當前提交
9aa3d977ba
共有 24 個檔案被更改,包括 548 行新增242 行删除
  1. +4
    -0
      src/main/java/com/ffii/fpsms/modules/jobOrder/entity/JobOrder.kt
  2. +26
    -0
      src/main/java/com/ffii/fpsms/modules/jobOrder/entity/JobOrderType.kt
  3. +19
    -0
      src/main/java/com/ffii/fpsms/modules/jobOrder/entity/JobOrderTypeRepository.kt
  4. +9
    -5
      src/main/java/com/ffii/fpsms/modules/jobOrder/service/JobOrderService.kt
  5. +2
    -0
      src/main/java/com/ffii/fpsms/modules/jobOrder/web/JobOrderController.kt
  6. +2
    -0
      src/main/java/com/ffii/fpsms/modules/jobOrder/web/model/CreateJobOrderRequest.kt
  7. +4
    -0
      src/main/java/com/ffii/fpsms/modules/master/entity/EquipmentDetail.kt
  8. +3
    -4
      src/main/java/com/ffii/fpsms/modules/master/entity/EquipmentDetailRepository.kt
  9. +4
    -4
      src/main/java/com/ffii/fpsms/modules/master/service/EquipmentDetailService.kt
  10. +2
    -1
      src/main/java/com/ffii/fpsms/modules/master/service/ProductionScheduleService.kt
  11. +2
    -2
      src/main/java/com/ffii/fpsms/modules/master/web/EquipmentDetailController.kt
  12. +212
    -167
      src/main/java/com/ffii/fpsms/modules/pickOrder/service/PickOrderService.kt
  13. +1
    -1
      src/main/java/com/ffii/fpsms/modules/pickOrder/web/PickOrderController.kt
  14. +1
    -1
      src/main/java/com/ffii/fpsms/modules/pickOrder/web/models/ConsoPickOrderRequest.kt
  15. +32
    -2
      src/main/java/com/ffii/fpsms/modules/pickOrder/web/models/ConsoPickOrderResponse.kt
  16. +5
    -3
      src/main/java/com/ffii/fpsms/modules/productProcess/entity/ProductProcessLine.kt
  17. +9
    -1
      src/main/java/com/ffii/fpsms/modules/productProcess/entity/projections/ProductProcessInfo.kt
  18. +156
    -49
      src/main/java/com/ffii/fpsms/modules/productProcess/service/ProductProcessService.kt
  19. +20
    -0
      src/main/java/com/ffii/fpsms/modules/productProcess/web/model/SaveProductProcessRequest.kt
  20. +1
    -0
      src/main/java/com/ffii/fpsms/modules/stock/service/SuggestedPickLotService.kt
  21. +2
    -2
      src/main/java/com/ffii/fpsms/modules/user/entity/UserRepository.java
  22. +6
    -0
      src/main/resources/db/changelog/changes/20251125_01_enson/04_altertable_enson.sql
  23. +18
    -0
      src/main/resources/db/changelog/changes/20251126_01_enson/01_altertable_enson.sql
  24. +8
    -0
      src/main/resources/db/changelog/changes/20251126_01_enson/02_altertable_enson.sql

+ 4
- 0
src/main/java/com/ffii/fpsms/modules/jobOrder/entity/JobOrder.kt 查看文件

@@ -76,4 +76,8 @@ open class JobOrder : BaseEntity<Long>() {
@JsonManagedReference
@OneToMany(mappedBy = "jobOrder", cascade = [CascadeType.ALL], orphanRemoval = true)
open var stockInLines: MutableList<StockInLine> = mutableListOf()


@Column(name = "jobTypeId")
open var jobTypeId: Long? = null
}

+ 26
- 0
src/main/java/com/ffii/fpsms/modules/jobOrder/entity/JobOrderType.kt 查看文件

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

import com.fasterxml.jackson.annotation.JsonManagedReference
import com.ffii.core.entity.BaseEntity
import com.ffii.fpsms.modules.jobOrder.enums.JobOrderStatus
import com.ffii.fpsms.modules.jobOrder.enums.JobOrderStatusConverter
import com.ffii.fpsms.modules.master.entity.Bom
import com.ffii.fpsms.modules.master.entity.ProductionScheduleLine
import com.ffii.fpsms.modules.stock.entity.StockInLine
import com.ffii.fpsms.modules.user.entity.User
import jakarta.persistence.*
import jakarta.validation.constraints.NotNull
import jakarta.validation.constraints.Size
import java.math.BigDecimal
import java.time.LocalDateTime

@Entity
@Table(name = "jobtype")
open class JobType : BaseEntity<Long>() {
@Size(max = 100)
@NotNull
@Column(name = "name", nullable = false, length = 100)
open var code: String? = null


}

+ 19
- 0
src/main/java/com/ffii/fpsms/modules/jobOrder/entity/JobOrderTypeRepository.kt 查看文件

@@ -0,0 +1,19 @@
package com.ffii.fpsms.modules.jobOrder.entity

import com.ffii.core.support.AbstractRepository
import com.ffii.fpsms.modules.jobOrder.enums.JoPickOrderStatus
import com.ffii.fpsms.modules.jobOrder.entity.JoPickOrder

import com.ffii.fpsms.modules.master.entity.projections.SearchId
import org.springframework.data.jpa.repository.JpaRepository
import org.springframework.data.jpa.repository.Query
import org.springframework.stereotype.Repository
import java.io.Serializable
import java.time.LocalDateTime
import com.ffii.fpsms.modules.jobOrder.entity.JobType
import java.util.Optional
@Repository
interface JobTypeRepository : JpaRepository<JobType, Long> {
//fun findByName(name: String): Optional<JobType>
//fun findByIdAndDeletedIsFalse(id: Long): Optional<JobType>
}

+ 9
- 5
src/main/java/com/ffii/fpsms/modules/jobOrder/service/JobOrderService.kt 查看文件

@@ -31,7 +31,7 @@ import java.time.LocalDate
import java.time.LocalDateTime
import java.time.format.DateTimeFormatter
import kotlin.jvm.optionals.getOrNull
import com.ffii.fpsms.modules.jobOrder.entity.JobTypeRepository
import com.ffii.fpsms.modules.jobOrder.web.model.*
import com.ffii.fpsms.modules.jobOrder.web.model.ExportPickRecordRequest
import com.ffii.fpsms.modules.jobOrder.web.model.PrintPickRecordRequest
@@ -71,12 +71,13 @@ open class JobOrderService(
val stockOutRepository: StockOutRepository,
val stockOutLineRepository: StockOutLIneRepository,
private val printerService: PrinterService,
val jobTypeRepository: JobTypeRepository
) {

open fun allJobOrdersByPage(request: SearchJobOrderInfoRequest): RecordsRes<JobOrderInfo> {
val pageable = PageRequest.of(request.pageNum ?: 0, request.pageSize ?: 10);

println("allJobOrdersByPage")
println(request)
val response = jobOrderRepository.findJobOrderInfoByCodeContainsAndBomNameContainsAndDeletedIsFalseOrderByIdDesc(
code = request.code ?: "",
bomName = request.itemName ?: "",
@@ -161,7 +162,9 @@ open class JobOrderService(
val prodScheduleLine = request.prodScheduleLineId?.let { productionScheduleLineRepository.findById(it).getOrNull() }
val code = assignJobNo()
val status = JobOrderStatus.entries.find { it.value == request.status }

//val jobTypeId = jobTypeRepository.findByName(request.jobType).orElse(null)?.id
val jobTypeId = request.jobTypeId
jo.apply {
this.code = code
this.bom = bom
@@ -173,6 +176,7 @@ open class JobOrderService(
type = request.type
this.approver = approver
this.prodScheduleLine = prodScheduleLine
this.jobTypeId = jobTypeId
}

val savedJo = jobOrderRepository.saveAndFlush(jo);
@@ -224,7 +228,7 @@ open class JobOrderService(
}
jobOrderRepository.save(jo)
val pols = jo.jobms.filter { it.item?.type != "mat"&& it.item?.type != "item"}.
val pols = jo.jobms.filter { it.item?.type != "CMB"&& it.item?.type != "item"}.
map {
SavePickOrderLineRequest(
itemId = it.item?.id,


+ 2
- 0
src/main/java/com/ffii/fpsms/modules/jobOrder/web/JobOrderController.kt 查看文件

@@ -47,6 +47,8 @@ class JobOrderController(

@GetMapping("/getRecordByPage")
fun allJobOrdersByPage(@ModelAttribute request: SearchJobOrderInfoRequest): RecordsRes<JobOrderInfo> {
println("getRecordByPage")
println(request)
return jobOrderService.allJobOrdersByPage(request);
}



+ 2
- 0
src/main/java/com/ffii/fpsms/modules/jobOrder/web/model/CreateJobOrderRequest.kt 查看文件

@@ -9,6 +9,8 @@ data class CreateJobOrderRequest (
val planStart: LocalDateTime? = null,
val planEnd: LocalDateTime? = null,
val reqQty: BigDecimal?,
//val jobType: String?=null,
val jobTypeId: Long?=null,
val type: String? = "detailed",
val approverId: Long? = null,
val prodScheduleLineId: Long? = null,


+ 4
- 0
src/main/java/com/ffii/fpsms/modules/master/entity/EquipmentDetail.kt 查看文件

@@ -26,4 +26,8 @@ open class EquipmentDetail : BaseEntity<Long>() {
open var description: String? = null


@Column(name = "equipmentTypeID")
open var equipmentTypeId: Long? = null


}

+ 3
- 4
src/main/java/com/ffii/fpsms/modules/master/entity/EquipmentDetailRepository.kt 查看文件

@@ -6,12 +6,11 @@ import org.springframework.stereotype.Repository
@Repository
interface EquipmentDetailRepository : AbstractRepository<EquipmentDetail, Long> {
fun findAllByDeletedFalse(): List<EquipmentDetail>;
fun findByCodeAndDeletedFalse(code: String): EquipmentDetail?
fun findByCode(code: String): EquipmentDetail?
fun findByIdAndDeletedFalse(id: Long): EquipmentDetail?;

fun findByCodeAndDeletedIsFalse(code: String): EquipmentDetail?;

fun findByNameAndDeletedIsFalse(name: String): EquipmentDetail?;

fun findByDescriptionAndDeletedIsFalse(description: String): EquipmentDetail?;


}

+ 4
- 4
src/main/java/com/ffii/fpsms/modules/master/service/EquipmentDetailService.kt 查看文件

@@ -39,11 +39,11 @@ open class EquipmentDetailService(
open fun findById(id: Long): EquipmentDetail? {
return equipmentDetailRepository.findByIdAndDeletedFalse(id)
}
/*
open fun findByCode(code: String): EquipmentDetail? {
return equipmentDetailRepository.findByCodeAndDeletedIsFalse(code)
}
*/
open fun findByName(name: String): EquipmentDetail? {
return equipmentDetailRepository.findByNameAndDeletedIsFalse(name)
}
@@ -51,7 +51,7 @@ open class EquipmentDetailService(
open fun findByDescription(description: String): EquipmentDetail? {
return equipmentDetailRepository.findByDescriptionAndDeletedIsFalse(description)
}
/*
@Transactional
open fun saveEquipmentDetail(request: NewEquipmentDetailRequest): EquipmentDetail {

@@ -78,7 +78,7 @@ open class EquipmentDetailService(

return equipmentDetailRepository.saveAndFlush(entity)
}
*/
@Transactional
open fun deleteEquipmentDetail(id: Long) {
val equipmentDetail = equipmentDetailRepository.findByIdAndDeletedFalse(id)


+ 2
- 1
src/main/java/com/ffii/fpsms/modules/master/service/ProductionScheduleService.kt 查看文件

@@ -388,7 +388,8 @@ open class ProductionScheduleService(
bomId = bom?.id,
reqQty = request.demandQty,
approverId = approver?.id,
prodScheduleLineId = request.id
prodScheduleLineId = request.id,
//jobType = null,
)

val jo = jobOrderService.createJobOrder(joRequest)


+ 2
- 2
src/main/java/com/ffii/fpsms/modules/master/web/EquipmentDetailController.kt 查看文件

@@ -60,13 +60,13 @@ fun getAllEquipmentDetailByPage(
fun getEquipmentDetail(@PathVariable id: Long): EquipmentDetail? {
return equipmentDetailService.findById(id)
}
/*
// 新增/编辑
@PostMapping("/save")
fun saveEquipmentDetail(@Valid @RequestBody equipmentDetail: NewEquipmentDetailRequest): EquipmentDetail {
return equipmentDetailService.saveEquipmentDetail(equipmentDetail)
}
*/
// 逻辑删除
@DeleteMapping("/delete/{id}")
fun deleteEquipmentDetail(@PathVariable id: Long) {


+ 212
- 167
src/main/java/com/ffii/fpsms/modules/pickOrder/service/PickOrderService.kt 查看文件

@@ -233,6 +233,7 @@ open class PickOrderService(
suggestedList = emptyList() // Empty list since you don't need suggestions
)
}

val groupName = po.id?.let { pickOrderId ->
groupsByPickOrderId[pickOrderId]?.firstOrNull()?.name
} ?: "No Group"
@@ -732,57 +733,69 @@ open class PickOrderService(
itemAvailableQtyMap[itemId] = totalAvailableQty
}

// Pick Orders
val releasePickOrderLineInfos = pos
.map { po ->
val releasePickOrderLineInfos = po.pickOrderLines.map { pol ->
val itemId = pol.item?.id
val availableQty = itemId?.let { itemAvailableQtyMap[it] } ?: zero

// Move stockOutLines declaration inside the pol loop
val stockOutLines = stockOutLinesByPickOrderLineId[pol.id] ?: emptyList()

// Calculate total picked quantity from stock out lines
println("=== PICKED QTY DEBUG: Line ${pol.id} ===")
println("Stock Out Lines: ${stockOutLines.map { "${it.id}(status=${it.status}, qty=${it.qty})" }}")

val totalPickedQty = stockOutLines
.sumOf { it.qty ?: zero }

println("Total Picked Qty: $totalPickedQty")
println("=== END DEBUG ===")

// Return
GetPickOrderLineInfo(
id = pol.id,
itemId = pol.item?.id,
itemCode = pol.item?.code,
itemName = pol.item?.name,
availableQty = availableQty, // Use pre-calculated value
requiredQty = pol.qty,
uomCode = pol.uom?.code,
uomDesc = pol.uom?.udfudesc,
suggestedList = suggestions.suggestedList.filter { it.pickOrderLine?.id == pol.id },
pickedQty = totalPickedQty
)
}
val groupName = po.id?.let { pickOrderId ->
groupsByPickOrderId[pickOrderId]?.firstOrNull()?.name
} ?: "No Group"
// Return
GetPickOrderInfo(
id = po.id,
code = po.code,
consoCode = po.consoCode,
targetDate = po.targetDate,
type = po.type?.value,
status = po.status?.value,
assignTo = po.assignTo?.id,
groupName = groupName,
pickOrderLines = releasePickOrderLineInfos
.map { po ->
val releasePickOrderLineInfos = po.pickOrderLines.map { pol ->
val itemId = pol.item?.id
val availableQty = itemId?.let { itemAvailableQtyMap[it] } ?: zero
// 取得這一行的所有 stock_out_line
val stockOutLines = stockOutLinesByPickOrderLineId[pol.id] ?: emptyList()
// Debug: 已拣数量
println("=== PICKED QTY DEBUG: Line ${pol.id} ===")
println("Stock Out Lines: ${stockOutLines.map { "${it.id}(status=${it.status}, qty=${it.qty})" }}")
val totalPickedQty = stockOutLines.sumOf { it.qty ?: zero }
println("Total Picked Qty: $totalPickedQty")
println("=== END DEBUG ===")
// ✅ 無批次的 stock_out_line(inventoryLotLineId 為空)
val noLotStockouts = stockOutLines
.filter { it.inventoryLotLineId == null } // 注意:用 inventoryLotLineId
.map { sol ->
NoLotLineDto(
stockOutLineId = sol.id!!,
status = sol.status,
qty = sol.qty ?: zero,
created = null, // 若實體沒有 created/modified 欄位,就先給 null
modified = null
)
}
// 回傳行資訊
GetPickOrderLineInfo(
id = pol.id,
itemId = pol.item?.id,
itemCode = pol.item?.code,
itemName = pol.item?.name,
availableQty = availableQty,
requiredQty = pol.qty,
uomCode = pol.uom?.code,
uomDesc = pol.uom?.udfudesc,
suggestedList = suggestions.suggestedList.filter { it.pickOrderLine?.id == pol.id },
pickedQty = totalPickedQty,
noLotLines = noLotStockouts // ✅ 關鍵:把無批次行帶出去
)
}

val groupName = po.id?.let { pickOrderId ->
groupsByPickOrderId[pickOrderId]?.firstOrNull()?.name
} ?: "No Group"
GetPickOrderInfo(
id = po.id,
code = po.code,
consoCode = po.consoCode,
targetDate = po.targetDate,
type = po.type?.value,
status = po.status?.value,
assignTo = po.assignTo?.id,
groupName = groupName,
pickOrderLines = releasePickOrderLineInfos
)
}
// Items
val currentInventoryInfos = requiredItems.map { item ->
val inventory = item.first?.let { inventories[it] }
@@ -829,132 +842,164 @@ open class PickOrderService(
return getPickOrdersInfo(releasedPickOrderIds)
}

open fun getPickOrderLineLotDetails(pickOrderLineId: Long): List<Map<String, Any>> {
open fun getPickOrderLineLotDetails(pickOrderLineId: Long): List<PickOrderLineLotDetailResponse> {
val today = LocalDate.now()

println("=== Debug: getPickOrderLineLotDetails ===")
val zero = BigDecimal.ZERO
println("=== Debug: getPickOrderLineLotDetails (Repository-based) ===")
println("pickOrderLineId: $pickOrderLineId")
println("today: $today")

val sql = """
SELECT
ill.id as lotId,
il.lotNo,
il.expiryDate,
w.name as location,
COALESCE(uc.udfudesc, 'N/A') as stockUnit,
(COALESCE(ill.inQty, 0) - COALESCE(ill.outQty, 0) - COALESCE(ill.holdQty, 0)) as availableQty,
COALESCE(spl.qty, 0) as requiredQty, -- 使用COALESCE处理null值
COALESCE(ill.inQty, 0) as inQty,
COALESCE(ill.outQty, 0) as outQty,
COALESCE(ill.holdQty, 0) as holdQty,
COALESCE(sol.qty, 0) as actualPickQty,
COALESCE(spl.id, 0) as suggestedPickLotId, -- 使用COALESCE处理null值
ill.status as lotStatus,
sol.id as stockOutLineId,
sol.status as stockOutLineStatus,
sol.qty as stockOutLineQty,
COALESCE(spl.suggestedLotLineId, ill.id) as debugSuggestedLotLineId, -- 使用COALESCE处理null值
ill.inventoryLotId as debugInventoryLotId,
-- Calculate total picked quantity by ALL pick orders for this lot
COALESCE((
SELECT SUM(sol_all.qty)
FROM fpsmsdb.stock_out_line sol_all
WHERE sol_all.inventoryLotLineId = ill.id
AND sol_all.deleted = false
AND sol_all.status IN ('pending', 'checked', 'partially_completed', 'completed')
), 0) as totalPickedByAllPickOrders,
-- FIXED: Calculate remaining available quantity correctly
(COALESCE(ill.inQty, 0) - COALESCE(ill.outQty, 0) - COALESCE(ill.holdQty, 0)) as remainingAfterAllPickOrders,
-- Add detailed debug fields for lotAvailability calculation
ill.status as debug_ill_status,
(il.expiryDate IS NOT NULL AND il.expiryDate < CURDATE()) as debug_is_expired,
sol.status as debug_sol_status,
(COALESCE(ill.inQty, 0) - COALESCE(ill.outQty, 0) - COALESCE(ill.holdQty, 0)) as debug_remaining_stock,
(COALESCE(spl.qty, 0) - COALESCE(sol.qty, 0)) as debug_required_after_picked,
CASE
-- FIXED: Check if lot is expired
WHEN (il.expiryDate IS NOT NULL AND il.expiryDate < CURDATE()) THEN 'expired'
-- FIXED: Check if lot has rejected stock out line for this pick order
WHEN sol.status = 'rejected' THEN 'rejected'
-- FIXED: Check if lot is unavailable due to insufficient stock
WHEN (COALESCE(ill.inQty, 0) - COALESCE(ill.outQty, 0) - COALESCE(ill.holdQty, 0)) <= 0 THEN 'insufficient_stock'
-- FIXED: Check if lot status is unavailable
WHEN ill.status = 'unavailable' THEN 'status_unavailable'
-- Default to available
ELSE 'available'
END as lotAvailability
FROM fpsmsdb.inventory_lot_line ill
JOIN fpsmsdb.inventory_lot il ON il.id = ill.inventoryLotId
LEFT JOIN fpsmsdb.warehouse w ON w.id = ill.warehouseId
LEFT JOIN fpsmsdb.item_uom sales_iu ON sales_iu.itemId = il.itemId AND sales_iu.salesUnit = true AND sales_iu.deleted = false
LEFT JOIN fpsmsdb.uom_conversion uc ON uc.id = sales_iu.uomId
-- FIXED: Include both suggested lots AND lots with stock out lines for this pick order
LEFT JOIN fpsmsdb.suggested_pick_lot spl ON spl.suggestedLotLineId = ill.id AND spl.pickOrderLineId = :pickOrderLineId
LEFT JOIN fpsmsdb.stock_out_line sol ON sol.pickOrderLineId = :pickOrderLineId AND sol.inventoryLotLineId = ill.id AND sol.deleted = false
-- FIXED: Only include lots that are either suggested OR have stock out lines for this pick order
WHERE (spl.pickOrderLineId = :pickOrderLineId OR sol.pickOrderLineId = :pickOrderLineId)
AND(sol.status IS NULL OR sol.status != 'completed')
AND ill.deleted = false
AND il.deleted = false
ORDER BY
CASE WHEN sol.status = 'rejected' THEN 0 ELSE 1 END, -- Show rejected lots first
il.expiryDate ASC,
il.lotNo ASC
""".trimIndent()

println("🔍 Executing SQL for lot details: $sql")
println("🔍 With parameters: pickOrderLineId = $pickOrderLineId")

val result = jdbcDao.queryForList(sql, mapOf("pickOrderLineId" to pickOrderLineId))

// Add detailed debug output for each lot
println("=== DETAILED LOT AVAILABILITY DEBUG ===")
result.forEach { row ->
val lotId = row["lotId"]
val lotNo = row["lotNo"]
val illStatus = row["debug_ill_status"]
val isExpired = row["debug_is_expired"]
val solStatus = row["debug_sol_status"]
val lotAvailability = row["lotAvailability"]

println("--- Lot: $lotNo (ID: $lotId) ---")
println(" ill.status: $illStatus")
println(" is_expired: $isExpired")
println(" sol.status: $solStatus")
println(" lotAvailability: $lotAvailability")

// Check each condition step by step
if (isExpired == true) {
println(" ❌ FAILED: lot is expired")
} else if (solStatus == "rejected") {
println(" ❌ FAILED: sol.status = 'rejected'")
// ✅ 1. 获取 PickOrderLine 以获取 UOM 信息
val pickOrderLine = pickOrderLineRepository.findById(pickOrderLineId).orElse(null)
if (pickOrderLine == null) {
println("❌ PickOrderLine not found: $pickOrderLineId")
return emptyList()
}
val uomDesc = pickOrderLine.uom?.udfudesc ?: "N/A"
// ✅ 2. 获取所有相关的 SuggestedPickLot
val suggestedPickLots = suggestPickLotRepository.findAllByPickOrderLineId(pickOrderLineId)
val suggestedPickLotsByLotId = suggestedPickLots
.mapNotNull { spl -> spl.suggestedLotLine?.id?.let { it to spl } }
.toMap()
// ✅ 3. 获取所有相关的 StockOutLine(包括有 lot 和没有 lot 的)
val stockOutLines = stockOutLIneRepository.findAllByPickOrderLineIdAndDeletedFalse(pickOrderLineId)
val stockOutLinesByLotId = stockOutLines
.filter { it.inventoryLotLineId != null }
.mapNotNull { sol -> sol.inventoryLotLineId?.let { it to sol } }
.toMap()
// ✅ 4. 获取所有 no-lot 的 StockOutLine
val noLotStockOutLines = stockOutLines.filter { it.inventoryLotLineId == null }
// ✅ 5. 获取所有相关的 InventoryLotLine IDs
val inventoryLotLineIds = (suggestedPickLots.mapNotNull { it.suggestedLotLine?.id } +
stockOutLines.mapNotNull { it.inventoryLotLineId }).distinct()
// ✅ 6. 批量加载 InventoryLotLine 实体
val inventoryLotLines = if (inventoryLotLineIds.isNotEmpty()) {
inventoryLotLineRepository.findAllById(inventoryLotLineIds)
.associateBy { it.id!! }
} else {
emptyMap()
}
// ✅ 7. 构建有 lot 的记录
val lotDetails = inventoryLotLines.values.mapNotNull { ill ->
val il = ill.inventoryLot
val w = ill.warehouse
val spl = ill.id?.let { suggestedPickLotsByLotId[it] }
val sol = ill.id?.let { stockOutLinesByLotId[it] }
// 计算可用数量
val inQty = ill.inQty ?: zero
val outQty = ill.outQty ?: zero
val holdQty = ill.holdQty ?: zero
val availableQty = inQty.minus(outQty).minus(holdQty)
// 计算所有 pick orders 的总已拣数量
val totalPickedByAllPickOrders = if (ill.id != null) {
stockOutLIneRepository.findAllByPickOrderLineIdAndDeletedFalse(pickOrderLineId)
.filter { it.inventoryLotLineId == ill.id }
.sumOf { it.qty ?: zero }
} else {
println(" PASSED: All conditions met, should be 'available'")
zero
}
println(" ---")
}
println("=== END DETAILED DEBUG ===")

// Filter out completed lots
val filteredResult = result.filter { row ->
val stockOutLineStatus = row["stockOutLineStatus"] as String?
val stockOutLineQty = row["stockOutLineQty"] as Number?
val requiredQty = row["requiredQty"] as Number?

// Show lot if:
// 1. No stock out line exists, OR
// 2. Stock out line is not completed, OR
// 3. Stock out line qty doesn't equal required qty
stockOutLineStatus != "completed" ||
stockOutLineQty?.toDouble() != requiredQty?.toDouble()
// 判断是否过期
val isExpired = il?.expiryDate?.let { it.isBefore(today) } == true
// 计算 lotAvailability
val lotAvailability = when {
isExpired -> "expired"
sol?.status == "rejected" -> "rejected"
availableQty <= zero -> "insufficient_stock"
ill.status?.value == "unavailable" -> "status_unavailable"
else -> "available"
}
// 过滤:只返回未完成的或需要显示的
val shouldInclude = sol == null ||
sol.status != "completed" ||
(sol.qty ?: zero) != (spl?.qty ?: zero)
if (!shouldInclude) {
return@mapNotNull null
}
PickOrderLineLotDetailResponse(
lotId = ill.id,
lotNo = il?.lotNo,
expiryDate = il?.expiryDate,
location = w?.name,
stockUnit = ill.stockUom?.uom?.udfudesc ?: uomDesc,
availableQty = availableQty,
requiredQty = spl?.qty ?: zero,
inQty = inQty,
outQty = outQty,
holdQty = holdQty,
actualPickQty = sol?.qty ?: zero,
suggestedPickLotId = spl?.id,
lotStatus = ill.status?.value,
stockOutLineId = sol?.id,
stockOutLineStatus = sol?.status,
stockOutLineQty = sol?.qty,
totalPickedByAllPickOrders = totalPickedByAllPickOrders,
remainingAfterAllPickOrders = availableQty,
lotAvailability = lotAvailability,
noLot = false
)
}
println("Final result count: ${filteredResult.size}")
filteredResult.forEach { row ->
println("Final Row: $row")
// ✅ 8. 构建 no-lot 的记录
val noLotDetails = noLotStockOutLines.mapNotNull { sol ->
// 过滤:只返回未完成的
if (sol.status == "completed") {
return@mapNotNull null
}
PickOrderLineLotDetailResponse(
lotId = null,
lotNo = null,
expiryDate = null,
location = null,
stockUnit = uomDesc,
availableQty = null,
requiredQty = zero,
inQty = zero,
outQty = zero,
holdQty = zero,
actualPickQty = sol.qty ?: zero,
suggestedPickLotId = null,
lotStatus = "unavailable",
stockOutLineId = sol.id,
stockOutLineStatus = sol.status,
stockOutLineQty = sol.qty,
totalPickedByAllPickOrders = zero,
remainingAfterAllPickOrders = null,
lotAvailability = "insufficient_stock",
noLot = true
)
}

return filteredResult
// ✅ 9. 合并并排序
val allDetails = (lotDetails + noLotDetails).sortedWith(
compareBy<PickOrderLineLotDetailResponse>(
{ it.noLot }, // no-lot 行排在后面
{ it.stockOutLineStatus == "rejected" }, // rejected 排在前面
{ it.expiryDate ?: LocalDate.MAX }, // 按过期日期排序
{ it.lotNo ?: "" } // 按 lotNo 排序
)
)
println("✅ Final result count: ${allDetails.size}")
println(" - With lot: ${lotDetails.size}")
println(" - No-lot: ${noLotDetails.size}")
return allDetails
}

@Transactional(rollbackFor = [java.lang.Exception::class])


+ 1
- 1
src/main/java/com/ffii/fpsms/modules/pickOrder/web/PickOrderController.kt 查看文件

@@ -158,7 +158,7 @@ class PickOrderController(
}

@GetMapping("/lot-details/{pickOrderLineId}")
fun getPickOrderLineLotDetails(@PathVariable pickOrderLineId: Long): List<Map<String, Any>> {
fun getPickOrderLineLotDetails(@PathVariable pickOrderLineId: Long): List<PickOrderLineLotDetailResponse> {
return pickOrderService.getPickOrderLineLotDetails(pickOrderLineId);
}
@GetMapping("/lot-details-by-do-pick-order-record/{doPickOrderRecordId}")


+ 1
- 1
src/main/java/com/ffii/fpsms/modules/pickOrder/web/models/ConsoPickOrderRequest.kt 查看文件

@@ -17,4 +17,4 @@ data class ReleaseConsoPickOrderRequest (
// Start Pick Order
data class StartConsoPickOrderRequest (
val consoCode: String,
)
)

+ 32
- 2
src/main/java/com/ffii/fpsms/modules/pickOrder/web/models/ConsoPickOrderResponse.kt 查看文件

@@ -4,7 +4,7 @@ import com.ffii.fpsms.modules.stock.entity.SuggestedPickLot
import com.ffii.fpsms.modules.stock.entity.projection.CurrentInventoryItemInfo
import java.math.BigDecimal
import java.time.LocalDateTime
import java.time.LocalDate
// Final Response - Release Conso Pick Order Page
data class ReleasePickOrderInfoResponse(
val consoCode: String,
@@ -63,8 +63,15 @@ data class GetPickOrderLineInfo(
val uomDesc: String?,
val suggestedList: List<SuggestedPickLot>?,
val pickedQty: BigDecimal?=BigDecimal.ZERO,
val noLotLines: List<NoLotLineDto>?=emptyList()
)
data class NoLotLineDto(
val stockOutLineId: Long,
val status: String?,
val qty: BigDecimal,
val created: LocalDateTime?,
val modified: LocalDateTime?
)

// Final Response - Conso Pick Order Detail
data class ConsoPickOrderResponse(
val consoCode: String,
@@ -148,4 +155,27 @@ data class IdCodeDesc(
val id: Long?,
val code: String?,
val desc: String?,
)

data class PickOrderLineLotDetailResponse(
val lotId: Long?,
val lotNo: String?,
val expiryDate: LocalDate?,
val location: String?,
val stockUnit: String?,
val availableQty: BigDecimal?,
val requiredQty: BigDecimal?,
val inQty: BigDecimal?,
val outQty: BigDecimal?,
val holdQty: BigDecimal?,
val actualPickQty: BigDecimal?,
val suggestedPickLotId: Long?,
val lotStatus: String?,
val stockOutLineId: Long?,
val stockOutLineStatus: String?,
val stockOutLineQty: BigDecimal?,
val totalPickedByAllPickOrders: BigDecimal?,
val remainingAfterAllPickOrders: BigDecimal?,
val lotAvailability: String?,
val noLot: Boolean = false
)

+ 5
- 3
src/main/java/com/ffii/fpsms/modules/productProcess/entity/ProductProcessLine.kt 查看文件

@@ -23,7 +23,9 @@ open class ProductProcessLine : BaseEntity<Long>() {
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "equipmentId")
open var equipment: Equipment? = null

@Column(name = "equipmentDetailId")
open var equipmentDetailId: Long? = null
@Size(max = 100)
@Column(name = "name", length = 100)
open var name: String? = null
@@ -72,8 +74,8 @@ open class ProductProcessLine : BaseEntity<Long>() {
@Column(name = "defectQty2", precision = 16, scale = 2)
open var defectQty2: Int? = null
@Column(name = "defectRemark2", length = 255)
open var defectRemark2: String? = null
@Column(name = "defectDescription2", length = 255)
open var defectDescription2: String? = null
@Column(name = "defectUom2", length = 20)
open var defectUom2: String? = null


+ 9
- 1
src/main/java/com/ffii/fpsms/modules/productProcess/entity/projections/ProductProcessInfo.kt 查看文件

@@ -4,7 +4,7 @@ import java.time.LocalDateTime
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

data class ProductProcessInfo(
val id: Long?,
@@ -21,6 +21,7 @@ data class ProductProcessInfo(
val bomId: Long?,
val jobOrderId: Long?,
val jobOrderCode: String?,
val jobOrderStatus: String?,
val isDark: String?,
val isDense: Int?,
val isFloat: String?,
@@ -56,6 +57,13 @@ data class ProductProcessLineInfo(
val scrapQty: Int?,
val defectQty: Int?,
val defectUom: String?,
val defectDescription: String?,
val defectQty2: Int?,
val defectUom2: String?,
val defectDescription2: String?,
val defectQty3: Int?,
val defectUom3: String?,
val defectDescription3: String?,
val outputFromProcessQty: Int?,
val outputFromProcessUom: String?,
val durationInMinutes: Int?,


+ 156
- 49
src/main/java/com/ffii/fpsms/modules/productProcess/service/ProductProcessService.kt 查看文件

@@ -38,6 +38,7 @@ import com.ffii.fpsms.modules.stock.service.StockInLineService
import com.ffii.fpsms.modules.stock.web.model.SaveStockInLineRequest
import com.ffii.fpsms.modules.master.entity.BomProcessMaterialRepository
import com.ffii.fpsms.modules.master.entity.BomMaterialRepository
import com.ffii.fpsms.modules.master.entity.EquipmentDetailRepository
@Service
@Transactional
open class ProductProcessService(
@@ -57,6 +58,7 @@ open class ProductProcessService(
private val stockInLineService: StockInLineService,
private val bomProcessMaterialRepository: BomProcessMaterialRepository,
private val bomMaterialRepository: BomMaterialRepository,
private val equipmentDetailRepository: EquipmentDetailRepository,
) {
open fun findAll(pageable: Pageable): Page<ProductProcess> {
@@ -552,6 +554,7 @@ open class ProductProcessService(
bomId = process.bom?.id?:0,
jobOrderId = process.jobOrder?.id?:0,
jobOrderCode = jobOrder?.code?:"",
jobOrderStatus = jobOrder?.status?.value?:"",
itemId = bom?.item?.id?:0,
itemCode = bom?.item?.code?:"",
itemName = bom?.item?.name?:"",
@@ -592,6 +595,13 @@ open class ProductProcessService(
scrapQty = line.scrapQty?:0,
defectQty = line.defectQty?:0,
defectUom = line.defectUom?:"",
defectDescription = line.defectDescription?:"",
defectQty2 = line.defectQty2?:0,
defectUom2 = line.defectUom2?:"",
defectDescription2 = line.defectDescription2?:"",
defectQty3 = line.defectQty3?:0,
defectUom3 = line.defectUom3?:"",
defectDescription3 = line.defectDescription3?:"",
outputFromProcessQty = line.outputFromProcessQty?:0,
outputFromProcessUom = line.outputFromProcessUom?:"",
startTime = line.startTime,
@@ -731,6 +741,64 @@ open class ProductProcessService(
}


return MessageResponse(
id = null,
code = null,
name = null,
type = null,
message = null,
errorPosition = null,
)
}

open fun NewUpdateProductProcessLineOperatorIdOrEquipmentIdAndEquipmentDetail(request: NewUpdateProductProcessLineOperatorIdOrEquipmentIdAndEquipmentDetailRequest): MessageResponse {
val productProcessLine = productProcessLineRepository.findById(request.productProcessLineId).orElse(null)
val equipmentDetail = equipmentDetailRepository.findByCode(request.EquipmentTypeSubTypeEquipmentNo)
val user = userRepository.findByName(request.Name?:"")
val bomProcess= bomProcessRepository.findById(productProcessLine?.bomProcess?.id?:0L).orElse(null)
val bomProcessEquipment=bomProcess?.equipment
if (equipmentDetail != null && user != null) {
// 检查 equipmentId 是否与 bomProcessEquipment 匹配
if (equipmentDetail.equipmentTypeId != bomProcessEquipment?.id) {
println("productProcessLine id${request.productProcessLineId}")
println("operator Name${request.Name}")
println("user ${user}")
println("bomProcess ${bomProcess?.id}")
println("not match equipment id${equipmentDetail.equipmentTypeId} and ${bomProcessEquipment?.id}")
// 返回错误响应
return MessageResponse(
id = request.productProcessLineId,
code = "400",
name = "Equipment Validation Failed",
type = "error",
message = "Input Equipment ID($equipmentDetail.equipmentTypeId ) and BOM Process Equipment ID(${bomProcessEquipment?.id}) not match",
errorPosition = "equipmentId"
)
}
}
if(equipmentDetail?.equipmentTypeId != null &&( equipmentDetail.equipmentTypeId ==bomProcessEquipment?.id)) {

val equipment = equipmentRepository.findById(equipmentDetail.equipmentTypeId).orElse(null)
productProcessLine?.equipment = equipment
productProcessLine?.equipmentDetailId = equipmentDetail.id
productProcessLineRepository.save(productProcessLine)
}
else
{ println("productProcessLine id${request.productProcessLineId}")
println("operator Name${request?.Name}")
println("user ${user}")
println("bomProcess ${bomProcess?.id}")
println("not match equipment id${equipmentDetail?.equipmentTypeId} and ${bomProcessEquipment?.id}")
}
if(user != null) {
//productProcessLine.operator = user.name
// productProcessLineRepository.save(productProcessLine)
}


return MessageResponse(
id = null,
code = null,
@@ -781,6 +849,13 @@ open class ProductProcessService(
outputFromProcessUom = productProcessLine.outputFromProcessUom?:"",
defectQty = productProcessLine.defectQty?:0,
defectUom = productProcessLine.defectUom?:"",
defectDescription = productProcessLine.defectDescription?:"",
defectQty2 = productProcessLine.defectQty2?:0,
defectUom2 = productProcessLine.defectUom2?:"",
defectDescription2 = productProcessLine.defectDescription2?:"",
defectQty3 = productProcessLine.defectQty3?:0,
defectUom3 = productProcessLine.defectUom3?:"",
defectDescription3 = productProcessLine.defectDescription3?:"",
scrapQty = productProcessLine.scrapQty?:0,
scrapUom = productProcessLine.scrapUom?:"",
byproductId = productProcessLine.byproduct?.id?:0,
@@ -834,55 +909,86 @@ open class ProductProcessService(
)
}

open fun updateProductProcessLineQty(request: UpdateProductProcessLineQtyRequest): MessageResponse {
val outputFromProcessQty = request.outputFromProcessQty
val outputFromProcessUom = request.outputFromProcessUom
val defectQty = request.defectQty
val defectUom = request.defectUom
val scrapQty = request.scrapQty
val scrapUom = request.scrapUom
val byproductName = request.byproductName
val byproductQty = request.byproductQty
val byproductUom = request.byproductUom
val productProcessLine = productProcessLineRepository.findById(request.productProcessLineId).orElse(null)
if(outputFromProcessQty != null) {
productProcessLine.outputFromProcessQty = outputFromProcessQty
}
if(outputFromProcessUom != null) {
productProcessLine.outputFromProcessUom = outputFromProcessUom
}
if(defectQty != null) {
productProcessLine.defectQty = defectQty
}
if(defectUom != null) {
productProcessLine.defectUom = defectUom
}
if(scrapQty != null) {
productProcessLine.scrapQty = scrapQty
}
if(scrapUom != null) {
productProcessLine.scrapUom = scrapUom
}
if(byproductName != null) {
productProcessLine.byproductName = byproductName
}
if(byproductQty != null) {
productProcessLine.byproductQty = byproductQty
}
if(byproductUom != null) {
productProcessLine.byproductUom = byproductUom
open fun updateProductProcessLineQty(request: UpdateProductProcessLineQtyRequest): MessageResponse {
val outputFromProcessQty = request.outputFromProcessQty
val outputFromProcessUom = request.outputFromProcessUom
val defectQty = request.defectQty
val defectUom = request.defectUom
val defectDescription = request.defectDescription
val defect2Qty = request.defect2Qty
val defect2Uom = request.defect2Uom
val defect2Description = request.defectDescription2
val defect3Qty = request.defect3Qty
val defect3Uom = request.defect3Uom
val defect3Description = request.defectDescription3
val scrapQty = request.scrapQty
val scrapUom = request.scrapUom
val byproductName = request.byproductName
val byproductQty = request.byproductQty
val byproductUom = request.byproductUom
val productProcessLine = productProcessLineRepository.findById(request.productProcessLineId).orElse(null)
if(outputFromProcessQty != null) {
productProcessLine.outputFromProcessQty = outputFromProcessQty
}
if(outputFromProcessUom != null) {
productProcessLine.outputFromProcessUom = outputFromProcessUom
}
if(defectQty != null) {
productProcessLine.defectQty = defectQty
}
if(defectUom != null) {
productProcessLine.defectUom = defectUom
}

if(defectDescription != null) {
productProcessLine.defectDescription = defectDescription
}
if(defect2Qty != null) {
productProcessLine.defectQty2 = defect2Qty
}
if(defect2Uom != null) {
productProcessLine.defectUom2 = defect2Uom
}
if(defect2Description != null) {
productProcessLine.defectDescription2 = defect2Description
}

if(defect3Qty != null) {
productProcessLine.defectQty3 = defect3Qty
}
if(defect3Uom != null) {
productProcessLine.defectUom3 = defect3Uom
}
if(defect3Description != null) {
productProcessLine.defectDescription3 = defect3Description
}
if(scrapQty != null) {
productProcessLine.scrapQty = scrapQty
}
if(scrapUom != null) {
productProcessLine.scrapUom = scrapUom
}
if(byproductName != null) {
productProcessLine.byproductName = byproductName
}
if(byproductQty != null) {
productProcessLine.byproductQty = byproductQty
}
if(byproductUom != null) {
productProcessLine.byproductUom = byproductUom
}
productProcessLineRepository.save(productProcessLine)
CompleteProductProcessLine(request.productProcessLineId)
return MessageResponse(
id = request.productProcessLineId,
code = "200",
name = "ProductProcessLine Qty Updated",
type = "success",
message = "ProductProcessLine Qty Updated",
errorPosition = null,
)
}
productProcessLineRepository.save(productProcessLine)
CompleteProductProcessLine(request.productProcessLineId)
return MessageResponse(
id = request.productProcessLineId,
code = "200",
name = "ProductProcessLine Qty Updated",
type = "success",
message = "ProductProcessLine Qty Updated",
errorPosition = null,
)
}

open fun getAllJoborderProductProcessInfo(): List<AllJoborderProductProcessInfoResponse> {
val productProcesses = productProcessRepository.findAllByDeletedIsFalse()
@@ -1057,4 +1163,5 @@ open fun updateProductProcessLineQty(request: UpdateProductProcessLineQtyRequest
errorPosition = null,
)
}
}
}


+ 20
- 0
src/main/java/com/ffii/fpsms/modules/productProcess/web/model/SaveProductProcessRequest.kt 查看文件

@@ -100,6 +100,13 @@ data class UpdateProductProcessLineQtyRequest(
val outputFromProcessUom: String?,
val defectQty: Int?,
val defectUom: String?,
val defectDescription: String?,
val defect2Qty: Int?,
val defect2Uom: String?,
val defectDescription2: String?,
val defect3Qty: Int?,
val defect3Uom: String?,
val defectDescription3: String?,
val scrapQty: Int?,
val scrapUom: String?,
val byproductName: String?,
@@ -127,6 +134,13 @@ data class JobOrderProcessLineDetailResponse(
val outputFromProcessUom: String?,
val defectQty: Int?,
val defectUom: String?,
val defectDescription: String?,
val defectQty2: Int?,
val defectUom2: String?,
val defectDescription2: String?,
val defectQty3: Int?,
val defectUom3: String?,
val defectDescription3: String?,
val scrapQty: Int?,
val scrapUom: String?,
val byproductId: Long?,
@@ -166,4 +180,10 @@ data class ProductProcessInfoResponse(
data class UpdateProductProcessLineStatusRequest(
val productProcessLineId: Long,
val status: String
)
data class NewUpdateProductProcessLineOperatorIdOrEquipmentIdAndEquipmentDetailRequest(
val productProcessLineId: Long,
val EquipmentTypeSubTypeEquipmentNo: String,
val staffNo: String?,
val Name: String?,
)

+ 1
- 0
src/main/java/com/ffii/fpsms/modules/stock/service/SuggestedPickLotService.kt 查看文件

@@ -329,6 +329,7 @@ open class SuggestedPickLotService(
val stockOutLine = StockOutLine().apply {
this.stockOut = stockOut
this.pickOrderLine = pickOrderLine
this.item = item
this.inventoryLotLine = null // No lot available
this.qty = (suggestion.qty ?: BigDecimal.ZERO).toDouble()
this.status = StockOutLineStatus.PENDING.status


+ 2
- 2
src/main/java/com/ffii/fpsms/modules/user/entity/UserRepository.java 查看文件

@@ -3,15 +3,15 @@ package com.ffii.fpsms.modules.user.entity;
import java.util.List;
import java.util.Optional;

import com.ffii.fpsms.modules.user.entity.projections.UserCombo;
import org.springframework.data.repository.query.Param;

import com.ffii.core.support.AbstractRepository;
import com.ffii.fpsms.modules.user.entity.projections.UserCombo;

public interface UserRepository extends AbstractRepository<User, Long> {

List<User> findByName(@Param("name") String name);
Optional<User> findByUsernameAndDeletedFalse(String username);

List<UserCombo> findUserComboByTitleNotNullAndDepartmentNotNullAndNameNotNullAndDeletedFalse();


+ 6
- 0
src/main/resources/db/changelog/changes/20251125_01_enson/04_altertable_enson.sql 查看文件

@@ -0,0 +1,6 @@
-- liquibase formatted sql
-- changeset enson:altertable_enson

ALTER TABLE `fpsmsdb`.`productprocessline`
DROP COLUMN `defectRemark2`,
ADD COLUMN `defectDescription2` VARCHAR(255) AFTER `defectUom2`;

+ 18
- 0
src/main/resources/db/changelog/changes/20251126_01_enson/01_altertable_enson.sql 查看文件

@@ -0,0 +1,18 @@
-- liquibase formatted sql
-- changeset enson:altertable_enson
create table jobType (
`id` INT NOT NULL AUTO_INCREMENT,
`name` VARCHAR(255) NOT NULL,
`created` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
`createdBy` VARCHAR(30) NULL DEFAULT NULL,
`version` INT NOT NULL DEFAULT '0',
`modified` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
`modifiedBy` VARCHAR(30) NULL DEFAULT NULL,
`deleted` TINYINT(1) NOT NULL DEFAULT '0',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;


ALTER TABLE `fpsmsdb`.`user`
ADD COLUMN `staffNo` VARCHAR(255) After `lotusNotesUser`;


+ 8
- 0
src/main/resources/db/changelog/changes/20251126_01_enson/02_altertable_enson.sql 查看文件

@@ -0,0 +1,8 @@
-- liquibase formatted sql
-- changeset enson:altertable_enson



ALTER TABLE `fpsmsdb`.`job_order`
ADD COLUMN `jobTypeId` INT After `type`;


Loading…
取消
儲存