gaoluyang
5 天以前 3913f39ba957d8738487be4d252af8d9e1d2aa4f
feat(mes,wls): 新增生产准备检查弹窗与收发存汇总页

- 生产主计划列表新增「准备检查」:弹窗展示物料齐套情况(需求/库存/在途/缺口)与
各工序绑定设备的可用状态,未通过时提示具体缺口,便于排产前判断能否开工。
- 新增仓储「收发存汇总」页:按物料+仓库展示期初、本期入库、本期出库、期末数量,
支持物料、仓库、统计期间筛选,默认近三个月。

Co-Authored-By: Claude Code <noreply@anthropic.com>
已添加3个文件
已修改3个文件
374 ■■■■■ 文件已修改
src/api/mes/pro/mps/index.ts 49 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/mes/wm/transaction/index.ts 31 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/pro/mps/index.vue 31 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/pro/mps/modules/preparation-check.vue 124 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/wls/stocksummary/data.ts 84 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/wls/stocksummary/index.vue 55 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/mes/pro/mps/index.ts
@@ -51,6 +51,48 @@
    stockSufficient?: boolean; // åº“存是否充足
  }
  /** ç”Ÿäº§å‡†å¤‡æ£€æŸ¥ - ç‰©æ–™æ˜Žç»† */
  export interface MaterialCheck {
    itemId?: number;
    itemCode?: string;
    itemName?: string;
    specification?: string;
    unitMeasureName?: string;
    requiredQuantity?: number;
    stockQuantity?: number;
    purchaseOrderQuantity?: number;
    shortageQuantity?: number;
    sufficient?: boolean;
  }
  /** ç”Ÿäº§å‡†å¤‡æ£€æŸ¥ - è®¾å¤‡æ˜Žç»† */
  export interface EquipmentCheck {
    processId?: number;
    processName?: string;
    machineryId?: number;
    machineryCode?: string;
    machineryName?: string;
    status?: number;
    statusName?: string;
    available?: boolean;
    reason?: string;
  }
  /** ç”Ÿäº§å‡†å¤‡æ£€æŸ¥ç»“æžœ */
  export interface PreparationCheck {
    mpsId?: number;
    mpsCode?: string;
    productId?: number;
    productName?: string;
    quantity?: number;
    materialReady?: boolean;
    equipmentReady?: boolean;
    ready?: boolean;
    summary?: string;
    materials?: MaterialCheck[];
    equipments?: EquipmentCheck[];
  }
  /** MPS åˆ†é¡µæŸ¥è¯¢å‚æ•° */
  export interface PageParams extends PageParam {
    code?: string;
@@ -139,6 +181,13 @@
  );
}
/** ç”Ÿäº§å‡†å¤‡æ£€æŸ¥ï¼ˆç‰©æ–™ã€è®¾å¤‡ï¼‰ */
export function getPreparationCheck(id: number) {
  return requestClient.get<MesProMpsApi.PreparationCheck>(
    `/mes/pro-mps/preparation-check?id=${id}`,
  );
}
/** æ ¹æ®è¾…料缺口生成采购申请单 */
export function generatePurchaseRequest(id: number) {
  return requestClient.post<number>(
src/api/mes/wm/transaction/index.ts
@@ -27,6 +27,29 @@
    createTime?: string; // åˆ›å»ºæ—¶é—´
  }
  /** æ”¶å‘存汇总 */
  export interface StockSummary {
    itemId?: number; // ç‰©æ–™ID
    itemCode?: string; // ç‰©æ–™ç¼–码
    itemName?: string; // ç‰©æ–™åç§°
    specification?: string; // è§„æ ¼
    unitMeasureName?: string; // å•位
    warehouseId?: number; // ä»“库ID
    warehouseName?: string; // ä»“库名称
    openingQuantity?: number; // æœŸåˆæ•°é‡
    inQuantity?: number; // æœ¬æœŸå…¥åº“
    outQuantity?: number; // æœ¬æœŸå‡ºåº“
    closingQuantity?: number; // æœŸæœ«æ•°é‡
  }
  /** æ”¶å‘存汇总查询参数 */
  export interface SummaryParams extends PageParam {
    itemId?: number;
    warehouseId?: number;
    startTime?: string;
    endTime?: string;
  }
  /** åˆ†é¡µæŸ¥è¯¢å‚æ•° */
  export interface PageParams extends PageParam {
    itemId?: number;
@@ -50,3 +73,11 @@
    { params },
  );
}
/** åˆ†é¡µæŸ¥è¯¢ä»“储收发存汇总 */
export function getStockSummaryPage(params: MesWmTransactionApi.SummaryParams) {
  return requestClient.get<PageResult<MesWmTransactionApi.StockSummary>>(
    '/mes/wm/transaction/summary',
    { params },
  );
}
src/views/mes/pro/mps/index.vue
@@ -26,6 +26,7 @@
import { MpsStatusEnum, useGridColumns, useGridFormSchema } from './data';
import Form from './modules/form.vue';
import ItemList from './modules/item-list.vue';
import PreparationCheck from './modules/preparation-check.vue';
/** MPS ç”Ÿäº§ä¸»è®¡åˆ’列表 */
defineOptions({ name: 'MesProMps' });
@@ -48,6 +49,16 @@
function handleShowItems(row: MesProMpsApi.Mps) {
  currentMpsId.value = row.id;
  itemListOpen.value = true;
}
/** ç”Ÿäº§å‡†å¤‡æ£€æŸ¥å¼¹çª— */
const preparationOpen = ref(false);
const preparationMpsId = ref<number>();
/** æ‰“开生产准备检查 */
function handlePreparationCheck(row: MesProMpsApi.Mps) {
  preparationMpsId.value = row.id;
  preparationOpen.value = true;
}
/** æäº¤å®¡æ ¸ */
@@ -351,6 +362,13 @@
              },
            },
            {
              label: '生产准备检查',
              type: 'link',
              icon: ACTION_ICON.VIEW,
              auth: ['mes:pro-mps:query'],
              onClick: handlePreparationCheck.bind(null, row),
            },
            {
              label: '生成采购单',
              type: 'link',
              icon: ACTION_ICON.ADD,
@@ -407,5 +425,18 @@
    >
      <ItemList v-if="itemListOpen && currentMpsId" :mps-id="currentMpsId" />
    </Modal>
    <Modal
      v-model:open="preparationOpen"
      title="生产准备检查(物料 / è®¾å¤‡ï¼‰"
      :footer="null"
      width="1100px"
      destroy-on-close
    >
      <PreparationCheck
        v-if="preparationOpen && preparationMpsId"
        :mps-id="preparationMpsId"
      />
    </Modal>
  </Page>
</template>
src/views/mes/pro/mps/modules/preparation-check.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,124 @@
<script lang="ts" setup>
import type { MesProMpsApi } from '#/api/mes/pro/mps';
import { onMounted, ref } from 'vue';
import { Alert, Spin, Table, Tag } from 'ant-design-vue';
import type { TableColumnsType } from 'ant-design-vue';
import { getPreparationCheck } from '#/api/mes/pro/mps';
const props = defineProps<{
  mpsId: number;
}>();
const loading = ref(false);
const result = ref<MesProMpsApi.PreparationCheck>();
const materialColumns: TableColumnsType = [
  { title: '物料编码', dataIndex: 'itemCode', key: 'itemCode', width: 140 },
  { title: '物料名称', dataIndex: 'itemName', key: 'itemName', minWidth: 160 },
  { title: '规格', dataIndex: 'specification', key: 'specification', width: 130 },
  { title: '单位', dataIndex: 'unitMeasureName', key: 'unitMeasureName', width: 80 },
  { title: '需求数量', dataIndex: 'requiredQuantity', key: 'requiredQuantity', width: 110, align: 'right' },
  { title: '现有库存', dataIndex: 'stockQuantity', key: 'stockQuantity', width: 110, align: 'right' },
  { title: '在途采购', dataIndex: 'purchaseOrderQuantity', key: 'purchaseOrderQuantity', width: 110, align: 'right' },
  { title: '缺口数量', dataIndex: 'shortageQuantity', key: 'shortageQuantity', width: 110, align: 'right' },
  { title: '状态', dataIndex: 'sufficient', key: 'sufficient', width: 90 },
];
const equipmentColumns: TableColumnsType = [
  { title: '工序', dataIndex: 'processName', key: 'processName', width: 130 },
  { title: '设备编码', dataIndex: 'machineryCode', key: 'machineryCode', width: 130 },
  { title: '设备名称', dataIndex: 'machineryName', key: 'machineryName', minWidth: 160 },
  { title: '设备状态', dataIndex: 'statusName', key: 'statusName', width: 110 },
  { title: '是否可用', dataIndex: 'available', key: 'available', width: 100 },
  { title: '说明', dataIndex: 'reason', key: 'reason', minWidth: 220 },
];
function formatNumber(value?: number) {
  if (value === undefined || value === null) {
    return '-';
  }
  return Number(value).toFixed(2);
}
onMounted(async () => {
  if (!props.mpsId) {
    return;
  }
  loading.value = true;
  try {
    result.value = await getPreparationCheck(props.mpsId);
  } finally {
    loading.value = false;
  }
});
</script>
<template>
  <Spin :spinning="loading">
    <Alert
      v-if="result"
      :type="result.ready ? 'success' : 'warning'"
      show-icon
      :message="result.ready ? '生产准备检查通过' : '生产准备检查未通过'"
      :description="result.summary"
      class="mb-4"
    />
    <div v-if="result" class="mb-2 font-bold">
      ç‰©æ–™æ£€æŸ¥ï¼ˆ{{ result.materialReady ? '齐备' : '存在短缺' }})
    </div>
    <Table
      v-if="result"
      :columns="materialColumns"
      :data-source="result.materials"
      :pagination="false"
      row-key="itemId"
      size="small"
      class="mb-6"
    >
      <template #bodyCell="{ column, record }">
        <template v-if="column.key === 'requiredQuantity'">
          {{ formatNumber(record.requiredQuantity) }}
        </template>
        <template v-else-if="column.key === 'stockQuantity'">
          {{ formatNumber(record.stockQuantity) }}
        </template>
        <template v-else-if="column.key === 'purchaseOrderQuantity'">
          {{ formatNumber(record.purchaseOrderQuantity) }}
        </template>
        <template v-else-if="column.key === 'shortageQuantity'">
          <span :class="{ 'text-red-600 font-bold': record.shortageQuantity > 0 }">
            {{ formatNumber(record.shortageQuantity) }}
          </span>
        </template>
        <template v-else-if="column.key === 'sufficient'">
          <Tag :color="record.sufficient ? 'success' : 'error'">
            {{ record.sufficient ? '充足' : '短缺' }}
          </Tag>
        </template>
      </template>
    </Table>
    <div v-if="result" class="mb-2 font-bold">
      è®¾å¤‡æ£€æŸ¥ï¼ˆ{{ result.equipmentReady ? '可用' : '存在不可用设备' }})
    </div>
    <Table
      v-if="result"
      :columns="equipmentColumns"
      :data-source="result.equipments"
      :pagination="false"
      row-key="machineryId"
      size="small"
    >
      <template #bodyCell="{ column, record }">
        <template v-if="column.key === 'available'">
          <Tag :color="record.available ? 'success' : 'error'">
            {{ record.available ? '可用' : '不可用' }}
          </Tag>
        </template>
      </template>
    </Table>
  </Spin>
