Bladeren bron

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 4 uur geleden
bovenliggende
commit
ab00f69c19
17 gewijzigde bestanden met toevoegingen van 499 en 320 verwijderingen
  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 Bestand weergeven

@@ -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 Bestand weergeven

@@ -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 Bestand weergeven

@@ -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 Bestand weergeven

@@ -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 Bestand weergeven

@@ -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 Bestand weergeven

@@ -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 Bestand weergeven

@@ -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 Bestand weergeven

@@ -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 Bestand weergeven

@@ -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 Bestand weergeven

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


+ 1
- 1
src/i18n/zh/items.json Bestand weergeven

@@ -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 Bestand weergeven

@@ -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 Bestand weergeven

@@ -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 Bestand weergeven

@@ -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 Bestand weergeven

@@ -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 Bestand weergeven

@@ -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 Bestand weergeven

@@ -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). */


Laden…
Annuleren
Opslaan