Procházet zdrojové kódy

po/do/jo settings

controllQRcodescanWarehouse
CANCERYS\kw093 před 3 dny
rodič
revize
d564c2a6ad
12 změnil soubory, kde provedl 2057 přidání a 118 odebrání
  1. +1
    -1
      src/app/(main)/settings/deliveryOrderFloor/page.tsx
  2. +221
    -0
      src/app/api/settings/deliveryOrderFloor/client.ts
  3. +136
    -1
      src/app/api/settings/deliveryOrderFloor/constants.ts
  4. +1393
    -74
      src/components/DeliveryOrderFloorSettings/DeliveryOrderFloorSettings.tsx
  5. +1
    -0
      src/components/DoWorkbench/WorkbenchGoodPickExecutionDetail.tsx
  6. +58
    -8
      src/components/DoWorkbench/WorkbenchLotLabelPrintModal.tsx
  7. +1
    -0
      src/components/JoWorkbench/newJobPickExecution.tsx
  8. +44
    -26
      src/components/Qc/QcStockInModal.tsx
  9. +99
    -2
      src/i18n/en/deliveryOrderFloor.json
  10. +2
    -2
      src/i18n/en/navigation.json
  11. +99
    -2
      src/i18n/zh/deliveryOrderFloor.json
  12. +2
    -2
      src/i18n/zh/navigation.json

+ 1
- 1
src/app/(main)/settings/deliveryOrderFloor/page.tsx Zobrazit soubor

@@ -4,7 +4,7 @@ import { Stack, Typography } from "@mui/material";
import { Metadata } from "next";

export const metadata: Metadata = {
title: "Delivery order floor",
title: "Pick rules",
};

