feat(mes,wls): 新增生产准备检查弹窗与收发存汇总页
- 生产主计划列表新增「准备检查」:弹窗展示物料齐套情况(需求/库存/在途/缺口)与
各工序绑定设备的可用状态,未通过时提示具体缺口,便于排产前判断能否开工。
- 新增仓储「收发存汇总」页:按物料+仓库展示期初、本期入库、本期出库、期末数量,
支持物料、仓库、统计期间筛选,默认近三个月。
Co-Authored-By: Claude Code <noreply@anthropic.com>
| | |
| | | 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; |
| | |
| | | ); |
| | | } |
| | | |
| | | /** ç产å夿£æ¥ï¼ç©æã设å¤ï¼ */ |
| | | 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>( |
| | |
| | | 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; |
| | |
| | | { params }, |
| | | ); |
| | | } |
| | | |
| | | /** å页æ¥è¯¢ä»å¨æ¶ååæ±æ» */ |
| | | export function getStockSummaryPage(params: MesWmTransactionApi.SummaryParams) { |
| | | return requestClient.get<PageResult<MesWmTransactionApi.StockSummary>>( |
| | | '/mes/wm/transaction/summary', |
| | | { params }, |
| | | ); |
| | | } |
| | |
| | | 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' }); |
| | |
| | | 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; |
| | | } |
| | | |
| | | /** æäº¤å®¡æ ¸ */ |
| | |
| | | }, |
| | | }, |
| | | { |
| | | label: 'ç产å夿£æ¥', |
| | | type: 'link', |
| | | icon: ACTION_ICON.VIEW, |
| | | auth: ['mes:pro-mps:query'], |
| | | onClick: handlePreparationCheck.bind(null, row), |
| | | }, |
| | | { |
| | | label: 'çæéè´å', |
| | | type: 'link', |
| | | icon: ACTION_ICON.ADD, |
| | |
| | | > |
| | | <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> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <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> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | 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); |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <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> |