chen.lin
19 小时以前 c67e3d0295858a61122354a15dec8835044bac0d
库位拣料出库数量调整
2个文件已添加
44个文件已修改
681 ■■■■ 已修改文件
rsf-admin/src/page/components/QuantityInput.jsx 23 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-admin/src/page/locItem/LocItemCreate.jsx 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-admin/src/page/locItem/LocItemEdit.jsx 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-admin/src/page/locItem/LocItemList.jsx 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-admin/src/page/orders/asnOrder/AsnCreateByPoModal.jsx 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-admin/src/page/orders/asnOrder/AsnOrderItemList.jsx 6 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-admin/src/page/orders/asnOrder/AsnOrderList.jsx 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-admin/src/page/orders/asnOrder/AsnOrderModal.jsx 3 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-admin/src/page/orders/asnOrder/AsnOrderPanel.jsx 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-admin/src/page/orders/asnOrder/OrderPrintPreview.jsx 6 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-admin/src/page/orders/asnOrder/POItemModal.jsx 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-admin/src/page/orders/check/SelectMatnrModal.jsx 3 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-admin/src/page/orders/outStock/MatnrInfoModal.jsx 15 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-admin/src/page/orders/outStock/OutOrderItemList.jsx 8 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-admin/src/page/orders/outStock/OutOrderList.jsx 6 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-admin/src/page/orders/outStock/OutOrderModal.jsx 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-admin/src/page/orders/outStock/OutOrderPreview.jsx 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-admin/src/page/orders/outStock/OutStockPublic.jsx 18 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-admin/src/page/orders/outStock/SelectMatnrModal.jsx 7 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-admin/src/page/orders/transfer/ManualCreate.jsx 3 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-admin/src/page/orders/transfer/TransferCreate.jsx 3 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-admin/src/page/statistics/stockManage/WarehouseStockInfo.jsx 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-admin/src/page/stockItem/StockItemCreate.jsx 11 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-admin/src/page/stockItem/StockItemEdit.jsx 11 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-admin/src/page/stockManage/locRevise/LocsReviseDetl.jsx 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-admin/src/page/stockManage/locRevise/ReviseLogItemList.jsx 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-admin/src/page/task/TaskItemList.jsx 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-admin/src/page/task/TaskPanel.jsx 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-admin/src/page/work/checkOutBound/CheckOutBoundList.jsx 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-admin/src/page/work/components/locItemInfoModal.jsx 3 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-admin/src/page/work/outBound/OutBoundList.jsx 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-admin/src/page/work/stockTransfer/stockTransferList.jsx 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-admin/src/utils/common.js 32 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-open-api/src/main/java/com/vincent/rsf/openApi/entity/params/ReportParams.java 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/api/controller/erp/params/ReportParams.java 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/api/controller/pda/PdaCheckOrderController.java 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/api/service/impl/PdaCheckOrderServiceImpl.java 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/api/service/impl/PdaOutStockServiceImpl.java 28 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/common/utils/QuantityUtils.java 77 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/manager/enums/OrderWorkType.java 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/manager/mapper/LocItemMapper.java 6 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/manager/schedules/ScheduleJobs.java 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/manager/service/impl/LocItemServiceImpl.java 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/manager/service/impl/OutStockServiceImpl.java 124 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/manager/service/impl/TaskServiceImpl.java 173 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/resources/mapper/manager/LocItemMapper.xml 21 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-admin/src/page/components/QuantityInput.jsx
New file
@@ -0,0 +1,23 @@
import React from 'react';
import { NumberInput } from 'react-admin';
import { maxDecimalPlaces } from '@/utils/common';
const MAX_DECIMALS = 6;
const QUANTITY_VALIDATE = [maxDecimalPlaces(MAX_DECIMALS, '最多6位小数')];
/**
 * 数量输入框:支持最多 6 位小数,超过时提示并阻止提交
 */
const QuantityInput = (props) => {
    const { validate = [], helperText, ...rest } = props;
    const mergedValidate = Array.isArray(validate) ? [...QUANTITY_VALIDATE, ...validate] : [...QUANTITY_VALIDATE, validate];
    return (
        <NumberInput
            validate={mergedValidate}
            helperText={helperText ?? '最多6位小数'}
            {...rest}
        />
    );
};
export default QuantityInput;
rsf-admin/src/page/locItem/LocItemCreate.jsx
@@ -30,6 +30,7 @@
import DialogCloseButton from "../components/DialogCloseButton";
import StatusSelectInput from "../components/StatusSelectInput";
import MemoInput from "../components/MemoInput";
import QuantityInput from "../components/QuantityInput";
const LocItemCreate = (props) => {
    const { open, setOpen } = props;
@@ -186,19 +187,19 @@
                                    />
                                </Grid>
                                <Grid item xs={6} display="flex" gap={1}>
                                    <NumberInput
                                    <QuantityInput
                                        label="table.field.locItem.anfme"
                                        source="anfme"
                                    />
                                </Grid>
                                <Grid item xs={6} display="flex" gap={1}>
                                    <NumberInput
                                    <QuantityInput
                                        label="table.field.locItem.qty"
                                        source="qty"
                                    />
                                </Grid>
                                <Grid item xs={6} display="flex" gap={1}>
                                    <NumberInput
                                    <QuantityInput
                                        label="table.field.locItem.workQty"
                                        source="workQty"
                                    />
rsf-admin/src/page/locItem/LocItemEdit.jsx
@@ -23,6 +23,7 @@
import { useWatch, useFormContext } from "react-hook-form";
import { Stack, Grid, Box, Typography } from '@mui/material';
import * as Common from '@/utils/common';
import QuantityInput from "../components/QuantityInput";
import { EDIT_MODE, REFERENCE_INPUT_PAGESIZE } from '@/config/setting';
import EditBaseAside from "../components/EditBaseAside";
import CustomerTopToolBar from "../components/EditTopToolBar";
@@ -165,19 +166,19 @@
                            />
                        </Stack>
                        <Stack direction='row' gap={2}>
                            <NumberInput
                            <QuantityInput
                                label="table.field.locItem.anfme"
                                source="anfme"
                            />
                        </Stack>
                        <Stack direction='row' gap={2}>
                            <NumberInput
                            <QuantityInput
                                label="table.field.locItem.qty"
                                source="qty"
                            />
                        </Stack>
                        <Stack direction='row' gap={2}>
                            <NumberInput
                            <QuantityInput
                                label="table.field.locItem.workQty"
                                source="workQty"
                            />
rsf-admin/src/page/locItem/LocItemList.jsx
@@ -163,7 +163,7 @@
                <TextField source="maktx" label="table.field.locItem.maktx" />,
                <TextField source="matnrCode" label="table.field.locItem.matnrCode" />,
                <TextField source="unit" label="table.field.locItem.unit" />,
                <NumberField source="anfme" label="table.field.locItem.anfme" />,
                <NumberField source="anfme" label="table.field.locItem.anfme" options={{ maximumFractionDigits: 6 }} />,
                <TextField source="batch" label="table.field.locItem.batch" />,
                <NumberField source="splrId" label="table.field.locItem.splrId" />,
                <TextField source="spec" label="table.field.locItem.spec" />,
rsf-admin/src/page/orders/asnOrder/AsnCreateByPoModal.jsx
@@ -175,8 +175,8 @@
                                    <TextField source="type$" label="table.field.purchase.type" />
                                    <TextField source="wkType$" label="table.field.purchase.wkType" />
                                    <TextField source="source" label="table.field.purchase.source" />
                                    <NumberField source="anfme" label="table.field.purchase.anfme" />
                                    <NumberField source="qty" label="table.field.purchase.qty" />
                                    <NumberField source="anfme" label="table.field.purchase.anfme" options={{ maximumFractionDigits: 6 }} />
                                    <NumberField source="qty" label="table.field.purchase.qty" options={{ maximumFractionDigits: 6 }} />
                                    <TextField source="channel" label="table.field.purchase.channel" />
                                    <TextField source="platCode" label="table.field.purchase.platCode" />
                                    <DateField source="preArr" label="table.field.purchase.preArr" showTime />
rsf-admin/src/page/orders/asnOrder/AsnOrderItemList.jsx
@@ -190,10 +190,10 @@
        <TextField source="projectCode" label="table.field.asnOrderItem.projectCode" />,
        <TextField source="spec" label="table.field.asnOrderItem.spec" />,
        <TextField source="model" label="table.field.asnOrderItem.model" />,
        <NumberField source="anfme" label="table.field.asnOrderItem.anfme" />,
        <NumberField source="qty" label="table.field.asnOrderItem.qty" />,
        <NumberField source="anfme" label="table.field.asnOrderItem.anfme" options={{ maximumFractionDigits: 6 }} />,
        <NumberField source="qty" label="table.field.asnOrderItem.qty" options={{ maximumFractionDigits: 6 }} />,
        <TextField source="stockUnit" label="table.field.asnOrderItem.stockUnit" />,
        <NumberField source="purQty" label="table.field.asnOrderItem.purQty" />,
        <NumberField source="purQty" label="table.field.asnOrderItem.purQty" options={{ maximumFractionDigits: 6 }} />,
        <TextField source="purUnit" label="table.field.asnOrderItem.purUnit" />,
        <TextField source="splrCode" label="table.field.asnOrderItem.splrCode" />,
        <TextField source="splrName" label="table.field.asnOrderItem.splrName" />,
rsf-admin/src/page/orders/asnOrder/AsnOrderList.jsx
@@ -166,8 +166,8 @@
          <NumberField source="poId" label="table.field.asnOrder.poId" />
          <TextField source="type$" label="table.field.asnOrder.type" />
          <TextField cellClassName="wkType" source="wkType$" label="table.field.asnOrder.wkType" />
          <NumberField source="anfme" label="table.field.asnOrder.anfme" />
          <NumberField source="qty" label="table.field.asnOrder.qty" />
          <NumberField source="anfme" label="table.field.asnOrder.anfme" options={{ maximumFractionDigits: 6 }} />
          <NumberField source="qty" label="table.field.asnOrder.qty" options={{ maximumFractionDigits: 6 }} />
          <DateField source="arrTime" label="table.field.asnOrder.arrTime" showTime />
          <TextField source="rleStatus$" label="table.field.asnOrder.rleStatus" sortable={false} />
          <TextField source="logisNo" label="table.field.asnOrder.logisNo" />
rsf-admin/src/page/orders/asnOrder/AsnOrderModal.jsx
@@ -59,6 +59,7 @@
import SaveIcon from '@mui/icons-material/Save';
import AsnWareModal from "./AsnWareModal";
import request from '@/utils/request';
import { formatQuantity } from '@/utils/common';
import _, { set } from 'lodash';
import "./asnOrder.css";
@@ -518,7 +519,7 @@
            minWidth: 100,
            flex: 1,
            editable: true,
            valueFormatter: (val) => val < 0 ? 0 : val,
            valueFormatter: (val) => formatQuantity(val != null && val >= 0 ? val : 0),
            headerClassName: "custom",
        },
        // {
