chen.lin
19 小时以前 bb7dd1f513149ecd2887895c807861fdd06a43f6
展示库存明细
1个文件已添加
24个文件已修改
598 ■■■■ 已修改文件
rsf-admin/src/context/TabDialogStateContext.jsx 75 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-admin/src/i18n/en.js 4 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-admin/src/i18n/zh.js 6 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-admin/src/layout/TabsBar.jsx 24 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-admin/src/layout/index.jsx 7 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-admin/src/page/components/PageEditDrawer.jsx 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-admin/src/page/locItem/LocItemList.jsx 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-admin/src/page/orders/outStock/MatnrInfoModal.jsx 68 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-admin/src/page/orders/outStock/OutOrderList.jsx 70 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-admin/src/page/orders/outStock/OutOrderModal.jsx 59 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-admin/src/page/orders/outStock/OutStockPublic.jsx 7 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-admin/src/page/orders/outStock/SelectMatnrModal.jsx 78 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-admin/src/page/work/outBound/OutBoundList.jsx 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/manager/controller/LocItemController.java 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/manager/controller/OutStockController.java 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/manager/entity/LocItem.java 14 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/manager/entity/Matnr.java 15 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/manager/enums/LocStsType.java 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/manager/mapper/LocItemMapper.java 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/manager/schedules/TaskSchedules.java 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/manager/service/OutStockService.java 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/manager/service/impl/MatnrServiceImpl.java 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/manager/service/impl/OutStockServiceImpl.java 68 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/manager/utils/LocManageUtil.java 10 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/resources/mapper/manager/LocItemMapper.xml 20 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-admin/src/context/TabDialogStateContext.jsx
New file
@@ -0,0 +1,75 @@
import React, { createContext, useCallback, useRef, useContext, useEffect } from 'react';
const STORAGE_KEY = 'rsf_tab_dialog_state';
// 按路径存储弹窗状态
const TabDialogStateContext = createContext(null);
// 规范化路径为「资源列表」路径
export const getDialogStatePath = (pathnameOrLocation) => {
  const pathname = typeof pathnameOrLocation === 'object' && pathnameOrLocation?.pathname != null
    ? pathnameOrLocation.pathname
    : String(pathnameOrLocation || '');
  if (!pathname || pathname === '/') return pathname || '/';
  const segments = pathname.replace(/^\//, '').split('/').filter(Boolean);
  const first = segments[0];
  return first ? `/${first}` : pathname;
};
const readFromStorage = () => {
  try {
    const raw = typeof window !== 'undefined' ? window.localStorage.getItem(STORAGE_KEY) : null;
    if (raw) {
      const parsed = JSON.parse(raw);
      return typeof parsed === 'object' && parsed !== null ? parsed : {};
    }
  } catch (_) {}
  return {};
};
const writeToStorage = (data) => {
  try {
    if (typeof window !== 'undefined') {
      window.localStorage.setItem(STORAGE_KEY, JSON.stringify(data));
    }
  } catch (_) {}
};
export const TabDialogStateProvider = ({ children }) => {
  const storeRef = useRef({});
  useEffect(() => {
    storeRef.current = readFromStorage();
  }, []);
  const getDialogState = useCallback((path) => {
    const fromRef = storeRef.current[path];
    if (fromRef !== undefined) return fromRef;
    const fromStorage = readFromStorage()[path];
    if (fromStorage !== undefined) {
      storeRef.current[path] = fromStorage;
      return fromStorage;
    }
    return undefined;
  }, []);
  const setDialogState = useCallback((path, state) => {
    storeRef.current[path] = state;
    const all = readFromStorage();
    all[path] = state;
    writeToStorage(all);
  }, []);
  const value = { getDialogState, setDialogState };
  return (
    <TabDialogStateContext.Provider value={value}>
      {children}
    </TabDialogStateContext.Provider>
  );
};
export const useTabDialogState = () => {
  const ctx = useContext(TabDialogStateContext);
  if (!ctx) return null;
  return ctx;
};
rsf-admin/src/i18n/en.js
@@ -374,6 +374,7 @@
                purUnit: "purchaseUnit",
                stockUnit: "stockUnit",
                stockLevel: "stockLeval",
                stockQty: "Stock Qty",
                isLabelMange: "isLabelMange",
                safeQty: "safetyQty",
                minQty: "minQty",
@@ -459,7 +460,8 @@
                flagLabelMange: "FlagLabelMange",
                locAttrs: "LocAttrs",
                useStatus: 'useStatus',
                locAreaId: 'locAreaId'
                locAreaId: 'locAreaId',
                locCode: 'Loc Code'
            },
            locType: {
                uuid: "uuid",
rsf-admin/src/i18n/zh.js
@@ -398,6 +398,7 @@
                purUnit: "采购单位",
                stockUnit: "库存单位",
                stockLevel: "ABC分类",
                stockQty: "库存数量",
                isLabelMange: "标签管理",
                safeQty: "安全值",
                minQty: "最小值",
@@ -491,7 +492,8 @@
                startLev: "起始层",
                startRow: "起始排",
                useStatus: '库位状态',
                locAreaId: '逻辑分区'
                locAreaId: '逻辑分区',
                locCode: '库位'
            },
            stockStatistic: {
                id: "id",
@@ -1413,6 +1415,8 @@
            closeRight: '关闭右侧标签',
            closeOthers: '关闭其他标签',
            closeAll: '关闭所有标签',
            sort: '排序',
            bulk_actions: '%{smart_count} 条被选中 |||| %{smart_count} 条被选中',
        },
        page: {
            empty_with_filters: '使用当前过滤条件未找到结果。',
rsf-admin/src/layout/TabsBar.jsx
@@ -321,14 +321,18 @@
    const handleContextMenu = (event, tab) => {
        event.preventDefault();
        event.stopPropagation();
        setContextMenu(
            contextMenu === null
                ? {
                      mouseX: event.clientX + 2,
                      mouseY: event.clientY - 6,
                  }
                : null
        );
        if (!tab) return;
        const tabObj = tabs.find(t => t.path === tab.path || isSameResource(t.path, tab.path));
        if (!tabObj) return;
        const hasItems = tabObj.closable ||
            canCloseLeftForTab(tab.path) ||
            canCloseRightForTab(tab.path) ||
            canCloseOthersForTab(tab.path);
        if (!hasItems) return;
        setContextMenu({
            mouseX: event.clientX + 2,
            mouseY: event.clientY - 6,
        });
        setContextMenuTab(tab);
    };
@@ -551,6 +555,8 @@
                        ? { top: contextMenu.mouseY, left: contextMenu.mouseX }
                        : undefined
                }
                disableScrollLock
                ModalProps={{ disablePortal: true }}
                PaperProps={{
                    sx: {
                        minWidth: 120,
@@ -564,7 +570,6 @@
                    sx: { py: 0 },
                }}
            >
                {/* 关闭当前标签
                {contextMenuTab && contextMenuTab.closable && (
                    <MenuItem
                        onClick={handleCloseCurrentTab}
@@ -578,7 +583,6 @@
                        {t('ra.action.close', '关闭当前标签')}
                    </MenuItem>
                )}
                */}
                {contextMenuTab && canCloseLeftForTab(contextMenuTab.path) && (
                    <MenuItem
                        onClick={handleCloseLeftTabs}
rsf-admin/src/layout/index.jsx
@@ -4,6 +4,7 @@
import { MyMenu } from './MyMenu';
import TabsBar from './TabsBar';
import { Box } from '@mui/material';
import { TabDialogStateProvider } from '@/context/TabDialogStateContext';
const LayoutContent = ({ children }) => {
  const [sidebarIsOpen] = useSidebarState();
@@ -15,7 +16,7 @@
      top: 48,
      left: sidebarWidth + 5,
      right: 0,
      zIndex: 1400, // 高于 Dialog/Modal(1300),通过 Portal 挂到 body 才能盖住弹窗
      zIndex: 1200, // 低于 Dialog/Modal 与 Select/Menu(1300),避免标签页遮盖下发窗口内的下拉(如出库策略:效率优先/先进先出)
      transition: (theme) =>
        theme.transitions.create('left', {
          easing: theme.transitions.easing.sharp,
@@ -47,7 +48,9 @@
      }}
    >
      {createPortal(tabsBarEl, document.body)}
      {children}
      <TabDialogStateProvider>
        {children}
      </TabDialogStateProvider>
      <CheckForApplicationUpdate />
    </RALayout>
  );
rsf-admin/src/page/components/PageEditDrawer.jsx
@@ -42,8 +42,8 @@
                            </Stack>
                        </Box>
                    </Card>
                    <Card sx={{mt: '1em'}}>
                        <Box>
                    <Card sx={{ mt: '1em' }}>
                        <Box sx={{ pt: 2 }}>
                            {children}
                        </Box>
                    </Card>
rsf-admin/src/page/locItem/LocItemList.jsx
@@ -146,6 +146,7 @@
                <NumberField source="id" />,
                <NumberField source="locId" label="table.field.locItem.locId" />,
                <TextField source="locCode" label="table.field.locItem.locCode" />,
                <TextField source="locUseStatus$" label="table.field.loc.useStatus" />,
                <NumberField source="matnrId" label="table.field.locItem.matnrId" />,
                <TextField source="maktx" label="table.field.locItem.maktx" />,
                <TextField source="matnrCode" label="table.field.locItem.matnrCode" />,
rsf-admin/src/page/orders/outStock/MatnrInfoModal.jsx
@@ -10,7 +10,11 @@
    Box,
    Button,
    Paper,
    styled
    styled,
    Select,
    MenuItem,
    FormControl,
    InputLabel
} from '@mui/material';
import DialogCloseButton from "../../components/DialogCloseButton";
import { EDIT_MODE, DEFAULT_START_PAGE, DEFAULT_PAGE_SIZE, REFERENCE_INPUT_PAGESIZE } from '@/config/setting';
@@ -32,7 +36,7 @@
        }
    };
    const [formData, setFormData] = useState({});
    const [formData, setFormData] = useState({ locUseStatus: 'F' });
    const [tableData, setTableData] = useState([]);
    const [dyFields, setDyFields] = useState([]);
    const [selectedRows, setSelectedRows] = useState([]);
