Ver a proveniência

bom excel export

bomUpdateTest
CANCERYS\kw093 há 1 semana
ascendente
cometimento
b8dc276393
6 ficheiros alterados com 444 adições e 23 eliminações
  1. +176
    -23
      src/main/java/com/ffii/fpsms/modules/master/service/BomService.kt
  2. +106
    -0
      src/main/java/com/ffii/fpsms/modules/master/service/ItemAveragePurchasePriceService.kt
  3. +87
    -0
      src/main/java/com/ffii/fpsms/modules/master/service/StandardUomMatrix.kt
  4. +35
    -0
      src/main/java/com/ffii/fpsms/modules/master/support/HomeFxRates.kt
  5. +37
    -0
      src/main/java/com/ffii/fpsms/modules/master/support/ItemAveragePriceFx.kt
  6. +3
    -0
      src/main/resources/application.yml

+ 176
- 23
src/main/java/com/ffii/fpsms/modules/master/service/BomService.kt Ver ficheiro

@@ -71,6 +71,7 @@ open class BomService(
private val bomMaterialQtyService: BomMaterialQtyService,
private val bomOutputQtyService: BomOutputQtyService,
private val bomRecipeUomOptionsService: BomRecipeUomOptionsService,
private val itemAveragePurchasePriceService: ItemAveragePurchasePriceService,
@Value("\${bom.import.temp-dir:\${java.io.tmpdir}/fpsms-bom-import}") private val bomImportTempDir: String,
) {
companion object {
@@ -1928,7 +1929,7 @@ open class BomService(
try {
val sheet = resolveImportBomSheet(workbook)
fillBomDetailOntoBlankTemplate(workbook, detail)
recalculateMaterialDerivedColumns(sheet)
recalculateMaterialDerivedColumns(sheet, usePoAveragePurchasePrice = true)
refreshOutputUomFormulaDependents(sheet)
evaluateBlankTemplateFormulaDependents(sheet)
val bytes = ByteArrayOutputStream().use { out ->
@@ -2406,11 +2407,36 @@ open class BomService(
* 轉用 (matrix from Excel 轉用單位), 銷售 / 採購 (system convert, not Excel).
*
* Columns: C使用份量 D使用單位 E轉用份量 F轉用單位 G銷售份量 H銷售單位 I採購單價 J採購單位
* Food cost: U (=J), V/X (=I 採購成本), Y/Z (=C/D), AA/AB (D→AC rate/qty), AC (=F),
* AD (=HKD/AC via item_uom 1 purchase→base→AC; code-parse fallback)
*
* @param usePoAveragePurchasePrice when true (BOM 明細匯出), I/V/X = on-the-fly HKD/purchase UOM
* from 2026+ PO lines; blank if not calculable.
* when false (修正匯出), I/V/X = items.latestMarketUnitPrice.
*/
private fun recalculateMaterialDerivedColumns(sheet: Sheet) {
private fun recalculateMaterialDerivedColumns(
sheet: Sheet,
usePoAveragePurchasePrice: Boolean = false,
) {
val headerRowIndex = findMaterialHeaderRowIndex(sheet) ?: return
var rowIdx = headerRowIndex + 1
val maxRowIndex = 200
val poAvgByItemId: Map<Long, BigDecimal> = if (usePoAveragePurchasePrice) {
val itemIds = mutableSetOf<Long>()
var scanIdx = headerRowIndex + 1
while (scanIdx < maxRowIndex) {
val row = sheet.getRow(scanIdx) ?: break
val firstCell = row.getCell(0)
if (firstCell == null || firstCell.cellType == CellType.BLANK) break
val code = readStringCellValue(firstCell)
code?.let { itemsRepository.findByCodeAndDeletedFalse(it)?.id }?.let { itemIds.add(it) }
scanIdx++
}
itemAveragePurchasePriceService.computeHkdPerPurchase(itemIds)
} else {
emptyMap()
}

var rowIdx = headerRowIndex + 1
while (rowIdx < maxRowIndex) {
val row = sheet.getRow(rowIdx) ?: break
val firstCell = row.getCell(0)
@@ -2424,18 +2450,6 @@ open class BomService(
null
}
val recipeUomCode = readStringCellValue(row.getCell(3))
val transferUomCode = readStringCellValue(row.getCell(5))

// 轉用: convert 配方數量 → Excel 轉用單位 via matrix; else null both
val fromMatrix = StandardUomMatrix.toMatrixUnit(recipeUomCode)
val toMatrix = StandardUomMatrix.toMatrixUnit(transferUomCode)
if (recipeQty != null && fromMatrix != null && toMatrix != null) {
val converted = StandardUomMatrix.convert(recipeQty, fromMatrix, toMatrix)
getOrCreateCell(sheet, rowIdx, 4).setCellValue(converted.toDouble())
} else {
clearCellValue(sheet, rowIdx, 4)
clearCellValue(sheet, rowIdx, 5)
}

val item = itemCode?.let { itemsRepository.findByCodeAndDeletedFalse(it) }
val itemId = item?.id
@@ -2447,40 +2461,179 @@ open class BomService(
} else {
null
}
if (derived?.saleQty != null) {
val salesCode = if (derived?.saleQty != null) {
getOrCreateCell(sheet, rowIdx, 6).setCellValue(derived.saleQty!!.toDouble())
val salesCode = derived.salesUnit?.code?.trim()?.takeIf { it.isNotEmpty() }
val code = derived.salesUnit?.code?.trim()?.takeIf { it.isNotEmpty() }
?: derived.salesUnitCode?.trim()?.takeIf { it.isNotEmpty() }
if (salesCode != null) {
getOrCreateCell(sheet, rowIdx, 7).setCellValue(salesCode)
if (code != null) {
getOrCreateCell(sheet, rowIdx, 7).setCellValue(code)
} else {
clearCellValue(sheet, rowIdx, 7)
}
code
} else {
clearCellValue(sheet, rowIdx, 6)
clearCellValue(sheet, rowIdx, 7)
null
}

// 採購: unit + latest market unit price from master (do not read Excel)
// Food cost Y/Z = C/D 使用份量/單位
if (recipeQty != null) {
getOrCreateCell(sheet, rowIdx, 24).setCellValue(recipeQty.toDouble())
} else {
clearCellValue(sheet, rowIdx, 24)
}
if (!recipeUomCode.isNullOrBlank()) {
getOrCreateCell(sheet, rowIdx, 25).setCellValue(recipeUomCode)
} else {
clearCellValue(sheet, rowIdx, 25)
}

// 轉用 F/AC:銷售單位尾巴 matrix(KG/L/ML…);E/AB:配方→matrix;AA:1 D→AC rate
val salesPack = StandardUomMatrix.resolvePackMatrix(derived?.salesUnit)
?: StandardUomMatrix.parsePackMatrixFromCode(salesCode)
if (salesPack != null) {
val transferUnit = salesPack.first
getOrCreateCell(sheet, rowIdx, 5).setCellValue(transferUnit.code)
getOrCreateCell(sheet, rowIdx, 28).setCellValue(transferUnit.code) // AC = F
val fromMatrix = StandardUomMatrix.toMatrixUnit(recipeUomCode)
val transferQty = when {
recipeQty != null && fromMatrix != null ->
StandardUomMatrix.convert(recipeQty, fromMatrix, transferUnit)
derived?.saleQty != null && salesPack.second > BigDecimal.ZERO ->
derived.saleQty!!.multiply(salesPack.second).setScale(4, RoundingMode.HALF_UP)
else -> null
}
if (transferQty != null) {
getOrCreateCell(sheet, rowIdx, 4).setCellValue(transferQty.toDouble())
getOrCreateCell(sheet, rowIdx, 27).setCellValue(transferQty.toDouble()) // AB = E
} else {
clearCellValue(sheet, rowIdx, 4)
clearCellValue(sheet, rowIdx, 27)
}
if (fromMatrix != null) {
val rate = StandardUomMatrix.convert(BigDecimal.ONE, fromMatrix, transferUnit)
getOrCreateCell(sheet, rowIdx, 26).setCellValue(rate.toDouble()) // AA
} else {
clearCellValue(sheet, rowIdx, 26)
}
} else {
clearCellValue(sheet, rowIdx, 4)
clearCellValue(sheet, rowIdx, 5)
clearCellValue(sheet, rowIdx, 26)
clearCellValue(sheet, rowIdx, 27)
clearCellValue(sheet, rowIdx, 28)
}

// 採購單位 J + Food cost U Unit Code(與 J 相同)
val purchaseUom = itemId?.let { itemUomService.findPurchaseUnitByItemId(it) }?.uom
val purchaseCode = purchaseUom?.code?.trim()?.takeIf { it.isNotEmpty() }
?: purchaseUom?.udfudesc?.trim()?.takeIf { it.isNotEmpty() }
if (purchaseCode != null) {
getOrCreateCell(sheet, rowIdx, 9).setCellValue(purchaseCode)
getOrCreateCell(sheet, rowIdx, 20).setCellValue(purchaseCode)
} else {
clearCellValue(sheet, rowIdx, 9)
clearCellValue(sheet, rowIdx, 20)
}
// I/V/X:採購單位成本;AD:HKD/AC(轉用)單位
val purchasePrice: BigDecimal? = if (usePoAveragePurchasePrice) {
itemId?.let { poAvgByItemId[it] }
} else {
item?.latestMarketUnitPrice?.let { BigDecimal.valueOf(it) }
}
val marketPrice = item?.latestMarketUnitPrice
if (marketPrice != null) {
getOrCreateCell(sheet, rowIdx, 8).setCellValue(marketPrice)
if (purchasePrice != null) {
val price = purchasePrice.toDouble()
getOrCreateCell(sheet, rowIdx, 8).setCellValue(price)
getOrCreateCell(sheet, rowIdx, 21).setCellValue(price) // V
getOrCreateCell(sheet, rowIdx, 23).setCellValue(price) // X
} else {
clearCellValue(sheet, rowIdx, 8)
clearCellValue(sheet, rowIdx, 21)
clearCellValue(sheet, rowIdx, 23)
}
val transferUnit = salesPack?.first
val adPrice = if (purchasePrice != null && transferUnit != null) {
resolveHkdPerTransferUnit(
itemId = itemId,
purchasePriceHkd = purchasePrice,
purchaseUom = purchaseUom,
salesUom = derived?.salesUnit,
transferUnit = transferUnit,
)
} else {
null
}
if (adPrice != null) {
getOrCreateCell(sheet, rowIdx, 29).setCellValue(adPrice.toDouble())
} else {
clearCellValue(sheet, rowIdx, 29)
}

rowIdx++
}
}

/**
* AD = purchase HKD / (AC qty in 1 purchase unit).
* Prefer [item_uom] ratio → base → matrix [transferUnit]; fallback to UOM code pack parse.
*/
private fun resolveHkdPerTransferUnit(
itemId: Long?,
purchasePriceHkd: BigDecimal,
purchaseUom: UomConversion?,
salesUom: UomConversion?,
transferUnit: StandardUomMatrix.MatrixUnit,
): BigDecimal? {
val qtyInTransfer = (itemId?.let {
resolveTransferQtyViaItemUom(it, purchaseUom, salesUom, transferUnit)
} ?: resolveTransferQtyViaUomCode(purchaseUom, salesUom, transferUnit))
?: return null
if (qtyInTransfer.compareTo(BigDecimal.ZERO) <= 0) return null
return purchasePriceHkd.divide(qtyInTransfer, 6, RoundingMode.HALF_UP)
}

/** 1 purchase (or sales) UOM → base via item_uom ratioN/ratioD → [transferUnit] qty. */
private fun resolveTransferQtyViaItemUom(
itemId: Long,
purchaseUom: UomConversion?,
salesUom: UomConversion?,
transferUnit: StandardUomMatrix.MatrixUnit,
): BigDecimal? {
val sourceUom = purchaseUom ?: salesUom ?: return null
val uomId = sourceUom.id ?: return null
val baseQty = itemUomService.convertQtyToBaseQtyPrecise(itemId, uomId, BigDecimal.ONE)
?: return null
if (baseQty.compareTo(BigDecimal.ZERO) <= 0) return null
val baseUom = itemUomService.findBaseUnitByItemId(itemId)?.uom ?: return null
val baseMatrix = StandardUomMatrix.toMatrixUnit(baseUom) ?: return null
return convertMatrixPrecise(baseQty, baseMatrix, transferUnit)
}

private fun resolveTransferQtyViaUomCode(
purchaseUom: UomConversion?,
salesUom: UomConversion?,
transferUnit: StandardUomMatrix.MatrixUnit,
): BigDecimal? {
val pack = StandardUomMatrix.resolvePackMatrix(purchaseUom)
?: StandardUomMatrix.resolvePackMatrix(salesUom)
?: return null
return if (pack.first == transferUnit) {
pack.second
} else {
convertMatrixPrecise(pack.second, pack.first, transferUnit)
}
}

private fun convertMatrixPrecise(
qty: BigDecimal,
from: StandardUomMatrix.MatrixUnit,
to: StandardUomMatrix.MatrixUnit,
): BigDecimal {
if (from == to) return qty
return StandardUomMatrix.convert(qty, from, to)
}

private fun setBasicInfoStringByHeader(sheet: Sheet, headerText: String, value: String) {
for (rowIdx in 0..15) {
for (colIdx in 0..15) {


+ 106
- 0
src/main/java/com/ffii/fpsms/modules/master/service/ItemAveragePurchasePriceService.kt Ver ficheiro

@@ -0,0 +1,106 @@
package com.ffii.fpsms.modules.master.service

import com.ffii.core.support.JdbcDao
import com.ffii.fpsms.modules.master.support.ItemAveragePriceFx
import org.springframework.stereotype.Service
import java.math.BigDecimal

/**
* On-the-fly PO weighted average in **HKD per purchase UOM**
* (item's purchaseUnit=1), without writing [items.AverageUnitPrice].
*
* SUM(lineAmtForeign * fx) / SUM(qty converted to purchase UOM), PO orderDate >= 2026-01-01.
* Items with no purchase unit or no qualifying PO lines are omitted.
*/
@Service
open class ItemAveragePurchasePriceService(
private val jdbcDao: JdbcDao,
private val itemAveragePriceFx: ItemAveragePriceFx,
) {
open fun computeHkdPerPurchase(itemIds: Collection<Long>): Map<Long, BigDecimal> {
if (itemIds.isEmpty()) return emptyMap()
val ids = itemIds.distinct()
val fx = itemAveragePriceFx.fxSql("po", "c")
val sql = """
WITH uom_one AS (
SELECT iu.*
FROM item_uom iu
INNER JOIN (
SELECT itemId, uomId, MIN(id) AS id
FROM item_uom
WHERE deleted = 0
GROUP BY itemId, uomId
) x ON x.id = iu.id
),
purchase_one AS (
SELECT iu.*
FROM item_uom iu
INNER JOIN (
SELECT itemId, MIN(id) AS id
FROM item_uom
WHERE deleted = 0 AND purchaseUnit = 1
GROUP BY itemId
) x ON x.id = iu.id
),
line_raw AS (
SELECT
pol.itemId,
($fx) AS fx,
COALESCE(pol.price, pol.up * COALESCE(pol.qtyM18, pol.qty)) AS lineAmt,
CASE
WHEN pou.id IS NULL THEN 0
WHEN pol.qtyM18 IS NOT NULL AND pol.qtyM18 > 0 AND pol.uomIdM18 IS NOT NULL THEN
CASE
WHEN src.id IS NULL THEN pol.qtyM18
WHEN src.uomId = pou.uomId THEN pol.qtyM18
ELSE pol.qtyM18
* (COALESCE(src.ratioN, 1) / COALESCE(NULLIF(src.ratioD, 0), 1))
* (COALESCE(pou.ratioD, 1) / COALESCE(NULLIF(pou.ratioN, 0), 1))
END
ELSE
CASE
WHEN pur.id IS NULL THEN COALESCE(pol.qty, 0)
WHEN pur.uomId = pou.uomId THEN COALESCE(pol.qty, 0)
ELSE COALESCE(pol.qty, 0)
* (COALESCE(pur.ratioN, 1) / COALESCE(NULLIF(pur.ratioD, 0), 1))
* (COALESCE(pou.ratioD, 1) / COALESCE(NULLIF(pou.ratioN, 0), 1))
END
END AS purchaseQty
FROM purchase_order_line pol
JOIN purchase_order po ON po.id = pol.purchaseOrderId AND po.deleted = 0
LEFT JOIN currency c ON c.id = po.currencyId AND c.deleted = 0
LEFT JOIN uom_one src ON src.itemId = pol.itemId AND src.uomId = pol.uomIdM18
LEFT JOIN uom_one pur ON pur.itemId = pol.itemId AND pur.uomId = pol.uomId
LEFT JOIN purchase_one pou ON pou.itemId = pol.itemId
WHERE pol.deleted = 0
AND po.orderDate >= '2026-01-01'
AND pol.up IS NOT NULL
AND COALESCE(pol.qtyM18, pol.qty, 0) > 0
AND pol.itemId IN (:itemIds)
),
line_ok AS (
SELECT itemId, fx, lineAmt, purchaseQty
FROM line_raw
WHERE fx IS NOT NULL AND purchaseQty > 0 AND lineAmt IS NOT NULL
),
calc AS (
SELECT
itemId,
ROUND(SUM(lineAmt * fx) / NULLIF(SUM(purchaseQty), 0), 4) AS avgUp
FROM line_ok
GROUP BY itemId
)
SELECT itemId, avgUp FROM calc
""".trimIndent()
val rows = jdbcDao.queryForList(sql, mapOf("itemIds" to ids))
return rows.mapNotNull { row ->
val id = (row["itemId"] as? Number)?.toLong() ?: return@mapNotNull null
val avg = when (val v = row["avgUp"]) {
is BigDecimal -> v
is Number -> BigDecimal.valueOf(v.toDouble())
else -> return@mapNotNull null
}
id to avg
}.toMap()
}
}

+ 87
- 0
src/main/java/com/ffii/fpsms/modules/master/service/StandardUomMatrix.kt Ver ficheiro

@@ -48,6 +48,93 @@ object StandardUomMatrix {

fun toMatrixUnit(uom: UomConversion?): MatrixUnit? = toMatrixUnit(uom?.code)

/**
* Sales/purchase pack code → (matrix unit, content qty **per 1 of that UOM**).
* Nested packs multiply all levels, e.g. CTN8PACK1.8KG → (KG, 14.4), not 1.8.
* Prefers unit1..unit4 on [UomConversion], then parses [UomConversion.code].
*/
fun resolvePackMatrix(uom: UomConversion?): Pair<MatrixUnit, BigDecimal>? {
if (uom == null) return null
toMatrixUnit(uom.code)?.let { return it to BigDecimal.ONE }
resolvePackMatrixFromUnitLevels(
listOf(
uom.unit1 to uom.unit1Qty,
uom.unit2 to uom.unit2Qty,
uom.unit3 to uom.unit3Qty,
uom.unit4 to uom.unit4Qty,
),
)?.let { return it }
// sizeInGram is total grams in one of this UOM (when populated by UomConversionService)
val sizeG = uom.sizeInGram?.takeIf { it > 0 }?.let { BigDecimal.valueOf(it) }
if (sizeG != null) {
parsePackMatrixFromCode(uom.code)?.let { (unit, _) ->
val gramsPer = gramsPerUnit[unit] ?: return@let null
return unit to sizeG.divide(gramsPer, 10, RoundingMode.HALF_UP)
}
}
return parsePackMatrixFromCode(uom.code)
}

/**
* Multiply container × … × matrix content.
* e.g. CTN(1) × PACK(8) × KG(1.8) → (KG, 14.4); PACK(1) × KG(40) → (KG, 40).
*/
private fun resolvePackMatrixFromUnitLevels(
levels: List<Pair<String?, Double?>>,
): Pair<MatrixUnit, BigDecimal>? {
var matrixIdx = -1
var matrixUnit: MatrixUnit? = null
for ((i, level) in levels.withIndex()) {
val name = level.first?.trim()?.takeIf { it.isNotEmpty() } ?: continue
val m = toMatrixUnit(name) ?: continue
matrixIdx = i
matrixUnit = m
}
val unit = matrixUnit ?: return null
if (matrixIdx < 0) return null
var qty = BigDecimal.ONE
for (i in 0..matrixIdx) {
val name = levels[i].first?.trim().orEmpty()
if (name.isEmpty() && i < matrixIdx) continue
val q = levels[i].second?.takeIf { it > 0 }?.let { BigDecimal.valueOf(it) }
?: BigDecimal.ONE
qty = qty.multiply(q)
}
return unit to qty
}

/**
* Parse nested codes: CTN8PACK1.8KG → (KG, 14.4); PACK40KG → (KG, 40); BTL800ML → (ML, 800).
*/
fun parsePackMatrixFromCode(code: String?): Pair<MatrixUnit, BigDecimal>? {
val raw = code?.trim()?.uppercase().orEmpty()
if (raw.isEmpty()) return null
toMatrixUnit(raw)?.let { return it to BigDecimal.ONE }
val tokens = Regex("""(\d+(?:\.\d+)?)?([A-Za-z]+)""")
.findAll(raw)
.map { m ->
val num = m.groupValues[1].toBigDecimalOrNull()
val name = m.groupValues[2]
name to num
}
.toList()
if (tokens.isEmpty()) return null
var matrixIdx = -1
var matrixUnit: MatrixUnit? = null
for ((i, token) in tokens.withIndex()) {
val m = toMatrixUnit(token.first) ?: continue
matrixIdx = i
matrixUnit = m
}
val unit = matrixUnit ?: return null
if (matrixIdx < 0) return null
var qty = BigDecimal.ONE
for (i in 0..matrixIdx) {
qty = qty.multiply(tokens[i].second ?: BigDecimal.ONE)
}
return unit to qty
}

fun convert(qty: BigDecimal, from: MatrixUnit, to: MatrixUnit): BigDecimal {
if (from == to) return qty.setScale(2, RoundingMode.HALF_UP)
val fromGrams = gramsPerUnit[from]


+ 35
- 0
src/main/java/com/ffii/fpsms/modules/master/support/HomeFxRates.kt Ver ficheiro

@@ -0,0 +1,35 @@
package com.ffii.fpsms.modules.master.support

import java.math.BigDecimal
import java.math.RoundingMode

/**
* V1 (`fx-source=local`) HKD rates. V2 reads [purchase_order.exchangeRate] from M18.
* JPY: 1 HKD = 20.35 JPY.
*/
object HomeFxRates {
const val HOME = "HKD"

private val jpyToHkd: BigDecimal =
BigDecimal.ONE.divide(BigDecimal("20.35"), 10, RoundingMode.HALF_UP)

val toHkd: Map<String, BigDecimal> = mapOf(
"HKD" to BigDecimal.ONE,
"USD" to BigDecimal("7.7823"),
"RMB" to BigDecimal("1.1700"),
"CNY" to BigDecimal("1.1700"),
"JPY" to jpyToHkd,
)

/** SQL fragment: local FX to HKD from a currency-code expression. */
fun localFxSql(currencyCodeExpr: String): String = """
CASE UPPER(TRIM(COALESCE($currencyCodeExpr, '')))
WHEN 'HKD' THEN 1
WHEN 'USD' THEN 7.7823
WHEN 'RMB' THEN 1.1700
WHEN 'CNY' THEN 1.1700
WHEN 'JPY' THEN (1 / 20.35)
ELSE NULL
END
""".trimIndent()
}

+ 37
- 0
src/main/java/com/ffii/fpsms/modules/master/support/ItemAveragePriceFx.kt Ver ficheiro

@@ -0,0 +1,37 @@
package com.ffii.fpsms.modules.master.support

import org.springframework.beans.factory.annotation.Value
import org.springframework.stereotype.Component
import java.math.BigDecimal

@Component
open class ItemAveragePriceFx(
@Value("\${fpsms.item-average-price.fx-source:local}")
private val fxSource: String,
) {
fun isM18Source(): Boolean = fxSource.equals("m18", ignoreCase = true)

fun resolveRate(currencyCode: String?, poExchangeRate: BigDecimal?): BigDecimal? {
val code = currencyCode?.trim()?.uppercase().orEmpty()
if (isM18Source()) {
if (poExchangeRate != null) return poExchangeRate
return if (code == HomeFxRates.HOME) BigDecimal.ONE else null
}
if (code.isEmpty()) return null
return HomeFxRates.toHkd[code]
}

/** SQL for FX to HKD; aliases [po] and [c] (currency). */
fun fxSql(po: String = "po", currency: String = "c"): String {
if (isM18Source()) {
return """
CASE
WHEN $po.exchangeRate IS NOT NULL THEN $po.exchangeRate
WHEN UPPER(TRIM(COALESCE($currency.code, ''))) = '${HomeFxRates.HOME}' THEN 1
ELSE NULL
END
""".trimIndent()
}
return HomeFxRates.localFxSql("$currency.code")
}
}

+ 3
- 0
src/main/resources/application.yml Ver ficheiro

@@ -68,6 +68,9 @@ scheduler:

# Nav: PO stock_in_line pending/receiving within last N days (see ProductProcessService for 工單 QC/上架:今日+昨日).
fpsms:
# local = hardcoded HKD rates (testing). m18 = per-PO exchangeRate from MPO sync.
item-average-price:
fx-source: local
purchase-stock-in-alert:
lookback-days: 7
# Device + printer monitoring: enable only on production profile (application-prod*.yml).


Carregando…
Cancelar
Guardar