| @@ -4,7 +4,7 @@ import { Stack, Typography } from "@mui/material"; | |||||
| import { Metadata } from "next"; | import { Metadata } from "next"; | ||||
| export const metadata: Metadata = { | export const metadata: Metadata = { | ||||
| title: "Delivery order floor", | |||||
| title: "Pick rules", | |||||
| }; | }; | ||||
| export default async function DeliveryOrderFloorPage() { | export default async function DeliveryOrderFloorPage() { | ||||
| @@ -3,8 +3,33 @@ | |||||
| import { clientAuthFetch } from "@/app/utils/clientAuthFetch"; | import { clientAuthFetch } from "@/app/utils/clientAuthFetch"; | ||||
| import { NEXT_PUBLIC_API_URL } from "@/config/api"; | import { NEXT_PUBLIC_API_URL } from "@/config/api"; | ||||
| import { | 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_2F, | ||||
| SETTING_DO_FLOOR_SUPPLIERS_4F, | 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"; | } from "./constants"; | ||||
| const base = NEXT_PUBLIC_API_URL; | const base = NEXT_PUBLIC_API_URL; | ||||
| @@ -17,6 +42,12 @@ export type ShopComboRow = { | |||||
| label: string; | label: string; | ||||
| }; | }; | ||||
| export type WarehousePickRow = ShopComboRow & { | |||||
| storeId: string; | |||||
| warehouse: string; | |||||
| area: string; | |||||
| }; | |||||
| export type SettingsRow = { | export type SettingsRow = { | ||||
| id: number; | id: number; | ||||
| name: string; | name: string; | ||||
| @@ -50,15 +81,205 @@ export async function fetchAllSettingsClient(): Promise<SettingsRow[]> { | |||||
| return parseJson<SettingsRow[]>(res); | 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<{ | export async function fetchDoFloorSettingsClient(): Promise<{ | ||||
| suppliers2F: string; | suppliers2F: string; | ||||
| suppliers4F: string; | suppliers4F: string; | ||||
| doExcludeWarehouses: string; | |||||
| joExcludeWarehouses: string; | |||||
| doExcludePrintMode: ExcludePrintMode; | |||||
| joExcludePrintMode: ExcludePrintMode; | |||||
| poAutoPutAway: AutoPutAwayRule[]; | |||||
| joAutoPutAway: AutoPutAwayRule[]; | |||||
| }> { | }> { | ||||
| const all = await fetchAllSettingsClient(); | const all = await fetchAllSettingsClient(); | ||||
| const get = (name: string) => all.find((s) => s.name === name)?.value ?? ""; | const get = (name: string) => all.find((s) => s.name === name)?.value ?? ""; | ||||
| const raw = (name: string) => all.find((s) => s.name === name)?.value; | |||||
| return { | return { | ||||
| suppliers2F: get(SETTING_DO_FLOOR_SUPPLIERS_2F), | suppliers2F: get(SETTING_DO_FLOOR_SUPPLIERS_2F), | ||||
| suppliers4F: get(SETTING_DO_FLOOR_SUPPLIERS_4F), | 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_2F = "DO.floor.suppliers.2F"; | ||||
| export const SETTING_DO_FLOOR_SUPPLIERS_4F = "DO.floor.suppliers.4F"; | 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} | statusTitleText={workbenchLotLabelStatusBanner.text} | ||||
| statusTitleSeverity={workbenchLotLabelStatusBanner.severity} | statusTitleSeverity={workbenchLotLabelStatusBanner.severity} | ||||
| warehouseCodePrefixFilter={lotFloorPrefixFilter} | warehouseCodePrefixFilter={lotFloorPrefixFilter} | ||||
| pickRuleScope="do" | |||||
| triggerLotAvailableQty={ | triggerLotAvailableQty={ | ||||
| workbenchLotLabelContextLot != null | workbenchLotLabelContextLot != null | ||||
| ? Number(workbenchLotLabelContextLot.availableQty) | ? Number(workbenchLotLabelContextLot.availableQty) | ||||
| @@ -36,6 +36,8 @@ import { | |||||
| fetchWorkbenchPrinters, | fetchWorkbenchPrinters, | ||||
| printWorkbenchLotLabel, | printWorkbenchLotLabel, | ||||
| } from "@/app/api/doworkbench/actions"; | } 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"; | import { QRCodeSVG } from "qrcode.react"; | ||||
| type ScanPayload = { | type ScanPayload = { | ||||
| @@ -114,6 +116,8 @@ export interface WorkbenchLotLabelPrintModalProps { | |||||
| /** Global submit qty shared with outer "Qty will submit". */ | /** Global submit qty shared with outer "Qty will submit". */ | ||||
| submitQty?: number | null; | submitQty?: number | null; | ||||
| onSubmitQtyChange?: (qty: number) => void; | onSubmitQtyChange?: (qty: number) => void; | ||||
| /** 揀貨規則:do 用送貨單排除倉,jo 用工單排除倉,控制列印清單/QR。 */ | |||||
| pickRuleScope?: "do" | "jo"; | |||||
| } | } | ||||
| function safeParseScanPayload(raw: string): ScanPayload | null { | function safeParseScanPayload(raw: string): ScanPayload | null { | ||||
| @@ -166,6 +170,7 @@ const WorkbenchLotLabelPrintModal: React.FC<WorkbenchLotLabelPrintModalProps> = | |||||
| onWorkbenchScanPick, | onWorkbenchScanPick, | ||||
| submitQty = null, | submitQty = null, | ||||
| onSubmitQtyChange, | onSubmitQtyChange, | ||||
| pickRuleScope, | |||||
| }) => { | }) => { | ||||
| const scanInputRef = useRef<HTMLInputElement | null>(null); | const scanInputRef = useRef<HTMLInputElement | null>(null); | ||||
| const [scanInput, setScanInput] = useState(""); | const [scanInput, setScanInput] = useState(""); | ||||
| @@ -187,6 +192,10 @@ const WorkbenchLotLabelPrintModal: React.FC<WorkbenchLotLabelPrintModalProps> = | |||||
| const [qrVisibleLotLineId, setQrVisibleLotLineId] = useState<number | null>( | const [qrVisibleLotLineId, setQrVisibleLotLineId] = useState<number | null>( | ||||
| null, | null, | ||||
| ); | ); | ||||
| const [excludePrintRule, setExcludePrintRule] = useState<{ | |||||
| codes: Set<string>; | |||||
| mode: ExcludePrintMode; | |||||
| } | null>(null); | |||||
| const [snackbar, setSnackbar] = useState<{ | const [snackbar, setSnackbar] = useState<{ | ||||
| open: boolean; | open: boolean; | ||||
| @@ -214,6 +223,35 @@ const WorkbenchLotLabelPrintModal: React.FC<WorkbenchLotLabelPrintModalProps> = | |||||
| return () => clearTimeout(t); | return () => clearTimeout(t); | ||||
| }, [open, resetAll]); | }, [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 () => { | const loadPrinters = useCallback(async () => { | ||||
| setPrintersLoading(true); | setPrintersLoading(true); | ||||
| try { | try { | ||||
| @@ -449,13 +487,20 @@ const WorkbenchLotLabelPrintModal: React.FC<WorkbenchLotLabelPrintModalProps> = | |||||
| const filteredLots = useMemo(() => { | const filteredLots = useMemo(() => { | ||||
| const prefix = String(warehouseCodePrefixFilter ?? "").trim(); | const prefix = String(warehouseCodePrefixFilter ?? "").trim(); | ||||
| if (!prefix) return availableLots; | |||||
| const excluded = excludePrintRule?.codes; | |||||
| const hideList = excludePrintRule?.mode === "hideList"; | |||||
| return availableLots.filter((lot) => { | 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(() => { | const selectedPrinter = useMemo(() => { | ||||
| if (selectedPrinterId === "") return null; | if (selectedPrinterId === "") return null; | ||||
| @@ -682,11 +727,15 @@ const WorkbenchLotLabelPrintModal: React.FC<WorkbenchLotLabelPrintModalProps> = | |||||
| const isPrinting = | const isPrinting = | ||||
| printingLotLineId === lot.inventoryLotLineId; | printingLotLineId === lot.inventoryLotLineId; | ||||
| const loc = String(lot.warehouseCode ?? "").trim(); | const loc = String(lot.warehouseCode ?? "").trim(); | ||||
| const suppressQr = Boolean( | |||||
| excludePrintRule?.codes.has(loc.toUpperCase()), | |||||
| ); | |||||
| const canShowLotQr = | const canShowLotQr = | ||||
| !!onWorkbenchScanPick && | !!onWorkbenchScanPick && | ||||
| !!analysis && | !!analysis && | ||||
| !analysisLoading && | !analysisLoading && | ||||
| !disableScanPick; | |||||
| !disableScanPick && | |||||
| !suppressQr; | |||||
| const lotQrPayload = | const lotQrPayload = | ||||
| Number.isFinite(Number(analysis?.itemId)) && | Number.isFinite(Number(analysis?.itemId)) && | ||||
| Number.isFinite(Number(lot.stockInLineId)) | Number.isFinite(Number(lot.stockInLineId)) | ||||
| @@ -750,7 +799,7 @@ const WorkbenchLotLabelPrintModal: React.FC<WorkbenchLotLabelPrintModalProps> = | |||||
| "列印標籤" | "列印標籤" | ||||
| )} | )} | ||||
| </Button> | </Button> | ||||
| {onWorkbenchScanPick ? ( | |||||
| {onWorkbenchScanPick && !suppressQr ? ( | |||||
| <Button | <Button | ||||
| variant="outlined" | variant="outlined" | ||||
| color="secondary" | color="secondary" | ||||
| @@ -777,7 +826,8 @@ const WorkbenchLotLabelPrintModal: React.FC<WorkbenchLotLabelPrintModalProps> = | |||||
| ) : null} | ) : null} | ||||
| </Stack> | </Stack> | ||||
| {qrVisibleLotLineId === lot.inventoryLotLineId && | {qrVisibleLotLineId === lot.inventoryLotLineId && | ||||
| lotQrPayload ? ( | |||||
| lotQrPayload && | |||||
| !suppressQr ? ( | |||||
| <Box | <Box | ||||
| sx={{ | sx={{ | ||||
| mt: 1.5, | mt: 1.5, | ||||
| @@ -4642,6 +4642,7 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onBackToList, printerCo | |||||
| workbenchLotLabelContextLot != null | workbenchLotLabelContextLot != null | ||||
| } | } | ||||
| warehouseCodePrefixFilter={lotFloorPrefixFilter} | warehouseCodePrefixFilter={lotFloorPrefixFilter} | ||||
| pickRuleScope="jo" | |||||
| triggerLotAvailableQty={ | triggerLotAvailableQty={ | ||||
| workbenchLotLabelContextLot != null | workbenchLotLabelContextLot != null | ||||
| ? Number(workbenchLotLabelContextLot.availableQty) | ? Number(workbenchLotLabelContextLot.availableQty) | ||||
| @@ -47,6 +47,14 @@ import LoadingComponent from "../General/LoadingComponent"; | |||||
| import { printFGStockInLabel, PrintFGStockInLabelRequest, fetchFGStockInLabel } from "@/app/api/jo/actions"; | import { printFGStockInLabel, PrintFGStockInLabelRequest, fetchFGStockInLabel } from "@/app/api/jo/actions"; | ||||
| import { fetchItemForPutAway } from "@/app/api/stockIn/actions"; | import { fetchItemForPutAway } from "@/app/api/stockIn/actions"; | ||||
| import { fetchWarehouseListClient } from "@/app/api/warehouse/client"; | 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 = { | const style = { | ||||
| position: "absolute", | position: "absolute", | ||||
| top: "50%", | top: "50%", | ||||
| @@ -507,40 +515,51 @@ const QcStockInModal: React.FC<Props> = ({ | |||||
| // confirmButtonText: t("confirm putaway"), html: ""}); | // confirmButtonText: t("confirm putaway"), html: ""}); | ||||
| // onOpenPutaway(); | // onOpenPutaway(); | ||||
| const isJobOrderSource = (stockInLineInfo?.jobOrderId != null || printSource === "productionProcess"); | 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 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]", { | console.log("[AUTO_PUTAWAY_DEBUG]", { | ||||
| silId: stockInLineInfo?.id, | silId: stockInLineInfo?.id, | ||||
| bomDescription: stockInLineInfo?.bomDescription, | bomDescription: stockInLineInfo?.bomDescription, | ||||
| isJobOrderSource, | isJobOrderSource, | ||||
| isWipBom, | |||||
| isFgBom, | |||||
| isFaItem, | |||||
| itemNo: stockInLineInfo?.itemNo, | |||||
| matchedTarget, | |||||
| itemLocationCode, | itemLocationCode, | ||||
| loc, | loc, | ||||
| warehouseCount: warehouse?.length, | warehouseCount: warehouse?.length, | ||||
| matchedWarehouse: matchedWarehouse ? { id: matchedWarehouse.id, code: matchedWarehouse.code } : null, | |||||
| defaultWarehouseId: stockInLineInfo?.defaultWarehouseId, | |||||
| resolvedWarehouseId, | resolvedWarehouseId, | ||||
| }); | }); | ||||
| if (!resolvedWarehouseId) { | |||||
| alert(t("Auto putaway failed. Please complete putaway manually.")); | |||||
| } else { | |||||
| // Get warehouse name from warehouse prop or use default | // Get warehouse name from warehouse prop or use default | ||||
| @@ -563,11 +582,10 @@ const QcStockInModal: React.FC<Props> = ({ | |||||
| }], | }], | ||||
| } as StockInLineEntry & ModalFormInput; | } as StockInLineEntry & ModalFormInput; | ||||
| console.log("[AUTO_PUTAWAY]", { | console.log("[AUTO_PUTAWAY]", { | ||||
| isFgBom, | |||||
| matchedTarget, | |||||
| itemLocationCode, | itemLocationCode, | ||||
| loc, | loc, | ||||
| warehouseCount: warehouse?.length, | warehouseCount: warehouse?.length, | ||||
| matchedWarehouse: isFgBom ? warehouse?.find(w => (w.code ?? "").trim().toUpperCase().endsWith(loc)) : null, | |||||
| resolvedWarehouseId, | resolvedWarehouseId, | ||||
| }); | }); | ||||
| try { | try { | ||||
| @@ -580,7 +598,7 @@ const QcStockInModal: React.FC<Props> = ({ | |||||
| console.error("Error during auto putaway:", error); | console.error("Error during auto putaway:", error); | ||||
| alert(t("Auto putaway failed. Please complete putaway manually.")); | alert(t("Auto putaway failed. Please complete putaway manually.")); | ||||
| } | } | ||||
| } | |||||
| } | } | ||||
| closeWithResult(qcRes); | 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", | "2F supplier": "2F supplier", | ||||
| "4F supplier": "4F supplier", | "4F supplier": "4F supplier", | ||||
| "Edit 2F": "Edit 2F", | "Edit 2F": "Edit 2F", | ||||
| @@ -45,7 +45,7 @@ | |||||
| "nav.settings.qcCategory": "QC Category", | "nav.settings.qcCategory": "QC Category", | ||||
| "nav.settings.qcItemAll": "QC Item All", | "nav.settings.qcItemAll": "QC Item All", | ||||
| "nav.settings.shopAndTruck": "Shop And Truck", | "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.demandForecast": "Demand Forecast Setting", | ||||
| "nav.settings.bomWeighting": "BOM Weighting Score List", | "nav.settings.bomWeighting": "BOM Weighting Score List", | ||||
| "nav.settings.masterDataIssues": "BOM / Item UOM Issues", | "nav.settings.masterDataIssues": "BOM / Item UOM Issues", | ||||
| @@ -69,7 +69,7 @@ | |||||
| "nav.breadcrumb.qcItemAll": "QC Item All", | "nav.breadcrumb.qcItemAll": "QC Item All", | ||||
| "nav.breadcrumb.qrCodeHandle": "QR Code Handle", | "nav.breadcrumb.qrCodeHandle": "QR Code Handle", | ||||
| "nav.breadcrumb.demandForecast": "Demand Forecast Setting", | "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.masterDataIssues": "BOM / Item UOM Issues", | ||||
| "nav.breadcrumb.equipment": "Equipment", | "nav.breadcrumb.equipment": "Equipment", | ||||
| "nav.breadcrumb.equipmentMaintenanceEdit": "Maintenance Edit", | "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 供應商", | "2F supplier": "2F 供應商", | ||||
| "4F supplier": "4F 供應商", | "4F supplier": "4F 供應商", | ||||
| "Edit 2F": "編輯 2F", | "Edit 2F": "編輯 2F", | ||||
| @@ -10,7 +10,7 @@ | |||||
| "nav.breadcrumb.chartPurchase": "採購", | "nav.breadcrumb.chartPurchase": "採購", | ||||
| "nav.breadcrumb.chartWarehouse": "庫存與倉儲", | "nav.breadcrumb.chartWarehouse": "庫存與倉儲", | ||||
| "nav.breadcrumb.demandForecast": "需求預測設定", | "nav.breadcrumb.demandForecast": "需求預測設定", | ||||
| "nav.breadcrumb.deliveryOrderFloor": "送貨單樓層", | |||||
| "nav.breadcrumb.deliveryOrderFloor": "揀貨規則", | |||||
| "nav.breadcrumb.doWorkbenchEdit": "DO Workbench 詳情", | "nav.breadcrumb.doWorkbenchEdit": "DO Workbench 詳情", | ||||
| "nav.breadcrumb.doWorkbenchPick": "DO Workbench 揀貨", | "nav.breadcrumb.doWorkbenchPick": "DO Workbench 揀貨", | ||||
| "nav.breadcrumb.doWorkbenchSearch": "DO Workbench 搜索", | "nav.breadcrumb.doWorkbenchSearch": "DO Workbench 搜索", | ||||
| @@ -72,7 +72,7 @@ | |||||
| "nav.settings": "設定", | "nav.settings": "設定", | ||||
| "nav.settings.bomWeighting": "BOM 權重得分", | "nav.settings.bomWeighting": "BOM 權重得分", | ||||
| "nav.settings.clientMonitor": "裝置連線監控", | "nav.settings.clientMonitor": "裝置連線監控", | ||||
| "nav.settings.deliveryOrderFloor": "送貨單樓層(供應商)", | |||||
| "nav.settings.deliveryOrderFloor": "揀貨規則設定", | |||||
| "nav.settings.demandForecast": "需求預測設定", | "nav.settings.demandForecast": "需求預測設定", | ||||
| "nav.settings.equipment": "設備", | "nav.settings.equipment": "設備", | ||||
| "nav.settings.importBom": "匯入 BOM", | "nav.settings.importBom": "匯入 BOM", | ||||