@@ -41,7 +45,8 @@
    const [isLoading, setIsLoading] = useState(false);
    const handleChange = (e) => {
        const { name, value } = e.target;
        setFormData(() => ({
        setFormData((prev) => ({
            ...prev,
            [name]: value
        }));
    };
@@ -50,50 +55,53 @@
        setFormData({
            name: null,
            code: null,
            groupId: null
            groupId: null,
            locUseStatus: 'F'
        })
    }
    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)));
        const value = selectedData.map((el => {
        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);
        const deduped = [...new Map(selectedData.map((s) => [s.id, s])).values()];
        const value = deduped.map((el, i) => {
            const dynamicFields = dyFields.reduce((acc, item) => {
                acc[item.fields] = el['extendFields']?.[item.fields] || '';
                return acc;
            }, {});
            return {
                _rowKey: `new_${Date.now()}_${i}`,
                matnrId: el.id,
                maktx: el.name,
                matnrCode: el.code,
                stockUnit: el.stockUnit || '',
                purUnit: el.purchaseUnit || '',
                ...dynamicFields
            }
        }))
            };
        });
        setData([...data, ...value]);
        setOpen(false);
        reset();
    };
    const getData = async () => {
        setIsLoading(true)
        console.log(page);
        const res = await request.post(`/matnr/page`, {
        setIsLoading(true);
        const res = await request.post(`/outStock/matnr/page`, {
            ...formData,
            current: page?.page,
            pageSize: page?.pageSize,
            orderBy: "create_time desc"
        });
        if (res?.data?.code === 200) {
            setTableData(res.data.data.records);
            setRowCount(res.data?.data?.total);
            setTableData(res.data.data.records || []);
            setRowCount(res.data?.data?.total ?? 0);
        } else {
            notify(res.data.msg);
            notify(res.data?.msg || '查询失败');
        }
        setIsLoading(false)
        setIsLoading(false);
    };
    useEffect(() => {
@@ -145,7 +153,7 @@
                                size="small"
                            />
                        </Grid>
                        <Grid item md={4}>
                        <Grid item md={3}>
                            <TreeSelectInput
                                label="table.field.matnr.groupId"
                                value={formData.groupId}
@@ -154,6 +162,24 @@
                                name="groupId"
                                onChange={handleChange}
                            />
                        </Grid>
                        <Grid item md={3}>
                            <FormControl size="small" fullWidth variant="filled">
                                <InputLabel>{translate('table.field.loc.useStatus')}</InputLabel>
                                <Select
                                    name="locUseStatus"
                                    value={formData.locUseStatus ?? ''}
                                    onChange={handleChange}
                                    label={translate('table.field.loc.useStatus')}
                                >
                                    <MenuItem value="">全部</MenuItem>
                                    {(JSON.parse(localStorage.getItem('sys_dicts')) || [])
                                        .filter((d) => d.dictTypeCode === 'sys_loc_use_stas')
                                        .map((d) => (
                                            <MenuItem key={d.value} value={d.value}>{d.label}</MenuItem>
                                        ))}
                                </Select>
                            </FormControl>
                        </Grid>
                    </Grid>
                </Box>
@@ -195,7 +221,6 @@
    const notify = useNotify();
    const [columns, setColumns] = useState([
        // { field: 'id', headerName: 'ID', width: 100 },
        { field: 'name', headerName: translate('table.field.matnr.name'), width: 300 },
        { field: 'code', headerName: translate('table.field.matnr.code'), width: 200 },
        { field: 'groupId$', headerName: translate('table.field.matnr.groupId'), width: 100 },
@@ -207,6 +232,9 @@
        { 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: '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 },
    ])
rsf-admin/src/page/orders/outStock/OutOrderList.jsx
@@ -1,5 +1,6 @@
import React, { useState, useRef, useEffect, useMemo, useCallback } from "react";
import React, { useState, useRef, useEffect, useLayoutEffect, useMemo, useCallback } from "react";
import { useLocation, useNavigate } from 'react-router-dom';
import { useTabDialogState, getDialogStatePath } from '@/context/TabDialogStateContext';
import {
  List,
  DatagridConfigurable,
@@ -83,10 +84,44 @@
}));
const OutOrderList = (props) => {
  const location = useLocation();
  const tabDialogState = useTabDialogState();
  const dicts = JSON.parse(localStorage.getItem('sys_dicts'))?.filter(dict => (dict.dictTypeCode == 'sys_business_type')) || [];
  const [createDialog, setCreateDialog] = useState(false);
  const [createDialog, setCreateDialogInner] = useState(false);
  const [manualDialog, setManualDialog] = useState(false);
  const pathKey = getDialogStatePath(
    location.pathname === '/' && typeof window !== 'undefined' && window.location?.hash
      ? (window.location.hash.replace(/^#/, '') || '/')
      : location.pathname
  );
  const setCreateDialog = useCallback((open) => {
    setCreateDialogInner(open);
    if (tabDialogState) {
      const current = tabDialogState.getDialogState(pathKey) || {};
      tabDialogState.setDialogState(pathKey, { ...current, createDialogOpen: open });
    }
  }, [pathKey, tabDialogState]);
  const saveCreateDialogForm = useCallback((formData) => {
    if (tabDialogState) {
      const current = tabDialogState.getDialogState(pathKey) || {};
      tabDialogState.setDialogState(pathKey, { ...current, createDialogOpen: true, createDialogForm: formData });
    }
  }, [pathKey, tabDialogState]);
  const savedCreateFormData = tabDialogState?.getDialogState(pathKey)?.createDialogForm;
  useLayoutEffect(() => {
    if (tabDialogState) {
      const saved = tabDialogState.getDialogState(pathKey);
      if (saved?.createDialogOpen) {
        setCreateDialogInner(true);
      }
    }
  }, [pathKey, tabDialogState]);
  const [drawerVal, setDrawerVal] = useState(false);
  const [waveRule, setWaveRule] = useState(false);
  const [selectIds, setSelectIds] = useState([]);
@@ -193,11 +228,15 @@
          <BillStatusField cellClassName="status" source="exceStatus" label="table.field.outStock.exceStatus" />
          <TextField source="memo" label="common.field.memo" sortable={false} />
          <WrapperField cellClassName="opt" label="common.field.opt" >
            <MyButton setCreateDialog={setManualDialog} setmodalType={setmodalType} />
            <EditButton label="toolbar.detail" icon={(<DetailsIcon />)}></EditButton>
            <OutOrderRowActions
              setCreateDialog={setManualDialog}
              setmodalType={setmodalType}
              setDrawerVal={setDrawerVal}
              drawerVal={drawerVal}
              setSelect={setSelect}
            />
            <CancelButton />
            <CompleteButton />
            <PublicButton setDrawerVal={setDrawerVal} drawerVal={drawerVal} setSelect={setSelect} />
          </WrapperField>
        </StyledDatagrid>
      </List>
@@ -216,6 +255,8 @@
        setOpen={setCreateDialog}
        preview={preview}
        setPreview={setPreview}
        initialFormData={savedCreateFormData}
        saveFormData={saveCreateDialogForm}
      />
      <OutStockWaveDialog open={waveRule} setOpen={setWaveRule} onClose={closeDialog} />
      <OutOrderPreview open={preview} setOpen={setPreview} />
@@ -276,6 +317,25 @@
  )
}
/** 出库单执行状态:10=初始化(仅此状态显示编辑/详情) */
const OUT_STOCK_EXCE_STATUS_INIT = 10;
const OutOrderRowActions = ({ setCreateDialog, setmodalType, setDrawerVal, drawerVal, setSelect }) => {
  const record = useRecordContext();
  const isInit = record?.exceStatus === OUT_STOCK_EXCE_STATUS_INIT || record?.exceStatus === '10';
  return (
    <>
      {isInit && (
        <>
          <MyButton setCreateDialog={setCreateDialog} setmodalType={setmodalType} />
          <EditButton label="toolbar.detail" icon={(<DetailsIcon />)} />
        </>
      )}
      <PublicButton setDrawerVal={setDrawerVal} drawerVal={drawerVal} setSelect={setSelect} />
    </>
  );
};
const MyButton = ({ setCreateDialog, setmodalType }) => {
  const record = useRecordContext();
  const handleEditClick = (btn) => {
rsf-admin/src/page/orders/outStock/OutOrderModal.jsx
@@ -1,5 +1,5 @@
import { Dialog, DialogActions, DialogContent, DialogTitle, Box, LinearProgress } from "@mui/material";
import React, { useState, useRef, useEffect, useMemo } from "react";
import React, { useState, useRef, useEffect, useMemo, useCallback } from "react";
import {
    List,
    DatagridConfigurable,
@@ -94,31 +94,64 @@
]
const OutOrderModal = (props) => {
    const { open, setOpen, preview, setPreview, record } = props;
    const { open, setOpen, preview, setPreview, record, initialFormData, saveFormData } = props;
    const [drawerVal, setDrawerVal] = useState(false);
    const [params, setParams] = useState({});
    const [params, setParams] = useState(() => initialFormData?.params ?? {});
    const [select, setSelect] = useState([]);
    const translate = useTranslate();
    const refresh = useRefresh();
    useEffect(() => {
        if (open && initialFormData?.params) {
            setParams(initialFormData.params);
        }
    }, [open, initialFormData?.params]);
    const handleClose = (event, reason) => {
        if (reason !== "backdropClick") {
            setOpen(false);
        }
    };
    useEffect(() => {
        if (open && params && Object.keys(params).length > 0 && saveFormData) {
            saveFormData({ params, formValues: params });
        }
    }, [open, params, saveFormData]);
    const handleFormValuesChange = useCallback((formValues) => {
        if (saveFormData) {
            saveFormData({ params: formValues, formValues });
        }
    }, [saveFormData]);
    const CustomFilter = () => {
        const { filterValues, setFilters, refetch } = useListContext();
        const [formValues, setFormValues] = useState(filterValues);
        const { filterValues } = useListContext();
        const initialFormValues = initialFormData?.formValues ?? initialFormData?.params ?? params ?? filterValues;
        const [formValues, setFormValues] = useState(initialFormValues || {});
        const initializedRef = useRef(false);
        useEffect(() => {
            if (open && (initialFormData?.formValues || initialFormData?.params) && !initializedRef.current) {
                const init = initialFormData?.formValues ?? initialFormData?.params ?? {};
                setFormValues(init);
                initializedRef.current = true;
            }
            if (!open) initializedRef.current = false;
        }, [open, initialFormData?.formValues, initialFormData?.params]);
        const handleChange = (event) => {
            if (event.target == undefined || event.target == null) { return }
            setFormValues(formValues => ({
            const next = {
                ...formValues,
                [event.target.name]: event.target.value,
            }));
            };
            setFormValues(next);
            handleFormValuesChange(next);
        };
        const handleSubmit = (event) => {
            setParams(formValues)
            setParams(formValues);
            if (saveFormData) saveFormData({ params: formValues, formValues });
        };
        return (
@@ -129,14 +162,14 @@
                            source="condition"
                            label="common.action.search"
                            resettable
                            defaultValue={params?.condition}
                            value={formValues?.condition ?? ''}
                            onChange={handleChange} />
                    </Stack>
                    <Stack>
                        <TextInput
                            source="deliveryCode"
                            label="table.field.deliveryItem.deliveryCode"
                            defaultValue={params?.deliveryCode}
                            value={formValues?.deliveryCode ?? ''}
                            onChange={handleChange}
                            resettable
                        />
@@ -145,7 +178,7 @@
                        <TextInput
                            source="maktx"
                            label="table.field.deliveryItem.matnrName"
                            defaultValue={params?.maktx}
                            value={formValues?.maktx ?? ''}
                            onChange={handleChange}
                            resettable
                        />
@@ -154,7 +187,7 @@
                        <TextInput
                            source="matnrCode"
                            label="table.field.deliveryItem.matnrCode"
                            defaultValue={params?.matnrCode}
                            value={formValues?.matnrCode ?? ''}
                            resettable
                            onChange={handleChange} />
                    </Stack>
@@ -162,7 +195,7 @@
                        <TextInput
                            source="splrName"
                            label="table.field.deliveryItem.splrName"
                            defaultValue={params?.splrName}
                            value={formValues?.splrName ?? ''}
                            resettable
                            onChange={handleChange} />
                    </Stack>
rsf-admin/src/page/orders/outStock/OutStockPublic.jsx
@@ -444,15 +444,18 @@
    const OutStockAnfme = React.memo(function OutStockAnfme(props) {
        const { value } = props;
        const num = Number(value);
        const hasStock = typeof num === 'number' && !Number.isNaN(num) && num > 1e-6;
        return (
            value > 0 ?
            hasStock ? (
                <Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
                    <span>{value}</span>
                </Box>
                :
            ) : (
                <Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
                    <span style={{ color: 'red' }}>{translate('common.edit.title.insuffInventory')}</span>
                </Box>
            )
        );
    });
rsf-admin/src/page/orders/outStock/SelectMatnrModal.jsx
@@ -122,38 +122,36 @@
    }
    const handleSubmit = async () => {
        setFinally()
        setDisabled(true)
        if (asnId === 0) {
            const parmas = {
                "orders": formData,
                "items": tabelData,
            }
            const res = await request.post(`/outStock/items/save`, parmas);
            if (res?.data?.code === 200) {
                setOpen(false);
                refresh();
                resetData()
        setFinally();
        setDisabled(true);
        try {
            if (asnId === 0) {
                const parmas = { "orders": formData, "items": tabelData };
                const res = await request.post(`/outStock/items/save`, parmas);
                if (res?.data?.code === 200) {
                    setOpen(false);
                    refresh();
                    resetData();
                } else {
                    notify(res?.data?.msg || '保存失败', { type: 'error' });
                }
            } else {
                notify(res.data.msg);
                const parmas = { "orders": formData, "items": tabelData };
                const res = await request.post(`/outStock/items/update`, parmas);
                if (res?.data?.code === 200) {
                    setOpen(false);
                    refresh();
                    resetData();
                } else {
                    notify(res?.data?.msg || '保存失败', { type: 'error' });
                }
            }
        } else {
            const parmas = {
                "orders": formData,
                "items": tabelData,
            }
            const res = await request.post(`/outStock/items/update`, parmas);
            if (res?.data?.code === 200) {
                setOpen(false);
                refresh();
                resetData()
            } else {
                notify(res.data.msg);
            }
        } catch (error) {
            const msg = error?.response?.data?.msg || error?.message || '保存失败,请重试';
            notify(msg, { type: 'error' });
        } finally {
            setDisabled(false);
        }
        setDisabled(false)
    };
@@ -187,9 +185,12 @@
    const [selectedRows, setSelectedRows] = useState([]);
    const getRowId = (row) => (row.id != null ? row.id : row._rowKey) ?? row.matnrId;
    const handleDeleteItem = () => {
        const newTableData = _.filter(tabelData, (item) => !selectedRows.includes(item.matnrId));
        const newTableData = tabelData.filter((item) => !selectedRows.includes(getRowId(item)));
        setTableData(newTableData);
        setSelectedRows([]);
    }
    return (
@@ -278,7 +279,7 @@
                        </Stack>
                    </Box>
                    <Box sx={{ mt: 2 }}>
                        <AsnOrderModalTable tabelData={tabelData} setTableData={setTableData} asnId={asnId} selectedRows={selectedRows} setSelectedRows={setSelectedRows} tableRef={tableRef}></AsnOrderModalTable>
                        <AsnOrderModalTable tabelData={tabelData} setTableData={setTableData} asnId={asnId} selectedRows={selectedRows} setSelectedRows={setSelectedRows} tableRef={tableRef} getRowId={getRowId}></AsnOrderModalTable>
                    </Box>
                </DialogContent>
                <DialogActions sx={{ position: 'sticky', bottom: 0, backgroundColor: 'background.paper', zIndex: 1000 }}>
@@ -410,7 +411,7 @@
const AsnOrderModalTable = ({ tabelData, setTableData, asnId, selectedRows, setSelectedRows, tableRef }) => {
const AsnOrderModalTable = ({ tabelData, setTableData, asnId, selectedRows, setSelectedRows, tableRef, getRowId: getRowIdProp }) => {
    const translate = useTranslate();
    const notify = useNotify();
@@ -530,17 +531,20 @@
    }
    const getRowId = getRowIdProp || ((row) => (row.id != null ? row.id : row._rowKey) ?? row.matnrId);
    const handleDelete = (row) => {
        const newData = _.filter(cdata.current, (item) => item.matnrId !== row.matnrId);
        const rowId = getRowId(row);
        const newData = cdata.current.filter((item) => getRowId(item) !== rowId);
        setTableData(newData);
    };
    const processRowUpdate = (newRow, oldRow) => {
        const rows = tabelData.map((r) =>
            r.matnrId === newRow.matnrId ? { ...newRow } : r
        )
        setTableData(rows)
            getRowId(r) === getRowId(oldRow) ? { ...newRow } : r
        );
        setTableData(rows);
        return newRow;
    };
@@ -557,7 +561,7 @@
                rows={tabelData}
                columns={columns}
                disableRowSelectionOnClick
                getRowId={(row) => row.matnrId ? row.matnrId : row.id}
                getRowId={(row) => getRowId(row)}
                disableColumnFilter
                disableColumnSelector
                disableColumnSorting
rsf-admin/src/page/work/outBound/OutBoundList.jsx
@@ -49,6 +49,7 @@
    Card,
} from '@mui/material';
import { EDIT_MODE, REFERENCE_INPUT_PAGESIZE } from '@/config/setting';
import _ from 'lodash';
import ConfirmButton from "../../components/ConfirmButton";
import TreeSelectInput from "@/page/components/TreeSelectInput";
import { DataGrid, useGridApiRef } from '@mui/x-data-grid';
rsf-server/src/main/java/com/vincent/rsf/server/manager/controller/LocItemController.java
@@ -13,6 +13,7 @@
import com.vincent.rsf.server.common.domain.PageParam;
import com.vincent.rsf.server.common.utils.FieldsUtils;
import com.vincent.rsf.server.manager.controller.params.LocToTaskParams;
import com.vincent.rsf.server.manager.entity.Loc;
import com.vincent.rsf.server.manager.entity.LocItem;
import com.vincent.rsf.server.manager.entity.ViewStockManage;
import com.vincent.rsf.server.manager.enums.TaskResouceType;
@@ -53,6 +54,14 @@
                Map<String, String> fields = FieldsUtils.getFields(record.getFieldsIndex());
                record.setExtendFields(fields);
            }
            // 填充库位状态,便于列表展示
            if (record.getLocId() != null) {
                Loc loc = locService.getById(record.getLocId());
                if (loc != null) {
                    record.setLocUseStatus(loc.getUseStatus());
                    record.setLocUseStatus$(loc.getUseStatus$());
                }
            }
        }
        page.setRecords(records);
@@ -81,6 +90,13 @@
                Map<String, String> fields = FieldsUtils.getFields(record.getFieldsIndex());
                record.setExtendFields(fields);
            }
            if (record.getLocId() != null) {
                Loc loc = locService.getById(record.getLocId());
                if (loc != null) {
                    record.setLocUseStatus(loc.getUseStatus());
                    record.setLocUseStatus$(loc.getUseStatus$());
                }
            }
        }
        page.setRecords(records);
rsf-server/src/main/java/com/vincent/rsf/server/manager/controller/OutStockController.java
@@ -13,6 +13,7 @@
import com.vincent.rsf.server.manager.controller.params.OrderOutTaskParam;
import com.vincent.rsf.server.manager.controller.params.OutStockToTaskParams;
import com.vincent.rsf.server.manager.entity.DeliveryItem;
import com.vincent.rsf.server.manager.entity.Matnr;
import com.vincent.rsf.server.manager.entity.WkOrder;
import com.vincent.rsf.server.manager.entity.WkOrderItem;
import com.vincent.rsf.server.manager.enums.OrderType;
@@ -93,6 +94,15 @@
        return R.ok().add(outStockService.getById(id));
    }
    @PreAuthorize("hasAuthority('manager:outStock:list')")
    @PostMapping("/outStock/matnr/page")
    @ApiOperation("出库单选物料分页(支持库位状态筛选、库存数量与库位状态展示)")
    public R pageMatnr(@RequestBody Map<String, Object> map) {
        BaseParam baseParam = buildParam(map, BaseParam.class);
        PageParam<Matnr, BaseParam> pageParam = new PageParam<>(baseParam, Matnr.class);
        return R.ok().add(outStockService.pageMatnrForOutStock(pageParam, map));
    }
    @PreAuthorize("hasAuthority('manager:outStock:save')")
    @OperationLog("Create 出库单据")
    @PostMapping("/outStock/save")
rsf-server/src/main/java/com/vincent/rsf/server/manager/entity/LocItem.java
@@ -107,6 +107,20 @@
    private Integer channel;
    /**
     * 库位状态(来自关联库位,非表字段)
     */
    @ApiModelProperty("库位状态")
    @TableField(exist = false)
    private String locUseStatus;
    /**
     * 库位状态显示值(来自关联库位,非表字段)
     */
    @ApiModelProperty("库位状态显示")
    @TableField(exist = false)
    private String locUseStatus$;
    /**
     * 物料名称
     */
    @ApiModelProperty(value= "物料名称")
rsf-server/src/main/java/com/vincent/rsf/server/manager/entity/Matnr.java
@@ -231,6 +231,21 @@
    @TableField(exist = false)
    private Map<String, String> extendFields;
    /** 库存数量(出库选物料时由库位汇总,非表字段) */
    @ApiModelProperty("库存数量")
    @TableField(exist = false)
    private Double stockQty;
    /** 库位状态展示(出库选物料时,非表字段) */
    @ApiModelProperty("库位状态")
    @TableField(exist = false)
    private String locUseStatus$;
    /** 库位展示(出库选物料时,有库存的库位编码,逗号分隔,非表字段) */
    @ApiModelProperty("库位")
    @TableField(exist = false)
    private String locCodes$;
    /**
     * 租户
     */
rsf-server/src/main/java/com/vincent/rsf/server/manager/enums/LocStsType.java
@@ -35,6 +35,15 @@
        return null;
    }
    /** 根据状态码(type)取描述,如 F -> 在库 */
    public static String getDescByType(String type) {
        if (type == null || type.isEmpty()) return type;
        for (LocStsType value : LocStsType.values()) {
            if (type.trim().equals(value.type)) return value.desc;
        }
        return type;
    }
    /**
     * @author Ryan
     * @date 2025/8/28
rsf-server/src/main/java/com/vincent/rsf/server/manager/mapper/LocItemMapper.java
@@ -9,10 +9,19 @@
import org.springframework.stereotype.Repository;
import java.util.List;
import java.util.Map;
@Mapper
@Repository
public interface LocItemMapper extends BaseMapper<LocItem> {
    List<LocItem> listByMatnr(@Param("type") String type, @Param("channel") String channel, @Param(Constants.WRAPPER) LambdaQueryWrapper<LocItem> matnr);
    /**
     * 按物料ID汇总库位库存数量;可选按库位状态过滤。
     * @param matnrIds 物料ID列表
     * @param locUseStatus 库位状态,为空则不过滤
     * @return 每行: matnrId, stockQty, locStatuses(逗号分隔的库位状态,仅当 locUseStatus 为空时返回)
     */
    List<Map<String, Object>> listStockByMatnrIds(@Param("matnrIds") List<Long> matnrIds, @Param("locUseStatus") String locUseStatus);
}
rsf-server/src/main/java/com/vincent/rsf/server/manager/schedules/TaskSchedules.java
@@ -248,7 +248,7 @@
    /**
     * 非光电站点任务下发
     */
    @Scheduled(cron = "0/55 * * * * ?  ")
    @Scheduled(cron = "0/35 * * * * ?  ")
    @Transactional(rollbackFor = Exception.class)
    public void pubTaskToWcs() {
        log.info("定时任务开始执行:任务下发到RCS");
rsf-server/src/main/java/com/vincent/rsf/server/manager/service/OutStockService.java
@@ -1,5 +1,8 @@
package com.vincent.rsf.server.manager.service;
import com.vincent.rsf.server.common.domain.PageParam;
import com.vincent.rsf.server.common.domain.BaseParam;
import com.vincent.rsf.server.manager.entity.Matnr;
import com.baomidou.mybatisplus.extension.service.IService;
import com.vincent.rsf.framework.common.R;
import com.vincent.rsf.server.manager.controller.params.AsnOrderAndItemsParams;
@@ -11,9 +14,16 @@
import com.vincent.rsf.server.manager.entity.DeliveryItem;
import java.util.List;
import java.util.Map;
public interface OutStockService extends IService<WkOrder> {
    /**
     * 出库单选物料分页:支持按库位状态筛选,并返回库存数量、库位状态展示
     */
    PageParam<Matnr, BaseParam> pageMatnrForOutStock(PageParam<Matnr, BaseParam> pageParam, Map<String, Object> params);
    R cancelOutOrder(String id);
    R genOutStock(List<DeliveryItem> ids, Long loginUserId);
rsf-server/src/main/java/com/vincent/rsf/server/manager/service/impl/MatnrServiceImpl.java
@@ -25,6 +25,7 @@
import com.vincent.rsf.server.system.constant.SerialRuleCode;
import com.vincent.rsf.server.system.service.FieldsService;
import com.vincent.rsf.server.system.utils.SerialRuleUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@@ -158,8 +159,18 @@
            }
        }
        // locUseStatus 仅用于下方 EXISTS 子查询,不能作为 man_matnr 表字段参与 buildWrapper
        Object locUseStatus = params.get("locUseStatus");
        if (pageParam.getWhere() != null && pageParam.getWhere().getMap() != null) {
            pageParam.getWhere().getMap().remove("locUseStatus");
        }
        QueryWrapper<Matnr> queryWrapper = pageParam.buildWrapper(true);
        queryWrapper.in(!longs.isEmpty(),"group_id", longs);
        // 出库选物料:按库位状态筛选(仅展示在该库位状态下有库存的物料)
        if (locUseStatus != null && StringUtils.isNotBlank(locUseStatus.toString())) {
            String useStatus = locUseStatus.toString().replace("'", "''");
            queryWrapper.apply("EXISTS (SELECT 1 FROM man_loc_item li INNER JOIN man_loc l ON li.loc_id = l.id WHERE li.matnr_id = man_matnr.id AND l.use_status = '" + useStatus + "')");
        }
        FieldsUtils.setFieldsFilters(queryWrapper,pageParam,Matnr.class);
        /**拼接扩展字段*/
rsf-server/src/main/java/com/vincent/rsf/server/manager/service/impl/OutStockServiceImpl.java
@@ -8,12 +8,16 @@
import com.vincent.rsf.framework.exception.CoolException;
import com.vincent.rsf.server.api.utils.LocUtils;
import com.vincent.rsf.server.common.constant.Constants;
import com.vincent.rsf.server.common.domain.BaseParam;
import com.vincent.rsf.server.common.domain.PageParam;
import com.vincent.rsf.server.manager.controller.dto.ExistDto;
import com.vincent.rsf.server.manager.controller.dto.OrderOutItemDto;
import com.vincent.rsf.server.manager.controller.params.*;
import com.vincent.rsf.server.manager.enums.*;
import com.vincent.rsf.server.manager.entity.Matnr;
import com.vincent.rsf.server.manager.entity.*;
import com.vincent.rsf.server.manager.mapper.AsnOrderMapper;
import com.vincent.rsf.server.manager.mapper.LocItemMapper;
import com.vincent.rsf.server.manager.service.*;
import com.vincent.rsf.server.manager.utils.LocManageUtil;
import com.vincent.rsf.server.manager.utils.OptimalAlgorithmUtil;
@@ -83,7 +87,71 @@
    private WaveOrderRelaServiceImpl waveOrderRelaService;
    @Autowired
    private TaskItemService taskItemService;
    @Autowired
    private LocItemMapper locItemMapper;
    @Override
    public PageParam<Matnr, BaseParam> pageMatnrForOutStock(PageParam<Matnr, BaseParam> pageParam, Map<String, Object> params) {
        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));
            }
        }
        for (Matnr record : records) {
            record.setStockQty(stockQtyMap.getOrDefault(record.getId(), 0d));
            record.setLocUseStatus$(locStatusDescMap.get(record.getId()));
            record.setLocCodes$(locCodesMap.get(record.getId()));
        }
        return page;
    }
    private static Long getLong(Map<String, Object> map, String... keys) {
        Object v = getAny(map, keys);
        if (v == null) return null;
        if (v instanceof Long) return (Long) v;
        if (v instanceof Number) return ((Number) v).longValue();
        try { return Long.parseLong(v.toString()); } catch (NumberFormatException e) { return null; }
    }
    private static String getStr(Map<String, Object> map, String... keys) {
        Object v = getAny(map, keys);
        return v != null ? v.toString() : null;
    }
    private static Object getAny(Map<String, Object> map, String... keys) {
        for (String key : keys) {
            Object v = map.get(key);
            if (v != null) return v;
        }
        return null;
    }
    /**
     * @param
rsf-server/src/main/java/com/vincent/rsf/server/manager/utils/LocManageUtil.java
@@ -96,7 +96,10 @@
    public static List<LocItem> getEfficiencyFirstItemList(String matnrCode, String splrBatch, Double anfme) {
        LambdaQueryWrapper<LocItem> locItemQueryWrapper = new LambdaQueryWrapper<>();
        locItemQueryWrapper.eq(LocItem::getMatnrCode, matnrCode);
        locItemQueryWrapper.eq(StringUtils.isNotBlank(splrBatch), LocItem::getBatch, splrBatch);
        // 有批次时:匹配库位批次=订单批次 或 库位批次为空(无批次库存可参与分配,避免误判库存不足)
        if (StringUtils.isNotBlank(splrBatch)) {
            locItemQueryWrapper.and(w -> w.eq(LocItem::getBatch, splrBatch).or().isNull(LocItem::getBatch));
        }
        String applySql = String.format(
                "EXISTS (SELECT 1 FROM man_loc ml " +
                        "WHERE ml.use_status = '%s'" +
@@ -127,7 +130,10 @@
    public static List<LocItem> getFirstInFirstOutItemList(String matnrCode, String splrBatch, Double anfme) {
        LambdaQueryWrapper<LocItem> locItemQueryWrapper = new LambdaQueryWrapper<>();
        locItemQueryWrapper.eq(LocItem::getMatnrCode, matnrCode);
        locItemQueryWrapper.eq(StringUtils.isNotEmpty(splrBatch), LocItem::getBatch, splrBatch);
        // 有批次时:匹配库位批次=订单批次 或 库位批次为空(无批次库存可参与分配,避免误判库存不足)
        if (StringUtils.isNotBlank(splrBatch)) {
            locItemQueryWrapper.and(w -> w.eq(LocItem::getBatch, splrBatch).or().isNull(LocItem::getBatch));
        }
        //如果批次不为空,按批次先后出库
        if (StringUtils.isNotBlank(splrBatch)) {
            locItemQueryWrapper.orderByAsc(LocItem::getBatch);
rsf-server/src/main/resources/mapper/manager/LocItemMapper.xml
@@ -34,4 +34,24 @@
            )t
            ${ew.customSqlSegment}
    </select>
    <select id="listStockByMatnrIds" resultType="java.util.HashMap">
        SELECT
            li.matnr_id AS matnrId,
            COALESCE(SUM(li.anfme), 0) AS stockQty,
            GROUP_CONCAT(DISTINCT li.loc_code ORDER BY li.loc_code) AS locCodes
        <if test="locUseStatus == null or locUseStatus == ''">
            , GROUP_CONCAT(DISTINCT l.use_status ORDER BY l.use_status) AS locStatuses
        </if>
        FROM man_loc_item li
        INNER JOIN man_loc l ON l.id = li.loc_id
        WHERE 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
    </select>
</mapper>