Parcourir la source

jo pick order resuggest reject W402 warehosue

consumable pickOrder new ui like do
consumable if user is 任文華, suggest and reusggest will reject W402 warehosue
production
CANCERYS\kw093 il y a 4 heures
Parent
révision
ab00f69c19
17 fichiers modifiés avec 499 ajouts et 320 suppressions
  1. +20
    -0
      src/app/api/pickOrder/actions.ts
  2. +281
    -301
      src/components/PickOrderSearch/WorkbenchPickExecution.tsx
  3. +99
    -2
      src/i18n/en/importBom.json
  4. +34
    -2
      src/i18n/en/masterDataIssue.json
  5. +2
    -2
      src/i18n/en/navigation.json
  6. +1
    -1
      src/i18n/zh/bomWeighting.json
  7. +1
    -1
      src/i18n/zh/common.json
  8. +1
    -0
      src/i18n/zh/do.json
  9. +1
    -1
      src/i18n/zh/doWorkbench.json
  10. +1
    -1
      src/i18n/zh/finishedgoodmanagement.json
  11. +1
    -1
      src/i18n/zh/items.json
  12. +3
    -1
      src/i18n/zh/jo.json
  13. +33
    -1
      src/i18n/zh/masterDataIssue.json
  14. +3
    -0
      src/i18n/zh/pickOrder.json
  15. +1
    -1
      src/i18n/zh/productionProcess.json
  16. +3
    -3
      src/i18n/zh/qcItemAll.json
  17. +14
    -2
      src/utils/workbenchPickLotUtils.ts

+ 20
- 0
src/app/api/pickOrder/actions.ts Voir le fichier

@@ -993,6 +993,26 @@ export const fetchConsumableWorkbenchPickOrderLotsHierarchical = cache(async (us
}
});

/** Bust pickorder cache then reload consumable workbench hierarchy (after scan-pick). */
export async function reloadConsumableWorkbenchPickOrderLotsHierarchical(userId: number): Promise<any> {
revalidateTag("pickorder");
try {
return await serverFetchJson<any>(
`${BASE_API_URL}/pickOrder/workbench/all-lots-hierarchical/${userId}`,
{
method: "GET",
next: { tags: ["pickorder"] },
},
);
} catch (error) {
console.error("❌ Error reloading consumable workbench hierarchical lot details:", error);
return {
fgInfo: null,
pickOrders: [],
};
}
}

/**
* Workbench assign: assign by delivery_order_pick_order id.
*/


+ 281
- 301
src/components/PickOrderSearch/WorkbenchPickExecution.tsx Voir le fichier