</template>
src/views/wls/stocksummary/data.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,84 @@
import type { VbenFormSchema } from '#/adapter/form';
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { MesWmTransactionApi } from '#/api/mes/wm/transaction';
import { markRaw } from 'vue';
import { MdmItemSelect } from '#/views/basicData/mdm/components';
import { WmWarehouseSelect } from '#/views/wls/warehouse/components';
/** åˆ—表的搜索表单 */
export function useGridFormSchema(): VbenFormSchema[] {
  return [
    {
      fieldName: 'itemId',
      label: '物料',
      component: markRaw(MdmItemSelect),
      componentProps: {
        placeholder: '请选择物料',
      },
    },
    {
      fieldName: 'warehouseId',
      label: '仓库',
      component: markRaw(WmWarehouseSelect),
      componentProps: {
        placeholder: '请选择仓库',
      },
    },
    {
      fieldName: 'dateRange',
      label: '统计期间',
      component: 'RangePicker',
      componentProps: {
        allowClear: true,
        format: 'YYYY-MM-DD',
        valueFormat: 'YYYY-MM-DD',
      },
    },
  ];
}
/** åˆ—表的字段 */
export function useGridColumns(): VxeTableGridOptions<MesWmTransactionApi.StockSummary>['columns'] {
  return [
    { field: 'itemCode', title: '物料编码', width: 140 },
    { field: 'itemName', title: '物料名称', minWidth: 160 },
    { field: 'specification', title: '规格', width: 140 },
    { field: 'warehouseName', title: '仓库', width: 120 },
    { field: 'unitMeasureName', title: '单位', width: 80 },
    {
      field: 'openingQuantity',
      title: '期初数量',
      width: 120,
      align: 'right',
      formatter: ({ cellValue }) => formatQty(cellValue),
    },
    {
      field: 'inQuantity',
      title: '本期入库',
      width: 120,
      align: 'right',
      formatter: ({ cellValue }) => formatQty(cellValue),
    },
    {
      field: 'outQuantity',
      title: '本期出库',
      width: 120,
      align: 'right',
      formatter: ({ cellValue }) => formatQty(cellValue),
    },
    {
      field: 'closingQuantity',
      title: '期末数量',
      width: 120,
      align: 'right',
      formatter: ({ cellValue }) => formatQty(cellValue),
    },
  ];
}
function formatQty(value?: number | string) {
  const num = Number(value ?? 0);
  return num.toFixed(2);
}
src/views/wls/stocksummary/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,55 @@
<script lang="ts" setup>
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { MesWmTransactionApi } from '#/api/mes/wm/transaction';
import { Page } from '@vben/common-ui';
import { useVbenVxeGrid } from '#/adapter/vxe-table';
import { getStockSummaryPage } from '#/api/mes/wm/transaction';
import { useGridColumns, useGridFormSchema } from './data';
defineOptions({ name: 'WlsStockSummary' });
const [Grid] = useVbenVxeGrid({
  formOptions: {
    schema: useGridFormSchema(),
  },
  gridOptions: {
    columns: useGridColumns(),
    height: 'auto',
    keepSource: true,
    proxyConfig: {
      ajax: {
        query: async ({ page }, formValues) => {
          const params: MesWmTransactionApi.SummaryParams = {
            pageNo: page.currentPage,
            pageSize: page.pageSize,
            itemId: formValues.itemId,
            warehouseId: formValues.warehouseId,
          };
          if (formValues.dateRange?.length === 2) {
            params.startTime = `${formValues.dateRange[0]} 00:00:00`;
            params.endTime = `${formValues.dateRange[1]} 23:59:59`;
          }
          return await getStockSummaryPage(params);
        },
      },
    },
    rowConfig: {
      keyField: 'itemId',
      isHover: true,
    },
    toolbarConfig: {
      refresh: true,
      search: true,
    },
  } as VxeTableGridOptions<MesWmTransactionApi.StockSummary>,
});
</script>
<template>
  <Page auto-content-height>
    <Grid table-title="收发存汇总" />
  </Page>
</template>