Ver código fonte

update

master
CANCERYS\kw093 1 dia atrás
pai
commit
a39af07790
6 arquivos alterados com 352 adições e 1 exclusões
  1. +11
    -0
      src/main/java/com/ffii/fpsms/modules/productProcess/entity/ProductProcessLineRepository.kt
  2. +3
    -0
      src/main/java/com/ffii/fpsms/modules/productProcess/entity/ProductProcessRepository.kt
  3. +267
    -0
      src/main/java/com/ffii/fpsms/modules/productProcess/service/ProductProcessService.kt
  4. +32
    -0
      src/main/java/com/ffii/fpsms/modules/productProcess/web/ProductProcessController.kt
  5. +7
    -0
      src/main/java/com/ffii/fpsms/modules/productProcess/web/model/SaveProductProcessRequest.kt
  6. +32
    -1
      src/main/java/com/ffii/fpsms/modules/stock/service/StockOutLineService.kt

+ 11
- 0
src/main/java/com/ffii/fpsms/modules/productProcess/entity/ProductProcessLineRepository.kt Ver arquivo

@@ -3,6 +3,7 @@ package com.ffii.fpsms.modules.productProcess.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
import java.time.LocalDateTime

@Repository
@@ -12,6 +13,16 @@ interface ProductProcessLineRepository : JpaRepository<ProductProcessLine, Long>
fun findByHandler_IdAndStartTimeIsNotNullAndEndTimeIsNull(handlerId: Long): List<ProductProcessLine>
fun findByProductProcess_IdIn(ids: List<Long>): List<ProductProcessLine>

@Query("""
SELECT l
FROM ProductProcessLine l
LEFT JOIN FETCH l.operator
LEFT JOIN FETCH l.equipment
WHERE l.deleted = false
AND l.productProcess.id IN :ids
""")
fun findByProductProcess_IdInWithOperatorAndEquipment(@Param("ids") ids: List<Long>): List<ProductProcessLine>

@Query("SELECT l FROM ProductProcessLine l LEFT JOIN FETCH l.equipment WHERE l.productProcess.id = :productProcessId")
fun findByProductProcess_IdWithEquipment(productProcessId: Long): List<ProductProcessLine>



+ 3
- 0
src/main/java/com/ffii/fpsms/modules/productProcess/entity/ProductProcessRepository.kt Ver arquivo

@@ -4,6 +4,7 @@ import org.springframework.data.jpa.repository.JpaRepository
import org.springframework.data.jpa.repository.JpaSpecificationExecutor
import org.springframework.stereotype.Repository
import java.util.*
import java.time.LocalDate

@Repository
interface ProductProcessRepository : JpaRepository<ProductProcess, Long>, JpaSpecificationExecutor<ProductProcess> {
@@ -14,4 +15,6 @@ interface ProductProcessRepository : JpaRepository<ProductProcess, Long>, JpaSpe
fun findByIdAndDeletedIsFalse(id: Long): Optional<ProductProcess>
fun findAllByDeletedIsFalse(): List<ProductProcess>
fun findByBom_Id(bomId: Long): List<ProductProcess>
fun findAllByDeletedIsFalseAndDate(date: LocalDate): List<ProductProcess>
fun findByJobOrder_IdInAndDeletedIsFalse(jobOrderIds: List<Long>): List<ProductProcess>
}

+ 267
- 0
src/main/java/com/ffii/fpsms/modules/productProcess/service/ProductProcessService.kt Ver arquivo

@@ -7,6 +7,7 @@ import com.ffii.fpsms.modules.productProcess.enums.ProductProcessStatus
import com.ffii.fpsms.modules.productProcess.web.model.*
import org.springframework.data.domain.Page
import org.springframework.data.domain.Pageable
import org.springframework.data.domain.PageRequest
import com.ffii.fpsms.modules.master.entity.EquipmentDetailRepository
import com.ffii.fpsms.modules.productProcess.entity.projections.jobOrderLineInfo
import org.springframework.stereotype.Service
@@ -1492,6 +1493,272 @@ open class ProductProcessService(
)
}

