Bladeren bron

replenishment update

production
kelvin.yau 1 week geleden
bovenliggende
commit
b705a8acb6
7 gewijzigde bestanden met toevoegingen van 161 en 1 verwijderingen
  1. +4
    -0
      src/main/java/com/ffii/fpsms/modules/deliveryOrder/entity/DoReplenishment.kt
  2. +12
    -1
      src/main/java/com/ffii/fpsms/modules/deliveryOrder/entity/DoReplenishmentRepository.kt
  3. +11
    -0
      src/main/java/com/ffii/fpsms/modules/deliveryOrder/service/DeliveryOrderService.kt
  4. +110
    -0
      src/main/java/com/ffii/fpsms/modules/deliveryOrder/service/DoReplenishmentService.kt
  5. +2
    -0
      src/main/java/com/ffii/fpsms/modules/deliveryOrder/web/models/DoDetailResponse.kt
  6. +5
    -0
      src/main/java/com/ffii/fpsms/modules/deliveryOrder/web/models/DoReplenishmentModels.kt
  7. +17
    -0
      src/main/resources/db/changelog/changes/20260611_KelvinY/02_add_do_replenishment_reason.sql

+ 4
- 0
src/main/java/com/ffii/fpsms/modules/deliveryOrder/entity/DoReplenishment.kt Bestand weergeven

