| @@ -0,0 +1,34 @@ | |||
| import { Metadata } from "next"; | |||
| import { getServerI18n } from "@/i18n"; | |||
| import Stack from "@mui/material/Stack"; | |||
| import Typography from "@mui/material/Typography"; | |||
| import { Suspense } from "react"; | |||
| import ExcelFileImport from "@/components/ExcelFileImport"; | |||
| export const metadata: Metadata = { | |||
| title: "Excel File Import", | |||
| }; | |||
| const ImportExcel: React.FC = async () => { | |||
| const { t } = await getServerI18n("common"); | |||
| return ( | |||
| <> | |||
| <Stack | |||
| direction="row" | |||
| justifyContent="space-between" | |||
| flexWrap="wrap" | |||
| rowGap={2} | |||
| > | |||
| <Typography variant="h4" marginInlineEnd={2}> | |||
| {t("Excel File Import")} | |||
| </Typography> | |||
| </Stack> | |||
| <Suspense> | |||
| <ExcelFileImport /> | |||
| </Suspense> | |||
| </> | |||
| ) | |||
| }; | |||
| export default ImportExcel; | |||
| @@ -2,7 +2,7 @@ | |||
| import { cache } from 'react'; | |||
| import { Pageable, serverFetchJson } from "@/app/utils/fetchUtil"; | |||
| import { Machine, Operator } from "."; | |||
| import { JoStatus, Machine, Operator } from "."; | |||
| import { BASE_API_URL } from "@/config/api"; | |||
| import { revalidateTag } from "next/cache"; | |||
| import { convertObjToURLSearchParams } from "@/app/utils/commonUtil"; | |||
| @@ -35,7 +35,7 @@ export interface SearchJoResult { | |||
| name: string; | |||
| reqQty: number; | |||
| uom: string; | |||
| status: string; | |||
| status: JoStatus; | |||
| } | |||
| export interface ReleaseJoRequest { | |||
| @@ -44,7 +44,7 @@ export interface ReleaseJoRequest { | |||
| export interface ReleaseJoResponse { | |||
| id: number; | |||
| entity: { status: string } | |||
| entity: { status: JoStatus } | |||
| } | |||
| export interface IsOperatorExistResponse<T> { | |||
| @@ -4,6 +4,9 @@ import { serverFetchJson } from "@/app/utils/fetchUtil"; | |||
| import { BASE_API_URL } from "@/config/api"; | |||
| import { cache } from "react"; | |||
| export type JoStatus = "planning" | "pending" | "processing" | "packaging" | "storing" | "completed" | |||
| export type JoBomMaterialStatus = "pending" | "completed" | |||
| export interface Operator { | |||
| id: number; | |||
| name: string; | |||
| @@ -24,7 +27,7 @@ export interface JoDetail { | |||
| reqQty: number; | |||
| uom: string; | |||
| pickLines: JoDetailPickLine[]; | |||
| status: string; | |||
| status: JoStatus; | |||
| planStart: number[]; | |||
| planEnd: number[]; | |||
| type: string; | |||
| @@ -34,10 +37,16 @@ export interface JoDetailPickLine { | |||
| id: number; | |||
| code: string; | |||
| name: string; | |||
| lotNo: string; | |||
| pickedLotNo?: JoDetailPickedLotNo[]; | |||
| reqQty: number; | |||
| uom: string; | |||
| status: string; | |||
| status: JoBomMaterialStatus; | |||
| } | |||
| export interface JoDetailPickedLotNo { | |||
| lotNo: string; | |||
| qty: number; | |||
| isScanned: boolean; | |||
| } | |||
| export const fetchJoDetail = cache(async (id: number) => { | |||
| @@ -0,0 +1,16 @@ | |||
| "use server"; | |||
| import { serverFetchString } from "@/app/utils/fetchUtil"; | |||
| import { BASE_API_URL } from "@/config/api"; | |||
| export const importStockTake = async (data: FormData) => { | |||
| const importStockTake = await serverFetchString<string>( | |||
| `${BASE_API_URL}/stockTake/import`, | |||
| { | |||
| method: "POST", | |||
| body: data, | |||
| }, | |||
| ); | |||
| console.log(importStockTake) | |||
| return importStockTake; | |||
| } | |||
| @@ -91,6 +91,25 @@ export async function serverFetchJson<T>(...args: FetchParams) { | |||
| } | |||
| } | |||
| export async function serverFetchString<T>(...args: FetchParams) { | |||
| const response = await serverFetch(...args); | |||
| if (response.ok) { | |||
| return response.text() as T; | |||
| } else { | |||
| switch (response.status) { | |||
| case 401: | |||
| signOutUser(); | |||
| default: | |||
| console.error(await response.text()); | |||
| throw new ServerFetchError( | |||
| "Something went wrong fetching data in server.", | |||
| response, | |||
| ); | |||
| } | |||
| } | |||
| } | |||
| export async function serverFetchBlob<T extends BlobResponse>(...args: FetchParams) { | |||
| const response = await serverFetch(...args); | |||
| @@ -0,0 +1,88 @@ | |||
| "use client"; | |||
| import { FileUpload } from "@mui/icons-material"; | |||
| import { Button, Grid, Stack } from "@mui/material"; | |||
| import React, { ChangeEvent, useCallback, useMemo } from "react"; | |||
| import { useTranslation } from "react-i18next"; | |||
| import { errorDialogWithContent, submitDialog, successDialog, successDialogWithContent } from "../Swal/CustomAlerts"; | |||
| import { importStockTake } from "@/app/api/stockTake/actions"; | |||
| interface Props { | |||
| } | |||
| const ExcelFileImport: React.FC<Props> = async ({ }) => { | |||
| const { t } = useTranslation("common"); | |||
| const buttonName: string[] = useMemo(() => { | |||
| return ["Import Stock Take"] | |||
| }, []) | |||
| const handleExcelFileImportClick = useCallback(async (event: ChangeEvent<HTMLInputElement>) => { | |||
| try { | |||
| if (event.target.files !== null) { | |||
| const file = event.target.files[0] | |||
| const formData = new FormData(); | |||
| formData.append('multipartFileList', file); | |||
| if (file.name.endsWith(".xlsx") || file.name.endsWith(".csv")) { | |||
| let response: String = "" | |||
| console.log(event.target.id) | |||
| switch (event.target.id) { | |||
| case "Import Stock Take": | |||
| response = await importStockTake(formData) | |||
| break; | |||
| } | |||
| if (response.includes("Import Excel success")) { | |||
| successDialogWithContent(t("Import Success"), t(`${response}`), t) | |||
| } else { | |||
| errorDialogWithContent(t("Import Fail"), t(`${response}`), t) | |||
| } | |||
| } | |||
| } | |||
| } catch (err) { | |||
| console.log(err) | |||
| return false | |||
| } | |||
| return | |||
| }, []) | |||
| return ( | |||
| <> | |||
| <Grid container rowGap={1.5}> | |||
| { | |||
| buttonName.map((name) => { | |||
| return ( | |||
| <Grid container> | |||
| <Button | |||
| variant="contained" | |||
| color="info" | |||
| startIcon={<FileUpload />} | |||
| component="label" | |||
| > | |||
| <input | |||
| id={name} | |||
| type='file' | |||
| accept='.xlsx' | |||
| hidden | |||
| onChange={(event) => { | |||
| handleExcelFileImportClick(event) | |||
| }} | |||
| /> | |||
| {t(name)} | |||
| </Button> | |||
| </Grid> | |||
| ) | |||
| }) | |||
| } | |||
| </Grid> | |||
| </> | |||
| ); | |||
| }; | |||
| export default ExcelFileImport; | |||
| @@ -0,0 +1,10 @@ | |||
| import React from "react"; | |||
| import ExcelFileImport from "./ExcelFileImport"; | |||
| const ExcelFileImportWrapper: React.FC = async () => { | |||
| return <ExcelFileImport/>; | |||
| }; | |||
| export default ExcelFileImportWrapper; | |||
| @@ -0,0 +1 @@ | |||
| export { default } from './ExcelFileImportWrapper' | |||
| @@ -0,0 +1,86 @@ | |||
| import { JoDetail } from "@/app/api/jo"; | |||
| import { Box, Button, Stack, Typography } from "@mui/material"; | |||
| import { useMemo } from "react"; | |||
| import { useFormContext } from "react-hook-form"; | |||
| import { useTranslation } from "react-i18next"; | |||
| import StartIcon from "@mui/icons-material/Start"; | |||
| import PlayCircleFilledWhiteIcon from '@mui/icons-material/PlayCircleFilledWhite'; | |||
| type Props = { | |||
| handleRelease: () => void; | |||
| handleStart: () => void; | |||
| }; | |||
| interface ErrorEntry { | |||
| qtyErr: boolean; | |||
| scanErr: boolean; | |||
| } | |||
| const ActionButtons: React.FC<Props> = ({ | |||
| handleRelease, | |||
| handleStart | |||
| }) => { | |||
| const { t } = useTranslation("jo"); | |||
| const { watch } = useFormContext<JoDetail>(); | |||
| const status = useMemo(() => { | |||
| return watch("status").toLowerCase() | |||
| }, [watch("status")]) | |||
| const pickLines = useMemo(() => { | |||
| return watch("pickLines") | |||
| }, [watch("pickLines")]) | |||
| // Check Error Handling (e.g. start jo) | |||
| const errors: ErrorEntry = useMemo(() => { | |||
| let qtyErr = false; | |||
| let scanErr = false; | |||
| pickLines.forEach((line) => { | |||
| if (!qtyErr) { | |||
| const pickedQty = line.pickedLotNo?.reduce((acc, cur) => acc + cur.qty, 0) ?? 0 | |||
| qtyErr = pickedQty > 0 && pickedQty >= line.reqQty | |||
| } | |||
| if (!scanErr) { | |||
| scanErr = line.pickedLotNo?.some((lotNo) => Boolean(lotNo.isScanned) === false) ?? false // default false | |||
| } | |||
| }) | |||
| return { | |||
| qtyErr: qtyErr, | |||
| scanErr: scanErr | |||
| } | |||
| }, [pickLines]) | |||
| return ( | |||
| <Stack direction="row" justifyContent="flex-start" gap={1}> | |||
| {status === "planning" && ( | |||
| <Button | |||
| variant="outlined" | |||
| startIcon={<StartIcon />} | |||
| onClick={handleRelease} | |||
| > | |||
| {t("Release")} | |||
| </Button> | |||
| )} | |||
| {status === "pending" && ( | |||
| <Box sx={{ flexDirection: 'column'}}> | |||
| <Button | |||
| variant="outlined" | |||
| startIcon={<PlayCircleFilledWhiteIcon />} | |||
| onClick={handleStart} | |||
| disabled={errors.qtyErr || errors.scanErr} | |||
| > | |||
| {t("Start Job Order")} | |||
| </Button> | |||
| {errors.scanErr && (<Typography variant="h3" color="error">{t("Please scan the item qr code.")}</Typography>)} | |||
| {errors.qtyErr && (<Typography variant="h3" color="error">{t("Please make sure the qty is enough.")}</Typography>)} | |||
| </Box> | |||
| )} | |||
| </Stack> | |||
| ) | |||
| } | |||
| export default ActionButtons; | |||
| @@ -4,13 +4,16 @@ import { useRouter } from "next/navigation"; | |||
| import { useTranslation } from "react-i18next"; | |||
| import useUploadContext from "../UploadProvider/useUploadContext"; | |||
| import { FormProvider, SubmitErrorHandler, SubmitHandler, useForm } from "react-hook-form"; | |||
| import { useCallback, useState } from "react"; | |||
| import React, { useCallback, useEffect, useLayoutEffect, useMemo, useState } from "react"; | |||
| import { Button, Stack, Typography } from "@mui/material"; | |||
| import StartIcon from "@mui/icons-material/Start"; | |||
| import ArrowBackIcon from '@mui/icons-material/ArrowBack'; | |||
| import { releaseJo } from "@/app/api/jo/actions"; | |||
| import InfoCard from "./InfoCard"; | |||
| import PickTable from "./PickTable"; | |||
| import ActionButtons from "./ActionButtons"; | |||
| import { useQrCodeScannerContext } from "../QrCodeScannerProvider/QrCodeScannerProvider"; | |||
| import { fetchStockInLineInfo } from "@/app/api/po/actions"; | |||
| type Props = { | |||
| id?: number; | |||
| @@ -24,12 +27,92 @@ const JoSave: React.FC<Props> = ({ | |||
| const { t } = useTranslation("jo") | |||
| const router = useRouter(); | |||
| const { setIsUploading } = useUploadContext(); | |||
| const scanner = useQrCodeScannerContext() | |||
| const [serverError, setServerError] = useState(""); | |||
| const finalDefaultValues = useMemo(() => { | |||
| const values = { | |||
| ...defaultValues, | |||
| pickLines: defaultValues?.pickLines?.map(line => ({ | |||
| ...line, | |||
| pickedLotNo: Array.isArray(line.pickedLotNo) | |||
| ? line.pickedLotNo.map(lot => ({ | |||
| ...lot, | |||
| isScanned: false | |||
| })) | |||
| : line.pickedLotNo | |||
| })) | |||
| } | |||
| return values; | |||
| }, [defaultValues]) | |||
| const formProps = useForm<JoDetail>({ | |||
| defaultValues: defaultValues | |||
| defaultValues: finalDefaultValues | |||
| }) | |||
| const pickLines = useMemo(() => { | |||
| return formProps.watch("pickLines") | |||
| }, [formProps.watch("pickLines")]) | |||
| // --------------------------------------------- Qr Code Scan --------------------------------------------- // | |||
| const needScan = useMemo(() => { | |||
| return pickLines.some((line) => line.status === "pending") | |||
| }, [pickLines.some((line) => line.status === "pending")]) | |||
| useLayoutEffect(() => { | |||
| if (needScan && !scanner.isScanning) { | |||
| scanner.startScan(); | |||
| } else if (!needScan) { | |||
| scanner.stopScan(); | |||
| } | |||
| }, [needScan]) | |||
| const checkScannedId = useCallback(async (stockInLineId: number | undefined) => { | |||
| try { | |||
| setIsUploading(true) | |||
| if (stockInLineId) { | |||
| const response = await fetchStockInLineInfo(stockInLineId); | |||
| // const pickLines = formProps.watch("pickLines") | |||
| const pickedLotNoIndex = pickLines.findIndex((line) => line.pickedLotNo?.some((pln) => pln.lotNo === response?.lotNo)) | |||
| if (pickedLotNoIndex >= 0) { | |||
| const updatedPickLines = [...pickLines] | |||
| const matchedLotNoIndex = updatedPickLines[pickedLotNoIndex].pickedLotNo?.findIndex((line) => line?.lotNo === response?.lotNo && Boolean(line?.isScanned) === false) | |||
| if (matchedLotNoIndex !== null && matchedLotNoIndex !== undefined && matchedLotNoIndex >= 0) { | |||
| const updatedPickedLotNo = [...(updatedPickLines[pickedLotNoIndex].pickedLotNo || [])] | |||
| updatedPickedLotNo[matchedLotNoIndex] = { | |||
| ...updatedPickedLotNo[matchedLotNoIndex], | |||
| isScanned: true, | |||
| } | |||
| updatedPickLines[pickedLotNoIndex] = { | |||
| ...updatedPickLines[pickedLotNoIndex], | |||
| pickedLotNo: updatedPickedLotNo, | |||
| }; | |||
| formProps.setValue("pickLines", updatedPickLines, { | |||
| shouldValidate: true, | |||
| shouldDirty: true, | |||
| }); | |||
| } | |||
| } | |||
| } | |||
| } finally { | |||
| scanner.resetScan() | |||
| setIsUploading(false) | |||
| } | |||
| }, []) | |||
| useEffect(() => { | |||
| const result = scanner.result | |||
| console.log(scanner.result) | |||
| if (result?.value) { | |||
| if (!isNaN(Number(result.value))) { checkScannedId(Number(result?.value)); } | |||
| } else if (result?.stockInLineId) { | |||
| checkScannedId(result?.stockInLineId) | |||
| } | |||
| }, [scanner.result]) | |||
| // --------------------------------------------- Button Actions --------------------------------------------- // | |||
| const handleBack = useCallback(() => { | |||
| router.replace(`/jo`) | |||
| }, []) | |||
| @@ -38,12 +121,9 @@ const JoSave: React.FC<Props> = ({ | |||
| try { | |||
| setIsUploading(true) | |||
| if (id) { | |||
| console.log(id) | |||
| const response = await releaseJo({ id: id }) | |||
| console.log(response.entity.status) | |||
| if (response) { | |||
| formProps.setValue("status", response.entity.status) | |||
| console.log(formProps.watch("status")) | |||
| } | |||
| } | |||
| @@ -56,6 +136,11 @@ const JoSave: React.FC<Props> = ({ | |||
| } | |||
| }, []) | |||
| const handleStart = useCallback(async () => { | |||
| console.log("first") | |||
| }, []) | |||
| // --------------------------------------------- Form Submit --------------------------------------------- // | |||
| const onSubmit = useCallback<SubmitHandler<JoDetail>>(async (data, event) => { | |||
| console.log(data) | |||
| }, [t]) | |||
| @@ -76,18 +161,7 @@ const JoSave: React.FC<Props> = ({ | |||
| {serverError} | |||
| </Typography> | |||
| )} | |||
| { | |||
| formProps.watch("status").toLowerCase() === "planning" && ( | |||
| <Stack direction="row" justifyContent="flex-start" gap={1}> | |||
| <Button | |||
| variant="outlined" | |||
| startIcon={<StartIcon />} | |||
| onClick={handleRelease} | |||
| > | |||
| {t("Release")} | |||
| </Button> | |||
| </Stack> | |||
| )} | |||
| <ActionButtons handleRelease={handleRelease} handleStart={handleStart}/> | |||
| <InfoCard /> | |||
| <PickTable /> | |||
| <Stack direction="row" justifyContent="flex-end" gap={1}> | |||
| @@ -17,7 +17,7 @@ const JoSaveWrapper: React.FC<Props> & SubComponents = async ({ | |||
| id, | |||
| }) => { | |||
| const jo = id ? await fetchJoDetail(id) : undefined | |||
| return <JoSave id={id} defaultValues={jo}/> | |||
| } | |||
| @@ -1,11 +1,15 @@ | |||
| import { JoDetail } from "@/app/api/jo"; | |||
| import { JoDetail, JoDetailPickLine } from "@/app/api/jo"; | |||
| import { decimalFormatter } from "@/app/utils/formatUtil"; | |||
| import { GridColDef } from "@mui/x-data-grid"; | |||
| import { GridColDef, GridRenderCellParams, GridValidRowModel } from "@mui/x-data-grid"; | |||
| import { isEmpty, upperFirst } from "lodash"; | |||
| import { useMemo } from "react"; | |||
| import { useCallback, useMemo } from "react"; | |||
| import { useFormContext } from "react-hook-form"; | |||
| import { useTranslation } from "react-i18next"; | |||
| import StyledDataGrid from "../StyledDataGrid/StyledDataGrid"; | |||
| import { Box, Grid, Icon, IconButton, Stack, Typography } from "@mui/material"; | |||
| import PendingOutlinedIcon from '@mui/icons-material/PendingOutlined'; | |||
| import CheckCircleOutlineOutlinedIcon from '@mui/icons-material/CheckCircleOutlineOutlined'; | |||
| import HelpOutlineOutlinedIcon from '@mui/icons-material/HelpOutlineOutlined'; | |||
| type Props = { | |||
| @@ -19,33 +23,74 @@ const PickTable: React.FC<Props> = ({ | |||
| watch | |||
| } = useFormContext<JoDetail>() | |||
| const notPickedStatusColumn = useMemo(() => { | |||
| return (<HelpOutlineOutlinedIcon fontSize={"large"} color={"error"} />) | |||
| }, []) | |||
| const scanStatusColumn = useCallback((status: boolean) => { | |||
| return status ? | |||
| <CheckCircleOutlineOutlinedIcon fontSize={"large"} sx={{ ml: "5px" }} color="success" /> | |||
| : <PendingOutlinedIcon fontSize={"large"} sx={{ ml: "5px" }} color="warning" /> | |||
| }, []) | |||
| const columns = useMemo<GridColDef[]>(() => [ | |||
| { | |||
| field: "code", | |||
| headerName: t("Code"), | |||
| flex: 1, | |||
| flex: 0.6, | |||
| }, | |||
| { | |||
| field: "name", | |||
| headerName: t("Name"), | |||
| flex: 1, | |||
| }, | |||
| { | |||
| field: "scanStatus", | |||
| headerName: t("Scan Status"), | |||
| flex: 0.4, | |||
| align: "right", | |||
| headerAlign: "right", | |||
| renderCell: (params: GridRenderCellParams<JoDetailPickLine>) => { | |||
| if (params.row.pickedLotNo === null || params.row.pickedLotNo === undefined) { | |||
| return notPickedStatusColumn | |||
| } | |||
| const scanStatus = params.row.pickedLotNo.map((pln) => Boolean(pln.isScanned)) | |||
| return isEmpty(scanStatus) ? notPickedStatusColumn : <Stack direction={"column"}>{scanStatus.map((status) => scanStatusColumn(status))}</Stack> | |||
| }, | |||
| }, | |||
| { | |||
| field: "lotNo", | |||
| headerName: t("Lot No."), | |||
| flex: 1, | |||
| renderCell: (row) => { | |||
| return isEmpty(row.value) ? "N/A" : row.value | |||
| renderCell: (params: GridRenderCellParams<JoDetailPickLine>) => { | |||
| if (params.row.pickedLotNo === null || params.row.pickedLotNo === undefined) { | |||
| return t("Pending for pick") | |||
| } | |||
| const lotNos = params.row.pickedLotNo.map((pln) => pln.lotNo) | |||
| return isEmpty(lotNos) ? t("Pending for pick") : lotNos.map((lotNo) => (<>{lotNo}<br /></>)) | |||
| }, | |||
| }, | |||
| { | |||
| field: "pickedQty", | |||
| headerName: t("Picked Qty"), | |||
| flex: 0.7, | |||
| align: "right", | |||
| headerAlign: "right", | |||
| renderCell: (params: GridRenderCellParams<JoDetailPickLine>) => { | |||
| if (params.row.pickedLotNo === null || params.row.pickedLotNo === undefined) { | |||
| return t("Pending for pick") | |||
| } | |||
| const qtys = params.row.pickedLotNo.map((pln) => pln.qty) | |||
| return isEmpty(qtys) ? t("Pending for pick") : qtys.map((qty) => <>{qty}<br /></>) | |||
| }, | |||
| }, | |||
| { | |||
| field: "reqQty", | |||
| headerName: t("Req. Qty"), | |||
| flex: 1, | |||
| flex: 0.7, | |||
| align: "right", | |||
| headerAlign: "right", | |||
| renderCell: (row) => { | |||
| return decimalFormatter.format(row.value) | |||
| renderCell: (params: GridRenderCellParams<JoDetailPickLine>) => { | |||
| return decimalFormatter.format(params.value) | |||
| }, | |||
| }, | |||
| { | |||
| @@ -59,8 +104,15 @@ const PickTable: React.FC<Props> = ({ | |||
| field: "status", | |||
| headerName: t("Status"), | |||
| flex: 1, | |||
| renderCell: (row) => { | |||
| return t(upperFirst(row.value)) | |||
| align: "right", | |||
| headerAlign: "right", | |||
| renderCell: (params: GridRenderCellParams<JoDetailPickLine>) => { | |||
| return ( | |||
| <> | |||
| {params.row.pickedLotNo?.every((lotNo) => Boolean(lotNo.isScanned)) ? t("Scanned") : t(upperFirst(params.value))} | |||
| {scanStatusColumn(Boolean(params.row.pickedLotNo?.every((lotNo) => Boolean(lotNo.isScanned))))} | |||
| </> | |||
| ) | |||
| }, | |||
| }, | |||
| ], []) | |||
| @@ -82,6 +134,7 @@ const PickTable: React.FC<Props> = ({ | |||
| disableColumnMenu | |||
| rows={watch("pickLines")} | |||
| columns={columns} | |||
| getRowHeight={() => 'auto'} | |||
| /> | |||
| </> | |||
| ) | |||
| @@ -295,6 +295,11 @@ const NavigationContent: React.FC = () => { | |||
| label: "Import Testing", | |||
| path: "/settings/m18ImportTesting", | |||
| }, | |||
| { | |||
| icon: <RequestQuote />, | |||
| label: "Import Excel", | |||
| path: "/settings/importExcel", | |||
| }, | |||
| ], | |||
| }, | |||
| ]; | |||
| @@ -1,19 +1,21 @@ | |||
| { | |||
| "Job Order": "工單", | |||
| "Create Job Order": "創建工單", | |||
| "Edit Job Order Detail": "編輯工單", | |||
| "Details": "細節", | |||
| "Code": "編號", | |||
| "Name": "名稱", | |||
| "Req. Qty": "需求數量", | |||
| "UoM": "單位", | |||
| "Status": "來貨狀態", | |||
| "Lot No.": "批號", | |||
| "Bom": "物料清單", | |||
| "Release": "發佈", | |||
| "Pending": "待提料", | |||
| "Planning": "計劃中" | |||
| } | |||
| "Job Order": "工單", | |||
| "Create Job Order": "創建工單", | |||
| "Edit Job Order Detail": "編輯工單", | |||
| "Details": "細節", | |||
| "Code": "編號", | |||
| "Name": "名稱", | |||
| "Picked Qty": "已提料數量", | |||
| "Req. Qty": "需求數量", | |||
| "UoM": "單位", | |||
| "Status": "來貨狀態", | |||
| "Lot No.": "批號", | |||
| "Bom": "物料清單", | |||
| "Release": "發佈", | |||
| "Pending": "待掃碼", | |||
| "Pending for pick": "待提料", | |||
| "Planning": "計劃中", | |||
| "Scanned": "已掃碼", | |||
| "Scan Status": "掃碼狀態", | |||
| "Start Job Order": "開始工單" | |||
| } | |||