/**
* QC 列表:按 jobOrder 粒度分页,qcReady 由该 jobOrder 下所有 productProcessLine 是否全部 Completed/Pass 决定。
*
* 注意:date/itemCode/jobOrderCode/bomIds 用来筛选“候选 jobOrder”,但 qcReady 判断会用该 jobOrder 下全部 productProcessLine。
*/
open fun searchJoborderProductProcessesPaged(
date: LocalDate?,
itemCode: String?,
jobOrderCode: String?,
bomIds: List<Long>?,
qcReady: Boolean?,
isDrink: Boolean?,
page: Int,
size: Int
): JobOrderProductProcessPageResponse {
val safeSize = if (size <= 0) 50 else size
val safePage = if (page < 0) 0 else page

val trimmedItemCode = itemCode?.trim()?.takeIf { it.isNotBlank() }
val trimmedJobOrderCode = jobOrderCode?.trim()?.takeIf { it.isNotBlank() }

// 1) 找出候选 jobOrder:用 date/itemCode/jobOrderCode/bomIds/isDrink 先把数据缩小到今天等范围
val candidateProcesses = if (date != null) {
productProcessRepository.findAllByDeletedIsFalseAndDate(date)
} else {
productProcessRepository.findAllByDeletedIsFalse()
}

val filteredCandidateProcesses = candidateProcesses.filter { p ->
if (isDrink != null) {
val bomIsDrink = p.bom?.isDrink
if (bomIsDrink != isDrink) return@filter false
}
if (trimmedItemCode != null) {
val code = p.item?.code
if (code == null || !code.contains(trimmedItemCode, ignoreCase = true)) return@filter false
}
if (trimmedJobOrderCode != null) {
val code = p.jobOrder?.code
if (code == null || !code.contains(trimmedJobOrderCode, ignoreCase = true)) return@filter false
}
if (bomIds != null && bomIds.isNotEmpty()) {
val bid = p.bom?.id
if (bid == null || !bomIds.contains(bid)) return@filter false
}
true
}

val candidateJobOrderIds = filteredCandidateProcesses
.mapNotNull { it.jobOrder?.id }
.distinct()

if (candidateJobOrderIds.isEmpty()) {
return JobOrderProductProcessPageResponse(
content = emptyList(),
totalJobOrders = 0L,
page = safePage,
size = safeSize
)
}

// 2) 把候选 jobOrder 下全部 productProcess / lines 取出来用于 qcReady 判断
val allCandidateProcesses = productProcessRepository.findByJobOrder_IdInAndDeletedIsFalse(candidateJobOrderIds)
val allCandidateProcessIds = allCandidateProcesses.mapNotNull { it.id }

val lines = if (allCandidateProcessIds.isNotEmpty()) {
productProcessLineRepository.findByProductProcess_IdInWithOperatorAndEquipment(allCandidateProcessIds)
} else {
emptyList()
}

val linesByProcessId = lines.groupBy { it.productProcess.id ?: 0L }
val processesByJobOrderId = allCandidateProcesses
.filter { it.jobOrder?.id != null }
.groupBy { it.jobOrder!!.id!! }

val jobOrders = jobOrderRepository.findAllById(candidateJobOrderIds)
val jobOrderById = jobOrders.associateBy { it.id }

val stockInLineByJobOrderId = candidateJobOrderIds.associateWith { jobOrderId ->
stockInLineRepository.findFirstByJobOrder_IdAndDeletedFalse(jobOrderId)
}

// 3) 计算每个 jobOrder 的 qcReady,并做排序/分页
data class JobOrderQcKey(
val jobOrderId: Long,
val maxDate: LocalDate,
val minPriority: Int,
val ready: Boolean
)

val qcKeys = candidateJobOrderIds.mapNotNull { jobOrderId ->
val jobOrder = jobOrderById[jobOrderId] ?: return@mapNotNull null
if (jobOrder.status == JobOrderStatus.PLANNING) return@mapNotNull null

val processes = processesByJobOrderId[jobOrderId].orEmpty()
if (processes.isEmpty()) return@mapNotNull null

val jobOrderLines = processes.flatMap { p ->
linesByProcessId[p.id ?: 0L].orEmpty()
}

val allLinesDone = jobOrderLines.isNotEmpty() &&
jobOrderLines.all { it.status == "Completed" || it.status == "Pass" }

val stockInLine = stockInLineByJobOrderId[jobOrderId]
val stockStatus = stockInLine?.status
val stockInEligibleForQc = stockStatus != "completed" && stockStatus != "rejected"

// 列表只排除 completed/rejected(和 planning),null stockInLine 也会显示在 tab0
val includedInList = stockInEligibleForQc
val ready = includedInList && allLinesDone && stockInLine != null

if (!includedInList) {
return@mapNotNull null
}

val maxDate = processes.mapNotNull { it.date }.maxOrNull() ?: LocalDate.MIN
val minPriority = processes.mapNotNull { it.productionPriority }.minOrNull() ?: Int.MAX_VALUE

JobOrderQcKey(
jobOrderId = jobOrderId,
maxDate = maxDate,
minPriority = minPriority,
ready = ready
)
}

val filteredQcKeys = qcKeys.filter { key -> qcReady == null || key.ready == qcReady }
val totalJobOrders = filteredQcKeys.size.toLong()

val sortedQcKeys = filteredQcKeys.sortedWith(
compareByDescending<JobOrderQcKey> { it.maxDate }.thenBy { it.minPriority }
)

val from = safePage * safeSize
if (from >= sortedQcKeys.size) {
return JobOrderProductProcessPageResponse(emptyList(), totalJobOrders, safePage, safeSize)
}

val to = minOf(from + safeSize, sortedQcKeys.size)
val pagedJobOrderIds = sortedQcKeys.subList(from, to).map { it.jobOrderId }
val pagedJobOrderIdSet = pagedJobOrderIds.toSet()

// 4) 构造 content:返回 paged jobOrders 下的全部 productProcess
val pickOrdersByJobOrderId = pagedJobOrderIds.associateWith { joId ->
pickOrderRepository.findAllByJobOrder_Id(joId).firstOrNull()
}
val joPickOrdersByPickOrderId = pickOrdersByJobOrderId.mapNotNull { (joId, pick) ->
val pickId = pick?.id ?: return@mapNotNull null
joId to pickId
}.map { it.second }.distinct().associateWith { pickOrderId ->
joPickOrderRepository.findByPickOrderId(pickOrderId)
}

val timeNeedToCompleteByBomId = pagedJobOrderIds.flatMap { _ ->
// placeholder, real cache built below
emptyList<Long>()
}.toSet()

// 缓存:按 bomId 计算 timeNeedToComplete
val bomIdsInScope = allCandidateProcesses
.filter { it.jobOrder?.id != null && pagedJobOrderIdSet.contains(it.jobOrder!!.id!!) }
.mapNotNull { it.bom?.id }
.distinct()

val timeNeedToCompleteCache = bomIdsInScope.associateWith { bomId ->
bomProcessRepository.findByBomId(bomId)
.filter { !it.deleted }
.sumOf { it.durationInMinute ?: 0 }
}

// 缓存:Uom(itemUom -> uomConversion.udfudesc)
val itemIdsInScope = allCandidateProcesses
.filter { it.jobOrder?.id != null && pagedJobOrderIdSet.contains(it.jobOrder!!.id!!) }
.mapNotNull { it.item?.id }
.distinct()

val uomByItemId = itemIdsInScope.associateWith { itemId ->
val itemUom = itemUomRepository.findByItemIdAndStockUnitIsTrueAndDeletedIsFalse(itemId)
val bomUom = uomConversionRepository.findById(itemUom?.uom?.id ?: 0L).orElse(null)
bomUom?.udfudesc
}

val contentProcesses = allCandidateProcesses
.filter { p -> p.jobOrder?.id != null && pagedJobOrderIdSet.contains(p.jobOrder!!.id!!) }

val sortedContentProcesses = contentProcesses.sortedWith(
compareByDescending<ProductProcess> { it.date ?: LocalDate.MIN }
.thenBy { it.productionPriority ?: Int.MAX_VALUE }
)

val content = sortedContentProcesses.map { productProcess ->
val jobOrderId = productProcess.jobOrder?.id ?: 0L
val jobOrder = jobOrderById[jobOrderId]
val stockInLine = stockInLineByJobOrderId[jobOrderId]
val pickOrder = pickOrdersByJobOrderId[jobOrderId]
val pickOrderId = pickOrder?.id
val joPickOrdersList = if (pickOrderId != null) joPickOrdersByPickOrderId[pickOrderId].orEmpty() else emptyList()

val productProcessLines = linesByProcessId[productProcess.id ?: 0L].orEmpty()
val finishedCount = productProcessLines.count { it.status == "Completed" }

val bomIsDrink = productProcess.bom?.isDrink
val matchStatus = if (joPickOrdersList.isNotEmpty() &&
joPickOrdersList.all { it.matchStatus == JoPickOrderStatus.completed }
) {
"completed"
} else if (joPickOrdersList.any { it.matchStatus == JoPickOrderStatus.scanned }) {
"scanned"
} else {
"pending"
}

val bomId = productProcess.bom?.id
val timeNeedToComplete = bomId?.let { timeNeedToCompleteCache[it] } ?: 0
val uom = productProcess.item?.id?.let { uomByItemId[it] }

AllJoborderProductProcessInfoResponse(
id = productProcess.id ?: 0L,
productProcessCode = productProcess.productProcessCode,
status = productProcess.status.value,
startTime = productProcess.startTime,
endTime = productProcess.endTime,
isDrink = bomIsDrink,
matchStatus = matchStatus,
RequiredQty = jobOrder?.reqQty?.toInt() ?: 0,
Uom = uom,
lotNo = stockInLine?.lotNo,
productionPriority = productProcess.productionPriority,
date = productProcess.date,
bomId = bomId,
TimeNeedToComplete = timeNeedToComplete,
assignedTo = pickOrder?.assignTo?.id,
itemName = productProcess.item?.name,
itemCode = productProcess.item?.code,
pickOrderId = pickOrder?.id,
pickOrderStatus = pickOrder?.status?.value,
jobOrderId = productProcess.jobOrder?.id,
stockInLineId = stockInLine?.id,
jobOrderCode = jobOrder?.code,
productProcessLineCount = productProcessLines.size,
FinishedProductProcessLineCount = finishedCount,
lines = productProcessLines.map { line ->
ProductProcessInfoResponse(
id = line.id ?: 0,
operatorId = line.operator?.id,
operatorName = line.operator?.name ?: "",
equipmentId = line.equipment?.id,
equipmentName = line.equipmentType ?: "",
startTime = line.startTime,
endTime = line.endTime,
status = line.status ?: ""
)
}
)
}

return JobOrderProductProcessPageResponse(
content = content,
totalJobOrders = totalJobOrders,
page = safePage,
size = safeSize
)
}