@@ -94,6 +94,10 @@ open class DoReplenishment : BaseEntity<Long>() {
@Column(name = "status", nullable = false, length = 20)
open var status: String = STATUS_PENDING

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

companion object {
const val STATUS_PENDING = "pending"
const val STATUS_PROCESSING = "processing"


+ 12
- 1
src/main/java/com/ffii/fpsms/modules/deliveryOrder/entity/DoReplenishmentRepository.kt Bestand weergeven

@@ -28,8 +28,19 @@ interface DoReplenishmentRepository : AbstractRepository<DoReplenishment, Long>
@Query(
"""
SELECT r FROM DoReplenishment r
LEFT JOIN DeliveryOrder targetDo ON targetDo.id = r.targetDoId AND targetDo.deleted = false
WHERE r.deleted = false
AND (:deliveryDate IS NULL OR r.deliveryDate = :deliveryDate)
AND (
:deliveryDate IS NULL
OR (
targetDo.id IS NOT NULL
AND DATE(targetDo.estimatedArrivalDate) = :deliveryDate
)
OR (
targetDo.id IS NULL
AND r.deliveryDate = :deliveryDate
)
)
AND (:status IS NULL OR r.status = :status)
ORDER BY r.created DESC, r.id DESC
""",


+ 11
- 0
src/main/java/com/ffii/fpsms/modules/deliveryOrder/service/DeliveryOrderService.kt Bestand weergeven

@@ -591,6 +591,16 @@ open class DeliveryOrderService(
val stockQtyByItemId = itemIds.associateWith { itemId ->
computeAvailableStockQtyForItem(itemId)
}
val fallbackQtyByItemId = deliveryOrder.deliveryOrderLines.mapNotNull { line ->
val itemId = line.item?.id ?: return@mapNotNull null
itemId to (line.qty ?: BigDecimal.ZERO)
}.toMap()
val actualShippedQtyByItemId = try {
doReplenishmentService.resolveActualShippedQtyForDeliveryOrder(id, fallbackQtyByItemId)
} catch (ex: Exception) {
log.warn("Failed to resolve actual shipped qty for delivery order {}: {}", id, ex.message)
fallbackQtyByItemId
}

return DoDetailResponse(
id = deliveryOrder.id!!,
@@ -617,6 +627,7 @@ open class DeliveryOrderService(
id = line.id!!,
itemNo = line.itemNo,
qty = line.qty,
actualShippedQty = itemId?.let { actualShippedQtyByItemId[it] },
price = line.price,
status = line.status?.value,
itemName = line.item?.name,


+ 110
- 0
src/main/java/com/ffii/fpsms/modules/deliveryOrder/service/DoReplenishmentService.kt Bestand weergeven

@@ -1,6 +1,7 @@
package com.ffii.fpsms.modules.deliveryOrder.service

import com.ffii.core.support.JdbcDao
import com.ffii.fpsms.modules.deliveryOrder.entity.DoPickOrderLineRepository
import com.ffii.fpsms.modules.deliveryOrder.entity.DoPickOrderRecordRepository
import com.ffii.fpsms.modules.deliveryOrder.entity.DoPickOrderRepository
import com.ffii.fpsms.modules.deliveryOrder.entity.DoReplenishment
@@ -18,6 +19,7 @@ import com.ffii.fpsms.modules.master.entity.UomConversionRepository
import com.ffii.fpsms.modules.pickOrder.entity.PickOrderLine
import com.ffii.fpsms.modules.pickOrder.entity.PickOrderLineRepository
import com.ffii.fpsms.modules.pickOrder.entity.PickOrderRepository
import com.ffii.fpsms.modules.stock.entity.StockOutLIneRepository
import com.ffii.fpsms.modules.pickOrder.enums.PickOrderLineStatus
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional
@@ -31,7 +33,9 @@ open class DoReplenishmentService(
private val deliveryOrderRepository: DeliveryOrderRepository,
private val deliveryOrderLineRepository: DeliveryOrderLineRepository,
private val doPickOrderRepository: DoPickOrderRepository,
private val doPickOrderLineRepository: DoPickOrderLineRepository,
private val doPickOrderRecordRepository: DoPickOrderRecordRepository,
private val stockOutLIneRepository: StockOutLIneRepository,
private val uomConversionRepository: UomConversionRepository,
private val pickOrderRepository: PickOrderRepository,
private val pickOrderLineRepository: PickOrderLineRepository,
@@ -72,6 +76,7 @@ open class DoReplenishmentService(
existingPending.truckLaneCode =
resolveSourceDoTruckLaneCode(deliveryOrder, lineReq.truckLaneCode)
}
lineReq.reason?.trim()?.takeIf { it.isNotEmpty() }?.let { existingPending.reason = it }
created += doReplenishmentRepository.save(existingPending)
continue
}
@@ -108,6 +113,7 @@ open class DoReplenishmentService(
shopName = shop?.name
truckLaneCode = resolveSourceDoTruckLaneCode(deliveryOrder, lineReq.truckLaneCode)
status = DoReplenishment.STATUS_PENDING
reason = lineReq.reason?.trim()?.takeIf { it.isNotEmpty() }
}
created += doReplenishmentRepository.save(entity)
}
@@ -382,6 +388,7 @@ open class DoReplenishmentService(
merged[key] = existing.copy(
replenishQty = existing.replenishQty.add(line.replenishQty),
truckLaneCode = existing.truckLaneCode?.takeIf { it.isNotBlank() } ?: line.truckLaneCode,
reason = existing.reason?.takeIf { it.isNotBlank() } ?: line.reason,
)
}
}
@@ -408,6 +415,77 @@ open class DoReplenishmentService(
return requestTruckLaneCode?.trim()?.takeIf { it.isNotEmpty() }
}

/**
* Actual shipped qty per item on a completed source DO: sum of [stock_out_line.qty]
* for the linked pick order line. Falls back to [fallbackQtyByItemId] when no pick link exists
* (same rule as delivery note PDF).
*/
open fun resolveActualShippedQtyForDeliveryOrder(
doId: Long,
fallbackQtyByItemId: Map<Long, BigDecimal> = emptyMap(),
): Map<Long, BigDecimal> {
val keys = fallbackQtyByItemId.keys.map { doId to it }
if (keys.isEmpty()) {
return emptyMap()
}
return resolveActualShippedQtyBySourceKeys(keys, keys.associateWith { fallbackQtyByItemId[it.second]!! })
.mapKeys { it.key.second }
}

private fun resolveActualShippedQtyBySourceKeys(
keys: List<Pair<Long, Long>>,
fallbackQtyByKey: Map<Pair<Long, Long>, BigDecimal>,
): Map<Pair<Long, Long>, BigDecimal> {
if (keys.isEmpty()) {
return emptyMap()
}

val doIds = keys.map { it.first }.distinct()
val pickOrderIdByDoId = doIds.mapNotNull { doId ->
resolvePickOrderIdForDo(doId)?.let { doId to it }
}.toMap()

val pickOrderIds = pickOrderIdByDoId.values.distinct()
val pickOrderLines = if (pickOrderIds.isEmpty()) {
emptyList()
} else {
pickOrderLineRepository.findAllByPickOrderIdInAndDeletedFalse(pickOrderIds)
}
val pickOrderLinesByPoId = pickOrderLines.groupBy { it.pickOrder?.id }

val polIds = pickOrderLines.mapNotNull { it.id }
val stockOutQtyByPolId = if (polIds.isEmpty()) {
emptyMap()
} else {
stockOutLIneRepository.findAllByPickOrderLineIdInAndDeletedFalse(polIds)
.groupBy { it.pickOrderLine?.id }
.mapValues { (_, lines) ->
lines.fold(BigDecimal.ZERO) { acc, line ->
acc.add(BigDecimal.valueOf(line.qty ?: 0.0))
}
}
}

return keys.associateWith { (doId, itemId) ->
val pickOrderId = pickOrderIdByDoId[doId]
val polId = pickOrderId?.let { poId ->
pickOrderLinesByPoId[poId]?.firstOrNull { it.item?.id == itemId }?.id
}
if (polId != null) {
stockOutQtyByPolId[polId] ?: BigDecimal.ZERO
} else {
fallbackQtyByKey[doId to itemId] ?: BigDecimal.ZERO
}
}
}

private fun resolvePickOrderIdForDo(doId: Long): Long? {
pickOrderRepository.findByDeliveryOrderId(doId).firstOrNull()?.id?.let { return it }
return doPickOrderLineRepository.findByDoOrderIdAndDeletedFalse(doId)
.mapNotNull { it.pickOrderId }
.firstOrNull()
}

private fun toResponses(entities: List<DoReplenishment>): List<DoReplenishmentResponse> {
val uomIds = entities.mapNotNull { it.uomId }.distinct()
val shortUomById = if (uomIds.isEmpty()) {
@@ -418,6 +496,35 @@ open class DoReplenishmentService(
}
}

val sourceLineIds = entities.mapNotNull { it.sourceDoLineId }.distinct()
val sourceLineQtyById = if (sourceLineIds.isEmpty()) {
emptyMap()
} else {
deliveryOrderLineRepository.findAllById(sourceLineIds).associate { line ->
line.id!! to line.qty
}
}

val targetDoIds = entities.mapNotNull { it.targetDoId }.distinct()
val targetDoEtaById = if (targetDoIds.isEmpty()) {
emptyMap()
} else {
deliveryOrderRepository.findAllById(targetDoIds).associate { deliveryOrder ->
deliveryOrder.id!! to deliveryOrder.estimatedArrivalDate?.toLocalDate()
}
}

val sourceKeys = entities.map { it.sourceDoId!! to it.itemId!! }.distinct()
val fallbackQtyBySourceKey = entities.associate { row ->
(row.sourceDoId!! to row.itemId!!) to (
row.sourceDoLineId?.let { sourceLineQtyById[it] } ?: BigDecimal.ZERO
)
}
val actualShippedQtyBySourceKey = resolveActualShippedQtyBySourceKeys(
keys = sourceKeys,
fallbackQtyByKey = fallbackQtyBySourceKey,
)

return entities.map { row ->
DoReplenishmentResponse(
id = row.id!!,
@@ -429,6 +536,7 @@ open class DoReplenishmentService(
itemId = row.itemId!!,
itemNo = row.itemNo,
itemName = row.itemName,
originalQty = actualShippedQtyBySourceKey[row.sourceDoId!! to row.itemId!!],
replenishQty = row.replenishQty!!,
shortUom = row.uomId?.let { shortUomById[it] },
shopCode = row.shopCode,
@@ -436,9 +544,11 @@ open class DoReplenishmentService(
truckLaneCode = row.truckLaneCode,
targetDoId = row.targetDoId,
targetDoCode = row.targetDoCode,
targetDoEstimatedArrivalDate = row.targetDoId?.let { targetDoEtaById[it] },
pickOrderLineId = row.pickOrderLineId,
deliveryOrderPickOrderId = row.deliveryOrderPickOrderId,
status = row.status,
reason = row.reason,
created = row.created,
)
}


+ 2
- 0
src/main/java/com/ffii/fpsms/modules/deliveryOrder/web/models/DoDetailResponse.kt Bestand weergeven

@@ -31,6 +31,8 @@ data class DoDetailLineResponse(
val id: Long,
val itemNo: String?,
val qty: java.math.BigDecimal?,
/** Sum of stock_out_line.qty for the linked pick order line; falls back to [qty] when unavailable. */
val actualShippedQty: java.math.BigDecimal?,
val price: java.math.BigDecimal?,
val status: String?,
val itemName: String?,


+ 5
- 0
src/main/java/com/ffii/fpsms/modules/deliveryOrder/web/models/DoReplenishmentModels.kt Bestand weergeven

@@ -21,6 +21,7 @@ data class SubmitDoReplenishmentLineRequest(
@field:Positive
val replenishQty: BigDecimal,
val truckLaneCode: String? = null,
val reason: String? = null,
)

data class SubmitDoReplenishmentRequest(
@@ -40,6 +41,7 @@ data class DoReplenishmentResponse(
val itemId: Long,
val itemNo: String?,
val itemName: String?,
val originalQty: BigDecimal?,
val replenishQty: BigDecimal,
val shortUom: String?,
val shopCode: String?,
@@ -47,9 +49,12 @@ data class DoReplenishmentResponse(
val truckLaneCode: String?,
val targetDoId: Long?,
val targetDoCode: String?,
@JsonFormat(pattern = "yyyy-MM-dd")
val targetDoEstimatedArrivalDate: LocalDate?,
val pickOrderLineId: Long?,
val deliveryOrderPickOrderId: Long?,
val status: String,
val reason: String?,
@JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss")
val created: LocalDateTime?,
)

+ 17
- 0
src/main/resources/db/changelog/changes/20260611_KelvinY/02_add_do_replenishment_reason.sql Bestand weergeven

@@ -0,0 +1,17 @@
-- liquibase formatted sql
-- changeset KelvinY:20260611_02_add_do_replenishment_reason
-- preconditions onFail:MARK_RAN
-- precondition-sql-check expectedResult:1 SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = DATABASE() AND table_name = 'do_replenishment'

SET @col_dr_reason := (
SELECT COUNT(*) FROM information_schema.columns
WHERE table_schema = DATABASE() AND table_name = 'do_replenishment' AND column_name = 'reason'
);
SET @sql_dr_reason := IF(
@col_dr_reason = 0,
'ALTER TABLE `do_replenishment` ADD COLUMN `reason` VARCHAR(500) NULL DEFAULT NULL AFTER `status`',
'SELECT 1'
);
PREPARE stmt_dr_reason FROM @sql_dr_reason;
EXECUTE stmt_dr_reason;
DEALLOCATE PREPARE stmt_dr_reason;

Laden…
Annuleren
Opslaan