Pārlūkot izejas kodu

added red spot for stock in po

master
Fai Luk pirms 2 stundām
vecāks
revīzija
0a5c5f0fb5
10 mainītis faili ar 287 papildinājumiem un 1 dzēšanām
  1. +9
    -0
      src/main/java/com/ffii/fpsms/config/security/SecurityConfig.java
  2. +83
    -0
      src/main/java/com/ffii/fpsms/modules/productProcess/service/ProductProcessService.kt
  3. +6
    -0
      src/main/java/com/ffii/fpsms/modules/productProcess/web/ProductProcessController.kt
  4. +21
    -0
      src/main/java/com/ffii/fpsms/modules/productProcess/web/model/SaveProductProcessRequest.kt
  5. +2
    -1
      src/main/java/com/ffii/fpsms/modules/purchaseOrder/web/PurchaseOrderController.kt
  6. +36
    -0
      src/main/java/com/ffii/fpsms/modules/stock/entity/StockInLineRepository.kt
  7. +86
    -0
      src/main/java/com/ffii/fpsms/modules/stock/service/StockInLineService.kt
  8. +20
    -0
      src/main/java/com/ffii/fpsms/modules/stock/web/StockInLineController.kt
  9. +19
    -0
      src/main/java/com/ffii/fpsms/modules/stock/web/model/PurchaseStockInAlertRow.kt
  10. +5
    -0
      src/main/resources/application.yml

+ 9
- 0
src/main/java/com/ffii/fpsms/config/security/SecurityConfig.java Parādīt failu

@@ -23,6 +23,8 @@ import org.springframework.security.web.authentication.UsernamePasswordAuthentic

import com.ffii.fpsms.config.security.jwt.JwtRequestFilter;

import org.springframework.http.HttpMethod;

import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
@@ -82,6 +84,13 @@ public class SecurityConfig {
authRequest -> authRequest
.requestMatchers(URL_WHITELIST).permitAll()
.requestMatchers(org.springframework.http.HttpMethod.OPTIONS, "/**").permitAll()
/* PO stock-in nav alerts: TESTING / ADMIN / STOCK (no @PreAuthorize on Kotlin controllers). */
.requestMatchers(HttpMethod.GET, "/stockInLine/alerts/purchase-incomplete-count")
.hasAnyAuthority("TESTING", "ADMIN", "STOCK")
.requestMatchers(HttpMethod.GET, "/stockInLine/alerts/purchase-incomplete")
.hasAnyAuthority("TESTING", "ADMIN", "STOCK")
.requestMatchers(HttpMethod.GET, "/product-process/Demo/Process/alerts/fg-qc-putaway")
.hasAuthority("TESTING")
.anyRequest().authenticated())
.httpBasic(httpBasic -> httpBasic.authenticationEntryPoint(
(request, response, authException) -> sendUnauthorizedJson(response, "Unauthorized", "UNAUTHORIZED")))


+ 83
- 0
src/main/java/com/ffii/fpsms/modules/productProcess/service/ProductProcessService.kt Parādīt failu

@@ -1760,6 +1760,89 @@ open class ProductProcessService(
)
}