export default async function DeliveryOrderFloorPage() {


+ 221
- 0
src/app/api/settings/deliveryOrderFloor/client.ts Zobrazit soubor

@@ -3,8 +3,33 @@
import { clientAuthFetch } from "@/app/utils/clientAuthFetch";
import { NEXT_PUBLIC_API_URL } from "@/config/api";
import {
DEFAULT_AUTO_PUTAWAY_ITEM_KEYWORDS,
DEFAULT_AUTO_PUTAWAY_WAREHOUSE,
DEFAULT_DO_PICK_EXCLUDE_WAREHOUSES,
DEFAULT_JO_AUTO_PUTAWAY_BOM_TYPES,
DEFAULT_JO_PICK_EXCLUDE_WAREHOUSES,
EMPTY_CODE_LIST_MARKER,
SETTING_DO_FLOOR_SUPPLIERS_2F,
SETTING_DO_FLOOR_SUPPLIERS_4F,
SETTING_DO_PICK_EXCLUDE_PRINT_MODE,
SETTING_DO_PICK_EXCLUDE_WAREHOUSES,
SETTING_JO_AUTO_PUTAWAY_BOM_TYPES,
SETTING_JO_AUTO_PUTAWAY_ITEM_KEYWORDS,
SETTING_JO_AUTO_PUTAWAY_RULES,
SETTING_JO_AUTO_PUTAWAY_WAREHOUSE,
SETTING_JO_PICK_EXCLUDE_PRINT_MODE,
SETTING_JO_PICK_EXCLUDE_WAREHOUSES,
SETTING_PO_AUTO_PUTAWAY_BOM_TYPES,
SETTING_PO_AUTO_PUTAWAY_ITEM_KEYWORDS,
SETTING_PO_AUTO_PUTAWAY_RULES,
SETTING_PO_AUTO_PUTAWAY_WAREHOUSE,
defaultJoAutoPutAwayRules,
defaultPoAutoPutAwayRules,
type AutoPutAwayException,
type AutoPutAwayRule,
type AutoPutAwayScope,
type ExcludePrintMode,
type PutAwayLocationMode,
} from "./constants";

const base = NEXT_PUBLIC_API_URL;
@@ -17,6 +42,12 @@ export type ShopComboRow = {
label: string;
};

export type WarehousePickRow = ShopComboRow & {
storeId: string;
warehouse: string;
area: string;
};

export type SettingsRow = {
id: number;
name: string;
@@ -50,15 +81,205 @@ export async function fetchAllSettingsClient(): Promise<SettingsRow[]> {
return parseJson<SettingsRow[]>(res);
}

function resolveExcludeCsv(raw: string | undefined, fallback: string): string {
if (raw == null) return fallback;
const trimmed = raw.trim();
if (!trimmed || trimmed === EMPTY_CODE_LIST_MARKER) return "";
return trimmed;
}

export async function fetchWarehouseCodeRowsClient(): Promise<WarehousePickRow[]> {
const res = await clientAuthFetch(`${base}/warehouse`, { method: "GET" });
const rows = await parseJson<
Array<{
id?: number;
code?: string | null;
name?: string | null;
store_id?: string | null;
warehouse?: string | null;
area?: string | null;
}>
>(res);
return rows
.map((r) => {
const code = (r.code ?? "").trim();
const name = (r.name ?? "").trim();
const parts = code.split("-");
return {
id: r.id ?? 0,
code,
name,
value: r.id ?? 0,
label: [code, name].filter(Boolean).join(" "),
storeId: (r.store_id ?? "").trim() || parts[0] || "",
warehouse: (r.warehouse ?? "").trim() || parts[1] || "",
area: (r.area ?? "").trim() || parts[2] || "",
};
})
.filter((r) => r.code);
}

function resolvePrintMode(raw: string | undefined): ExcludePrintMode {
return raw?.trim() === "hideQr" ? "hideQr" : "hideList";
}

/** Missing row keeps the historical default. Saved `-` or blank turns that value off. */
function resolveOptionalCsv(raw: string | undefined, fallbackWhenMissing: string): string {
if (raw == null) return fallbackWhenMissing;
const trimmed = raw.trim();
if (!trimmed || trimmed === EMPTY_CODE_LIST_MARKER) return "";
return trimmed;
}

function asLocationMode(value: unknown): PutAwayLocationMode {
return value === "itemLocation" ? "itemLocation" : "warehouse";
}

function normalizeException(value: unknown): AutoPutAwayException | null {
if (!value || typeof value !== "object") return null;
const row = value as Partial<AutoPutAwayException>;
return {
itemKeywords: String(row.itemKeywords ?? "").trim(),
};
}

function normalizeRule(value: unknown, scope: AutoPutAwayScope): AutoPutAwayRule | null {
if (!value || typeof value !== "object") return null;
const row = value as Partial<AutoPutAwayRule> & { listMode?: string };
const rawExceptions = Array.isArray(row.exceptions) ? row.exceptions : [];
return {
itemKeywords: String(row.itemKeywords ?? "").trim(),
bomTypes: scope === "jo" ? String(row.bomTypes ?? "").trim() : "",
locationMode: asLocationMode(row.locationMode),
warehouseCode: String(row.warehouseCode ?? "").trim() || DEFAULT_AUTO_PUTAWAY_WAREHOUSE,
enabled: row.enabled !== false && row.listMode !== "blacklist",
exceptions: rawExceptions
.map((item) => normalizeException(item))
.filter((item): item is AutoPutAwayException => item != null),
};
}

function legacyAutoPutAwayRules(
scope: AutoPutAwayScope,
warehouseCode: string,
itemKeywords: string,
bomTypes: string,
): AutoPutAwayRule[] {
const warehouse = warehouseCode || DEFAULT_AUTO_PUTAWAY_WAREHOUSE;
if (scope === "po") {
return [
{
itemKeywords: itemKeywords || DEFAULT_AUTO_PUTAWAY_ITEM_KEYWORDS,
bomTypes: "",
locationMode: "warehouse",
warehouseCode: warehouse,
enabled: true,
exceptions: [],
},
];
}
const boms = bomTypes
.split(",")
.map((part) => part.trim().toUpperCase())
.filter(Boolean);
const rules: AutoPutAwayRule[] = [];
if (boms.includes("FG")) {
rules.push({
itemKeywords: "",
bomTypes: "FG",
locationMode: "itemLocation",
warehouseCode: warehouse,
enabled: true,
exceptions: [],
});
}
const otherBoms = boms.filter((bom) => bom !== "FG");
if (otherBoms.length > 0) {
rules.push({
itemKeywords: "",
bomTypes: otherBoms.join(","),
locationMode: "warehouse",
warehouseCode: warehouse,
enabled: true,
exceptions: [],
});
}
return rules.length > 0 ? rules : defaultJoAutoPutAwayRules();
}

/** Drop the seeded JO item-code FA rule. Job orders use BOM rules only. */
function withoutSeededJoFaRule(rules: AutoPutAwayRule[]): AutoPutAwayRule[] {
if (rules.length !== 3) return rules;
const [fg, wip, fa] = rules;
const isFg = !fg.itemKeywords.trim() && fg.bomTypes.trim().toUpperCase() === "FG" && fg.locationMode === "itemLocation";
const isWip = !wip.itemKeywords.trim() && wip.bomTypes.trim().toUpperCase() === "WIP" && wip.locationMode === "warehouse";
const isFa =
fa.itemKeywords.trim().toUpperCase() === "FA" &&
!fa.bomTypes.trim() &&
fa.locationMode === "warehouse";
return isFg && isWip && isFa ? rules.slice(0, 2) : rules;
}

function resolveAutoPutAwayRules(
raw: (name: string) => string | undefined,
scope: AutoPutAwayScope,
): AutoPutAwayRule[] {
const rulesKey = scope === "po" ? SETTING_PO_AUTO_PUTAWAY_RULES : SETTING_JO_AUTO_PUTAWAY_RULES;
const stored = raw(rulesKey);
if (stored != null) {
const trimmed = stored.trim();
if (!trimmed || trimmed === EMPTY_CODE_LIST_MARKER) return [];
try {
const parsed = JSON.parse(trimmed) as unknown;
if (Array.isArray(parsed)) {
const rules = parsed
.map((row) => normalizeRule(row, scope))
.filter((row): row is AutoPutAwayRule => row != null);
return scope === "jo" ? withoutSeededJoFaRule(rules) : rules;
}
} catch {
// Fall through to legacy columns.
}
}
if (stored == null) {
const warehouseKey = scope === "po" ? SETTING_PO_AUTO_PUTAWAY_WAREHOUSE : SETTING_JO_AUTO_PUTAWAY_WAREHOUSE;
const keywordKey = scope === "po" ? SETTING_PO_AUTO_PUTAWAY_ITEM_KEYWORDS : SETTING_JO_AUTO_PUTAWAY_ITEM_KEYWORDS;
const bomKey = scope === "po" ? SETTING_PO_AUTO_PUTAWAY_BOM_TYPES : SETTING_JO_AUTO_PUTAWAY_BOM_TYPES;
const hasLegacy = raw(warehouseKey) != null || raw(keywordKey) != null || raw(bomKey) != null;
if (hasLegacy) {
return legacyAutoPutAwayRules(
scope,
resolveOptionalCsv(raw(warehouseKey), DEFAULT_AUTO_PUTAWAY_WAREHOUSE),
resolveOptionalCsv(raw(keywordKey), DEFAULT_AUTO_PUTAWAY_ITEM_KEYWORDS),
resolveOptionalCsv(raw(bomKey), scope === "jo" ? DEFAULT_JO_AUTO_PUTAWAY_BOM_TYPES : ""),
);
}
}
return scope === "po" ? defaultPoAutoPutAwayRules() : defaultJoAutoPutAwayRules();
}

export async function fetchDoFloorSettingsClient(): Promise<{
suppliers2F: string;
suppliers4F: string;
doExcludeWarehouses: string;
joExcludeWarehouses: string;
doExcludePrintMode: ExcludePrintMode;
joExcludePrintMode: ExcludePrintMode;
poAutoPutAway: AutoPutAwayRule[];
joAutoPutAway: AutoPutAwayRule[];
}> {
const all = await fetchAllSettingsClient();
const get = (name: string) => all.find((s) => s.name === name)?.value ?? "";
const raw = (name: string) => all.find((s) => s.name === name)?.value;
return {
suppliers2F: get(SETTING_DO_FLOOR_SUPPLIERS_2F),
suppliers4F: get(SETTING_DO_FLOOR_SUPPLIERS_4F),
doExcludeWarehouses: resolveExcludeCsv(raw(SETTING_DO_PICK_EXCLUDE_WAREHOUSES), DEFAULT_DO_PICK_EXCLUDE_WAREHOUSES),
joExcludeWarehouses: resolveExcludeCsv(raw(SETTING_JO_PICK_EXCLUDE_WAREHOUSES), DEFAULT_JO_PICK_EXCLUDE_WAREHOUSES),
doExcludePrintMode: resolvePrintMode(raw(SETTING_DO_PICK_EXCLUDE_PRINT_MODE)),
joExcludePrintMode: resolvePrintMode(raw(SETTING_JO_PICK_EXCLUDE_PRINT_MODE)),
poAutoPutAway: resolveAutoPutAwayRules(raw, "po"),
joAutoPutAway: resolveAutoPutAwayRules(raw, "jo"),
};
}



+ 136
- 1
src/app/api/settings/deliveryOrderFloor/constants.ts Zobrazit soubor

@@ -2,4 +2,139 @@
export const SETTING_DO_FLOOR_SUPPLIERS_2F = "DO.floor.suppliers.2F";
export const SETTING_DO_FLOOR_SUPPLIERS_4F = "DO.floor.suppliers.4F";

export const SETTING_DO_FLOOR_CATEGORY = "DO_FLOOR";
/** 逗號分隔倉庫 code。存 `-` 代表不排除任何倉。 */
export const SETTING_DO_PICK_EXCLUDE_WAREHOUSES = "DO.pick.excludeWarehouses";
export const SETTING_JO_PICK_EXCLUDE_WAREHOUSES = "JO.pick.excludeWarehouses";
export const SETTING_DO_PICK_EXCLUDE_PRINT_MODE = "DO.pick.excludePrintMode";
export const SETTING_JO_PICK_EXCLUDE_PRINT_MODE = "JO.pick.excludePrintMode";
/** hideList:清單不出現,掃到實物 QR 也不通過。hideQr:清單仍顯示但不顯示 QR,掃到實物 QR 可以通過。 */
export type ExcludePrintMode = "hideList" | "hideQr";
export const EMPTY_CODE_LIST_MARKER = "-";

export const DEFAULT_DO_PICK_EXCLUDE_WAREHOUSES = "2F-W202-01-00,2F-W200-#A-00";
export const DEFAULT_JO_PICK_EXCLUDE_WAREHOUSES =
"4F-W402-01-00,4F-W402-02-00,4F-W402-03-00,4F-W402-04-00,4F-W402-05-00,4F-W402-#A-00,4F-W402-#B-00,4F-W402-#C-00,4F-W402-#D-00,4F-W402-#E-00,4F-W402-#F-00,4F-W402-#G-00,4F-W402-#H-00,4F-W402-#I-00,4F-W402-#J-00,4F-W402-#K-00,4F-W402-#L-00,4F-W402-#M-00,4F-W402-#N-00,4F-W402-#O-00,4F-W402-#P-00,4F-W402-#Q-00,4F-W402-#R-00,4F-W402-#S-00";

export const SETTING_DO_FLOOR_CATEGORY = "DO_FLOOR";

/** 舊版單筆欄位。新資料改存 rules JSON。 */
export const SETTING_PO_AUTO_PUTAWAY_WAREHOUSE = "PO.autoPutAway.warehouse";
export const SETTING_PO_AUTO_PUTAWAY_ITEM_KEYWORDS = "PO.autoPutAway.itemKeywords";
export const SETTING_PO_AUTO_PUTAWAY_BOM_TYPES = "PO.autoPutAway.bomTypes";
export const SETTING_JO_AUTO_PUTAWAY_WAREHOUSE = "JO.autoPutAway.warehouse";
export const SETTING_JO_AUTO_PUTAWAY_ITEM_KEYWORDS = "JO.autoPutAway.itemKeywords";
export const SETTING_JO_AUTO_PUTAWAY_BOM_TYPES = "JO.autoPutAway.bomTypes";
export const SETTING_PO_AUTO_PUTAWAY_RULES = "PO.autoPutAway.rules";
export const SETTING_JO_AUTO_PUTAWAY_RULES = "JO.autoPutAway.rules";

/** Warehouse id 1141. */
export const DEFAULT_AUTO_PUTAWAY_WAREHOUSE = "2F-W200-#A-00";
export const DEFAULT_AUTO_PUTAWAY_ITEM_KEYWORDS = "FA";
export const DEFAULT_JO_AUTO_PUTAWAY_BOM_TYPES = "WIP,FG";

export type AutoPutAwayScope = "po" | "jo";
export type PutAwayLocationMode = "itemLocation" | "warehouse";

/** Item keywords on a rule that skip automatic put-away. */
export type AutoPutAwayException = {
itemKeywords: string;
};

export type AutoPutAwayRule = {
/** Empty means every item. */
itemKeywords: string;
/** Job order only. Empty means every BOM. */
bomTypes: string;
locationMode: PutAwayLocationMode;
warehouseCode: string;
/** Missing or true means the rule is used. False skips it. */
enabled: boolean;
exceptions: AutoPutAwayException[];
};

export type AutoPutAwayTarget = {
locationMode: PutAwayLocationMode;
warehouseCode: string;
};

export function defaultPoAutoPutAwayRules(): AutoPutAwayRule[] {
return [
{
itemKeywords: DEFAULT_AUTO_PUTAWAY_ITEM_KEYWORDS,
bomTypes: "",
locationMode: "warehouse",
warehouseCode: DEFAULT_AUTO_PUTAWAY_WAREHOUSE,
enabled: true,
exceptions: [],
},
];
}

export function defaultJoAutoPutAwayRules(): AutoPutAwayRule[] {
return [
{
itemKeywords: "",
bomTypes: "FG",
locationMode: "itemLocation",
warehouseCode: DEFAULT_AUTO_PUTAWAY_WAREHOUSE,
enabled: true,
exceptions: [],
},
{
itemKeywords: "",
bomTypes: "WIP",
locationMode: "warehouse",
warehouseCode: DEFAULT_AUTO_PUTAWAY_WAREHOUSE,
enabled: true,
exceptions: [],
},
];
}

export function emptyAutoPutAwayRule(scope: AutoPutAwayScope): AutoPutAwayRule {
return {
itemKeywords: "",
bomTypes: scope === "jo" ? DEFAULT_JO_AUTO_PUTAWAY_BOM_TYPES : "",
locationMode: "warehouse",
warehouseCode: DEFAULT_AUTO_PUTAWAY_WAREHOUSE,
enabled: true,
exceptions: [],
};
}

function csvTokensUpper(raw: string): string[] {
return raw
.split(",")
.map((part) => part.trim().toUpperCase())
.filter(Boolean);
}

function ruleMatchesItem(rule: AutoPutAwayRule, scope: AutoPutAwayScope, item: string, bom: string): boolean {
if (rule.enabled === false) return false;
const keywords = csvTokensUpper(rule.itemKeywords);
const keywordOk = keywords.length === 0 || keywords.some((keyword) => item.includes(keyword));
const boms = scope === "jo" ? csvTokensUpper(rule.bomTypes) : [];
const bomOk = scope !== "jo" || boms.length === 0 || (Boolean(bom) && boms.includes(bom));
return keywordOk && bomOk;
}

/** First rule that matches and is not excluded by its own exception. An exception skips that rule and continues. */
export function resolveAutoPutAwayTarget(
rules: AutoPutAwayRule[],
scope: AutoPutAwayScope,
itemNo: string | null | undefined,
bomDescription: string | null | undefined,
): AutoPutAwayTarget | null {
const item = (itemNo ?? "").toUpperCase();
const bom = (bomDescription ?? "").trim().toUpperCase();
for (const rule of rules) {
if (!ruleMatchesItem(rule, scope, item, bom)) continue;
const exception = (rule.exceptions ?? []).find((row) => {
const keywords = csvTokensUpper(row.itemKeywords);
return keywords.length > 0 && keywords.some((keyword) => item.includes(keyword));
});
if (exception) continue;
return { locationMode: rule.locationMode, warehouseCode: rule.warehouseCode };
}
return null;
}

+ 1393
- 74
src/components/DeliveryOrderFloorSettings/DeliveryOrderFloorSettings.tsx
Diff nebyl zobrazen, protože je příliš veliký
Zobrazit soubor


+ 1
- 0
src/components/DoWorkbench/WorkbenchGoodPickExecutionDetail.tsx Zobrazit soubor

@@ -4696,6 +4696,7 @@ paginatedData.map((row, index) => {
statusTitleText={workbenchLotLabelStatusBanner.text}
statusTitleSeverity={workbenchLotLabelStatusBanner.severity}
warehouseCodePrefixFilter={lotFloorPrefixFilter}
pickRuleScope="do"
triggerLotAvailableQty={
workbenchLotLabelContextLot != null
? Number(workbenchLotLabelContextLot.availableQty)


+ 58
- 8
src/components/DoWorkbench/WorkbenchLotLabelPrintModal.tsx Zobrazit soubor

@@ -36,6 +36,8 @@ import {
fetchWorkbenchPrinters,
printWorkbenchLotLabel,
} from "@/app/api/doworkbench/actions";
import { fetchDoFloorSettingsClient } from "@/app/api/settings/deliveryOrderFloor/client";
import type { ExcludePrintMode } from "@/app/api/settings/deliveryOrderFloor/constants";
import { QRCodeSVG } from "qrcode.react";

type ScanPayload = {
@@ -114,6 +116,8 @@ export interface WorkbenchLotLabelPrintModalProps {
/** Global submit qty shared with outer "Qty will submit". */
submitQty?: number | null;
onSubmitQtyChange?: (qty: number) => void;
/** 揀貨規則:do 用送貨單排除倉,jo 用工單排除倉,控制列印清單/QR。 */
pickRuleScope?: "do" | "jo";
}

function safeParseScanPayload(raw: string): ScanPayload | null {
@@ -166,6 +170,7 @@ const WorkbenchLotLabelPrintModal: React.FC<WorkbenchLotLabelPrintModalProps> =
onWorkbenchScanPick,
submitQty = null,
onSubmitQtyChange,
pickRuleScope,
}) => {
const scanInputRef = useRef<HTMLInputElement | null>(null);
const [scanInput, setScanInput] = useState("");
@@ -187,6 +192,10 @@ const WorkbenchLotLabelPrintModal: React.FC<WorkbenchLotLabelPrintModalProps> =
const [qrVisibleLotLineId, setQrVisibleLotLineId] = useState<number | null>(
null,
);
const [excludePrintRule, setExcludePrintRule] = useState<{
codes: Set<string>;
mode: ExcludePrintMode;
} | null>(null);

const [snackbar, setSnackbar] = useState<{
open: boolean;
@@ -214,6 +223,35 @@ const WorkbenchLotLabelPrintModal: React.FC<WorkbenchLotLabelPrintModalProps> =
return () => clearTimeout(t);
}, [open, resetAll]);

useEffect(() => {
if (!open || !pickRuleScope) {
setExcludePrintRule(null);
return;
}
let cancelled = false;
void fetchDoFloorSettingsClient()
.then((settings) => {
if (cancelled) return;
const csv =
pickRuleScope === "do" ? settings.doExcludeWarehouses : settings.joExcludeWarehouses;
const mode =
pickRuleScope === "do" ? settings.doExcludePrintMode : settings.joExcludePrintMode;
const codes = new Set(
csv
.split(",")
.map((code) => code.trim().toUpperCase())
.filter(Boolean),
);
setExcludePrintRule({ codes, mode });
})
.catch(() => {
if (!cancelled) setExcludePrintRule(null);
});
return () => {
cancelled = true;
};
}, [open, pickRuleScope]);

const loadPrinters = useCallback(async () => {
setPrintersLoading(true);
try {
@@ -449,13 +487,20 @@ const WorkbenchLotLabelPrintModal: React.FC<WorkbenchLotLabelPrintModalProps> =

const filteredLots = useMemo(() => {
const prefix = String(warehouseCodePrefixFilter ?? "").trim();
if (!prefix) return availableLots;
const excluded = excludePrintRule?.codes;
const hideList = excludePrintRule?.mode === "hideList";
return availableLots.filter((lot) => {
// 使用者從本列開啟視窗:即使 API 未帶 warehouseCode,仍應顯示目前這筆批號
if (lot._scanned) return true;
return String(lot.warehouseCode ?? "").startsWith(prefix);
if (prefix && !lot._scanned) {
const code = String(lot.warehouseCode ?? "");
if (!code.startsWith(prefix)) return false;
}
if (hideList && excluded && !lot._scanned) {
const code = String(lot.warehouseCode ?? "").trim().toUpperCase();
if (code && excluded.has(code)) return false;
}
return true;
});
}, [availableLots, warehouseCodePrefixFilter]);
}, [availableLots, warehouseCodePrefixFilter, excludePrintRule]);

const selectedPrinter = useMemo(() => {
if (selectedPrinterId === "") return null;
@@ -682,11 +727,15 @@ const WorkbenchLotLabelPrintModal: React.FC<WorkbenchLotLabelPrintModalProps> =
const isPrinting =
printingLotLineId === lot.inventoryLotLineId;
const loc = String(lot.warehouseCode ?? "").trim();
const suppressQr = Boolean(
excludePrintRule?.codes.has(loc.toUpperCase()),
);
const canShowLotQr =
!!onWorkbenchScanPick &&
!!analysis &&
!analysisLoading &&
!disableScanPick;
!disableScanPick &&
!suppressQr;
const lotQrPayload =
Number.isFinite(Number(analysis?.itemId)) &&
Number.isFinite(Number(lot.stockInLineId))
@@ -750,7 +799,7 @@ const WorkbenchLotLabelPrintModal: React.FC<WorkbenchLotLabelPrintModalProps> =
"列印標籤"
)}
</Button>
{onWorkbenchScanPick ? (
{onWorkbenchScanPick && !suppressQr ? (
<Button
variant="outlined"
color="secondary"
@@ -777,7 +826,8 @@ const WorkbenchLotLabelPrintModal: React.FC<WorkbenchLotLabelPrintModalProps> =
) : null}
</Stack>
{qrVisibleLotLineId === lot.inventoryLotLineId &&
lotQrPayload ? (
lotQrPayload &&
!suppressQr ? (
<Box
sx={{
mt: 1.5,


+ 1
- 0
src/components/JoWorkbench/newJobPickExecution.tsx Zobrazit soubor

@@ -4642,6 +4642,7 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onBackToList, printerCo
workbenchLotLabelContextLot != null
}
warehouseCodePrefixFilter={lotFloorPrefixFilter}
pickRuleScope="jo"
triggerLotAvailableQty={
workbenchLotLabelContextLot != null
? Number(workbenchLotLabelContextLot.availableQty)


+ 44
- 26
src/components/Qc/QcStockInModal.tsx Zobrazit soubor

@@ -47,6 +47,14 @@ import LoadingComponent from "../General/LoadingComponent";
import { printFGStockInLabel, PrintFGStockInLabelRequest, fetchFGStockInLabel } from "@/app/api/jo/actions";
import { fetchItemForPutAway } from "@/app/api/stockIn/actions";
import { fetchWarehouseListClient } from "@/app/api/warehouse/client";
import { fetchDoFloorSettingsClient } from "@/app/api/settings/deliveryOrderFloor/client";
import {
defaultJoAutoPutAwayRules,
defaultPoAutoPutAwayRules,
resolveAutoPutAwayTarget,
type AutoPutAwayRule,
type AutoPutAwayScope,
} from "@/app/api/settings/deliveryOrderFloor/constants";
const style = {
position: "absolute",
top: "50%",
@@ -507,40 +515,51 @@ const QcStockInModal: React.FC<Props> = ({
// confirmButtonText: t("confirm putaway"), html: ""});
// onOpenPutaway();
const isJobOrderSource = (stockInLineInfo?.jobOrderId != null || printSource === "productionProcess");
const isWipBom = isJobOrderSource && stockInLineInfo?.bomDescription === "WIP";
const isFgBom = isJobOrderSource && stockInLineInfo?.bomDescription === "FG";
const isFaItem = (stockInLineInfo?.itemNo ?? "").toUpperCase().includes("FA");
const shouldAutoPutaway = isWipBom || isFgBom || isFaItem;
if (shouldAutoPutaway) {
// Auto putaway to default warehouse
const putAwayScope: AutoPutAwayScope = isJobOrderSource ? "jo" : "po";
let putAwayRules: AutoPutAwayRule[] = isJobOrderSource ? defaultJoAutoPutAwayRules() : defaultPoAutoPutAwayRules();
try {
const floorSettings = await fetchDoFloorSettingsClient();
putAwayRules = isJobOrderSource ? floorSettings.joAutoPutAway : floorSettings.poAutoPutAway;
} catch (settingsError) {
console.error("Auto putaway settings unavailable, using defaults", settingsError);
}
const matchedTarget = resolveAutoPutAwayTarget(
putAwayRules,
putAwayScope,
stockInLineInfo?.itemNo,
stockInLineInfo?.bomDescription,
);
if (matchedTarget) {
const useItemLocation = matchedTarget.locationMode === "itemLocation";
const loc = (itemLocationCode ?? "").trim().toUpperCase();
const warehouseListForLookup =
isFgBom && ((warehouse?.length ?? 0) === 0)
? await fetchWarehouseListClient()
: (warehouse ?? []);
const matchedWarehouse =
isFgBom && loc.length > 0
? warehouseListForLookup.find((w) => (w.code ?? "").trim().toUpperCase().endsWith(loc))
: undefined;
const resolvedWarehouseId =
(isFgBom ? matchedWarehouse?.id : undefined)
?? stockInLineInfo?.defaultWarehouseId
?? 1141;
let warehouseListForLookup =
(warehouse?.length ?? 0) === 0 ? await fetchWarehouseListClient() : (warehouse ?? []);
const matchByCode = (code: string) =>
warehouseListForLookup.find((w) => (w.code ?? "").trim().toUpperCase() === code.trim().toUpperCase());
const matchByLocation = (locationCode: string) =>
warehouseListForLookup.find((w) => (w.code ?? "").trim().toUpperCase().endsWith(locationCode));
const configuredCode = matchedTarget.warehouseCode.trim();
if ((configuredCode && !useItemLocation && !matchByCode(configuredCode)) || (useItemLocation && loc.length > 0 && !matchByLocation(loc))) {
warehouseListForLookup = await fetchWarehouseListClient();
}
const resolvedWarehouseId = useItemLocation
? (loc.length > 0 ? matchByLocation(loc)?.id : undefined)
: (configuredCode ? matchByCode(configuredCode)?.id : undefined);

console.log("[AUTO_PUTAWAY_DEBUG]", {
silId: stockInLineInfo?.id,
bomDescription: stockInLineInfo?.bomDescription,
isJobOrderSource,
isWipBom,
isFgBom,
isFaItem,
itemNo: stockInLineInfo?.itemNo,
matchedTarget,
itemLocationCode,
loc,
warehouseCount: warehouse?.length,
matchedWarehouse: matchedWarehouse ? { id: matchedWarehouse.id, code: matchedWarehouse.code } : null,
defaultWarehouseId: stockInLineInfo?.defaultWarehouseId,
resolvedWarehouseId,
});
if (!resolvedWarehouseId) {
alert(t("Auto putaway failed. Please complete putaway manually."));
} else {
// Get warehouse name from warehouse prop or use default
@@ -563,11 +582,10 @@ const QcStockInModal: React.FC<Props> = ({
}],
} as StockInLineEntry & ModalFormInput;
console.log("[AUTO_PUTAWAY]", {
isFgBom,
matchedTarget,
itemLocationCode,
loc,
warehouseCount: warehouse?.length,
matchedWarehouse: isFgBom ? warehouse?.find(w => (w.code ?? "").trim().toUpperCase().endsWith(loc)) : null,
resolvedWarehouseId,
});
try {
@@ -580,7 +598,7 @@ const QcStockInModal: React.FC<Props> = ({
console.error("Error during auto putaway:", error);
alert(t("Auto putaway failed. Please complete putaway manually."));
}
}
}
closeWithResult(qcRes);


+ 99
- 2
src/i18n/en/deliveryOrderFloor.json Zobrazit soubor

@@ -1,6 +1,103 @@
{
"title": "Delivery order / workbench floor (supplier codes)",
"Intro": "Manage supplier codes assigned to each floor. Use the edit button to add or remove suppliers from a floor.",
"title": "Pick rule settings",
"Intro": "Manage delivery-order floor suppliers, pick-exclude warehouses, and automatic put-away after purchase-order or job-order QC.",
"Tab purchase order": "Purchase order put-away",
"Section auto putaway": "Automatic put-away",
"PO putaway hint": "Purchase orders can have several rules. The first match wins. An empty item code means every item.",
"JO putaway hint": "Job orders can have several rules. The first match wins. An empty item code means every item. Each rule can limit BOM types before the put-away location.",
"Putaway rules hint": "Whitelist: only matches are put away. Blacklist: matches are not put away.",
"Putaway rule n": "Rule {{n}}",
"Rule enabled": "Enabled",
"Putaway rule overlap": "Overlaps rule {{rules}}. Within the same order type, the first match wins, so overlapping items will not use this rule.",
"Empty putaway rules": "No automatic put-away rules.",
"Rule removed pending save": "This rule is removed. Save to keep the change.",
"All items": "All items",
"All bom": "All BOMs",
"Empty keywords mean all": "Leave item codes empty to match every item.",
"Putaway location choice": "Put-away location (choose one)",
"Putaway location": "Put-away location",
"Location item code": "Item location code",
"Location item code hint": "Use the item location code and match a warehouse whose code ends with it.",
"Location warehouse": "Target warehouse",
"Exception n": "Exception {{n}}",
"Exception keywords": "Exception item no.",
"Add exception": "Add exception",
"Exception needs keywords": "Enter exception item keywords",
"Exception outside rule": "These keywords are outside this rule, so the exception will not run.",
"Exception overlap": "Overlaps exception {{n}}. On the same rule, the earlier exception is used.",
"Add putaway rule": "Add rule",
"Edit putaway": "Edit put-away",
"Edit PO putaway title": "Edit purchase-order put-away",
"Edit JO putaway title": "Edit job-order put-away",
"Putaway warehouse": "Fallback warehouse",
"Putaway bom": "BOM types",
"Putaway keywords": "Item no.",
"Empty putaway warehouse": "No fallback warehouse. Automatic put-away is skipped when the item has no default warehouse.",
"Empty putaway condition": "Not set",
"Choose warehouse": "Choose warehouse",
"Clear warehouse": "Clear",
"Use this warehouse": "Use this warehouse",
"Select one warehouse": "This area has more than one location. Choose one.",
"Putaway slot": "Location",
"Putaway token placeholder": "Item keyword",
"BOM WIP only": "WIP only",
"BOM FG only": "FG only",
"BOM both": "Both",
"Add bom": "Add BOM",
"Add keyword": "Add keyword",
"Tab delivery order": "Delivery order pick rules",
"Tab job order": "Job order pick rules",
"Empty exclude chips": "No warehouses are excluded.",
"Section suppliers": "Delivery order floor (suppliers)",
"Supplier card hint": "Supplier codes handled on each floor",
"Supplier list": "Suppliers",
"Edit mapping button": "Edit mapping",
"Section exclude warehouses": "Excluded warehouses",
"Lot not suggested": "Lots not suggested",
"DO exclude hint": "Delivery-order pick suggestions skip these locations",
"JO exclude hint": "Job-order pick suggestions skip these locations",
"Edit exclude list": "Edit exclude list",
"Edit JO exclude list": "Edit job-order exclude list",
"Excluded count": "{{count}} excluded",
"Search slot": "Search locations...",
"Expand more": "Show more (+{{count}})",
"Collapse slots": "Show less",
"Exclude dialog hint": "View, add, or remove excluded locations",
"Search warehouse": "Search warehouse code or name...",
"Add warehouse slot": "Add excluded location",
"Row count": "{{count}} rows",
"Save changes": "Save changes",
"Print list label": "How excluded warehouses appear",
"Print hide list": "Hide from list",
"Print hide list hint": "These locations are omitted from the print list. Scanning the physical QR does not pass.",
"Print hide qr": "Show in list, hide QR",
"Print hide qr hint": "These locations stay on the print list, but the QR is hidden. Scanning the physical QR still passes.",
"DO exclude warehouses": "DO excluded warehouses",
"JO exclude warehouses": "Job order excluded warehouses",
"Edit DO exclude": "Edit DO excluded warehouses",
"Edit JO exclude": "Edit job order excluded warehouses",
"Edit DO exclude title": "Edit DO excluded warehouses",
"Edit JO exclude title": "Edit job order excluded warehouses",
"Add warehouse": "Add warehouse",
"Add warehouse title": "Add excluded warehouse",
"Add warehouse placeholder": "Warehouse code",
"Col warehouse code": "Warehouse code",
"Col warehouse name": "Warehouse name",
"Empty exclude list": "No warehouses are excluded. Use “Add warehouse”.",
"Unknown warehouse name": "(Not in master data or empty name)",
"Warehouse list unavailable": "Could not load the warehouse list. Try again later.",
"Enter warehouse code": "Enter a warehouse code.",
"Warehouse code not found": "This warehouse code does not exist in the system.",
"Duplicate warehouse": "This warehouse is already in the list.",
"List too long": "The list is too long. Remove some warehouse codes and save again.",
"Floor": "Floor",
"Warehouse": "Warehouse",
"Area": "Area",
"All": "All",
"Select floor first": "Select a floor first",
"Select warehouse first": "Select a warehouse first",
"Select area first": "Select an area first",
"No warehouse matched": "No warehouse matches this selection.",
"2F supplier": "2F supplier",
"4F supplier": "4F supplier",
"Edit 2F": "Edit 2F",


+ 2
- 2
src/i18n/en/navigation.json Zobrazit soubor

@@ -45,7 +45,7 @@
"nav.settings.qcCategory": "QC Category",
"nav.settings.qcItemAll": "QC Item All",
"nav.settings.shopAndTruck": "Shop And Truck",
"nav.settings.deliveryOrderFloor": "DO floor settings",
"nav.settings.deliveryOrderFloor": "Pick rules",
"nav.settings.demandForecast": "Demand Forecast Setting",
"nav.settings.bomWeighting": "BOM Weighting Score List",
"nav.settings.masterDataIssues": "BOM / Item UOM Issues",
@@ -69,7 +69,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 Settings",
"nav.breadcrumb.deliveryOrderFloor": "Pick rules",
"nav.breadcrumb.masterDataIssues": "BOM / Item UOM Issues",
"nav.breadcrumb.equipment": "Equipment",
"nav.breadcrumb.equipmentMaintenanceEdit": "Maintenance Edit",


+ 99
- 2
src/i18n/zh/deliveryOrderFloor.json Zobrazit soubor

@@ -1,6 +1,103 @@
{
"title": "送貨單樓層設定(供應商代碼)",
"Intro": "管理各樓層的供應商代碼。點擊編輯按鈕可新增或移除供應商。",
"title": "揀貨規則設定",
"Intro": "管理送貨單樓層對應供應商、揀貨排除倉庫,以及採購單與工單 QC 通過後的自動上架。",
"Tab purchase order": "採購單上架",
"Section auto putaway": "自動上架",
"PO putaway hint": "採購單可有多條規則,由上到下第一條符合的生效。貨號留空代表全部貨品。",
"JO putaway hint": "工單可有多條規則,由上到下第一條符合的生效。貨號留空代表全部貨品。每條規則在上架位置前可限 BOM 類型。",
"Putaway rules hint": "白名單:符合才自動上架。黑名單:符合就不要自動上架。",
"Putaway rule n": "規則 {{n}}",
"Rule enabled": "啟用",
"Putaway rule overlap": "與規則 {{rules}} 條件重疊。",
"Empty putaway rules": "目前沒有自動上架規則。",
"Rule removed pending save": "這條規則已移除,按儲存變更後才會寫入。",
"All items": "全部貨品",
"All bom": "全部 BOM",
"Empty keywords mean all": "不填貨號代表全部貨品。",
"Putaway location choice": "上架位置(二選一)",
"Putaway location": "上架位置",
"Location item code": "貨品預設出倉位置",
"Location item code hint": "用貨品自己的儲位代碼,對倉庫代碼結尾符合的儲位。",
"Location warehouse": "指定倉庫",
"Exception n": "例外 {{n}}",
"Exception keywords": "例外貨品編號",
"Add exception": "新增例外",
"Exception needs keywords": "請填例外貨號",
"Exception outside rule": "這個例外的貨號不在這條規則裡,不會生效。",
"Exception overlap": "與例外 {{n}} 貨號重疊。同一條規則先用上面的例外。",
"Add putaway rule": "新增規則",
"Edit putaway": "編輯自動上架",
"Edit PO putaway title": "編輯採購單自動上架",
"Edit JO putaway title": "編輯工單自動上架",
"Putaway warehouse": "後備倉庫",
"Putaway bom": "BOM 類型",
"Putaway keywords": "貨品編號",
"Empty putaway warehouse": "未指定後備倉庫。沒有物料預設倉時不會自動上架。",
"Empty putaway condition": "未設定",
"Choose warehouse": "選擇倉庫",
"Clear warehouse": "清除",
"Use this warehouse": "使用此倉庫",
"Select one warehouse": "這個區域有多個儲位,請再選一個。",
"Putaway slot": "儲位",
"Putaway token placeholder": "輸入貨號關鍵字",
"BOM WIP only": "只限 WIP",
"BOM FG only": "只限 FG",
"BOM both": "兩者",
"Add bom": "加入 BOM",
"Add keyword": "加入關鍵字",
"Tab delivery order": "送貨單揀貨規則",
"Tab job order": "工單揀貨規則",
"Empty exclude chips": "目前不排除任何倉庫。",
"Section suppliers": "送貨單樓層對應(供應商)",
"Supplier card hint": "指定各樓層預設處理之供應商代碼",
"Supplier list": "供應商名單",
"Edit mapping button": "編輯對應",
"Section exclude warehouses": "排除倉庫",
"Lot not suggested": "批號不建議",
"DO exclude hint": "送貨單自動建議揀貨時將過濾此處儲位",
"JO exclude hint": "工單備貨建議時,系統將跳過以下指定的排除儲位",
"Edit exclude list": "編輯排除名單",
"Edit JO exclude list": "編輯工單排除名單",
"Excluded count": "{{count}} 個排除位",
"Search slot": "快速搜尋儲位...",
"Expand more": "展開更多 (+{{count}} 個儲位)",
"Collapse slots": "收合",
"Exclude dialog hint": "可檢視、新增或移除目前設定之排除儲位名單",
"Search warehouse": "搜尋倉庫代碼或名稱...",
"Add warehouse slot": "新增倉庫排除位",
"Row count": "共 {{count}} 筆資料",
"Save changes": "儲存變更",
"Print list label": "排除倉庫在清單上怎麼處理",
"Print hide list": "不顯示在清單",
"Print hide list hint": "列印清單不出現這些儲位。找到實物 QR 再掃,也不通過。",
"Print hide qr": "顯示在清單,但不顯示 QR",
"Print hide qr hint": "列印清單仍看得到,但不顯示 QR。找到實物 QR 再掃,可以通過。",
"DO exclude warehouses": "送貨單排除倉庫",
"JO exclude warehouses": "工單排除倉庫",
"Edit DO exclude": "編輯送貨單排除倉庫",
"Edit JO exclude": "編輯工單排除倉庫",
"Edit DO exclude title": "編輯送貨單排除倉庫",
"Edit JO exclude title": "編輯工單排除倉庫",
"Add warehouse": "新增倉庫",
"Add warehouse title": "新增排除倉庫",
"Add warehouse placeholder": "輸入倉庫代碼",
"Col warehouse code": "倉庫代碼",
"Col warehouse name": "倉庫名稱",
"Empty exclude list": "目前不排除任何倉庫。按「新增倉庫」加入。",
"Unknown warehouse name": "(主檔無此代碼或名稱為空)",
"Warehouse list unavailable": "無法載入倉庫清單,請稍後再試。",
"Enter warehouse code": "請輸入倉庫代碼。",
"Warehouse code not found": "此倉庫代碼不存在於系統主檔。",
"Duplicate warehouse": "此倉庫已在清單中。",
"List too long": "清單過長,請減少倉庫代碼後再儲存。",
"Floor": "樓層",
"Warehouse": "倉庫",
"Area": "區域",
"All": "全部",
"Select floor first": "請先選擇樓層",
"Select warehouse first": "請先選擇倉庫",
"Select area first": "請先選擇區域",
"No warehouse matched": "沒有符合的倉庫。",
"2F supplier": "2F 供應商",
"4F supplier": "4F 供應商",
"Edit 2F": "編輯 2F",


+ 2
- 2
src/i18n/zh/navigation.json Zobrazit soubor

@@ -10,7 +10,7 @@
"nav.breadcrumb.chartPurchase": "採購",
"nav.breadcrumb.chartWarehouse": "庫存與倉儲",
"nav.breadcrumb.demandForecast": "需求預測設定",
"nav.breadcrumb.deliveryOrderFloor": "送貨單樓層",
"nav.breadcrumb.deliveryOrderFloor": "揀貨規則",
"nav.breadcrumb.doWorkbenchEdit": "DO Workbench 詳情",
"nav.breadcrumb.doWorkbenchPick": "DO Workbench 揀貨",
"nav.breadcrumb.doWorkbenchSearch": "DO Workbench 搜索",
@@ -72,7 +72,7 @@
"nav.settings": "設定",
"nav.settings.bomWeighting": "BOM 權重得分",
"nav.settings.clientMonitor": "裝置連線監控",
"nav.settings.deliveryOrderFloor": "送貨單樓層(供應商)",
"nav.settings.deliveryOrderFloor": "揀貨規則設定",
"nav.settings.demandForecast": "需求預測設定",
"nav.settings.equipment": "設備",
"nav.settings.importBom": "匯入 BOM",


Načítá se…
Zrušit
Uložit