rsf-admin/src/page/orders/asnOrder/AsnOrderPanel.jsx
@@ -8,7 +8,7 @@
    useListContext,
} from 'react-admin';
import PanelTypography from "../../components/PanelTypography";
import * as Common from '@/utils/common'
import { formatQuantity } from '@/utils/common';
import { styled } from "@mui/material/styles";
import request from '@/utils/request';
import debounce from 'lodash/debounce';
@@ -75,7 +75,8 @@
        },
        {
            field: 'anfme',
            headerName: translate('table.field.asnOrderItem.purQty')
            headerName: translate('table.field.asnOrderItem.purQty'),
            valueFormatter: (v) => formatQuantity(v)
        },
        {
            field: 'stockUnit',
@@ -91,7 +92,8 @@
        },
        {
            field: 'qty',
            headerName: translate('table.field.asnOrderItem.qty')
            headerName: translate('table.field.asnOrderItem.qty'),
            valueFormatter: (v) => formatQuantity(v)
        },
        {
            field: 'splrBatch',
rsf-admin/src/page/orders/asnOrder/OrderPrintPreview.jsx
@@ -208,9 +208,9 @@
                <TextField source="matnrCode" label="table.field.asnOrderItem.matnrCode" />,
                <TextField source="maktx" label="table.field.asnOrderItem.maktx" />,
                <TextField source="splrBatch" label="table.field.asnOrderItem.splrBatch" />,
                <NumberField source="anfme" label="table.field.asnOrderItem.anfme" />,
                <NumberField source="qty" label="table.field.asnOrderItem.qty" />,
                <NumberField source="purQty" label="table.field.asnOrderItem.purQty" />,
                <NumberField source="anfme" label="table.field.asnOrderItem.anfme" options={{ maximumFractionDigits: 6 }} />,
                <NumberField source="qty" label="table.field.asnOrderItem.qty" options={{ maximumFractionDigits: 6 }} />,
                <NumberField source="purQty" label="table.field.asnOrderItem.purQty" options={{ maximumFractionDigits: 6 }} />,
                // <TextField source="splrName" label="table.field.asnOrderItem.splrName" />,
                <TextField source="isptResult$" label="table.field.asnOrderItem.isptResult" />,
                // <TextField source="trackCode" label="table.field.asnOrderItem.barcode" />,
rsf-admin/src/page/orders/asnOrder/POItemModal.jsx
@@ -50,6 +50,7 @@
import { useForm, Controller, useWatch, FormProvider, useFormContext } from "react-hook-form";
import SaveIcon from '@mui/icons-material/Save';
import request from '@/utils/request';
import { formatQuantity } from '@/utils/common';
import { Add, Edit, Delete } from '@mui/icons-material';
import _, { set } from 'lodash';
import { DataGrid, useGridApiRef, GRID_DATE_COL_DEF, GRID_DATETIME_COL_DEF, getGridDateOperators, useGridApiContext } from '@mui/x-data-grid';
@@ -277,7 +278,7 @@
            minWidth: 100,
            flex: 1,
            editable: true,
            valueFormatter: (val) => val < 0 ? 0 : val,
            valueFormatter: (val) => formatQuantity(val != null && val >= 0 ? val : 0),
            headerClassName: "custom",
        },
        {
@@ -286,7 +287,7 @@
            type: 'number',
            minWidth: 100,
            flex: 1,
            valueFormatter: (val) => val < 0 ? 0 : val,
            valueFormatter: (val) => formatQuantity(val != null && val >= 0 ? val : 0),
        },
        {
            field: 'unit',
rsf-admin/src/page/orders/check/SelectMatnrModal.jsx
@@ -48,6 +48,7 @@
import MatnrInfoModal from "./MatnrInfoModal";
import SaveIcon from '@mui/icons-material/Save';
import request from '@/utils/request';
import { formatQuantity } from '@/utils/common';
import { Add, Edit, Delete } from '@mui/icons-material';
import { DataGrid, useGridApiRef } from '@mui/x-data-grid';
import DictionarySelect from "../../components/DictionarySelect";
@@ -416,7 +417,7 @@
            minWidth: 120,
            flex: 1,
            editable: true,
            valueFormatter: (val) => val < 0 ? 0 : val,
            valueFormatter: (val) => formatQuantity(val != null && val >= 0 ? val : 0),
            headerClassName: "custom",
        },
        {
rsf-admin/src/page/orders/outStock/MatnrInfoModal.jsx
@@ -20,6 +20,7 @@
import { EDIT_MODE, DEFAULT_START_PAGE, DEFAULT_PAGE_SIZE, REFERENCE_INPUT_PAGESIZE } from '@/config/setting';
import { useTranslate, useNotify, useRefresh } from 'react-admin';
import request from '@/utils/request';
import { formatQuantity } from '@/utils/common';
import { DataGrid } from '@mui/x-data-grid';
import SaveIcon from '@mui/icons-material/Save';
import TreeSelectInput from "@/page/components/TreeSelectInput";
@@ -60,12 +61,14 @@
        })
    }
    const getRowId = (row) => (row.locUseStatus$ != null ? `${row.id}_${row.locUseStatus$}` : String(row.id));
    const handleSubmit = () => {
        const hasarr = data.map(el => +el.matnrId);
        const selectedData = selectedRows
            .filter((item) => !hasarr.includes(item))
            .map((id) => tableData.find((row) => row.id === id))
            .filter(Boolean);
            .map((id) => tableData.find((row) => getRowId(row) === id))
            .filter(Boolean)
            .filter((row) => !hasarr.includes(row.id));
        const deduped = [...new Map(selectedData.map((s) => [s.id, s])).values()];
        const value = deduped.map((el, i) => {
            const dynamicFields = dyFields.reduce((acc, item) => {
@@ -200,6 +203,7 @@
                        setDyFields={setDyFields}
                        selectedRows={selectedRows}
                        setSelectedRows={setSelectedRows}
                        getRowId={getRowId}
                    />
                </Box>
            </DialogContent>
@@ -216,7 +220,7 @@
export default MatnrInfoModal;
const AsnWareModalTable = ({ tableData, setTableData, page, isLoading, pageSize, setPage, rowCount, selectedRows, setSelectedRows, dyFields, setDyFields }) => {
const AsnWareModalTable = ({ tableData, setTableData, page, isLoading, pageSize, setPage, rowCount, selectedRows, setSelectedRows, dyFields, setDyFields, getRowId }) => {
    const translate = useTranslate();
    const notify = useNotify();
@@ -232,7 +236,7 @@
        { field: 'unit', headerName: translate('table.field.matnr.unit'), width: 100 },
        { field: 'purchaseUnit', headerName: translate('table.field.matnr.purUnit'), width: 100 },
        { field: 'stockUnit', headerName: translate('table.field.matnr.stockUnit'), width: 100 },
        { field: 'stockQty', headerName: translate('table.field.matnr.stockQty') || '库存数量', width: 110, type: 'number', valueFormatter: (v) => (v != null ? Number(v) : 0) },
        { field: 'stockQty', headerName: translate('table.field.matnr.stockQty') || '库存数量', width: 110, type: 'number', valueFormatter: (v) => formatQuantity(v) },
        { field: 'locUseStatus$', headerName: translate('table.field.loc.useStatus'), width: 120 },
        { field: 'locCodes$', headerName: translate('table.field.loc.locCode'), width: 180, flex: 1 },
        { field: 'stockLeval$', headerName: translate('table.field.matnr.stockLevel'), width: 100, sortable: false },
@@ -273,6 +277,7 @@
        <div style={{ height: 400, width: '100%' }}>
            <DataGrid
                size="small"
                getRowId={getRowId}
                rows={tableData}
                columns={columns}
                checkboxSelection
rsf-admin/src/page/orders/outStock/OutOrderItemList.jsx
@@ -181,10 +181,10 @@
        <TextField source="matnrCode" label="table.field.outStockItem.matnrCode" />,
        <TextField source="maktx" label="table.field.outStockItem.maktx" />,
        <TextField source="platOrderCode" label="table.field.outStockItem.platOrderCode" />,
        <NumberField source="anfme" label="table.field.outStockItem.anfme" />,
        <NumberField source="purQty" label="table.field.outStockItem.purQty" />,
        <NumberField source="workQty" label="table.field.outStockItem.workQty" />,
        <NumberField source="qty" label="table.field.outStockItem.qty" />,
        <NumberField source="anfme" label="table.field.outStockItem.anfme" options={{ maximumFractionDigits: 6 }} />,
        <NumberField source="purQty" label="table.field.outStockItem.purQty" options={{ maximumFractionDigits: 6 }} />,
        <NumberField source="workQty" label="table.field.outStockItem.workQty" options={{ maximumFractionDigits: 6 }} />,
        <NumberField source="qty" label="table.field.outStockItem.qty" options={{ maximumFractionDigits: 6 }} />,
        <TextField source="stockUnit" label="table.field.outStockItem.stockUnit" />,
        <TextField source="splrBatch" label="table.field.outStockItem.splrBatch" />,
        <TextField source="purUnit" label="table.field.outStockItem.purUnit" />,
rsf-admin/src/page/orders/outStock/OutOrderList.jsx
@@ -216,9 +216,9 @@
          <TextField source="poCode" label="table.field.outStock.poCode" />
          <TextField source="type$" label="table.field.outStock.type" />
          <TextField cellClassName="wkType" source="wkType$" label="table.field.outStock.wkType" />
          <NumberField source="anfme" label="table.field.outStock.anfme" />
          <NumberField source="workQty" label="table.field.outStock.workQty" />
          <NumberField source="qty" label="table.field.outStock.qty" />
          <NumberField source="anfme" label="table.field.outStock.anfme" options={{ maximumFractionDigits: 6 }} />
          <NumberField source="workQty" label="table.field.outStock.workQty" options={{ maximumFractionDigits: 6 }} />
          <NumberField source="qty" label="table.field.outStock.qty" options={{ maximumFractionDigits: 6 }} />
          <TextField source="logisNo" label="table.field.outStock.logisNo" />
          <TextField source="rleStatus$" label="table.field.outStock.rleStatus" sortable={false} />
          <TextField source="updateBy$" label="common.field.updateBy" />
rsf-admin/src/page/orders/outStock/OutOrderModal.jsx
@@ -270,8 +270,8 @@
                                    <TextField source="matnrCode" label="table.field.deliveryItem.matnrCode" />
                                    <TextField source="maktx" label="table.field.deliveryItem.matnrName" />
                                    <TextField source="unit" label="table.field.deliveryItem.unit" />
                                    <NumberField source="anfme" label="table.field.deliveryItem.anfme" />
                                    <NumberField source="workQty" label="table.field.outStockItem.workQty" />
                                    <NumberField source="anfme" label="table.field.deliveryItem.anfme" options={{ maximumFractionDigits: 6 }} />
                                    <NumberField source="workQty" label="table.field.outStockItem.workQty" options={{ maximumFractionDigits: 6 }} />
                                    <TextField source="splrName" label="table.field.deliveryItem.splrName" />
                                    <TextField source="splrBatch" label="table.field.deliveryItem.splrBatch" />
                                    <TextField source="updateBy$" label="common.field.updateBy" />
rsf-admin/src/page/orders/outStock/OutOrderPreview.jsx
@@ -9,6 +9,7 @@
    useGetList,
} from 'react-admin';
import request from '@/utils/request';
import { formatQuantity } from '@/utils/common';
import { styled } from '@mui/material/styles';
import { PAGE_DRAWER_WIDTH, OPERATE_MODE, DEFAULT_PAGE_SIZE } from '@/config/setting';
import { DataGrid, useGridApiContext, GridActionsCellItem, useGridApiRef } from '@mui/x-data-grid';
@@ -105,6 +106,7 @@
        { field: 'maktx', headerName: '物料名称', width: 190 },
        {
            field: 'anfme', headerName: '出库数量', width: 110, type: 'number', editable: true,
            valueFormatter: (v) => formatQuantity(v),
            valueGetter: (value, row) => {
                return row.anfme - row.workQty - row.qty;
            },
@@ -119,6 +121,7 @@
        },
        {
            field: 'workQty', headerName: '剩余数量', width: 110, type: 'number',
            valueFormatter: (v) => formatQuantity(v),
            valueGetter: (value, row) => {
                return row.anfme - row.workQty - row.qty;
            },
rsf-admin/src/page/orders/outStock/OutStockPublic.jsx
@@ -43,6 +43,7 @@
import { styled } from '@mui/material/styles';
import { DataGrid, useGridApiContext, GridActionsCellItem, useGridApiRef } from '@mui/x-data-grid';
import request from '@/utils/request';
import { formatQuantity } from '@/utils/common';
import ConfirmationNumberOutlinedIcon from '@mui/icons-material/ConfirmationNumberOutlined';
import CloseSharpIcon from '@mui/icons-material/CloseSharp';
import ConfirmButton from '../../components/ConfirmButton';
@@ -227,12 +228,14 @@
                console.log('API返回的数据:', responseData);
                // 处理返回的数据,确保数据结构正确
                const processedData = (responseData || []).map((item, index) => {
                    // 出库单下发任务:优先用后端返回的 siteNo/sitesNo,没有则默认 1001
                    const defaultSiteNo = item.siteNo || item.sitesNo || '1001';
                    // 如果数据有 locItem 嵌套结构,需要展开
                    if (item.locItem) {
                        return {
                            ...item.locItem,
                            id: item.locItem.id || `temp_${index}`,
                            siteNo: item.siteNo,
                            siteNo: defaultSiteNo,
                            staNos: item.staNos || [],
                            sourceId: item.sourceId,
                            source: item.source
@@ -241,7 +244,8 @@
                    // 如果数据已经是扁平结构,直接返回
                    return {
                        ...item,
                        id: item.id || `temp_${index}`
                        id: item.id || `temp_${index}`,
                        siteNo: defaultSiteNo
                    };
                });
                console.log('处理后的数据:', processedData);
@@ -334,9 +338,9 @@
                                    <TextField source="poCode" label="table.field.outStockItem.poCode" />
                                    <TextField source="matnrCode" label="table.field.outStockItem.matnrCode" />
                                    <TextField source="maktx" label="table.field.outStockItem.maktx" />
                                    <NumberField source="anfme" label="table.field.outStockItem.anfme" />
                                    <NumberField source="workQty" label="table.field.outStockItem.workQty" />
                                    <NumberField source="qty" label="table.field.outStockItem.qty" />
                                    <NumberField source="anfme" label="table.field.outStockItem.anfme" options={{ maximumFractionDigits: 6 }} />
                                    <NumberField source="workQty" label="table.field.outStockItem.workQty" options={{ maximumFractionDigits: 6 }} />
                                    <NumberField source="qty" label="table.field.outStockItem.qty" options={{ maximumFractionDigits: 6 }} />
                                    <TextField source="stockUnit" label="table.field.outStockItem.stockUnit" />
                                    <TextField source="splrName" label="table.field.outStockItem.splrName" />
                                </StyledDatagrid>
@@ -393,7 +397,7 @@
        { field: 'matnrCode', headerName: '物料编码', width: 120 },
        { field: 'batch', headerName: '批次', width: 90 },
        { field: 'unit', headerName: '单位', width: 60 },
        { field: 'outQty', headerName: '出库数量', width: 110, },
        { field: 'outQty', headerName: '出库数量', width: 110, valueFormatter: (v) => formatQuantity(v) },
        {
            field: 'anfme', headerName: '库存数量', width: 110,
            renderCell: (params) => (
@@ -449,7 +453,7 @@
        return (
            hasStock ? (
                <Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
                    <span>{value}</span>
                    <span>{formatQuantity(value)}</span>
                </Box>
            ) : (
                <Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
rsf-admin/src/page/orders/outStock/SelectMatnrModal.jsx
@@ -54,6 +54,7 @@
import SaveIcon from '@mui/icons-material/Save';
import MatnrInfoModal from "./MatnrInfoModal";
import request from '@/utils/request';
import { formatQuantity, hasMoreThan6Decimals } from '@/utils/common';
import "./asnOrder.css";
const SelectMatnrModal = (props) => {
@@ -123,6 +124,10 @@
    const handleSubmit = async () => {
        setFinally();
        if (tabelData.some((item) => hasMoreThan6Decimals(item.anfme))) {
            notify('最多6位小数', { type: 'error' });
            return;
        }
        setDisabled(true);
        try {
            if (asnId === 0) {
@@ -435,7 +440,7 @@
            minWidth: 100,
            flex: 1,
            editable: true,
            valueFormatter: (val) => val < 0 ? 0 : val,
            valueFormatter: (val) => formatQuantity(val != null && val >= 0 ? val : 0),
            headerClassName: "custom",
        },
        {
rsf-admin/src/page/orders/transfer/ManualCreate.jsx
@@ -47,6 +47,7 @@
import { minHeight, padding } from "@mui/system";
import SaveIcon from '@mui/icons-material/Save';
import request from '@/utils/request';
import { formatQuantity } from '@/utils/common';
import _, { set } from 'lodash';
const ManualCreate = (props) => {
@@ -363,7 +364,7 @@
            minWidth: 100,
            flex: 1,
            editable: true,
            valueFormatter: (val) => val < 0 ? 0 : val,
            valueFormatter: (val) => formatQuantity(val != null && val >= 0 ? val : 0),
        },
        {
            field: 'splrCode',
rsf-admin/src/page/orders/transfer/TransferCreate.jsx
@@ -36,6 +36,7 @@
import DictSelect from "../../components/DictSelect";
import SaveIcon from '@mui/icons-material/Save';
import request from '@/utils/request';
import { formatQuantity } from '@/utils/common';
const TransferCreate = (props) => {
    const { open, setOpen, orderId } = props;
@@ -233,7 +234,7 @@
            minWidth: 100,
            flex: 1,
            editable: true,
            valueFormatter: (val) => val < 0 ? 0 : val,
            valueFormatter: (val) => formatQuantity(val != null && val >= 0 ? val : 0),
            headerClassName: "custom",
        },
        {
rsf-admin/src/page/statistics/stockManage/WarehouseStockInfo.jsx
@@ -49,6 +49,7 @@
import { useForm, Controller, useWatch, FormProvider, useFormContext } from "react-hook-form";
import SaveIcon from '@mui/icons-material/Save';
import request from '@/utils/request';
import { formatQuantity } from '@/utils/common';
import { Add, Edit, Delete } from '@mui/icons-material';
import { DataGrid, useGridApiRef, GRID_DATE_COL_DEF, GRID_DATETIME_COL_DEF, getGridDateOperators, useGridApiContext } from '@mui/x-data-grid';
import { LocalizationProvider, DatePicker, DateTimePicker } from '@mui/x-date-pickers';
@@ -293,7 +294,7 @@
            minWidth: 100,
            flex: 1,
            editable: false,
            valueFormatter: (val) => val < 0 ? 0 : val,
            valueFormatter: (val) => formatQuantity(val != null && val >= 0 ? val : 0),
        },
        {
            field: 'workQty',
@@ -301,7 +302,7 @@
            type: 'number',
            minWidth: 100,
            flex: 1,
            valueFormatter: (val) => val < 0 ? 0 : val,
            valueFormatter: (val) => formatQuantity(val != null && val >= 0 ? val : 0),
        },
        {
            field: 'unit',
rsf-admin/src/page/stockItem/StockItemCreate.jsx
@@ -28,6 +28,7 @@
    Box,
} from '@mui/material';
import DialogCloseButton from "../components/DialogCloseButton";
import QuantityInput from "../components/QuantityInput";
import StatusSelectInput from "../components/StatusSelectInput";
import MemoInput from "../components/MemoInput";
@@ -125,10 +126,10 @@
                                    />
                                </Grid>
                                <Grid item xs={6} display="flex" gap={1}>
                                    <NumberInput
                                    <QuantityInput
                                        label="table.field.stockItem.anfme"
                                        source="anfme"
                                        validate={required()}
                                        validate={[required()]}
                                    />
                                </Grid>
                                <Grid item xs={6} display="flex" gap={1}>
@@ -139,10 +140,10 @@
                                    />
                                </Grid>
                                <Grid item xs={6} display="flex" gap={1}>
                                    <NumberInput
                                    <QuantityInput
                                        label="table.field.stockItem.workQty"
                                        source="workQty"
                                        validate={required()}
                                        validate={[required()]}
                                    />
                                </Grid>
                                <Grid item xs={6} display="flex" gap={1}>
@@ -160,7 +161,7 @@
                                    />
                                </Grid>
                                <Grid item xs={6} display="flex" gap={1}>
                                    <NumberInput
                                    <QuantityInput
                                        label="table.field.stockItem.qty"
                                        source="qty"
                                    />
rsf-admin/src/page/stockItem/StockItemEdit.jsx
@@ -28,6 +28,7 @@
import CustomerTopToolBar from "../components/EditTopToolBar";
import MemoInput from "../components/MemoInput";
import StatusSelectInput from "../components/StatusSelectInput";
import QuantityInput from "../components/QuantityInput";
const FormToolbar = () => {
    const { getValues } = useFormContext();
@@ -104,10 +105,10 @@
                            />
                        </Stack>
                        <Stack direction='row' gap={2}>
                            <NumberInput
                            <QuantityInput
                                label="table.field.stockItem.anfme"
                                source="anfme"
                                validate={required()}
                                validate={[required()]}
                            />
                        </Stack>
                        <Stack direction='row' gap={2}>
@@ -118,10 +119,10 @@
                            />
                        </Stack>
                        <Stack direction='row' gap={2}>
                            <NumberInput
                            <QuantityInput
                                label="table.field.stockItem.workQty"
                                source="workQty"
                                validate={required()}
                                validate={[required()]}
                            />
                        </Stack>
                        <Stack direction='row' gap={2}>
@@ -139,7 +140,7 @@
                            />
                        </Stack>
                        <Stack direction='row' gap={2}>
                            <NumberInput
                            <QuantityInput
                                label="table.field.stockItem.qty"
                                source="qty"
                            />
rsf-admin/src/page/stockManage/locRevise/LocsReviseDetl.jsx
@@ -24,6 +24,7 @@
import SelectMatnrInfo from "./SelectMatnrInfo";
import SaveIcon from '@mui/icons-material/Save';
import request from '@/utils/request';
import { formatQuantity, hasMoreThan6Decimals } from '@/utils/common';
import "./asnOrder.css";
const StyledDatagrid = styled(DatagridConfigurable)(({ theme }) => ({
@@ -80,6 +81,10 @@
            }
        }));
        if (value.some((el) => hasMoreThan6Decimals(el.anfme) || hasMoreThan6Decimals(el.reviseQty))) {
            notify('最多6位小数');
            return;
        }
        saveReviseLog(value);
    };
@@ -238,6 +243,7 @@
            minWidth: 100,
            flex: 1,
            editable: false,
            valueFormatter: (v) => formatQuantity(v),
        },
        {
            field: 'reviseQty',
@@ -247,6 +253,7 @@
            flex: 1,
            editable: true,
            headerClassName: "custom",
            valueFormatter: (v) => formatQuantity(v),
        },
        {
            field: 'batch',
rsf-admin/src/page/stockManage/locRevise/ReviseLogItemList.jsx
@@ -95,8 +95,8 @@
                    <TextField source="matnrCode" label="table.field.locItem.matnrCode" />
                    <TextField source="maktx" label="table.field.locItem.maktx" />
                    <TextField source="unit" label="table.field.locItem.unit" />
                    <NumberField source="anfme" label="table.field.locItem.anfme" />
                    <NumberField source="reviseQty" label="table.field.locItem.reviseQty" />
                    <NumberField source="anfme" label="table.field.locItem.anfme" options={{ maximumFractionDigits: 6 }} />
                    <NumberField source="reviseQty" label="table.field.locItem.reviseQty" options={{ maximumFractionDigits: 6 }} />
                    <CheckDiffField source="diffQty" label="table.field.locItem.diffQty" />
                    <TextField source="batch" label="table.field.locItem.batch" />
                    <TextField source="spec" label="table.field.locItem.spec" />
rsf-admin/src/page/task/TaskItemList.jsx
@@ -153,8 +153,8 @@
                <TextField source="maktx" label="table.field.taskItem.maktx" />,
                <TextField source="matnrCode" label="table.field.taskItem.matnrCode" />,
                <TextField source="unit" label="table.field.taskItem.unit" />,
                <NumberField source="anfme" label="table.field.taskItem.anfme" />,
                <NumberField source="qty" label="table.field.taskItem.qty" />,
                <NumberField source="anfme" label="table.field.taskItem.anfme" options={{ maximumFractionDigits: 6 }} />,
                <NumberField source="qty" label="table.field.taskItem.qty" options={{ maximumFractionDigits: 6 }} />,
                <TextField source="platOrderCode" label="table.field.asnOrderItem.platOrderCode" />,
                <TextField source="platWorkCode" label="table.field.asnOrderItem.platWorkCode" />,
                <TextField source="projectCode" label="table.field.asnOrderItem.projectCode" />,
rsf-admin/src/page/task/TaskPanel.jsx
@@ -70,7 +70,7 @@
                        <TextField source="maktx" label="table.field.taskItem.maktx" />
                        <TextField source="matnrCode" label="table.field.taskItem.matnrCode" />
                        <TextField source="unit" label="table.field.taskItem.unit" />
                        <NumberField source="anfme" label="table.field.taskItem.anfme" />
                        <NumberField source="anfme" label="table.field.taskItem.anfme" options={{ maximumFractionDigits: 6 }} />
                        <TextField source="batch" label="table.field.taskItem.batch" />
                        <TextField source="spec" label="table.field.taskItem.spec" />
                        <TextField source="model" label="table.field.taskItem.model" />
rsf-admin/src/page/work/checkOutBound/CheckOutBoundList.jsx
@@ -56,6 +56,7 @@
import AddIcon from '@mui/icons-material/Add';
import DeleteIcon from '@mui/icons-material/Delete';
import request from '@/utils/request';
import { formatQuantity } from '@/utils/common';
import LocItemInfoModal from "../components/locItemInfoModal";
import { Delete } from '@mui/icons-material';
import _, { set } from 'lodash';
@@ -220,6 +221,7 @@
            type: 'number',
            width: 100,
            editable: false,
            valueFormatter: (v) => formatQuantity(v),
        },
        // {
        //     field: 'workQty',
rsf-admin/src/page/work/components/locItemInfoModal.jsx
@@ -15,6 +15,7 @@
import DialogCloseButton from "../../components/DialogCloseButton";
import { useTranslate, useNotify, useRefresh } from 'react-admin';
import request from '@/utils/request';
import { formatQuantity } from '@/utils/common';
import { DataGrid } from '@mui/x-data-grid';
import SaveIcon from '@mui/icons-material/Save';
import TreeSelectInput from "@/page/components/TreeSelectInput";
@@ -179,7 +180,7 @@
        { field: 'matnrCode', headerName: translate('table.field.locItem.matnrCode'), width: 200 },
        { field: 'maktx', headerName: translate('table.field.locItem.maktx'), width: 300 },
        { field: 'batch', headerName: translate('table.field.locItem.batch'), width: 100 },
        { field: 'anfme', headerName: translate('table.field.locItem.anfme'), width: 100 },
        { field: 'anfme', headerName: translate('table.field.locItem.anfme'), width: 100, valueFormatter: (v) => formatQuantity(v) },
        { field: 'unit', headerName: translate('table.field.locItem.unit'), width: 100 },
    ])
rsf-admin/src/page/work/outBound/OutBoundList.jsx
@@ -57,6 +57,7 @@
import AddIcon from '@mui/icons-material/Add';
import DeleteIcon from '@mui/icons-material/Delete';
import request from '@/utils/request';
import { formatQuantity } from '@/utils/common';
import LocItemInfoModal from "../components/locItemInfoModal";
import { Delete } from '@mui/icons-material';
import StaSelect from "../components/StaSelect";
@@ -219,8 +220,8 @@
            width: 100,
            type: 'number',
            editable: true,
            headerClassName: "custom",
            headerClassName: "custom",
            valueFormatter: (v) => formatQuantity(v),
        },
        {
            field: 'anfme',
@@ -228,6 +229,7 @@
            type: 'number',
            width: 100,
            editable: false,
            valueFormatter: (v) => formatQuantity(v),
        },
        // {
        //     field: 'workQty',
rsf-admin/src/page/work/stockTransfer/stockTransferList.jsx
@@ -57,6 +57,7 @@
import AddIcon from '@mui/icons-material/Add';
import DeleteIcon from '@mui/icons-material/Delete';
import request from '@/utils/request';
import { formatQuantity } from '@/utils/common';
import LocItemInfoModal from "../components/locItemInfoModal";
import { Delete } from '@mui/icons-material';
import _, { set } from 'lodash';
@@ -268,6 +269,7 @@
            type: 'number',
            width: 100,
            editable: false,
            valueFormatter: (v) => formatQuantity(v),
        },
        // {
        //     field: 'workQty',
rsf-admin/src/utils/common.js
@@ -1,4 +1,36 @@
/** 库存/数量显示:保留最多6位小数,去掉末尾多余的0(不强制补零) */
export const formatQuantity = (value) => {
    if (value == null || value === '') return '0';
    const n = Number(value);
    if (Number.isNaN(n)) return '0';
    if (n < 0) return '0';
    return n % 1 === 0 ? String(n) : n.toFixed(6).replace(/\.?0+$/, '');
};
/** 校验最多 N 位小数,用于数量类字段;超过时返回错误信息并阻止提交 */
export const maxDecimalPlaces = (maxDecimals, message) => {
    const factor = Math.pow(10, maxDecimals);
    const msg = message || `最多${maxDecimals}位小数`;
    return (value) => {
        if (value == null || value === '') return undefined;
        const n = Number(value);
        if (Number.isNaN(n)) return undefined;
        const rounded = Math.round(n * factor) / factor;
        if (Math.abs(n - rounded) > 1e-10) return msg;
        return undefined;
    };
};
/** 判断数值是否超过 6 位小数(用于提交前校验) */
export const hasMoreThan6Decimals = (value) => {
    if (value == null || value === '') return false;
    const n = Number(value);
    if (Number.isNaN(n)) return false;
    const rounded = Math.round(n * 1e6) / 1e6;
    return Math.abs(n - rounded) > 1e-10;
};
export const extractNavMenus = (data) => {
    if (!data) {
        return;
rsf-open-api/src/main/java/com/vincent/rsf/openApi/entity/params/ReportParams.java
@@ -10,7 +10,7 @@
@Data
@Accessors(chain = true)
@ApiModel(value = "ReportParams", description = "希日ERP上报参数")
@ApiModel(value = "ReportParams", description = "ERP上报参数")
public class ReportParams implements Serializable {
    @ApiModelProperty("订单类型")
rsf-server/src/main/java/com/vincent/rsf/server/api/controller/erp/params/ReportParams.java
@@ -11,7 +11,7 @@
@Data
@Accessors(chain = true)
@ApiModel(value = "ReportParams", description = "希日ERP上报参数")
@ApiModel(value = "ReportParams", description = "ERP上报参数")
public class ReportParams implements Serializable {
    @ApiModelProperty("订单类型")
rsf-server/src/main/java/com/vincent/rsf/server/api/controller/pda/PdaCheckOrderController.java
@@ -46,7 +46,7 @@
        return pdaCheckOrderService.getCheckTaskItemList2(map.get("barcode"));
    }
    @ApiOperation("希日无单据临时盘点")
    @ApiOperation("无单据临时盘点")
    @PostMapping("/check/temp/items")
    public R  tempCheckItem(@RequestBody Map<String, String> map) {
        if (Objects.isNull(map)) {
rsf-server/src/main/java/com/vincent/rsf/server/api/service/impl/PdaCheckOrderServiceImpl.java
@@ -263,7 +263,7 @@
    /**
     * @author Ryan
     * @date 2025/11/5
     * @description: 希日无单据临时盘点
     * @description: 无单据临时盘点
     * @version 1.0
     */
    @Override
rsf-server/src/main/java/com/vincent/rsf/server/api/service/impl/PdaOutStockServiceImpl.java
@@ -79,7 +79,9 @@
    @Override
    public R getOutStockTaskItem(String barcode) {
        LambdaQueryWrapper<Task> lambdaQueryWrapper = new LambdaQueryWrapper<>();
        lambdaQueryWrapper.eq(Task::getBarcode, barcode);
        lambdaQueryWrapper.eq(Task::getBarcode, barcode)
                .orderByDesc(Task::getId)
                .last("limit 1");
        Task task = taskService.getOne(lambdaQueryWrapper);
        if (null == task) {
            return R.error("未查询到相关任务");
@@ -97,12 +99,16 @@
    @Synchronized
    public R saveOutTaskSts(String barcode) {
        LambdaQueryWrapper<Task> lambdaQueryWrapper = new LambdaQueryWrapper<>();
        lambdaQueryWrapper.eq(Task::getBarcode, barcode);
        lambdaQueryWrapper.eq(Task::getBarcode, barcode)
                .orderByDesc(Task::getId)
                .last("limit 1");
        Task task = taskService.getOne(lambdaQueryWrapper);
        if (null == task) {
            throw new CoolException("未找到容器号对应任务");
        }
        if (!task.getTaskStatus().equals(TaskStsType.WAVE_SEED.id)) {
        // 允许 199(WAVE_SEED 播种中/待确认)或 196(AWAIT 等待确认),与盘点 PDA 逻辑一致
        if (!task.getTaskStatus().equals(TaskStsType.WAVE_SEED.id)
                && !task.getTaskStatus().equals(TaskStsType.AWAIT.id)) {
            return R.error("任务状态不是等待确认");
        }
        
@@ -153,7 +159,9 @@
        if (Cools.isEmpty(barcode)) {
            throw new CoolException("参数有误");
        }
        Task task = taskService.getOne(new LambdaQueryWrapper<Task>().eq(Task::getBarcode, barcode));
        Task task = taskService.getOne(new LambdaQueryWrapper<Task>().eq(Task::getBarcode, barcode)
                .orderByDesc(Task::getId)
                .last("limit 1"));
        if (null == task) {
            throw new CoolException("未找到容器号对应任务");
        }
@@ -232,7 +240,9 @@
        if (Objects.isNull(param.get("orderId"))) {
            return R.error("订单ID不能为空!!");
        }
        Task task = taskService.getOne(new LambdaQueryWrapper<Task>().eq(Task::getBarcode, param.get("barcode").toString()));
        Task task = taskService.getOne(new LambdaQueryWrapper<Task>().eq(Task::getBarcode, param.get("barcode").toString())
                .orderByDesc(Task::getId)
                .last("limit 1"));
        if (Objects.isNull(task)) {
            throw new CoolException("数据错误,任务档已不存在!!");
        }
@@ -280,7 +290,9 @@
        if (Objects.isNull(params.getTaskItems()) || params.getTaskItems().isEmpty()) {
            return R.error("拣货明细不能为空!");
        }
        Task task = taskService.getOne(new LambdaQueryWrapper<Task>().eq(Task::getBarcode, params.getBarcode()));
        Task task = taskService.getOne(new LambdaQueryWrapper<Task>().eq(Task::getBarcode, params.getBarcode())
                .orderByDesc(Task::getId)
                .last("limit 1"));
        if (null == task) {
            return R.error("未找到托盘对应的任务");
        }
@@ -542,7 +554,9 @@
            return R.error("数据错误!!");
        }
        Task task = taskService.getOne(new LambdaQueryWrapper<Task>().eq(Task::getBarcode, containerWaveParam.getContainer()));
        Task task = taskService.getOne(new LambdaQueryWrapper<Task>().eq(Task::getBarcode, containerWaveParam.getContainer())
                .orderByDesc(Task::getId)
                .last("limit 1"));
        if (null == task) {
            return R.error("未找到托盘对应的任务");
        }
rsf-server/src/main/java/com/vincent/rsf/server/common/utils/QuantityUtils.java
New file
@@ -0,0 +1,77 @@
package com.vincent.rsf.server.common.utils;
import java.math.BigDecimal;
import java.math.RoundingMode;
/**
 * 数量运算工具:使用 BigDecimal 避免 Double 精度问题。
 * 实体仍为 Double 时,运算用本工具再转回 Double(保留 6 位小数)。
 */
public final class QuantityUtils {
    private static final int SCALE = 6;
    private static final RoundingMode ROUNDING = RoundingMode.HALF_UP;
    private QuantityUtils() {
    }
    private static BigDecimal toBigDecimal(Double v) {
        if (v == null) {
            return BigDecimal.ZERO;
        }
        return BigDecimal.valueOf(v);
    }
    /**
     * 加法:a + b,结果保留 6 位小数转 Double
     */
    public static Double add(Double a, Double b) {
        return toBigDecimal(a).add(toBigDecimal(b)).setScale(SCALE, ROUNDING).doubleValue();
    }
    /**
     * 减法:a - b,结果保留 6 位小数转 Double
     */
    public static Double subtract(Double a, Double b) {
        return toBigDecimal(a).subtract(toBigDecimal(b)).setScale(SCALE, ROUNDING).doubleValue();
    }
    /**
     * 比较:a 与 b 大小,避免直接用 Double 比较。
     * 返回 a.compareTo(b):负/0/正
     */
    public static int compare(Double a, Double b) {
        return toBigDecimal(a).compareTo(toBigDecimal(b));
    }
    /**
     * a &gt; 0
     */
    public static boolean isPositive(Double a) {
        return compare(a, 0.0) > 0;
    }
    /**
     * a &gt;= 0
     */
    public static boolean isNonNegative(Double a) {
        return compare(a, 0.0) >= 0;
    }
    /**
     * a &lt;= 0
     */
    public static boolean isNonPositive(Double a) {
        return compare(a, 0.0) <= 0;
    }
    /**
     * 统一舍入到 6 位小数后转 Double(用于存库/展示,避免浮点尾差)
     */
    public static Double roundToScale(Double v) {
        if (v == null) {
            return 0.0;
        }
        return toBigDecimal(v).setScale(SCALE, ROUNDING).doubleValue();
    }
}
rsf-server/src/main/java/com/vincent/rsf/server/manager/enums/OrderWorkType.java
@@ -15,7 +15,7 @@
    ORDER_WORK_TYPE_SALE("4", "销售退回入库单"),
    ORDER_WORK_TYPE_OTHER_IN("5", "其它入库单"),
    ORDER_WORK_TYPE_OTHER_TERANSFER_IN("6", "调拔入库单"),
    ORDER_WORK_TYPE_OTHER_TERANSFER("8", "调拔入库单"),//希日项目临时修改
    ORDER_WORK_TYPE_OTHER_TERANSFER("8", "调拔入库单"),//项目临时修改
    ORDER_WORK_TYPE_STOCK_REVISE("7", "库存调整单"),
    ORDER_WORK_TYPE_SUPPLIER("11", "销售出库单"),
    ORDER_WORK_TYPE_RETURN_ORDER("12", "领料出库单"),
rsf-server/src/main/java/com/vincent/rsf/server/manager/mapper/LocItemMapper.java
@@ -23,5 +23,9 @@
     * @param locUseStatus 库位状态,为空则不过滤
     * @return 每行: matnrId, stockQty, locStatuses(逗号分隔的库位状态,仅当 locUseStatus 为空时返回)
     */
    List<Map<String, Object>> listStockByMatnrIds(@Param("matnrIds") List<Long> matnrIds, @Param("locUseStatus") String locUseStatus);
    /**
     * 按物料+状态+库位分组:每行 (matnrId, useStatus, locCode, locQty)。
     * 汇总 stockQty 与拼接 "库位(数量)" 在 Java 中完成,减轻数据库压力。
     */
    List<Map<String, Object>> listStockByMatnrIdsGroupByStatusAndLoc(@Param("matnrIds") List<Long> matnrIds, @Param("locUseStatus") String locUseStatus);
}
rsf-server/src/main/java/com/vincent/rsf/server/manager/schedules/ScheduleJobs.java
@@ -193,7 +193,7 @@
        WarehouseAreasItem warehousItem = warehouseAreasItemService.getOne(new LambdaQueryWrapper<WarehouseAreasItem>().eq(StringUtils.isNotBlank(orderItem.getFieldsIndex()), WarehouseAreasItem::getFieldsIndex, orderItem.getFieldsIndex()));
        if (!Objects.isNull(warehousItem)) {
            //希日相同票号,收货区不可新增相同票号数据
            //相同票号,收货区不可新增相同票号数据
            FieldsItem fieldsItem = fieldsItemService.getOne(new LambdaQueryWrapper<FieldsItem>()
                    .eq(FieldsItem::getUuid, orderItem.getFieldsIndex()).last("LIMIT 1"));
            if (!Objects.isNull(fieldsItem)) {
rsf-server/src/main/java/com/vincent/rsf/server/manager/service/impl/LocItemServiceImpl.java
@@ -66,13 +66,11 @@
    @Synchronized
    @Transactional(rollbackFor = Exception.class)
    public synchronized void generateTask(Short resouce, LocToTaskParams map, Long loginUserId) throws Exception {
        if (Objects.isNull(map.getSiteNo())) {
            throw new CoolException("站点不能为空!");
        }
        // 出库口未传时默认 1001
        String siteNo = StringUtils.isNotBlank(map.getSiteNo()) ? map.getSiteNo() : "1001";
        if (Objects.isNull(map.getItems()) || map.getItems().isEmpty()) {
            throw new CoolException("明细不能为空!");
        }
        String siteNo = map.getSiteNo();
        List<LocItem> items = map.getItems();
        Map<Long, List<LocItem>> listMap = items.stream().collect(Collectors.groupingBy(LocItem::getLocId));
        WkOrder order;
rsf-server/src/main/java/com/vincent/rsf/server/manager/service/impl/OutStockServiceImpl.java
@@ -92,44 +92,96 @@
    @Override
    public PageParam<Matnr, BaseParam> pageMatnrForOutStock(PageParam<Matnr, BaseParam> pageParam, Map<String, Object> params) {
        // 在 getMatnrPage 之前取出 locUseStatus:getMatnrPage 会从 where.map 中 remove 掉该键(与 params 同引用),导致后续取不到
        String locUseStatus = params.get("locUseStatus") != null ? params.get("locUseStatus").toString().trim() : null;
        if (locUseStatus != null && locUseStatus.isEmpty()) locUseStatus = null;
        PageParam<Matnr, BaseParam> page = matnrService.getMatnrPage(pageParam, params);
        List<Matnr> records = page.getRecords();
        if (records == null || records.isEmpty()) {
            return page;
        }
        List<Long> matnrIds = records.stream().map(Matnr::getId).collect(Collectors.toList());
        String locUseStatus = params.get("locUseStatus") != null ? params.get("locUseStatus").toString() : null;
        List<Map<String, Object>> stockList = locItemMapper.listStockByMatnrIds(matnrIds, locUseStatus);
        Map<Long, Double> stockQtyMap = new HashMap<>();
        Map<Long, String> locStatusDescMap = new HashMap<>();
        Map<Long, String> locCodesMap = new HashMap<>();
        for (Map<String, Object> row : stockList) {
            Long matnrId = getLong(row, "matnrId", "matnrid");
            if (matnrId == null) continue;
            Object qty = getAny(row, "stockQty", "stockqty");
            double v = qty instanceof Number ? ((Number) qty).doubleValue() : 0d;
            stockQtyMap.put(matnrId, v);
            String locCodes = getStr(row, "locCodes", "loccodes");
            if (locCodes != null && !locCodes.isEmpty()) {
                locCodesMap.put(matnrId, locCodes);
            }
            String locStatuses = getStr(row, "locStatuses", "locstatuses");
            if (locStatuses != null && !locStatuses.isEmpty()) {
                String desc = Arrays.stream(locStatuses.split(","))
                        .map(String::trim)
                        .map(LocStsType::getDescByType)
                        .collect(Collectors.joining(","));
                locStatusDescMap.put(matnrId, desc);
            } else if (locUseStatus != null && !locUseStatus.isEmpty()) {
                locStatusDescMap.put(matnrId, LocStsType.getDescByType(locUseStatus));
            }
        }
        List<Map<String, Object>> stockByLocList = locItemMapper.listStockByMatnrIdsGroupByStatusAndLoc(matnrIds, locUseStatus);
        Map<Long, List<Map<String, Object>>> rowsByMatnr = buildRowsByMatnrFromPerLoc(stockByLocList);
        List<Matnr> expanded = new ArrayList<>();
        for (Matnr record : records) {
            record.setStockQty(stockQtyMap.getOrDefault(record.getId(), 0d));
            record.setLocUseStatus$(locStatusDescMap.get(record.getId()));
            record.setLocCodes$(locCodesMap.get(record.getId()));
            List<Map<String, Object>> statusRows = rowsByMatnr.get(record.getId());
            if (statusRows == null || statusRows.isEmpty()) {
                record.setStockQty(0d);
                record.setLocUseStatus$(null);
                record.setLocCodes$(null);
                expanded.add(record);
                continue;
            }
            for (Map<String, Object> row : statusRows) {
                double v = row.get("stockQty") instanceof Number ? ((Number) row.get("stockQty")).doubleValue() : 0d;
                String useStatus = getStr(row, "useStatus", "usestatus");
                String statusDesc = useStatus != null ? LocStsType.getDescByType(useStatus) : null;
                String locCodesWithQty = getStr(row, "locCodes$", "loccodes$");
                Matnr copy = cloneMatnrForRow(record);
                copy.setStockQty(v);
                copy.setLocUseStatus$(statusDesc);
                copy.setLocCodes$(locCodesWithQty);
                expanded.add(copy);
            }
        }
        page.setRecords(expanded);
        return page;
    }
    /** 复制物料用于按状态展开行(仅复制展示用字段,id 保持原样供前端选行用) */
    private static Matnr cloneMatnrForRow(Matnr source) {
        Matnr copy = new Matnr();
        BeanUtils.copyProperties(source, copy, "stockQty", "locUseStatus$", "locCodes$");
        return copy;
    }
    /**
     * 仅用「按库位明细」查询结果在内存中分组汇总:按 (matnrId, useStatus) 聚合,
     * 得到 stockQty、locCodes$(库位(数量),...),减轻数据库压力。
     */
    private static Map<Long, List<Map<String, Object>>> buildRowsByMatnrFromPerLoc(List<Map<String, Object>> stockByLocList) {
        Map<String, List<Map<String, Object>>> perLocByMatnrAndStatus = new HashMap<>();
        for (Map<String, Object> locRow : stockByLocList) {
            Long mid = getLong(locRow, "matnrId", "matnrid");
            String us = getStr(locRow, "useStatus", "usestatus");
            if (mid == null || us == null) continue;
            String key = mid + ":" + us;
            perLocByMatnrAndStatus.computeIfAbsent(key, k -> new ArrayList<>()).add(locRow);
        }
        Map<Long, List<Map<String, Object>>> rowsByMatnr = new HashMap<>();
        for (Map.Entry<String, List<Map<String, Object>>> e : perLocByMatnrAndStatus.entrySet()) {
            String[] parts = e.getKey().split(":", 2);
            if (parts.length != 2) continue;
            Long matnrId;
            try { matnrId = Long.parseLong(parts[0]); } catch (NumberFormatException ex) { continue; }
            String useStatus = parts[1];
            List<Map<String, Object>> locs = e.getValue();
            double stockQty = 0d;
            StringBuilder sb = new StringBuilder();
            for (Map<String, Object> locRow : locs) {
                Object q = getAny(locRow, "locQty", "locqty");
                double qty = q instanceof Number ? ((Number) q).doubleValue() : 0d;
                stockQty += qty;
                String code = getStr(locRow, "locCode", "loccode");
                if (sb.length() > 0) sb.append(",");
                sb.append(code != null ? code : "").append("(").append(formatQtyForLoc(qty)).append(")");
            }
            Map<String, Object> statusRow = new HashMap<>();
            statusRow.put("matnrId", matnrId);
            statusRow.put("useStatus", useStatus);
            statusRow.put("stockQty", stockQty);
            statusRow.put("locCodes$", sb.toString());
            rowsByMatnr.computeIfAbsent(matnrId, k -> new ArrayList<>()).add(statusRow);
        }
        return rowsByMatnr;
    }
    private static String formatQtyForLoc(double qty) {
        if (qty == (long) qty) return String.valueOf((long) qty);
        String s = BigDecimal.valueOf(qty).setScale(6, RoundingMode.HALF_UP).stripTrailingZeros().toPlainString();
        return s;
    }
    private static Long getLong(Map<String, Object> map, String... keys) {
@@ -748,11 +800,13 @@
                locItems.add(locItem);
                LocToTaskParams taskParams = new LocToTaskParams();
                // 出库单下发任务时,出库口未传则默认 1001
                String siteNo = StringUtils.isNotBlank(param.getSiteNo()) ? param.getSiteNo() : "1001";
                taskParams.setType(Constants.TASK_TYPE_ORDER_OUT_STOCK)
                        .setOrgLoc(loc.getCode())
                        .setItems(locItems)
                        .setSourceId(outId)
                        .setSiteNo(param.getSiteNo());
                        .setSiteNo(siteNo);
                try {
                    //生成出库任务
                    locItemService.generateTask(TaskResouceType.TASK_RESOUCE_ORDER_TYPE.val, taskParams, loginUserId);
@@ -892,6 +946,14 @@
                            .eq(DeviceSite::getChannel, loc.getChannel())
                            .eq(DeviceSite::getType, issued.doubleValue() >= locItem.getAnfme() && itemList.size() == 1 ? TaskType.TASK_TYPE_OUT.type : TaskType.TASK_TYPE_PICK_AGAIN_OUT.type)
                    );
                    // 出库口列表排序:1001 排第一,作为默认
                    deviceSites.sort((a, b) -> {
                        boolean a1001 = "1001".equals(a.getSite());
                        boolean b1001 = "1001".equals(b.getSite());
                        if (a1001 && !b1001) return -1;
                        if (!a1001 && b1001) return 1;
                        return 0;
                    });
                    if (!deviceSites.isEmpty()) {
                        List<OrderOutItemDto.staListDto> maps = new ArrayList<>();
@@ -904,7 +966,7 @@
                        orderOutItemDto.setStaNos(maps);
                        //默认获取第一站点
                        DeviceSite deviceSite = deviceSites.stream().findFirst().get();
                        orderOutItemDto.setSiteNo(deviceSite.getSite());
                        orderOutItemDto.setSitesNo(deviceSite.getSite());
                    }
                    list.add(orderOutItemDto);
rsf-server/src/main/java/com/vincent/rsf/server/manager/service/impl/TaskServiceImpl.java
@@ -18,6 +18,7 @@
import com.vincent.rsf.server.api.entity.params.WcsTaskParams;
import com.vincent.rsf.server.api.service.WcsService;
import com.vincent.rsf.server.common.constant.Constants;
import com.vincent.rsf.server.common.utils.QuantityUtils;
import com.vincent.rsf.server.manager.controller.params.LocToTaskParams;
import com.vincent.rsf.server.manager.controller.params.PakinItem;
import com.vincent.rsf.server.manager.enums.*;
@@ -860,33 +861,37 @@
            throw new CoolException("任务明细不存在!!");
        }
        List<LocItem> items = new ArrayList<>();
        for (TaskItem taskItem : taskItems) {
            LocItem locItem = new LocItem();
            LocItemWorking locWorking = locItemWorkingService.getOne(new LambdaQueryWrapper<LocItemWorking>()
                    .eq(LocItemWorking::getTaskId, taskItem.getTaskId())
                    .eq(StringUtils.isNotBlank(taskItem.getFieldsIndex()), LocItemWorking::getFieldsIndex, taskItem.getFieldsIndex())
                    .eq(StringUtils.isNotEmpty(taskItem.getBatch()), LocItemWorking::getBatch, taskItem.getBatch())
                    .eq(LocItemWorking::getMatnrId, taskItem.getMatnrId()));
            if (Objects.isNull(locWorking)) {
               continue;
        if (TaskType.TASK_TYPE_PICK_IN.type.equals(task.getTaskType())) {
            // 拣料再入库:出库时已在 pickOrCheckTask 中扣减原库位(1100 -> 1089.899),
            // 入库完成时不再回写/累加库位明细,保持 1 条 1089.899,避免出现两条 1100
            // 仅更新库位状态、清理 LocItemWorking、任务状态及流水
        } else {
            // 盘点入库等:沿用原逻辑,从 LocItemWorking 回写并 saveBatch
            List<LocItem> items = new ArrayList<>();
            for (TaskItem taskItem : taskItems) {
                LocItem locItem = new LocItem();
                LocItemWorking locWorking = locItemWorkingService.getOne(new LambdaQueryWrapper<LocItemWorking>()
                        .eq(LocItemWorking::getTaskId, taskItem.getTaskId())
                        .eq(StringUtils.isNotBlank(taskItem.getFieldsIndex()), LocItemWorking::getFieldsIndex, taskItem.getFieldsIndex())
                        .eq(StringUtils.isNotEmpty(taskItem.getBatch()), LocItemWorking::getBatch, taskItem.getBatch())
                        .eq(LocItemWorking::getMatnrId, taskItem.getMatnrId()));
                if (Objects.isNull(locWorking)) {
                   continue;
                }
                if (task.getTaskType().equals(TaskType.TASK_TYPE_CHECK_IN.type)) {
                    locWorking.setAnfme(taskItem.getAnfme());
                }
                BeanUtils.copyProperties(locWorking, locItem);
                locItem.setWorkQty(0.0).setQty(0.0).setLocCode(loc.getCode()).setLocId(loc.getId()).setId(null).setUpdateBy(loginUserId).setUpdateTime(new Date());
                //数量为零的不入库
                if (locItem.getAnfme().compareTo(0.0) > 0) {
                    items.add(locItem);
                }
            }
            if (task.getTaskType().equals(TaskType.TASK_TYPE_CHECK_IN.type)) {
                locWorking.setAnfme(taskItem.getAnfme());
            } else if (task.getTaskType().equals(TaskType.TASK_TYPE_PICK_IN.type) && taskItem.getQty() != null && taskItem.getQty().compareTo(0.0) > 0) {
                // 拣料再入库:入库数量为本次拣料数量(taskItem.qty),保证与出库扣减一致
                locWorking.setAnfme(taskItem.getQty());
            }
            BeanUtils.copyProperties(locWorking, locItem);
            locItem.setWorkQty(0.0).setQty(0.0).setLocCode(loc.getCode()).setLocId(loc.getId()).setId(null).setUpdateBy(loginUserId).setUpdateTime(new Date());
            //数量为零的不入库
            if (locItem.getAnfme().compareTo(0.0) > 0) {
                items.add(locItem);
            }
        }
        if (!locItemService.saveBatch(items)) {
            if (!items.isEmpty() && !locItemService.saveBatch(items)) {
//            throw new CoolException("作业库存回写失败!!");
            }
        }
        TaskItem taskItem = taskItems.stream().findFirst().get();
@@ -1290,7 +1295,7 @@
//        if (Objects.isNull(locInfo)) {
//            throw new CoolException("获取库位失败!!");
//        }
        //希日上报物有情况,不需要获取新库位
        //上报物有情况,不需要获取新库位
        task.setTargLoc(task.getOrgLoc())
                .setOrgSite(task.getTargSite());
@@ -1299,16 +1304,70 @@
        }
        //获取因当前任务出库的所有物料信息
        List<LocItemWorking> tempLocs = locItemWorkingService.list(new LambdaQueryWrapper<LocItemWorking>().eq(LocItemWorking::getTaskId, task.getId()));
        if (tempLocs.isEmpty()) {
            throw new CoolException("数据错误,作业中库存数据丢失!!");
        }
        List<TaskItem> taskItems = taskItemService.list(new LambdaQueryWrapper<TaskItem>().eq(TaskItem::getTaskId, task.getId()));
        if (taskItems.isEmpty()) {
            throw new CoolException("数据错误:任务明细为空!!");
        }
        // 与查询一致:若无作业中库存但存在任务明细,用任务明细在内存中兜底,避免“能查到却无法确认”
        if (tempLocs.isEmpty()) {
            tempLocs = taskItems.stream().map(ti -> {
                LocItemWorking w = new LocItemWorking();
                w.setTaskId(task.getId());
                w.setFieldsIndex(ti.getFieldsIndex());
                w.setAnfme(ti.getAnfme());
                w.setMatnrId(ti.getMatnrId());
                w.setMaktx(ti.getMaktx());
                w.setMatnrCode(ti.getMatnrCode());
                w.setSpec(ti.getSpec());
                w.setBatch(ti.getBatch());
                w.setUnit(ti.getUnit());
                w.setModel(ti.getModel());
                return w;
            }).collect(Collectors.toList());
        }
        // 拣料入库:在生成拣料入库单时扣减原库位数量,不依赖出库完成时库位状态为R
        // 拣料入库:先算剩余数量并更新 taskItem.anfme,已拣数量 taskItem.qty = 原库位 - 剩余(保证 数量=100、已拣数量=1、库存明细=100)
        if (TaskType.TASK_TYPE_PICK_IN.type.equals(type)) {
            log.debug("[拣料入库] 开始处理 taskId={}, taskCode={}, orgLoc={}, tempLocs.size={}, taskItems.size={}",
                    task.getId(), task.getTaskCode(), task.getOrgLoc(), tempLocs.size(), taskItems.size());
            tempLocs.forEach(working -> {
                taskItems.forEach(taskItem -> {
                    if (Objects.equals(taskItem.getFieldsIndex(), working.getFieldsIndex())) {
                        // 已拣数量:优先用 taskItem.qty;为 0 时从出库单明细取 执行数(workQty) 或 订单数量(anfme),避免手动完结未填 qty 导致不扣减
                        Double pickedQty = taskItem.getQty() != null && QuantityUtils.isPositive(taskItem.getQty())
                                ? taskItem.getQty()
                                : 0.0;
                        log.debug("[拣料入库] taskItemId={}, fieldsIndex={}, working.anfme={}, taskItem.qty={}, taskItem.orderItemId={}, pickedQty(初)={}",
                                taskItem.getId(), taskItem.getFieldsIndex(), working.getAnfme(), taskItem.getQty(), taskItem.getOrderItemId(), pickedQty);
                        if (pickedQty <= 0 && taskItem.getOrderItemId() != null) {
                            WkOrderItem orderItem = asnOrderItemService.getById(taskItem.getOrderItemId());
                            log.debug("[拣料入库] 查出库单明细 orderItemId={}, orderItem={}, workQty={}, anfme={}",
                                    taskItem.getOrderItemId(), orderItem != null ? "存在" : "null",
                                    orderItem != null ? orderItem.getWorkQty() : null, orderItem != null ? orderItem.getAnfme() : null);
                            if (orderItem != null) {
                                if (orderItem.getWorkQty() != null && QuantityUtils.isPositive(orderItem.getWorkQty())) {
                                    pickedQty = orderItem.getWorkQty();
                                } else if (orderItem.getAnfme() != null && QuantityUtils.isPositive(orderItem.getAnfme())) {
                                    pickedQty = orderItem.getAnfme();
                                }
                            }
                        }
                        Double minQty = QuantityUtils.subtract(working.getAnfme(), pickedQty);
                        log.debug("[拣料入库] 计算后 pickedQty={}, minQty(剩余)={}, 将更新 taskItem.anfme={}, taskItem.qty={}",
                                pickedQty, minQty, minQty, pickedQty);
                        if (QuantityUtils.isNonNegative(minQty)) {
                            taskItem.setAnfme(minQty);
                            taskItem.setQty(pickedQty);
                            if (!taskItemService.updateById(taskItem)) {
                                throw new CoolException("任务明细修改失败!!");
                            }
                        } else {
                            log.warn("[拣料入库] minQty<0 未更新 taskItem, taskItemId={}", taskItem.getId());
                        }
                    }
                });
            });
            log.debug("[拣料入库] 即将扣减库位 locId={}, locCode={}", loc.getId(), loc.getCode());
            subtractLocItemByTaskItems(loc, taskItems, SystemAuthUtils.getLoginUserId());
        }
@@ -1317,19 +1376,19 @@
                if (Objects.equals(taskItem.getFieldsIndex(), working.getFieldsIndex())) {
                    Double minQty = taskItem.getAnfme();
                    if (!task.getTaskType().equals(TaskType.TASK_TYPE_CHECK_IN.type)) {
                        // 计算剩余数量:从LocItemWorking中减去TaskItem的拣料数量
                        minQty = Math.round((working.getAnfme() - taskItem.getQty()) * 1000000) / 1000000.0;
                        // 计算剩余数量
                        minQty = QuantityUtils.subtract(working.getAnfme(), taskItem.getQty());
                    }
                    if (minQty.compareTo(0.0) >= 0) {
                    if (QuantityUtils.isNonNegative(minQty)) {
                        // 更新TaskItem的剩余数量
                        taskItem.setAnfme(minQty);
                        if (!taskItemService.updateById(taskItem)) {
                            throw new CoolException("任务明细修改失败!!");
                        }
                        // 更新LocItemWorking的剩余数量(非盘点入库时需要更新)
                        // 更新LocItemWorking的剩余数量(非盘点入库时需要更新);仅持久化记录才写库
                        if (!task.getTaskType().equals(TaskType.TASK_TYPE_CHECK_IN.type)) {
                            working.setAnfme(minQty);
                            if (!locItemWorkingService.updateById(working)) {
                            if (working.getId() != null && !locItemWorkingService.updateById(working)) {
                                throw new CoolException("作业库存数量更新失败!!");
                            }
                        }
@@ -1384,9 +1443,9 @@
                    .setQty(0.0)
                    .setLocId(loc1.getId())
                    .setLocCode(loc1.getCode());
            // 拣料再入库:目标库位数量应为本次拣料数量(taskItem.qty),不是原库位剩余数量(taskItem.anfme)
            if (TaskType.TASK_TYPE_PICK_IN.type.equals(task.getTaskType()) && taskItem.getQty() != null && taskItem.getQty().compareTo(0.0) > 0) {
                itemWorking.setAnfme(taskItem.getQty());
            // 拣料再入库:目标库位数量应为回库的剩余数量(taskItem.anfme),即原库位减掉本次拣出后的数量(如 1000 出库 10 → 990)
            if (TaskType.TASK_TYPE_PICK_IN.type.equals(task.getTaskType()) && taskItem.getAnfme() != null && taskItem.getAnfme().compareTo(0.0) > 0) {
                itemWorking.setAnfme(taskItem.getAnfme());
            }
            workings.add(itemWorking);
        });
@@ -2066,18 +2125,37 @@
    @Transactional(rollbackFor = Exception.class)
    public void subtractLocItemByTaskItems(Loc loc, List<TaskItem> taskItems, Long loginUserId) {
        for (TaskItem taskItem : taskItems) {
            // 查询对应的库位明细
            LocItem locItem = locItemService.getOne(new LambdaQueryWrapper<LocItem>()
            LambdaQueryWrapper<LocItem> locItemWrapper = new LambdaQueryWrapper<LocItem>()
                    .eq(LocItem::getLocId, loc.getId())
                    .eq(LocItem::getMatnrId, taskItem.getMatnrId())
                    .eq(StringUtils.isNotBlank(taskItem.getBatch()), LocItem::getBatch, taskItem.getBatch())
                    .eq(StringUtils.isNotBlank(taskItem.getFieldsIndex()), LocItem::getFieldsIndex, taskItem.getFieldsIndex()));
                    .eq(LocItem::getMatnrId, taskItem.getMatnrId());
            if (StringUtils.isNotBlank(taskItem.getBatch())) {
                locItemWrapper.eq(LocItem::getBatch, taskItem.getBatch());
            } else {
                locItemWrapper.and(w -> w.isNull(LocItem::getBatch).or().eq(LocItem::getBatch, ""));
            }
            if (StringUtils.isNotBlank(taskItem.getFieldsIndex())) {
                locItemWrapper.eq(LocItem::getFieldsIndex, taskItem.getFieldsIndex());
            } else {
                locItemWrapper.and(w -> w.isNull(LocItem::getFieldsIndex).or().eq(LocItem::getFieldsIndex, ""));
            }
            LocItem locItem = locItemService.getOne(locItemWrapper);
            log.info("[拣料入库-扣减库位] taskItemId={}, locId={}, matnrId={}, batch={}, fieldsIndex={}, locItem={}, locItem.anfme={}, taskItem.qty={}, taskItem.anfme={}",
                    taskItem.getId(), loc.getId(), taskItem.getMatnrId(), taskItem.getBatch(), taskItem.getFieldsIndex(),
                    locItem != null ? "存在" : "null", locItem != null ? locItem.getAnfme() : null, taskItem.getQty(), taskItem.getAnfme());
            if (Objects.nonNull(locItem)) {
                // 计算扣减后的数量
                Double newAnfme = Math.round((locItem.getAnfme() - taskItem.getQty()) * 1000000) / 1000000.0;
                if (newAnfme.compareTo(0.0) <= 0) {
                // 扣减量:优先用本次拣料数量 taskItem.getQty();若为 0 且 taskItem.anfme 已为剩余数量,则扣减 = 原库位 - 剩余(用 BigDecimal 避免 Double 精度问题)
                Double deductQty = QuantityUtils.isPositive(taskItem.getQty())
                        ? taskItem.getQty()
                        : (taskItem.getAnfme() != null && QuantityUtils.compare(taskItem.getAnfme(), locItem.getAnfme()) < 0
                                ? QuantityUtils.subtract(locItem.getAnfme(), taskItem.getAnfme())
                                : 0.0);
                Double newAnfme = QuantityUtils.subtract(locItem.getAnfme(), deductQty);
                log.info("[拣料入库-扣减库位] locItemId={}, deductQty={}, 原anfme={}, newAnfme={}, 操作={}",
                        locItem.getId(), deductQty, locItem.getAnfme(), newAnfme, QuantityUtils.isNonPositive(newAnfme) ? "删除" : "更新");
                if (QuantityUtils.isNonPositive(newAnfme)) {
                    // 数量小于等于0,删除库位明细
                    locItemService.removeById(locItem.getId());
                } else {
@@ -2089,6 +2167,9 @@
                        throw new CoolException("库位明细数量扣减失败!!");
                    }
                }
            } else {
                log.warn("[拣料入库-扣减库位] 未查到库位明细 locId={}, matnrId={}, batch={}, fieldsIndex={},未扣减",
                        loc.getId(), taskItem.getMatnrId(), taskItem.getBatch(), taskItem.getFieldsIndex());
            }
        }
    }
rsf-server/src/main/resources/mapper/manager/LocItemMapper.xml
@@ -55,4 +55,25 @@
        </if>
        GROUP BY li.matnr_id
    </select>
    <!-- 按物料+状态+库位分组:每行一个库位及其数量,汇总与拼接在 Java 中完成以减轻库压力 -->
    <select id="listStockByMatnrIdsGroupByStatusAndLoc" resultType="java.util.HashMap">
        SELECT
            li.matnr_id AS matnrId,
            l.use_status AS useStatus,
            li.loc_code AS locCode,
            COALESCE(SUM(li.anfme), 0) AS locQty
        FROM man_loc_item li
        INNER JOIN man_loc l ON l.id = li.loc_id AND (l.deleted = 0 OR l.deleted IS NULL)
        WHERE li.deleted = 0
        AND li.matnr_id IN
        <foreach collection="matnrIds" item="id" open="(" separator="," close=")">
            #{id}
        </foreach>
        <if test="locUseStatus != null and locUseStatus != ''">
            AND l.use_status = #{locUseStatus}
        </if>
        GROUP BY li.matnr_id, l.use_status, li.loc_code
        ORDER BY li.matnr_id, l.use_status, li.loc_code
    </select>
</mapper>