open fun updateProductProcessLineStartTime(productProcessLineId: Long): MessageResponse {
val productProcessLine = productProcessLineRepository.findById(productProcessLineId).orElse(null)
productProcessLine.startTime = LocalDateTime.now()


+ 32
- 0
src/main/java/com/ffii/fpsms/modules/productProcess/web/ProductProcessController.kt Ver arquivo

@@ -198,6 +198,38 @@ class ProductProcessController(
): List<AllJoborderProductProcessInfoResponse> {
return productProcessService.getAllJoborderProductProcessInfo(isDrink)
}

@GetMapping("/Demo/Process/search")
fun demoprocesssearch(
@RequestParam(required = false) date: String?,
@RequestParam(required = false) itemCode: String?,
@RequestParam(required = false) jobOrderCode: String?,
@RequestParam(required = false) bomIds: String?,
@RequestParam(required = false) qcReady: Boolean?,
@RequestParam(required = false) isDrink: Boolean?,
@RequestParam(defaultValue = "0") page: Int,
@RequestParam(defaultValue = "50") size: Int
): JobOrderProductProcessPageResponse {
val parsedDate = date?.takeIf { it.isNotBlank() }?.let {
LocalDate.parse(it, DateTimeFormatter.ISO_DATE)
}

val parsedBomIds: List<Long>? = bomIds
?.takeIf { it.isNotBlank() }
?.split(",")
?.mapNotNull { token -> token.trim().toLongOrNull() }

return productProcessService.searchJoborderProductProcessesPaged(
date = parsedDate,
itemCode = itemCode,
jobOrderCode = jobOrderCode,
bomIds = parsedBomIds,
qcReady = qcReady,
isDrink = isDrink,
page = page,
size = size
)
}
@PostMapping("/Demo/ProcessLine/start/{lineId}")
fun startProductProcessLine(@PathVariable lineId: Long): MessageResponse {
return productProcessService.StartProductProcessLine(lineId)


+ 7
- 0
src/main/java/com/ffii/fpsms/modules/productProcess/web/model/SaveProductProcessRequest.kt Ver arquivo

@@ -185,6 +185,13 @@ data class AllJoborderProductProcessInfoResponse(
val isDrink: Boolean?,
val lines: List<ProductProcessInfoResponse>
)

data class JobOrderProductProcessPageResponse(
val content: List<AllJoborderProductProcessInfoResponse>,
val totalJobOrders: Long,
val page: Int,
val size: Int
)
data class ProductProcessInfoResponse(
val id: Long,
val operatorId: Long?,


+ 32
- 1
src/main/java/com/ffii/fpsms/modules/stock/service/StockOutLineService.kt Ver arquivo

@@ -630,7 +630,7 @@ private fun getStockOutIdFromPickOrderLine(pickOrderLineId: Long): Long {
val stockOutLine = stockOutLineRepository.findById(request.id).orElseThrow {
IllegalArgumentException("StockOutLine not found with ID: ${request.id}")
}
println("Updating StockOutLine ID: ${request.id}")
println("Current status: ${stockOutLine.status}")
println("New status: ${request.status}")
@@ -652,6 +652,37 @@ private fun getStockOutIdFromPickOrderLine(pickOrderLineId: Long): Long {
stockOutLine.qty = (newQty)
}
val savedStockOutLine = stockOutLineRepository.saveAndFlush(stockOutLine)

val statusLower = request.status.trim().lowercase()
val isPickEnd = statusLower == "completed" || statusLower == "partially_completed"
val deltaQty = request.qty

// non jo/do: 手工拣货增量扣减时,補寫出库 ledger(幂等 + 跳过 jo/do,避免 double write)
if (isPickEnd && deltaQty != null && deltaQty > 0 && savedStockOutLine.id != null) {
val itemId = savedStockOutLine.item?.id
val po = savedStockOutLine.pickOrderLine?.pickOrder

if (itemId != null && po != null) {
val isJoOrDo = (po.jobOrder != null || po.deliveryOrder != null)
if (!isJoOrDo) {
val exists = jdbcDao.queryForInt(
"SELECT COUNT(*) FROM stock_ledger WHERE deleted = false AND stockOutLineId = :id",
mapOf("id" to savedStockOutLine.id)
) > 0

if (!exists) {
val invBefore = inventoryRepository.findByItemId(itemId).orElse(null)
val onHandBefore = invBefore?.onHandQty?.toDouble() ?: 0.0

createStockLedgerForPickDelta(
stockOutLineId = savedStockOutLine.id!!,
deltaQty = BigDecimal(deltaQty.toString()),
onHandQtyBeforeUpdate = onHandBefore
)
}
}
}
}
println("Updated StockOutLine: ${savedStockOutLine.id} with status: ${savedStockOutLine.status}")
// If this stock out line is in end status, try completing its pick order line
if (isEndStatus(savedStockOutLine.status)) {


Carregando…
Cancelar
Salvar