| @@ -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() { | |||
| @@ -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"), | |||
| }; | |||
| } | |||
| @@ -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; | |||
| } | |||
| @@ -4696,6 +4696,7 @@ paginatedData.map((row, index) => { | |||
| statusTitleText={workbenchLotLabelStatusBanner.text} | |||
| statusTitleSeverity={workbenchLotLabelStatusBanner.severity} | |||
| warehouseCodePrefixFilter={lotFloorPrefixFilter} | |||
| pickRuleScope="do" | |||
| triggerLotAvailableQty={ | |||
| workbenchLotLabelContextLot != null | |||
| ? Number(workbenchLotLabelContextLot.availableQty) | |||
| @@ -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, | |||
| @@ -4642,6 +4642,7 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onBackToList, printerCo | |||
| workbenchLotLabelContextLot != null | |||
| } | |||
| warehouseCodePrefixFilter={lotFloorPrefixFilter} | |||
| pickRuleScope="jo" | |||
| triggerLotAvailableQty={ | |||
| workbenchLotLabelContextLot != null | |||
| ? Number(workbenchLotLabelContextLot.availableQty) | |||
| @@ -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); | |||
| @@ -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", | |||
| @@ -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", | |||
| @@ -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", | |||
| @@ -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", | |||