/**
* FG QC / 上架 reminders: same eligibility as 完成QC工單 (qcReady): all lines Completed/Pass, stock-in exists and not completed/rejected;
* only job orders that have a product process dated **today or yesterday** (server local date).
*/
open fun findJobOrderFgQcAndPutAwayAlertsForTodayYesterday(): JobOrderFgAlertsResponse {
val today = LocalDate.now()
val yesterday = today.minusDays(1)

val processesToday = productProcessRepository.findAllByDeletedIsFalseAndDate(today)
val processesYesterday = productProcessRepository.findAllByDeletedIsFalseAndDate(yesterday)
val candidateJobOrderIds = (processesToday + processesYesterday)
.asSequence()
.filter { it.jobOrder?.isHidden != true }
.mapNotNull { it.jobOrder?.id }
.distinct()
.toList()

if (candidateJobOrderIds.isEmpty()) {
return JobOrderFgAlertsResponse(qc = emptyList(), putAway = emptyList())
}

val allProcesses = productProcessRepository.findByJobOrder_IdInAndDeletedIsFalse(candidateJobOrderIds)
val allProcessIds = allProcesses.mapNotNull { it.id }
val lines = if (allProcessIds.isNotEmpty()) {
productProcessLineRepository.findByProductProcess_IdInWithOperatorAndEquipment(allProcessIds)
} else {
emptyList()
}

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

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

val qcRows = mutableListOf<JobOrderFgAlertRowResponse>()
val putAwayRows = mutableListOf<JobOrderFgAlertRowResponse>()

for (jobOrderId in candidateJobOrderIds) {
val jobOrder = jobOrderById[jobOrderId] ?: continue
if (jobOrder.status == JobOrderStatus.PLANNING) continue

val stockInLine = stockInLineRepository.findFirstByJobOrder_IdAndDeletedFalse(jobOrderId) ?: continue
if (stockInLine.purchaseOrderLine != null) continue

val statusNorm = (stockInLine.status ?: "").trim().lowercase()
if (statusNorm == "completed" || statusNorm == "rejected") continue

val processes = processesByJobOrderId[jobOrderId].orEmpty()
if (processes.isEmpty()) continue

val jobOrderLines = processes.flatMap { p -> linesByProcessId[p.id ?: 0L].orEmpty() }
val allLinesDone = jobOrderLines.isNotEmpty() &&
jobOrderLines.all { it.status == "Completed" || it.status == "Pass" }
if (!allLinesDone) continue

val maxDate = processes.mapNotNull { it.date }.maxOrNull()
val row = JobOrderFgAlertRowResponse(
stockInLineId = stockInLine.id!!,
jobOrderId = jobOrderId,
jobOrderCode = jobOrder.code,
itemNo = stockInLine.itemNo,
itemName = stockInLine.item?.name,
status = stockInLine.status,
processDate = maxDate,
lotNo = stockInLine.lotNo,
)

when (statusNorm) {
"received", "partially_completed" -> putAwayRows.add(row)
else -> qcRows.add(row)
}
}

val sortByDate = compareByDescending<JobOrderFgAlertRowResponse> { it.processDate ?: LocalDate.MIN }
return JobOrderFgAlertsResponse(
qc = qcRows.sortedWith(sortByDate),
putAway = putAwayRows.sortedWith(sortByDate),
)
}

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


+ 6
- 0
src/main/java/com/ffii/fpsms/modules/productProcess/web/ProductProcessController.kt Parādīt failu

@@ -230,6 +230,12 @@ class ProductProcessController(
size = size
)
}

/** Nav: QC vs 上架 reminders (今日/昨日產程、等同完成QC工單列表資格). */
@GetMapping("/Demo/Process/alerts/fg-qc-putaway")
fun getFgQcPutAwayAlerts(): JobOrderFgAlertsResponse =
productProcessService.findJobOrderFgQcAndPutAwayAlertsForTodayYesterday()