@@ -25,15 +25,13 @@ import { useSession } from "next-auth/react";
import { useTranslation } from "react-i18next";
import dayjs from "dayjs";
import arraySupport from "dayjs/plugin/arraySupport";
import SearchBox, { Criterion } from "../SearchBox";
import { OUTPUT_DATE_FORMAT, arrayToDayjs } from "@/app/utils/formatUtil";
import { SessionWithTokens } from "@/config/authConfig";
import {
fetchPickOrderWithStockClient,
fetchWorkbenchPickOrderLineDetailV2,
fetchConsumableWorkbenchPickOrderLotsHierarchical,
reloadConsumableWorkbenchPickOrderLotsHierarchical,
confirmLotSubstitution,
suggestPickOrderWorkbenchV2,
type PickOrderLotDetailResponse,
} from "@/app/api/pickOrder/actions";
import { workbenchScanPick } from "@/app/api/doworkbench/actions";
import { fetchStockInLineInfo } from "@/app/api/po/actions";
@@ -48,6 +46,7 @@ import {
isExpiredWorkbenchReminderMessage,
isInventoryLotLineUnavailable,
isLotAvailabilityExpired,
isNoLotWorkbenchRow,
isWorkbenchSourceLotExpired,
isWorkbenchZeroCompleteLot,
translateWorkbenchRejectMessage,
@@ -56,19 +55,30 @@ import {

dayjs.extend(arraySupport);

type TopRow = {
type LineRow = {
rowKey: string;
pickOrderId: number;
pickOrderLineId: number;
pickOrderCode: string;
itemCode: string;
itemName: string;
itemId?: number;
requiredQty: number;
currentStock: number;
pickedQty: number;
stockUnit: string;
targetDate: string | number[];
status: string;
lotsRaw: unknown[];
};

type PickOrderTopRow = {
rowKey: string;
pickOrderId: number;
pickOrderCode: string;
targetDate: string;
status: string;
lineCount: number;
completedLineCount: number;
lines: LineRow[];
};

type LotRow = {
@@ -80,11 +90,13 @@ type LotRow = {
itemName: string;
uomDesc: string;
requiredQty: number;
pickOrderLineRequiredQty?: number;
availableQty: number;
itemTotalAvailableQty?: number | null;
stockOutLineId: number;
status: string;
pickedQty: number;
stockOutLineQty?: number;
lotNo: string;
location: string;
itemId?: number;
@@ -93,6 +105,7 @@ type LotRow = {
lotAvailability?: string;
lotStatus?: string;
expiryDate?: string;
noLot?: boolean;
stockOutLineRejectMessage?: string;
};

@@ -235,53 +248,105 @@ function safeDisplayTargetDate(targetDate: string | number[]): string {
}
}

function lineHasStockOutOrSuggestion(details: PickOrderLotDetailResponse[]): boolean {
if (!details.length) return false;
return details.some((d) => {
const sol = toNum(d.stockOutLineId);
const spl = toNum(d.suggestedPickLotId);
return sol > 0 || spl > 0 || d.noLot === true;
function lineHierarchicalNeedsSuggest(line: LineRow): boolean {
if (isCompletedStatus(line.status)) return false;
if (!line.lotsRaw.length) return true;
return line.lotsRaw.every((lotRaw) => {
const lot = lotRaw as Record<string, unknown>;
return toNum(lot.stockOutLineId) <= 0;
});
}

function flattenLotsFromPickOrder(po: PickOrderTopRow): LotRow[] {
const rows: LotRow[] = [];
po.lines.forEach((line) => {
line.lotsRaw.forEach((lotRaw, i) => {
const lot = lotRaw as Record<string, unknown>;
const solId = toNum(lot.stockOutLineId);
const lotId = toNum(lot.id ?? lot.lotId, i);
const stockInLineId = toNum(lot.stockInLineId);
rows.push({
key: solId > 0 ? `sol:${solId}` : `line:${line.pickOrderLineId}:lot:${lotId}:${i}`,
pickOrderId: po.pickOrderId,
pickOrderLineId: line.pickOrderLineId,
pickOrderCode: po.pickOrderCode,
itemCode: line.itemCode,
itemName: line.itemName,
uomDesc: toStr(lot.stockUnit) || line.stockUnit,
requiredQty: toNum(lot.requiredQty, line.requiredQty),
pickOrderLineRequiredQty: line.requiredQty,
availableQty: toNum(lot.availableQty),
stockOutLineId: solId,
status: toStr(lot.stockOutLineStatus ?? lot.processingStatus ?? "pending"),
pickedQty: toNum(lot.actualPickQty ?? lot.stockOutLineQty),
stockOutLineQty: toNum(lot.stockOutLineQty ?? lot.actualPickQty),
lotNo: toStr(lot.lotNo),
location: toStr(lot.location),
itemId: line.itemId,
stockInLineId: stockInLineId > 0 ? stockInLineId : undefined,
suggestedPickLotId: toNum(lot.suggestedPickLotId) > 0 ? toNum(lot.suggestedPickLotId) : undefined,
lotAvailability: toStr(lot.lotAvailability),
lotStatus: toStr(lot.lotStatus),
expiryDate: toStr(lot.expiryDate),
noLot: lot.noLot === true || (solId > 0 && !toStr(lot.lotNo)),
});
});
});
return rows;
}

function sumLinePickedQtyFromLots(lots: unknown[]): number {
return lots.reduce<number>((acc, lot) => {
const row = lot as Record<string, unknown>;
const st = String(row?.stockOutLineStatus ?? row?.processingStatus ?? "").toLowerCase();
const counted =
st === "completed" ||
st === "complete" ||
st === "partially_completed" ||
st === "partially_complete";
if (!counted) return acc;
return acc + toNum(row?.actualPickQty ?? row?.stockOutLineQty);
}, 0);
}

function mapLotDetailsToRows(
details: PickOrderLotDetailResponse[],
ctx: {
pickOrderId: number;
pickOrderLineId: number;
pickOrderCode: string;
itemCode: string;
itemName: string;
totalAvailableQty?: number | null;
},
): LotRow[] {
return details.map((d, i) => {
const solId = toNum(d.stockOutLineId);
const lotId = toNum(d.lotId, i);
const stockInLineId = toNum(d.stockInLineId);
function mapHierarchicalToPickOrders(data: unknown): PickOrderTopRow[] {
const root = data as { pickOrders?: unknown[] };
const pos = Array.isArray(root?.pickOrders) ? root.pickOrders : [];
return pos.map((poRaw) => {
const po = poRaw as Record<string, unknown>;
const pickOrderId = toNum(po?.pickOrderId);
const pickOrderCode = toStr(po?.pickOrderCode);
const rawLines = Array.isArray(po?.pickOrderLines) ? po.pickOrderLines : [];
const lines: LineRow[] = rawLines.map((lineRaw, idx) => {
const line = lineRaw as Record<string, unknown>;
const item = (line?.item as Record<string, unknown>) || {};
const lots = Array.isArray(line?.lots) ? line.lots : [];
const pickOrderLineId = toNum(line?.id, idx);
return {
rowKey: `po:${pickOrderId}:line:${pickOrderLineId}`,
pickOrderId,
pickOrderLineId,
pickOrderCode,
itemCode: toStr(item?.code),
itemName: toStr(item?.name),
itemId: toNum(item?.id) || undefined,
requiredQty: toNum(line?.requiredQty),
pickedQty: sumLinePickedQtyFromLots(lots),
stockUnit: toStr(item?.uomDesc ?? item?.uomCode),
status: toStr(line?.status),
lotsRaw: lots,
};
});
const completedLineCount = lines.filter((l) => isCompletedStatus(l.status)).length;
return {
key: solId > 0 ? `sol:${solId}` : `lot:${lotId}:${i}`,
pickOrderId: ctx.pickOrderId,
pickOrderLineId: ctx.pickOrderLineId,
pickOrderCode: ctx.pickOrderCode,
itemCode: ctx.itemCode,
itemName: ctx.itemName,
uomDesc: toStr(d.stockUnit),
requiredQty: toNum(d.requiredQty),
availableQty: toNum(d.remainingAfterAllPickOrders ?? d.availableQty),
itemTotalAvailableQty: toNum(ctx.totalAvailableQty),
stockOutLineId: solId,
status: toStr(d.stockOutLineStatus ?? "pending"),
pickedQty: toNum(d.actualPickQty ?? d.stockOutLineQty),
lotNo: toStr(d.lotNo),
location: toStr(d.location),
itemId: toNum(d.itemId) || undefined,
stockInLineId: stockInLineId > 0 ? stockInLineId : undefined,
suggestedPickLotId: toNum(d.suggestedPickLotId) || undefined,
lotAvailability: toStr((d as any).lotAvailability),
lotStatus: toStr((d as any).lotStatus),
expiryDate: toStr((d as any).expiryDate),
stockOutLineRejectMessage: toStr((d as any).stockOutLineRejectMessage),
rowKey: `po:${pickOrderId}`,
pickOrderId,
pickOrderCode,
targetDate: toStr(po?.targetDate ?? ""),
status: toStr(po?.status),
lineCount: lines.length,
completedLineCount,
lines,
};
});
}
@@ -291,29 +356,21 @@ const WorkbenchPickExecution: React.FC<Props> = ({ filterArgs }) => {
const { data: session } = useSession() as { data: SessionWithTokens | null };
const userId = session?.id ? parseInt(session.id, 10) : 0;

const [originalTopRows, setOriginalTopRows] = useState<TopRow[]>([]);
const [filteredTopRows, setFilteredTopRows] = useState<TopRow[]>([]);
const [pickOrders, setPickOrders] = useState<PickOrderTopRow[]>([]);
const [pickOrderLoading, setPickOrderLoading] = useState(false);
const [pagingController, setPagingController] = useState({ pageNum: 1, pageSize: 10 });
const [totalCountItems, setTotalCountItems] = useState(0);
const [poPagingController, setPoPagingController] = useState({ pageNum: 1, pageSize: 10 });
const [totalCountPickOrders, setTotalCountPickOrders] = useState(0);
const localizeBackendMessage = (msg: unknown, fallbackKey: string) => {
const text = typeof msg === "string" ? msg.trim() : "";
if (!text) return t(fallbackKey);
return t(text, { defaultValue: text });
};
const [selectedPickOrderLineId, setSelectedPickOrderLineId] = useState<number | null>(null);
const [selectedPickOrderId, setSelectedPickOrderId] = useState<number | null>(null);
const [selectedTopMeta, setSelectedTopMeta] = useState<{
pickOrderCode: string;
itemCode: string;
itemName: string;
totalAvailableQty?: number;
} | null>(null);

const [lotRows, setLotRows] = useState<LotRow[]>([]);
const [qtyBySolId, setQtyBySolId] = useState<Record<number, number>>({});
const [qtyEditableBySolId, setQtyEditableBySolId] = useState<Record<number, boolean>>({});
const [lotPagingController, setLotPagingController] = useState({ pageNum: 0, pageSize: 10 });
const [lotPagingController, setLotPagingController] = useState({ pageNum: 0, pageSize: -1 });
const [loading, setLoading] = useState(false);
const [submittingSolId, setSubmittingSolId] = useState<number | null>(null);
const [message, setMessage] = useState("");
@@ -341,12 +398,13 @@ const WorkbenchPickExecution: React.FC<Props> = ({ filterArgs }) => {

const { values: qrValues, isScanning, startScan, resetScan } = useQrCodeScannerContext();

const paginatedTopRows = useMemo(() => {
const start = (pagingController.pageNum - 1) * pagingController.pageSize;
return filteredTopRows.slice(start, start + pagingController.pageSize);
}, [filteredTopRows, pagingController]);
const paginatedPickOrders = useMemo(() => {
const start = (poPagingController.pageNum - 1) * poPagingController.pageSize;
return pickOrders.slice(start, start + poPagingController.pageSize);
}, [pickOrders, poPagingController]);

const paginatedLotRows = useMemo(() => {
if (lotPagingController.pageSize === -1) return lotRows;
const start = lotPagingController.pageNum * lotPagingController.pageSize;
return lotRows.slice(start, start + lotPagingController.pageSize);
}, [lotRows, lotPagingController]);
@@ -388,57 +446,25 @@ const WorkbenchPickExecution: React.FC<Props> = ({ filterArgs }) => {
return { byItemId, byStockInLineId, activeLotsByItemId };
}, [lotRows]);

const fetchNewPageItems = useCallback(
async (paging: { pageNum: number; pageSize: number }, extra: Record<string, unknown>) => {
if (!userId) return;
const loadHierarchicalPickOrders = useCallback(
async (reload = false) => {
if (!userId) return [] as PickOrderTopRow[];
setPickOrderLoading(true);
setError("");
try {
const params = {
...extra,
pageNum: 0,
pageSize: 9999,
status: "released",
type: "consumable",
assignTo: userId,
};
const res = await fetchPickOrderWithStockClient(params);
const records = Array.isArray(res?.records) ? res.records : [];
const rows: TopRow[] = records.flatMap((r: any) => {
const pickOrderId = toNum(r?.id);
const code = toStr(r?.code);
const status = toStr(r?.status);
const targetDate = r?.targetDate;
const lines = Array.isArray(r?.pickOrderLines) ? r.pickOrderLines : [];
return lines.map((line: any, idx: number) => ({
rowKey: `po:${pickOrderId}:line:${toNum(line?.id, idx)}`,
pickOrderId,
pickOrderLineId: toNum(line?.id),
pickOrderCode: code,
itemCode: toStr(line?.itemCode),
itemName: toStr(line?.itemName),
requiredQty: toNum(line?.requiredQty),
currentStock: toNum(line?.availableQty),
pickedQty: toNum(line?.pickedQty),
stockUnit: toStr(line?.uomDesc ?? line?.uomShortDesc),
targetDate: targetDate ?? "",
status,
}));
});
setOriginalTopRows(rows);
setFilteredTopRows(rows);
const pageSize = paging.pageSize || 10;
const pageNum = paging.pageNum || 1;
setTotalCountItems(rows.length);
setPagingController({ pageNum, pageSize });
return rows;
const data = reload
? await reloadConsumableWorkbenchPickOrderLotsHierarchical(userId)
: await fetchConsumableWorkbenchPickOrderLotsHierarchical(userId);
const orders = mapHierarchicalToPickOrders(data);
setPickOrders(orders);
setTotalCountPickOrders(orders.length);
return orders;
} catch (e) {
console.error(e);
setError(t("Load released pick orders failed"));
setOriginalTopRows([]);
setFilteredTopRows([]);
setTotalCountItems(0);
return [] as TopRow[];
setPickOrders([]);
setTotalCountPickOrders(0);
return [] as PickOrderTopRow[];
} finally {
setPickOrderLoading(false);
}
@@ -446,125 +472,62 @@ const WorkbenchPickExecution: React.FC<Props> = ({ filterArgs }) => {
[t, userId],
);

const refreshReleasedTopRowsAfterMutation = useCallback(async () => {
const latestRows =
(await fetchNewPageItems(
pagingController,
(filterArgs || {}) as Record<string, unknown>,
)) || [];
if (
selectedPickOrderLineId != null &&
!latestRows.some((r) => r.pickOrderLineId === selectedPickOrderLineId)
) {
setSelectedPickOrderLineId(null);
setSelectedPickOrderId(null);
setSelectedTopMeta(null);
setLotRows([]);
setQtyBySolId({});
setQtyEditableBySolId({});
setLotPagingController({ pageNum: 0, pageSize: 10 });
}
}, [fetchNewPageItems, filterArgs, pagingController, selectedPickOrderLineId]);

const searchCriteria: Criterion<any>[] = useMemo(
() => [
{ label: t("Item Code"), paramName: "itemCode", type: "text" },
{ label: t("Pick Order Code"), paramName: "pickOrderCode", type: "text" },
{ label: t("Item Name"), paramName: "itemName", type: "text" },
{ label: t("Target Date From"), label2: t("Target Date To"), paramName: "targetDate", type: "dateRange" },
],
[t],
);

const handleSearch = useCallback(
(query: Record<string, string>) => {
const filtered = originalTopRows.filter((row) => {
const itemCodeMatch = !query.itemCode || row.itemCode.toLowerCase().includes(query.itemCode.toLowerCase());
const pickOrderCodeMatch =
!query.pickOrderCode || row.pickOrderCode.toLowerCase().includes(query.pickOrderCode.toLowerCase());
const itemNameMatch = !query.itemName || row.itemName.toLowerCase().includes(query.itemName.toLowerCase());
const targetDate = Array.isArray(row.targetDate)
? arrayToDayjs(row.targetDate)
: dayjs(typeof row.targetDate === "string" ? row.targetDate : "");
let dateMatch = true;
if (query.targetDate || query.targetDateTo) {
const fromDate = query.targetDate ? dayjs(query.targetDate) : null;
const toDate = query.targetDateTo ? dayjs(query.targetDateTo) : null;
if (targetDate.isValid()) {
if (fromDate && fromDate.isValid()) dateMatch = dateMatch && (targetDate.isSame(fromDate, "day") || targetDate.isAfter(fromDate, "day"));
if (toDate && toDate.isValid()) dateMatch = dateMatch && (targetDate.isSame(toDate, "day") || targetDate.isBefore(toDate, "day"));
}
}
return itemCodeMatch && pickOrderCodeMatch && itemNameMatch && dateMatch;
});
setFilteredTopRows(filtered);
setTotalCountItems(filtered.length);
setPagingController((prev) => ({ ...prev, pageNum: 1 }));
},
[originalTopRows],
);

const handleReset = useCallback(() => {
setFilteredTopRows(originalTopRows);
setTotalCountItems(originalTopRows.length);
setPagingController((prev) => ({ ...prev, pageNum: 1 }));
}, [originalTopRows]);
const applyLotsFromPickOrder = useCallback((po: PickOrderTopRow) => {
setLotRows(flattenLotsFromPickOrder(po));
setQtyEditableBySolId({});
}, []);

useEffect(() => {
if (userId) fetchNewPageItems(pagingController, (filterArgs || {}) as Record<string, unknown>);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [userId, filterArgs, fetchNewPageItems]);

const loadLineDetailV2 = useCallback(
async (
pickOrderId: number,
pickOrderLineId: number,
meta: {
pickOrderCode: string;
itemCode: string;
itemName: string;
totalAvailableQty?: number;
},
) => {
if (!userId || pickOrderLineId <= 0) return;
const ensurePickOrderLotsIfNeeded = useCallback(
async (po: PickOrderTopRow) => {
if (!userId || !po.lines.some(lineHierarchicalNeedsSuggest)) return;
setLoading(true);
setError("");
setMessage("");
try {
let details = await fetchWorkbenchPickOrderLineDetailV2(pickOrderLineId);
let list = Array.isArray(details) ? details : [];
if (!lineHasStockOutOrSuggestion(list)) {
const suggestRes = await suggestPickOrderWorkbenchV2(pickOrderId, userId);
if (suggestRes.code !== "SUCCESS") {
setError(t("Suggest pick failed"));
setLotRows([]);
return;
}
details = await fetchWorkbenchPickOrderLineDetailV2(pickOrderLineId);
list = Array.isArray(details) ? details : [];
const suggestRes = await suggestPickOrderWorkbenchV2(po.pickOrderId, userId);
if (suggestRes.code !== "SUCCESS") {
setError(t("Suggest pick failed"));
return;
}
setLotRows(
mapLotDetailsToRows(list, {
pickOrderId,
pickOrderLineId,
pickOrderCode: meta.pickOrderCode,
itemCode: meta.itemCode,
itemName: meta.itemName,
totalAvailableQty: meta.totalAvailableQty,
}),
);
setQtyEditableBySolId({});
const data = await reloadConsumableWorkbenchPickOrderLotsHierarchical(userId);
const orders = mapHierarchicalToPickOrders(data);
setPickOrders(orders);
setTotalCountPickOrders(orders.length);
const refreshed = orders.find((p) => p.pickOrderId === po.pickOrderId);
if (refreshed) applyLotsFromPickOrder(refreshed);
} catch (e) {
console.error(e);
setError(t("Load workbench data failed"));
setLotRows([]);
} finally {
setLoading(false);
}
},
[t, userId],
[applyLotsFromPickOrder, t, userId],
);

const refreshWorkbenchAfterMutation = useCallback(async () => {
const latestOrders = (await loadHierarchicalPickOrders(true)) || [];
if (
selectedPickOrderId != null &&
!latestOrders.some((p) => p.pickOrderId === selectedPickOrderId)
) {
setSelectedPickOrderId(null);
setLotRows([]);
setQtyBySolId({});
setQtyEditableBySolId({});
setLotPagingController({ pageNum: 0, pageSize: -1 });
return;
}
if (selectedPickOrderId != null) {
const po = latestOrders.find((p) => p.pickOrderId === selectedPickOrderId);
if (po) applyLotsFromPickOrder(po);
}
}, [applyLotsFromPickOrder, loadHierarchicalPickOrders, selectedPickOrderId]);

useEffect(() => {
if (userId) void loadHierarchicalPickOrders(false);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [userId, filterArgs, loadHierarchicalPickOrders]);

const submitRow = useCallback(
async (row: LotRow, forceQty?: number) => {
if (!userId) return;
@@ -605,13 +568,10 @@ const WorkbenchPickExecution: React.FC<Props> = ({ filterArgs }) => {
setQrScanError(false);
setQrScanSuccess(true);
});
if (selectedPickOrderId != null && selectedPickOrderLineId != null && selectedTopMeta) {
await loadLineDetailV2(selectedPickOrderId, selectedPickOrderLineId, selectedTopMeta);
}
setWorkbenchLotLabelModalOpen(false);
setWorkbenchLotLabelContextLot(null);
setWorkbenchLotLabelInitialPayload(null);
await refreshReleasedTopRowsAfterMutation();
await refreshWorkbenchAfterMutation();
} catch (e) {
console.error(e);
setError(t("Scan pick failed"));
@@ -624,7 +584,7 @@ const WorkbenchPickExecution: React.FC<Props> = ({ filterArgs }) => {
setSubmittingSolId(null);
}
},
[qtyBySolId, loadLineDetailV2, refreshReleasedTopRowsAfterMutation, selectedPickOrderId, selectedPickOrderLineId, selectedTopMeta, t, userId],
[qtyBySolId, refreshWorkbenchAfterMutation, t, userId],
);

const hasQtyOverrideBySolId = useCallback(
@@ -638,6 +598,15 @@ const WorkbenchPickExecution: React.FC<Props> = ({ filterArgs }) => {
if (typeof override === "number" && Number.isFinite(override) && override >= 0) {
return override;
}
const st = String(lot.status || "").toLowerCase();
if (
st === "completed" ||
st === "partially_completed" ||
st === "partially_complete"
) {
return Number(lot.pickedQty ?? lot.stockOutLineQty ?? 0);
}
if (isWorkbenchZeroCompleteLot(lot)) return 0;
return Number(lot.requiredQty) || 0;
},
[qtyBySolId],
@@ -657,6 +626,14 @@ const WorkbenchPickExecution: React.FC<Props> = ({ filterArgs }) => {
const resolveLockedSubmitQtyDisplay = useCallback(
(lot: LotRow): number => {
if (isWorkbenchZeroCompleteLot(lot)) return 0;
const st = String(lot.status || "").toLowerCase();
if (
st === "completed" ||
st === "partially_completed" ||
st === "partially_complete"
) {
return Number(lot.pickedQty ?? lot.stockOutLineQty ?? 0);
}
return resolveSingleSubmitQty(lot);
},
[resolveSingleSubmitQty],
@@ -686,6 +663,8 @@ const WorkbenchPickExecution: React.FC<Props> = ({ filterArgs }) => {

const lotNo = String(row.lotNo || "").trim();
const isZeroComplete = isWorkbenchZeroCompleteLot(row);
const isNoLot = isNoLotWorkbenchRow(row);
const isUnavailable = isInventoryLotLineUnavailable(row);
const hasExplicitOverride = hasQtyOverrideBySolId(row.stockOutLineId);
const explicitQty = hasExplicitOverride ? Number(qtyBySolId[row.stockOutLineId]) : NaN;
const qtyPayload = workbenchScanPickQtyFromLot(row);
@@ -693,6 +672,8 @@ const WorkbenchPickExecution: React.FC<Props> = ({ filterArgs }) => {

const canPostScanPick =
isZeroComplete ||
isNoLot ||
isUnavailable ||
(lotNo !== "" &&
((hasExplicitOverride && Number.isFinite(explicitQty) && explicitQty === 0) ||
(wbJustQty != null && wbJustQty > 0)));
@@ -709,7 +690,10 @@ const WorkbenchPickExecution: React.FC<Props> = ({ filterArgs }) => {
}

const qtyToSend =
isZeroComplete || (hasExplicitOverride && Number.isFinite(explicitQty) && explicitQty === 0)
isZeroComplete ||
isNoLot ||
isUnavailable ||
(hasExplicitOverride && Number.isFinite(explicitQty) && explicitQty === 0)
? 0
: Number(wbJustQty);

@@ -718,41 +702,28 @@ const WorkbenchPickExecution: React.FC<Props> = ({ filterArgs }) => {
[hasQtyOverrideBySolId, qtyBySolId, submitRow, t, workbenchScanPickQtyFromLot],
);

const handleLineSelect = useCallback(
async (row: TopRow, checked: boolean) => {
const handlePickOrderSelect = useCallback(
async (row: PickOrderTopRow, checked: boolean) => {
if (!checked) {
if (selectedPickOrderLineId === row.pickOrderLineId) {
setSelectedPickOrderLineId(null);
if (selectedPickOrderId === row.pickOrderId) {
setSelectedPickOrderId(null);
setSelectedTopMeta(null);
setLotRows([]);
setQtyBySolId({});
setQtyEditableBySolId({});
setLotPagingController({ pageNum: 0, pageSize: 10 });
setLotPagingController({ pageNum: 0, pageSize: -1 });
}
return;
}
setSelectedPickOrderLineId(row.pickOrderLineId);
setSelectedPickOrderId(row.pickOrderId);
setSelectedTopMeta({
pickOrderCode: row.pickOrderCode,
itemCode: row.itemCode,
itemName: row.itemName,
totalAvailableQty: row.currentStock,
});
setLotRows([]);
setQtyBySolId({});
setQtyEditableBySolId({});
setLotPagingController({ pageNum: 0, pageSize: 10 });
setLotPagingController({ pageNum: 0, pageSize: -1 });
setMessage("");
await loadLineDetailV2(row.pickOrderId, row.pickOrderLineId, {
pickOrderCode: row.pickOrderCode,
itemCode: row.itemCode,
itemName: row.itemName,
totalAvailableQty: row.currentStock,
});
applyLotsFromPickOrder(row);
await ensurePickOrderLotsIfNeeded(row);
},
[loadLineDetailV2, selectedPickOrderLineId],
[applyLotsFromPickOrder, ensurePickOrderLotsIfNeeded, selectedPickOrderId],
);

const openWorkbenchLotLabelModalForLot = useCallback(
@@ -854,20 +825,15 @@ const WorkbenchPickExecution: React.FC<Props> = ({ filterArgs }) => {
if (res.code !== "SUCCESS") {
throw new Error((res.message as string) || t("Scan pick failed"));
}
if (selectedPickOrderId != null && selectedPickOrderLineId != null && selectedTopMeta) {
await loadLineDetailV2(selectedPickOrderId, selectedPickOrderLineId, selectedTopMeta);
}
await refreshWorkbenchAfterMutation();
setWorkbenchLotLabelModalOpen(false);
setWorkbenchLotLabelReminderText(null);
setWorkbenchLotLabelContextLot(null);
setWorkbenchLotLabelInitialPayload(null);
},
[
loadLineDetailV2,
refreshWorkbenchAfterMutation,
resolveLockedSubmitQtyDisplay,
selectedPickOrderId,
selectedPickOrderLineId,
selectedTopMeta,
t,
userId,
workbenchLotLabelContextLot,
@@ -1047,10 +1013,7 @@ const WorkbenchPickExecution: React.FC<Props> = ({ filterArgs }) => {
setQrScanSuccess(true);
setQrScanSuccessMsg(t("Scan pick success"));
});
if (selectedPickOrderId != null && selectedPickOrderLineId != null && selectedTopMeta) {
await loadLineDetailV2(selectedPickOrderId, selectedPickOrderLineId, selectedTopMeta);
}
await refreshReleasedTopRowsAfterMutation();
await refreshWorkbenchAfterMutation();
clearLotConfirmationState(true);
} catch (e) {
console.error(e);
@@ -1069,12 +1032,8 @@ const WorkbenchPickExecution: React.FC<Props> = ({ filterArgs }) => {
[
clearLotConfirmationState,
expectedLotData,
loadLineDetailV2,
refreshReleasedTopRowsAfterMutation,
refreshWorkbenchAfterMutation,
scannedLotData,
selectedPickOrderId,
selectedPickOrderLineId,
selectedTopMeta,
t,
userId,
workbenchScanPickQtyFromLot,
@@ -1347,11 +1306,11 @@ const WorkbenchPickExecution: React.FC<Props> = ({ filterArgs }) => {
}, [isScanning, startScan, userId]);

useEffect(() => {
if (!selectedPickOrderLineId) {
if (!selectedPickOrderId) {
lastProcessedQrRef.current = "";
processedQrCodesRef.current.clear();
}
}, [selectedPickOrderLineId]);
}, [selectedPickOrderId]);

useEffect(() => {
if (!qrValues.length || lotRows.length === 0) return;
@@ -1443,7 +1402,9 @@ const WorkbenchPickExecution: React.FC<Props> = ({ filterArgs }) => {
<Stack spacing={2}>
<Paper variant="outlined" sx={{ p: 2 }}>
<Stack spacing={1}>
<SearchBox criteria={searchCriteria} onSearch={handleSearch} onReset={handleReset} />
<Typography variant="subtitle2" color="text.secondary">
{t("Pick Orders")}
</Typography>
<Grid container rowGap={1}>
<Grid item xs={12}>
{pickOrderLoading ? (
@@ -1455,41 +1416,33 @@ const WorkbenchPickExecution: React.FC<Props> = ({ filterArgs }) => {
<TableRow>
<TableCell>{t("Selected")}</TableCell>
<TableCell>{t("Pick Order Code")}</TableCell>
<TableCell>{t("Item Code")}</TableCell>
<TableCell>{t("Item Name")}</TableCell>
<TableCell align="right">{t("Order Quantity")}</TableCell>
<TableCell align="right">{t("Current Stock")}</TableCell>
<TableCell align="right">{t("Picked Qty")}</TableCell>
<TableCell>{t("Stock Unit")}</TableCell>
<TableCell>{t("Pick Order Lines")}</TableCell>
<TableCell>{t("Target Date")}</TableCell>
<TableCell>{t("Status")}</TableCell>
</TableRow>
</TableHead>
<TableBody>
{paginatedTopRows.length === 0 ? (
{paginatedPickOrders.length === 0 ? (
<TableRow>
<TableCell colSpan={10}>
<TableCell colSpan={5}>
<Typography variant="body2" color="text.secondary">
{t("No data available")}
</Typography>
</TableCell>
</TableRow>
) : (
paginatedTopRows.map((row) => (
paginatedPickOrders.map((row) => (
<TableRow key={row.rowKey}>
<TableCell padding="checkbox">
<Checkbox
checked={selectedPickOrderLineId === row.pickOrderLineId}
onChange={(_, checked) => void handleLineSelect(row, checked)}
checked={selectedPickOrderId === row.pickOrderId}
onChange={(_, checked) => void handlePickOrderSelect(row, checked)}
/>
</TableCell>
<TableCell>{row.pickOrderCode || "-"}</TableCell>
<TableCell>{row.itemCode || "-"}</TableCell>
<TableCell>{row.itemName || "-"}</TableCell>
<TableCell align="right">{row.requiredQty.toLocaleString()}</TableCell>
<TableCell align="right">{row.currentStock.toLocaleString()}</TableCell>
<TableCell align="right">{row.pickedQty.toLocaleString()}</TableCell>
<TableCell>{row.stockUnit || "-"}</TableCell>
<TableCell>
{row.completedLineCount}/{row.lineCount}
</TableCell>
<TableCell>{safeDisplayTargetDate(row.targetDate)}</TableCell>
<TableCell>{t(row.status || "-")}</TableCell>
</TableRow>
@@ -1503,12 +1456,14 @@ const WorkbenchPickExecution: React.FC<Props> = ({ filterArgs }) => {
<Grid item xs={12}>
<TablePagination
component="div"
count={totalCountItems}
page={pagingController.pageNum - 1}
rowsPerPage={pagingController.pageSize}
onPageChange={(_e, newPage) => setPagingController((prev) => ({ ...prev, pageNum: newPage + 1 }))}
count={totalCountPickOrders}
page={poPagingController.pageNum - 1}
rowsPerPage={poPagingController.pageSize}
onPageChange={(_e, newPage) =>
setPoPagingController((prev) => ({ ...prev, pageNum: newPage + 1 }))
}
onRowsPerPageChange={(e) =>
setPagingController({
setPoPagingController({
pageNum: 1,
pageSize: parseInt(e.target.value, 10),
})
@@ -1560,6 +1515,7 @@ const WorkbenchPickExecution: React.FC<Props> = ({ filterArgs }) => {
? Number(qtyBySolId[r.stockOutLineId])
: lockedSubmitQty;
const rowStatus = String(r.status || "").toLowerCase();
const isNoLot = isNoLotWorkbenchRow(r);
const isRowRejected =
rowStatus === "rejected" || String(r.lotAvailability || "").toLowerCase() === "rejected";
const isRowExpired =
@@ -1568,17 +1524,15 @@ const WorkbenchPickExecution: React.FC<Props> = ({ filterArgs }) => {

return (
<TableRow key={r.key}>
<TableCell>{idx === 0 ? lotPagingController.pageNum * lotPagingController.pageSize + 1 : ""}</TableCell>
<TableCell>
{idx === 0 ? (
<>
{r.itemCode || "-"} <br />
{r.itemName || "-"} <br />
{r.uomDesc || "-"}
</>
) : (
""
)}
{lotPagingController.pageSize === -1
? idx + 1
: lotPagingController.pageNum * lotPagingController.pageSize + idx + 1}
</TableCell>
<TableCell>
{r.itemCode || "-"} <br />
{r.itemName || "-"} <br />
{r.uomDesc || "-"}
</TableCell>
<TableCell>{r.location || "-"}</TableCell>
<TableCell>
@@ -1608,7 +1562,11 @@ const WorkbenchPickExecution: React.FC<Props> = ({ filterArgs }) => {
r.lotNo
)
) : (
"-"
<Box component="span" sx={{ fontSize: "0.85rem", lineHeight: 1.4 }}>
{t(
"Please check around have QR code or not, may be have just now stock in or transfer in or transfer out.",
)}
</Box>
)}
</Typography>
{r.stockOutLineId > 0 ? (
@@ -1657,6 +1615,27 @@ const WorkbenchPickExecution: React.FC<Props> = ({ filterArgs }) => {
/>
);
}
if (
isNoLot &&
(rowStatus === "partially_completed" ||
rowStatus === "partially_complete" ||
rowStatus === "completed")
) {
return (
<Checkbox
checked
disabled
size="small"
sx={{
color: "error.main",
"&.Mui-checked": { color: "error.main" },
}}
/>
);
}
if (isNoLot) {
return null;
}
return (
<Checkbox
checked={isCompletedStatus(r.status) || isCheckedStatus(r.status)}
@@ -1759,7 +1738,7 @@ const WorkbenchPickExecution: React.FC<Props> = ({ filterArgs }) => {
<TableRow>
<TableCell colSpan={9} align="center" sx={{ textAlign: "center" }}>
<Typography variant="body2" color="text.secondary" align="center">
{t("No lot rows. Select a line in the table above.")}
{t("No lot rows. Select a pick order above.")}
</Typography>
</TableCell>
</TableRow>
@@ -1773,13 +1752,14 @@ const WorkbenchPickExecution: React.FC<Props> = ({ filterArgs }) => {
page={lotPagingController.pageNum}
rowsPerPage={lotPagingController.pageSize}
onPageChange={(_e, newPage) => setLotPagingController((prev) => ({ ...prev, pageNum: newPage }))}
onRowsPerPageChange={(e) =>
onRowsPerPageChange={(e) => {
const newPageSize = parseInt(e.target.value, 10);
setLotPagingController({
pageNum: 0,
pageSize: parseInt(e.target.value, 10),
})
}
rowsPerPageOptions={[10, 25, 50]}
pageSize: newPageSize === -1 ? -1 : newPageSize,
});
}}
rowsPerPageOptions={[10, 25, 50, -1]}
labelRowsPerPage={t("Rows per page")}
/>
</Paper>


+ 99
- 2
src/i18n/en/importBom.json Voir le fichier

@@ -10,11 +10,81 @@
"Is Drink": "Is Drink",
"Drink": "Drink",
"Powder_Mixture": "Powder Mixture",
"Other": "Other",
"Product Type": "Product type",
"Material count": "Materials",
"Process count": "Processes",
"Preview load failed": "Failed to load preview data",
"Review import data": "Review import data",
"Attribute scales": "Attribute scales",
"No materials parsed": "No materials parsed",
"No processes parsed": "No processes parsed",
"Import code required": "Item code is required",
"Import name required": "Item name is required",
"Import output qty required": "Output quantity must be greater than 0",
"Material item code required": "Material item code is required",
"Material uom required": "Material recipe UOM is required",
"Material qty required": "Material recipe quantity must be greater than 0",
"Material item not in master": "Material item {{code}} does not exist",
"Join step not in process list": "Join step must match an existing process sequence",
"Allergic has": "Has allergen",
"Allergic none": "No allergen",
"No scale data": "No scale data",
"Back to reselect files": "Back to reselect files",
"Upload check summary": "Uploaded {{uploaded}} file(s). Check result: {{total}} total — {{correct}} passed, {{failed}} failed",
"Upload count mismatch hint": "Upload count does not match check count; duplicate file names may have been renamed with _2, _3, etc.",
"Search file name": "Search file name",
"Search code or name": "Search code or name",
"Format check issues": "Format check issues",
"Review and fix": "Review and fix",
"Issue count": "{{count}} issue(s)",
"Revalidate failed": "Re-validation failed",
"Download corrected excel": "Download corrected Excel",
"Downloading...": "Downloading…",
"Download corrected excel failed": "Failed to download corrected Excel",
"Confirm import": "Confirm import",
"Download issue log": "Download issue log Excel",
"Import completed": "Import completed",
"Import failed": "Import failed. See console for details.",
"None": "None",
"Import summary prefix": "Will import {{count}} BOM(s)",
"Import summary wip": "; {{wip}} also as WIP",
"Import summary drink": "; {{drink}} drink",
"Import summary powder": "; {{powder}} powder mixture",
"Import summary other": "; {{other}} other",
"WIP": "WIP",
"Bom Kind Mode": "Kind",
"Bom Kind FG": "FG",
"Bom Kind WIP": "WIP",
"Bom Kind Both": "both",
"File Name": "File Name",
"Base Score": "Base Score",
"BOM Status": "BOM Status",
"BOM Status Active": "Active",
"BOM Status Inactive": "Inactive",
"Save Status": "Save Status",
"BOM Revision": "Revision",
"Version Compare": "Version compare",
"Version": "Version",
"Compare Version": "Compare version",
"End Compare": "End compare",
"Old Version": "Old version",
"New Version": "New version",
"Loading versions...": "Loading versions...",
"Loading compare...": "Loading compare...",
"Select two different versions to compare": "Select two different versions to compare",
"Field": "Field",
"Material Changes": "Material changes",
"Old Recipe Qty": "Old recipe qty",
"New Recipe Qty": "New recipe qty",
"Old Base Qty": "Old base qty",
"New Base Qty": "New base qty",
"Recipe Qty": "Recipe qty",
"Recipe UOM": "Recipe UOM",
"Edit Materials": "Edit materials",
"Save New Version": "Save as new version",
"Material edit creates new version hint": "Editing material qty/UOM creates a new version and activates it. Status-only changes do not create a new version.",
"Content": "Content",
"Saving...": "Saving...",
"Code": "BOM Code",
"Name": "BOM Name",
@@ -23,12 +93,14 @@
"Type": "Type",
"Loading BOM Detail...": "Loading BOM Detail...",
"Basic Info": "Basic Info",
"Edit Basic Info": "Edit basic info",
"Edit": "Edit",
"Loading...": "Loading...",
"Save": "Save",
"Cancel": "Cancel",
"Allergic Substances": "Allergic Substances",
"Depth": "Depth",
"Depth": "Color depth",
"Depth hint": "Dark 1 → Light 5",
"Float": "Float",
"Density": "Density",
"Time Sequence": "Time Sequence",
@@ -42,6 +114,15 @@
"Stock UOM": "Stock UOM",
"Sales Qty": "Sales Qty",
"Sales UOM": "Sales UOM",
"Join Step": "Join step",
"Putaway Location": "Putaway location",
"Putaway Floor": "Floor",
"Putaway Warehouse": "Warehouse",
"Putaway Area": "Area",
"Putaway Slot": "Slot",
"Item Location Code": "Item location code",
"Putaway location item hint": "QC auto putaway still uses item master LocationCode; this field is stored per BOM revision.",
"Putaway edit creates new version hint": "Changing putaway location creates a new version and updates the item master location.",
"Process & Equipment": "Process & Equipment",
"Process Code": "Process Code",
"Process Description": "Process Description",
@@ -51,5 +132,21 @@
"Post Prod Time (Minutes)": "Post Prod Time (Minutes)",
"Add": "Add",
"Sequence": "Sequence",
"Actions": "Actions"
"Actions": "Actions",
"Edit Processes": "Edit processes",
"Byproduct": "Byproduct",
"Equipment": "Equipment",
"Equipment Description": "Equipment description",
"Equipment Name": "Equipment name",
"Not applicable": "Not applicable",
"Packaging bags": "Packaging bags",
"Select process": "Select process",
"Load process master failed": "Failed to load process/equipment master data",
"Process code required": "Process code is required on every line",
"Process not in master": "Process {{code}} is not in master data",
"Equipment description and name both required": "Equipment description and name must both be set or both empty",
"Equipment not in master": "Equipment \"{{pair}}\" is not in master data",
"Bag item not in master": "Bag item {{code}} is not in master data",
"Byproduct item not in master": "Byproduct item {{code}} does not exist",
"Item code": "Item code"
}

+ 34
- 2
src/i18n/en/masterDataIssue.json Voir le fichier

@@ -5,7 +5,13 @@
"masterDataIssue_BOM_MATERIAL_MISSING_ITEM": "masterDataIssue_BOM_MATERIAL_MISSING_ITEM",
"masterDataIssue_BOM_MATERIAL_SALES_UOM_MISMATCH": "masterDataIssue_BOM_MATERIAL_SALES_UOM_MISMATCH",
"masterDataIssue_BOM_MATERIAL_STOCK_UOM_MISMATCH": "masterDataIssue_BOM_MATERIAL_STOCK_UOM_MISMATCH",
"masterDataIssue_BOM_MATERIAL_UOM_FK_INVALID": "masterDataIssue_BOM_MATERIAL_UOM_FK_INVALID",
"masterDataIssue_BOM_MATERIAL_UOM_FK_INVALID": "BOM material UOM reference invalid or deleted",
"masterDataIssue_BOM_ITEM_CODE_MISMATCH": "BOM code does not match linked item",
"masterDataIssue_BOM_ITEM_NAME_MISMATCH": "BOM product name does not match linked item",
"masterDataIssue_col_linked_item": "Linked item",
"masterDataIssue_col_product_name": "Product name",
"masterDataIssue_line_itemCodeMismatch": "{{bom}}: should link item \"{{expected}}\", actually \"{{actual}}\"",
"masterDataIssue_line_itemNameMismatch": "{{bom}}: BOM/Excel name \"{{actual}}\", item master \"{{expected}}\"",
"masterDataIssue_BOM_OUTPUT_UOM_MISMATCH_SALES": "masterDataIssue_BOM_OUTPUT_UOM_MISMATCH_SALES",
"masterDataIssue_BOM_OUTPUT_UOM_TEXT_DRIFT": "masterDataIssue_BOM_OUTPUT_UOM_TEXT_DRIFT",
"masterDataIssue_DELETED_BASE_UOM": "masterDataIssue_DELETED_BASE_UOM",
@@ -97,5 +103,31 @@
"masterDataIssue_unit_sales": "masterDataIssue_unit_sales",
"masterDataIssue_unit_stock": "masterDataIssue_unit_stock",
"masterDataIssue_usedInBom": "masterDataIssue_usedInBom",
"masterDataIssue_viewDetail": "masterDataIssue_viewDetail"
"masterDataIssue_viewDetail": "masterDataIssue_viewDetail",
"masterDataIssue_align_preview": "Preview fix (align to M18)",
"masterDataIssue_align_row": "Align to M18",
"masterDataIssue_align_title": "Preview: align BOM UOM to M18",
"masterDataIssue_align_info_m18": "If M18 item master is correct: use this to align BOM units and quantities. Header output qty defaults unchanged; material sale/stock/base qty recalculate via item_uom when edited. Recipe qty is independent in v1.",
"masterDataIssue_align_info_excel": "If BOM Excel is correct: do not apply this fix; update item UOM in M18 sync instead.",
"masterDataIssue_align_summary": "Fixable: {{headers}} header(s) · {{materials}} material(s) · {{skipped}} skipped",
"masterDataIssue_align_tab_headers": "BOM header ({{count}})",
"masterDataIssue_align_tab_materials": "BOM materials ({{count}})",
"masterDataIssue_align_tab_skipped": "Skipped ({{count}})",
"masterDataIssue_align_none_headers": "No fixable BOM headers.",
"masterDataIssue_align_none_materials": "No fixable BOM materials.",
"masterDataIssue_align_field": "Field",
"masterDataIssue_align_after": "After fix",
"masterDataIssue_align_outputQty": "Output qty",
"masterDataIssue_align_recipeQty": "Recipe qty",
"masterDataIssue_align_confirm": "I confirm M18 item master is the source of truth",
"masterDataIssue_align_apply": "Apply fix",
"masterDataIssue_align_applying": "Applying…",
"masterDataIssue_align_loadFailed": "Failed to load alignment preview.",
"masterDataIssue_align_applyFailed": "Apply failed. Please try again.",
"masterDataIssue_align_not_set": "Not set",
"masterDataIssue_align_warn_1to1": "BOM sales unit cannot be converted via M18 (likely a legacy Excel import). Sale qty was prefilled 1:1 — please verify before applying.",
"masterDataIssue_align_warn_bom_qty_null": "BOM qty not set on the left ({{fields}}). Adopt the M18 values on the right, or fix Excel and re-import.",
"masterDataIssue_align_warn_field_sale": "sale qty",
"masterDataIssue_align_warn_field_stock": "stock qty",
"masterDataIssue_align_warn_field_base": "base qty"
}

+ 2
- 2
src/i18n/en/navigation.json Voir le fichier

@@ -43,7 +43,7 @@
"nav.settings.qcCategory": "QC Category",
"nav.settings.qcItemAll": "QC Item All",
"nav.settings.shopAndTruck": "Shop And Truck",
"nav.settings.deliveryOrderFloor": "DO floor (supplier)",
"nav.settings.deliveryOrderFloor": "DO floor settings",
"nav.settings.demandForecast": "Demand Forecast Setting",
"nav.settings.bomWeighting": "BOM Weighting Score List",
"nav.settings.masterDataIssues": "BOM / Item UOM Issues",
@@ -67,7 +67,7 @@
"nav.breadcrumb.qcItemAll": "QC Item All",
"nav.breadcrumb.qrCodeHandle": "QR Code Handle",
"nav.breadcrumb.demandForecast": "Demand Forecast Setting",
"nav.breadcrumb.deliveryOrderFloor": "Delivery Order Floor",
"nav.breadcrumb.deliveryOrderFloor": "Delivery Order Floor Settings",
"nav.breadcrumb.masterDataIssues": "BOM / Item UOM Issues",
"nav.breadcrumb.equipment": "Equipment",
"nav.breadcrumb.equipmentMaintenanceEdit": "Maintenance Edit",


+ 1
- 1
src/i18n/zh/bomWeighting.json Voir le fichier

@@ -7,6 +7,6 @@
"Edit BOM Weighting Score": "編輯 BOM 權重得分",
"Scoring Item": "評分項目",
"Range": "範圍",
"Item Code": "品編號",
"Item Code": "品編號",
"Item Name": "物品名稱"
}

+ 1
- 1
src/i18n/zh/common.json Voir le fichier

@@ -45,7 +45,7 @@
"Invoice": "發票",
"Invoice Date": "發票日期",
"IP": "IP 地址",
"Item Code": "品編號",
"Item Code": "品編號",
"Item Name": "物品名稱",
"Loading": "載入中...",
"Loading order summary": "正在載入訂單摘要",


+ 1
- 0
src/i18n/zh/do.json Voir le fichier

@@ -77,6 +77,7 @@
"Source DO code is required": "請輸入原送貨單編號",
"Source DO must be completed": "原送貨單須為已送貨狀態",
"Source DO not found": "找不到原送貨單",
"Merge Extra ticket": "合併加單",
"Submit": "提交",
"Target DO": "目標送貨單",
"This item is already in the draft list": "此物品已在待提交列表中",


+ 1
- 1
src/i18n/zh/doWorkbench.json Voir le fichier

@@ -12,7 +12,7 @@
"Delivery Order": "送貨訂單",
"Edit Delivery Order Detail": "編輯送貨訂單詳情",
"DO Workbench Search": "DO Workbench 搜索",
"Item Code": "品編號",
"Item Code": "品編號",
"Item Name": "物品名稱",
"Lot No": "批號",
"Location": "位置",


+ 1
- 1
src/i18n/zh/finishedgoodmanagement.json Voir le fichier

@@ -1,7 +1,7 @@
{
"Finished Good Management": "成品出倉管理",
"提料順序": "提料順序",
"Item Code": "品編號",
"Item Code": "品編號",
"Item Name": "物品名稱",
"Search & Jump": "搜索並跳轉",
"Jump": "跳轉",


+ 1
- 1
src/i18n/zh/items.json Voir le fichier

@@ -60,7 +60,7 @@
"Item": "貨品",
"Code or name": "編號或名稱",
"Product": "物品",
"Item Code": "品編號",
"Item Code": "品編號",
"Item Name": "物品名稱",
"Sales Qty": "銷售數量",
"Sales UOM": "銷售單位",


+ 3
- 1
src/i18n/zh/jo.json Voir le fichier

@@ -173,8 +173,10 @@
"Is Float": "浮沉",
"Issue": "問題",
"Issue Remark": "問題備註",
"Update Target Production Date": "更新目標生產日期",
"Update Required Quantity": "更新需求數量",
"Item": "成品/半成品",
"Item Code": "物品編號",
"Item Code": "品編號",
"Item Name": "物品名稱",
"Item already exists in created items": "物品已存在於創建的物品中",
"Items": "物品",


+ 33
- 1
src/i18n/zh/masterDataIssue.json Voir le fichier

@@ -53,6 +53,12 @@
"masterDataIssue_BOM_MATERIAL_BASE_UOM_MISMATCH": "BOM 原料基本單位與貨品主檔不一致",
"masterDataIssue_BOM_MATERIAL_STOCK_UOM_MISMATCH": "BOM 原料庫存單位與貨品主檔不一致",
"masterDataIssue_BOM_MATERIAL_UOM_FK_INVALID": "BOM 原料 UOM 參照無效或已刪除",
"masterDataIssue_BOM_ITEM_CODE_MISMATCH": "BOM 編號與關聯貨品不一致",
"masterDataIssue_BOM_ITEM_NAME_MISMATCH": "BOM 產品名稱與關聯貨品不一致",
"masterDataIssue_col_linked_item": "關聯貨品",
"masterDataIssue_col_product_name": "產品名稱",
"masterDataIssue_line_itemCodeMismatch": "{{bom}}:應關聯貨品「{{expected}}」,實際為「{{actual}}」",
"masterDataIssue_line_itemNameMismatch": "{{bom}}:Excel/BOM 名稱為「{{actual}}」,貨品主檔為「{{expected}}」",
"masterDataIssue_group_count": "共 {{groups}} 筆 · {{issues}} 項問題",
"masterDataIssue_col_subject": "主體",
"masterDataIssue_col_summary": "問題摘要",
@@ -97,5 +103,31 @@
"masterDataIssue_line_pairBoth": "銷售/庫存單位應為「{{expected}}」,BOM 為「{{actual}}」",
"masterDataIssue_line_pairSales": "銷售單位應為「{{expected}}」,BOM 為「{{actual}}」",
"masterDataIssue_line_pairStock": "庫存單位應為「{{expected}}」,BOM 為「{{actual}}」",
"masterDataIssue_line_pairBase": "基本單位應為「{{expected}}」,BOM 為「{{actual}}」"
"masterDataIssue_line_pairBase": "基本單位應為「{{expected}}」,BOM 為「{{actual}}」",
"masterDataIssue_align_preview": "預覽修正(對齊 M18)",
"masterDataIssue_align_row": "對齊 M18",
"masterDataIssue_align_title": "預覽:BOM 單位對齊 M18",
"masterDataIssue_align_info_m18": "若 M18 貨品主檔為準:可使用本功能將 BOM 單位與數量對齊 M18。BOM 表頭產出數量預設不變;原料銷售/庫存/基本數量修改後將按 item_uom 換算聯動。配方用量可單獨修改,與銷售/庫存/基本無聯動。",
"masterDataIssue_align_info_excel": "若 BOM Excel 才是正確來源:請勿使用本功能修改 BOM,應在 M18 同步中更新該貨品的銷售/庫存單位。",
"masterDataIssue_align_summary": "可修正:BOM 表頭 {{headers}} 筆 · 原料 {{materials}} 筆 · 略過 {{skipped}} 筆",
"masterDataIssue_align_tab_headers": "BOM 總表 ({{count}})",
"masterDataIssue_align_tab_materials": "BOM 原材料 ({{count}})",
"masterDataIssue_align_tab_skipped": "無法修正 ({{count}})",
"masterDataIssue_align_none_headers": "沒有可修正的 BOM 表頭。",
"masterDataIssue_align_none_materials": "沒有可修正的 BOM 原料。",
"masterDataIssue_align_field": "欄位",
"masterDataIssue_align_after": "修正後",
"masterDataIssue_align_outputQty": "產出數量",
"masterDataIssue_align_recipeQty": "配方用量",
"masterDataIssue_align_confirm": "我已確認應以 M18 貨品主檔為準",
"masterDataIssue_align_apply": "確認套用",
"masterDataIssue_align_applying": "套用中…",
"masterDataIssue_align_loadFailed": "無法載入修正預覽。",
"masterDataIssue_align_applyFailed": "套用失敗,請稍後再試。",
"masterDataIssue_align_not_set": "未設定",
"masterDataIssue_align_warn_1to1": "BOM 銷售單位無法依 M18 換算(可能為舊 Excel 匯入錯誤單位),已暫以 1:1 預填銷售數量,請確認後再套用。",
"masterDataIssue_align_warn_bom_qty_null": "BOM 左側數量未設定({{fields}})。可採用右側 M18 建議值,或修正 Excel 後重匯。",
"masterDataIssue_align_warn_field_sale": "銷售數量",
"masterDataIssue_align_warn_field_stock": "庫存數量",
"masterDataIssue_align_warn_field_base": "基本數量"
}

+ 3
- 0
src/i18n/zh/pickOrder.json Voir le fichier

@@ -2,6 +2,8 @@
"Purchase Order": "採購訂單",
"Code": "編號",
"Pick Order Code": "提料單編號",
"Workbench SOL completed with qty=0 (no inventory posting)": "工作台採購訂單已完成",
"No lot rows. Select a pick order above": "沒有批次行。請選擇一個提料單。",
"Item Code": "貨品編號",
"OrderDate": "下單日期",
"Details": "詳情",
@@ -10,6 +12,7 @@
"N/A": "不適用",
"Release Pick Orders": "放單",
"released": "已放單",
"No lot rows. Select a pick order above.": "沒有批次行。請選擇一個提料單。",
"Loading...": "載入中...",
"Suggestion success": "建議成功",
"Scan pick success": "掃描提料成功",


+ 1
- 1
src/i18n/zh/productionProcess.json Voir le fichier

@@ -68,7 +68,7 @@
"Invalid Stock In Line Id": "無效庫存行ID",
"Is Dark | Dense | Float| Scrap Rate| Allergic Substance | Time Sequence | Complexity": "顔色深淺度 | 濃淡 | 浮沉 | 損耗率 | 過敏原 | 時間次序 | 複雜度",
"Item": "物品",
"Item Code": "品編號",
"Item Code": "品編號",
"Item Name": "物品名稱",
"Job Details": "工單編號及生產產品",
"Job Order": "工單",


+ 3
- 3
src/i18n/zh/qcItemAll.json Voir le fichier

@@ -19,10 +19,10 @@
"Qc Item": "品檢項目",
"Item": "物品",
"Item code not found": "物品不存在",
"Error validating item code": "驗證品編號時發生錯誤",
"Error validating item code": "驗證品編號時發生錯誤",
"Category": "模板",
"Category Type": "模板類型",
"Enter item code to validate": "請輸入品編號以驗證",
"Enter item code to validate": "請輸入品編號以驗證",
"Code": "編號",
"Name": "名稱",
"Description": "描述",
@@ -61,7 +61,7 @@
"Select Qc Category": "選擇品檢模板",
"Select Qc Item": "選擇品檢項目",
"Select Type": "選擇類型",
"Item Code": "品編號",
"Item Code": "品編號",
"Item Name": "產品名稱",
"Qc Category Code": "品檢模板編號",
"Qc Category Name": "品檢模板名稱",


+ 14
- 2
src/utils/workbenchPickLotUtils.ts Voir le fichier

@@ -51,11 +51,23 @@ export function isWorkbenchSourceLotExpired(
return false;
}

/** 過期或不可用:單筆 Just Complete / 顯示數量與批量提交一致,固定 qty=0 */
/** 無批號缺口列(stock_out_line 未綁定 inventory lot line) */
export function isNoLotWorkbenchRow(
lot: WorkbenchPickLotLike | null | undefined,
): boolean {
if (!lot) return false;
return lot.noLot === true || !lot.lotNo || String(lot.lotNo).trim() === "";
}

/** 過期、不可用或無批號:單筆 Just Complete / 顯示數量與批量提交一致,固定 qty=0 */
export function isWorkbenchZeroCompleteLot(
lot: WorkbenchPickLotLike | null | undefined,
): boolean {
return isLotAvailabilityExpired(lot) || isInventoryLotLineUnavailable(lot);
return (
isLotAvailabilityExpired(lot) ||
isInventoryLotLineUnavailable(lot) ||
isNoLotWorkbenchRow(lot)
);
}

/** Backend messages with dynamic ids — map prefix to i18n key (pickOrder namespace). */


Chargement…
Annuler
Enregistrer