Kaynağa Gözat

added unit description for the number before submit in PO stock in

production
PC-20260115JRSN\Administrator 2 gün önce
ebeveyn
işleme
6e912bd5dc
8 değiştirilmiş dosya ile 245 ekleme ve 23 silme
  1. +1
    -0
      src/app/api/po/index.ts
  2. +2
    -0
      src/app/api/settings/uom/index.ts
  3. +7
    -1
      src/components/PoDetail/PoDetail.tsx
  4. +131
    -19
      src/components/PoDetail/PoDetailRow.tsx
  5. +4
    -3
      src/components/PoDetail/PoInputGrid.tsx
  6. +96
    -0
      src/components/PoDetail/poPurchaseUom.ts
  7. +2
    -0
      src/i18n/en/purchaseOrder.json
  8. +2
    -0
      src/i18n/zh/purchaseOrder.json

+ 1
- 0
src/app/api/po/index.ts Dosyayı Görüntüle

@@ -54,6 +54,7 @@ export interface StockUomForPoLine {
id: number;
stockUomCode: string;
stockUomDesc: string;
stockUomShortDesc?: string | null;
stockQty: number;
stockRatioN: number;
stockRatioD: number;


+ 2
- 0
src/app/api/settings/uom/index.ts Dosyayı Görüntüle

@@ -9,6 +9,8 @@ export interface Uom {
id: number;
code: string;
udfudesc: string;
/** Short UoM label from M18 (`udfShortDesc`), e.g. 箱. */
udfShortDesc?: string | null;
unit1: string;
unit1Qty: number;
unit2?: string;


+ 7
- 1
src/components/PoDetail/PoDetail.tsx Dosyayı Görüntüle

@@ -875,7 +875,13 @@ const PoDetail: React.FC<Props> = ({ po, warehouse, printerCombo }) => {
<TableCell align="left" sx={{ width: '75px' }}>{t("status")}</TableCell>
{/* {renderFieldCondition(FIRST_IN_FIELD) ? <TableCell align="right">{t("receivedQty")}</TableCell> : undefined} */}
<TableCell align="center" sx={{ width: '150px' }}>{t("productLotNo")}</TableCell>
{renderFieldCondition(SECOND_IN_FIELD) ? <TableCell align="center" sx={{ width: '150px' }}>{t("dnQty")}<br/>(以訂單單位計算)</TableCell> : undefined}
{renderFieldCondition(SECOND_IN_FIELD) ? (
<TableCell align="center" sx={{ width: 360, minWidth: 360 }}>
{t("dnQty")}
<br />
({t("dnQtyOrderUnitHint")})
</TableCell>
) : undefined}
<TableCell align="center" sx={{ width: '100px' }}></TableCell>
</TableRow>
</TableHead>


+ 131
- 19
src/components/PoDetail/PoDetailRow.tsx Dosyayı Görüntüle

@@ -4,10 +4,13 @@ import { PurchaseOrderLine } from "@/app/api/po";
import {
Box,
Button,
InputAdornment,
Radio,
Stack,
TableCell,
TableRow,
TextField,
Typography,
alpha,
} from "@mui/material";
import { memo, useCallback, useEffect, useRef, useState } from "react";
@@ -16,6 +19,12 @@ import { decimalFormatter, integerFormatter } from "@/app/utils/formatUtil";
import { submitDialogWithWarning } from "../Swal/CustomAlerts";
import { createStockInLine } from "@/app/api/stockIn/actions";
import { previewPoBatchStockQty } from "./stockQtyRound";
import {
formatQtyWithPurchaseUom,
formatQtyWithStockUom,
poLineNeedsStockQtyConversion,
purchaseUomShortDesc,
} from "./poPurchaseUom";

const PURCHASE_STOCK_IN_ALERT_STATUSES = new Set(["pending", "receiving"]);

@@ -186,6 +195,27 @@ export const PoDetailRow = memo(function PoDetailRow({
Number(receivedTotalText.replace(/,/g, "")) <= 0 ? "red" : "inherit";
const needsStockInAttention =
canSeeStockInReminders && purchaseOrderLineHasIncompleteStockIn(row);
const uomShort = purchaseUomShortDesc(row.uom);
const orderQtyWithUnit = formatQtyWithPurchaseUom(row.qty, row.uom);
const enteredBatchQty = Number(dnQtyInput.trim());
const hasEnteredBatchQty = Number.isInteger(enteredBatchQty) && enteredBatchQty > 0;
const showStockConversion =
hasEnteredBatchQty &&
poLineNeedsStockQtyConversion(
Number(row.qty ?? 0),
Number(row.stockUom?.stockQty ?? 0),
row.uom,
row.stockUom,
);
const convertedStockQty = previewPoBatchStockQty(
Number(row.qty ?? 0),
Number(row.stockUom?.stockQty ?? 0),
enteredBatchQty,
);
const convertedStockQtyWithUnit = formatQtyWithStockUom(
convertedStockQty,
row.stockUom,
);

return (
<TableRow
@@ -243,7 +273,19 @@ export const PoDetailRow = memo(function PoDetailRow({
>
{row.itemName}
</TableCell>
<TableCell align="right">{integerFormatter.format(row.qty)}</TableCell>
<TableCell
align="right"
sx={{
fontWeight: 800,
fontVariantNumeric: "tabular-nums",
whiteSpace: "nowrap",
fontSize: "1.05rem",
color: "text.primary",
}}
title={row.uom?.udfudesc || uomShort || undefined}
>
{orderQtyWithUnit}
</TableCell>
<TableCell align="right">{integerFormatter.format(row.processed)}</TableCell>
<TableCell align="left">{row.uom?.udfudesc}</TableCell>
<TableCell sx={{ color: highlightColor }} align="right">
@@ -268,25 +310,95 @@ export const PoDetailRow = memo(function PoDetailRow({
/>
</TableCell>
{showDnQty ? (
<TableCell align="center">
<TextField
id={`dnQty-${row.id}`}
label="此批來貨數量"
type="text"
variant="outlined"
value={dnQtyInput}
onChange={(e) => setDnQtyInput(e.target.value.replace(/[^\d]/g, ""))}
onBlur={() => onInputBlur(row.id, lotNoInput, dnQtyInput)}
<TableCell align="center" sx={{ py: 1, overflow: "visible" }}>
<Stack
direction="row"
spacing={1.25}
alignItems="center"
justifyContent="center"
flexWrap="nowrap"
sx={{ width: "max-content" }}
onClick={(e) => e.stopPropagation()}
InputProps={{
inputProps: {
min: 1,
step: 1,
inputMode: "numeric",
pattern: "[0-9]*",
},
}}
/>
>
<TextField
id={`dnQty-${row.id}`}
label={t("dnQty")}
type="text"
variant="outlined"
value={dnQtyInput}
onChange={(e) => setDnQtyInput(e.target.value.replace(/[^\d]/g, ""))}
onBlur={() => onInputBlur(row.id, lotNoInput, dnQtyInput)}
onClick={(e) => e.stopPropagation()}
sx={{ minWidth: 132, width: 148, flexShrink: 0 }}
InputProps={{
endAdornment: uomShort ? (
<InputAdornment position="end">
<Typography
component="span"
sx={{
fontWeight: 800,
fontSize: "1.05rem",
color: "primary.main",
lineHeight: 1,
}}
>
{uomShort}
</Typography>
</InputAdornment>
) : undefined,
inputProps: {
min: 1,
step: 1,
inputMode: "numeric",
pattern: "[0-9]*",
"aria-label": `${t("dnQty")} ${uomShort}`.trim(),
},
}}
/>
{showStockConversion && Number.isFinite(convertedStockQty) && convertedStockQty > 0 ? (
<Box
sx={{
flexShrink: 0,
width: "max-content",
boxSizing: "border-box",
px: 1.25,
py: 0.75,
borderRadius: 1,
bgcolor: (theme) => alpha(theme.palette.primary.main, 0.1),
border: 1,
borderColor: "primary.main",
textAlign: "center",
}}
title={row.stockUom?.stockUomDesc || convertedStockQtyWithUnit}
>
<Typography
component="div"
sx={{
fontSize: "0.7rem",
lineHeight: 1.2,
color: "text.secondary",
fontWeight: 600,
}}
>
{t("stockQtyRef")}
</Typography>
<Typography
component="div"
sx={{
fontWeight: 800,
fontSize: "1rem",
lineHeight: 1.3,
color: "primary.main",
fontVariantNumeric: "tabular-nums",
whiteSpace: "nowrap",
px: 0.25,
}}
>
{convertedStockQtyWithUnit}
</Typography>
</Box>
) : null}
</Stack>
</TableCell>
) : null}
<TableCell align="center">


+ 4
- 3
src/components/PoDetail/PoInputGrid.tsx Dosyayı Görüntüle

@@ -61,7 +61,7 @@ import DoDisturbIcon from "@mui/icons-material/DoDisturb";
import { useSession } from "next-auth/react";
// import { SessionWithTokens } from "src/config/authConfig";
import QcStockInModal from "../Qc/QcStockInModal";
import { decimalFormatter, integerFormatter } from "@/app/utils/formatUtil";
import { decimalFormatter } from "@/app/utils/formatUtil";
import { PrinterCombo } from "@/app/api/settings/printer";
import { EscalationResult } from "@/app/api/escalation";
import { fetchEscalationLogsByStockInLines } from "@/app/api/escalation/actions";
@@ -70,10 +70,11 @@ import { EscalationCombo } from "@/app/api/user";
import { deleteDialog } from "../Swal/CustomAlerts";
import StockInLineRowActions from "./StockInLineRowActions";
import { StockQtyRoundMode, needsPoQcStockQtyRound } from "./stockQtyRound";
import { formatQtyWithPurchaseUom } from "./poPurchaseUom";

// 3 buttons after QC (view + print QR + delete): 176*3 + gap + cell padding
const ACTIONS_COLUMN_WIDTH = 580;
const PURCHASE_QTY_COLUMN_WIDTH = 72;
const PURCHASE_QTY_COLUMN_WIDTH = 96;
const UOM_COLUMN_WIDTH = 124;
const STOCK_QTY_COLUMN_WIDTH = 110;
const STOCK_IN_ROW_HEIGHT = 58;
@@ -719,7 +720,7 @@ function PoInputGrid({
type: "number",
renderCell: (params) => {
const qty = params.row.purchaseAcceptedQty ?? 0;
return integerFormatter.format(qty);
return formatQtyWithPurchaseUom(qty, itemDetail.uom);
},
},
{


+ 96
- 0
src/components/PoDetail/poPurchaseUom.ts Dosyayı Görüntüle

@@ -0,0 +1,96 @@
import { decimalFormatter, integerFormatter } from "@/app/utils/formatUtil";

type PurchaseUomLike = {
code?: string | null;
udfudesc?: string | null;
udfShortDesc?: string | null;
} | null | undefined;

type StockUomLike = {
stockUomCode?: string | null;
stockUomDesc?: string | null;
stockUomShortDesc?: string | null;
} | null | undefined;

function isCompactUnitLabel(value: string): boolean {
return value.length > 0 && value.length <= 6 && !/[x×]/i.test(value);
}

function compactUomLabel(
shortDesc?: string | null,
fullDesc?: string | null,
code?: string | null,
): string {
const short = shortDesc?.trim();
if (short) return short;
const full = fullDesc?.trim() ?? "";
if (isCompactUnitLabel(full)) return full;
const unitCode = code?.trim() ?? "";
if (isCompactUnitLabel(unitCode)) return unitCode;
return "";
}

function normUnitKey(...parts: Array<string | null | undefined>): string {
return parts.map((p) => p?.trim().toLowerCase()).find((p) => p) ?? "";
}

/** Prefer UoM short desc (箱). Do not return long packing strings like 150克X100包X1箱. */
export function purchaseUomShortDesc(uom: PurchaseUomLike): string {
return compactUomLabel(uom?.udfShortDesc, uom?.udfudesc, uom?.code);
}

export function stockUomShortLabel(stockUom: StockUomLike): string {
return compactUomLabel(
stockUom?.stockUomShortDesc,
stockUom?.stockUomDesc,
stockUom?.stockUomCode,
);
}

export function purchaseUnitDiffersFromStock(
uom: PurchaseUomLike,
stockUom: StockUomLike,
): boolean {
const purchaseKey = normUnitKey(uom?.code, uom?.udfShortDesc, uom?.udfudesc);
const stockKey = normUnitKey(
stockUom?.stockUomCode,
stockUom?.stockUomShortDesc,
stockUom?.stockUomDesc,
);
if (!purchaseKey || !stockKey) return false;
return purchaseKey !== stockKey;
}

export function poLineNeedsStockQtyConversion(
orderQty: number,
orderStockQty: number,
uom: PurchaseUomLike,
stockUom: StockUomLike,
): boolean {
if (purchaseUnitDiffersFromStock(uom, stockUom)) return true;
return (
Number.isFinite(orderQty) &&
Number.isFinite(orderStockQty) &&
orderQty > 0 &&
orderStockQty > 0 &&
Math.abs(orderStockQty - orderQty) > 1e-6
);
}

/** e.g. 384箱 */
export function formatQtyWithPurchaseUom(qty: number, uom: PurchaseUomLike): string {
const n = integerFormatter.format(qty);
const unit = purchaseUomShortDesc(uom);
return unit ? `${n}${unit}` : n;
}

/** e.g. 57,600克 */
export function formatQtyWithStockUom(qty: number, stockUom: StockUomLike): string {
const rounded = Math.round(qty);
const n =
Number.isFinite(qty) && Math.abs(qty - rounded) < 1e-9
? integerFormatter.format(rounded)
: decimalFormatter.format(qty);
const unit = stockUomShortLabel(stockUom) || stockUom?.stockUomDesc?.trim() || "";
return unit ? `${n}${unit}` : n;
}

+ 2
- 0
src/i18n/en/purchaseOrder.json Dosyayı Görüntüle

@@ -126,6 +126,8 @@
"Please scan warehouse qr code.": "Please scan warehouse QR code.",
"receivedQty": "Received Qty",
"dnQty": "Delivery Qty (This Batch)",
"dnQtyOrderUnitHint": "in order unit",
"stockQtyRef": "Stock",
"Accept submit": "Accept Delivery",
"qc processing": "Delivery & QC Processing",
"putaway processing": "Delivery & Put Away Processing",


+ 2
- 0
src/i18n/zh/purchaseOrder.json Dosyayı Görüntüle

@@ -126,6 +126,8 @@
"Please scan warehouse qr code.": "請掃描倉庫 QR 碼。",
"receivedQty": "已來貨數量",
"dnQty": "本批來貨數量",
"dnQtyOrderUnitHint": "以訂單單位計算",
"stockQtyRef": "庫存",
"Accept submit": "接受來貨",
"qc processing": "處理來貨及品檢",
"putaway processing": "處理來貨及上架",


Yükleniyor…
İptal
Kaydet