@PostMapping("/Demo/ProcessLine/start/{lineId}")
fun startProductProcessLine(@PathVariable lineId: Long): MessageResponse {
return productProcessService.StartProductProcessLine(lineId)


+ 21
- 0
src/main/java/com/ffii/fpsms/modules/productProcess/web/model/SaveProductProcessRequest.kt Parādīt failu

@@ -192,6 +192,27 @@ data class JobOrderProductProcessPageResponse(
val page: Int,
val size: Int
)

/**
* Nav alerts aligned with 完成QC工單 list: all product process lines Completed/Pass, stock-in not completed/rejected;
* job order has at least one process dated today or yesterday. [qc] = before received; [putAway] = received / partially_completed.
*/
data class JobOrderFgAlertRowResponse(
val stockInLineId: Long,
val jobOrderId: Long,
val jobOrderCode: String?,
val itemNo: String?,
val itemName: String?,
val status: String?,
val processDate: LocalDate?,
val lotNo: String?,
)

data class JobOrderFgAlertsResponse(
val qc: List<JobOrderFgAlertRowResponse>,
val putAway: List<JobOrderFgAlertRowResponse>,
)

data class ProductProcessInfoResponse(
val id: Long,
val operatorId: Long?,


+ 2
- 1
src/main/java/com/ffii/fpsms/modules/purchaseOrder/web/PurchaseOrderController.kt Parādīt failu

@@ -57,7 +57,8 @@ class PurchaseOrderController(

return RecordsRes(paginatedList, fullList.size)
}
@GetMapping("/po/summary")
/** Class mapping is `/po`; path must be `/summary` → full path `/api/po/summary` (not `/po/po/summary`). */
@GetMapping("/summary")
fun getPoSummaries(
@RequestParam ids: List<Long>
): List<PurchaseOrderSummary> {


+ 36
- 0
src/main/java/com/ffii/fpsms/modules/stock/entity/StockInLineRepository.kt Parādīt failu

@@ -3,10 +3,13 @@ package com.ffii.fpsms.modules.stock.entity
import com.ffii.core.support.AbstractRepository
import com.ffii.fpsms.modules.stock.entity.projection.QrCodeInfo
import com.ffii.fpsms.modules.stock.entity.projection.StockInLineInfo
import com.ffii.fpsms.modules.stock.web.model.PurchaseStockInAlertRow
import org.springframework.data.domain.Pageable
import org.springframework.data.jpa.repository.Query
import org.springframework.stereotype.Repository
import java.util.Optional
import java.time.LocalDate
import java.time.LocalDateTime
import org.springframework.data.repository.query.Param


@@ -118,4 +121,37 @@ fun findFirstByJobOrder_IdAndDeletedFalse(jobOrderId: Long): StockInLine?
and sil.lotNo like concat(:prefix, '%')
""")
fun findLatestLotNoByPrefix(@Param("prefix") prefix: String): String?

/**
* PO stock-in lines in pending / receiving, created on or after [since] (recent backlog).
* ([StockInLineService] filters out lines whose POL already has put-away >= order qty in stock units.)
*/
@Query(
"""
SELECT new com.ffii.fpsms.modules.stock.web.model.PurchaseStockInAlertRow(
sil.id,
po.id,
pol.id,
po.code,
sil.itemNo,
item.name,
sil.status,
sil.created,
sil.receiptDate,
sil.lotNo
)
FROM StockInLine sil
JOIN sil.purchaseOrderLine pol
JOIN sil.purchaseOrder po
JOIN sil.item item
WHERE sil.deleted = false
AND LOWER(COALESCE(sil.status, '')) IN ('pending', 'receiving')
AND sil.created >= :since
ORDER BY sil.created DESC
"""
)
fun listPurchaseStockInAlertsSince(
@Param("since") since: LocalDateTime,
pageable: Pageable,
): List<PurchaseStockInAlertRow>
}

+ 86
- 0
src/main/java/com/ffii/fpsms/modules/stock/service/StockInLineService.kt Parādīt failu

@@ -54,6 +54,7 @@ import com.ffii.fpsms.modules.deliveryOrder.web.models.PrintQrCodeForDoRequest
import com.ffii.fpsms.modules.stock.service.InventoryLotLineService
import com.ffii.fpsms.modules.stock.entity.StockLedgerRepository
import com.ffii.fpsms.modules.stock.entity.InventoryRepository
import org.springframework.data.domain.PageRequest
import org.springframework.http.HttpStatus
import org.springframework.web.server.ResponseStatusException
import kotlin.text.toDouble
@@ -105,6 +106,8 @@ open class StockInLineService(
@Lazy private val m18PurchaseOrderService: M18PurchaseOrderService,
/** When false (default), M18 GRN create API is not called — set true in production only. */
@Value("\${scheduler.m18Grn.createEnabled:false}") private val m18GrnCreateEnabled: Boolean,
/** Recent window for PO stock-in alerts (pending / receiving) in nav / list UI. */
@Value("\${fpsms.purchase-stock-in-alert.lookback-days:7}") private val purchaseStockInAlertLookbackDays: Int,
) : AbstractBaseEntityService<StockInLine, Long, StockInLineRepository>(jdbcDao, stockInLineRepository) {

private val logger = LoggerFactory.getLogger(StockInLineService::class.java)
@@ -118,6 +121,89 @@ open class StockInLineService(
open fun getReceivedStockInLineInfo(stockInLineId: Long): StockInLineInfo {
return stockInLineRepository.findStockInLineInfoByIdAndStatusAndDeletedFalse(id = stockInLineId, status = StockInLineStatus.RECEIVED.status).orElseThrow()
}

open fun purchaseStockInAlertSinceDays(overrideDays: Int?): LocalDateTime {
val d = (overrideDays ?: purchaseStockInAlertLookbackDays).coerceIn(1, 90)
return LocalDateTime.now().minusDays(d.toLong())
}

/**
* When total put-away (inventory lot line inQty, stock unit) for the POL is already at or above
* order demand in stock units, do not count / list the line for PO stock-in reminders.
*/
@Transactional(readOnly = true)
open fun countPurchaseStockInAlertsSince(since: LocalDateTime): Long {
val suppressCache = mutableMapOf<Long, Boolean>()
var total = 0L
var page = 0
val pageSize = 100
while (true) {
val batch = stockInLineRepository.listPurchaseStockInAlertsSince(since, PageRequest.of(page, pageSize))
if (batch.isEmpty()) break
total += batch.count { !polPutAwaySuppressesPoStockInAlert(it.purchaseOrderLineId, suppressCache) }
page++
if (batch.size < pageSize) break
}
return total
}

@Transactional(readOnly = true)
open fun listPurchaseStockInAlerts(since: LocalDateTime, limit: Int): List<PurchaseStockInAlertRow> {
val cap = limit.coerceIn(1, 200)
val out = mutableListOf<PurchaseStockInAlertRow>()
val suppressCache = mutableMapOf<Long, Boolean>()
var page = 0
val pageSize = 50
while (out.size < cap) {
val batch = stockInLineRepository.listPurchaseStockInAlertsSince(since, PageRequest.of(page, pageSize))
if (batch.isEmpty()) break
for (row in batch) {
if (!polPutAwaySuppressesPoStockInAlert(row.purchaseOrderLineId, suppressCache)) {
out.add(row)
}
if (out.size >= cap) break
}
page++
if (batch.size < pageSize) break
}
return out
}

private fun polPutAwaySuppressesPoStockInAlert(polId: Long, cache: MutableMap<Long, Boolean>): Boolean =
cache.getOrPut(polId) {
val pol = polRepository.findById(polId).orElse(null) ?: return@getOrPut false
val orderStock = polOrderStockQtyForAlert(pol)
if (orderStock <= BigDecimal.ZERO) return@getOrPut false
val putAway = totalPutAwayStockQtyForPol(polId)
putAway.compareTo(orderStock) >= 0
}

private fun polOrderStockQtyForAlert(pol: PurchaseOrderLine): BigDecimal {
val itemId = pol.item?.id ?: return BigDecimal.ZERO
val qtyM18 = pol.qtyM18
val uomM18Id = pol.uomM18?.id
val stockQtyFromM18 =
if (qtyM18 != null && uomM18Id != null) {
itemUomService.convertQtyToStockQtyPrecise(itemId, uomM18Id, qtyM18)
} else {
BigDecimal.ZERO
}
return if (stockQtyFromM18 > BigDecimal.ZERO) {
stockQtyFromM18
} else {
itemUomService.convertPurchaseQtyToStockQtyPrecise(itemId, pol.qty ?: BigDecimal.ZERO)
}
}

private fun totalPutAwayStockQtyForPol(polId: Long): BigDecimal {
return stockInLineRepository.findStockInLineInfoByPurchaseOrderLineIdAndDeletedFalse(polId)
.asSequence()
.flatMap { it.putAwayLines?.asSequence() ?: emptySequence() }
.fold(BigDecimal.ZERO) { acc, line ->
acc + (line.stockQty ?: BigDecimal.ZERO).coerceAtLeast(BigDecimal.ZERO)
}
}

/*
open fun assignLotNo(): String {
val prefix = "LT"


+ 20
- 0
src/main/java/com/ffii/fpsms/modules/stock/web/StockInLineController.kt Parādīt failu

@@ -6,6 +6,7 @@ import com.ffii.fpsms.modules.stock.entity.StockInLine
import com.ffii.fpsms.modules.stock.entity.projection.StockInLineInfo
import com.ffii.fpsms.modules.stock.service.StockInLineService
import com.ffii.fpsms.modules.stock.web.model.ExportQrCodeRequest
import com.ffii.fpsms.modules.stock.web.model.PurchaseStockInAlertRow
import com.ffii.fpsms.modules.stock.web.model.PrintQrCodeForSilRequest
import com.ffii.fpsms.modules.stock.web.model.SaveStockInLineRequest
import jakarta.servlet.http.HttpServletResponse
@@ -25,6 +26,25 @@ import java.text.ParseException
class StockInLineController(
private val stockInLineService: StockInLineService
) {
/** Count of recent PO stock-in lines in pending / receiving (same filter as [listPurchaseStockInAlerts]). */
@GetMapping("/alerts/purchase-incomplete-count")
fun getPurchaseStockInIncompleteCount(
@RequestParam(name = "days", required = false) days: Int?,
): Map<String, Long> {
val since = stockInLineService.purchaseStockInAlertSinceDays(days)
return mapOf("count" to stockInLineService.countPurchaseStockInAlertsSince(since))
}

/** Actionable list: recent PO stock-in lines in pending / receiving (deep-link to PO edit). */
@GetMapping("/alerts/purchase-incomplete")
fun listPurchaseStockInAlerts(
@RequestParam(name = "days", required = false) days: Int?,
@RequestParam(name = "limit", defaultValue = "50") limit: Int,
): List<PurchaseStockInAlertRow> {
val since = stockInLineService.purchaseStockInAlertSinceDays(days)
return stockInLineService.listPurchaseStockInAlerts(since, limit)
}

@GetMapping("/{stockInLineId}")
fun get(@PathVariable stockInLineId: Long): StockInLineInfo {
return stockInLineService.getStockInLineInfo(stockInLineId)


+ 19
- 0
src/main/java/com/ffii/fpsms/modules/stock/web/model/PurchaseStockInAlertRow.kt Parādīt failu

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

import java.time.LocalDateTime

/**
* PO-linked stock-in line needing user action (pending / receiving), for alert list UI.
*/
data class PurchaseStockInAlertRow(
val stockInLineId: Long,
val purchaseOrderId: Long,
val purchaseOrderLineId: Long,
val poCode: String?,
val itemNo: String?,
val itemName: String?,
val status: String?,
val lineCreated: LocalDateTime?,
val receiptDate: LocalDateTime?,
val lotNo: String?,
)

+ 5
- 0
src/main/resources/application.yml Parādīt failu

@@ -25,6 +25,11 @@ scheduler:
inventoryLotExpiry:
enabled: true

# Nav: PO stock_in_line pending/receiving within last N days (see ProductProcessService for 工單 QC/上架:今日+昨日).
fpsms:
purchase-stock-in-alert:
lookback-days: 7

spring:
servlet:
multipart:


Notiek ielāde…
Atcelt
Saglabāt