gaoluyang
4 天以前 34b5d13e7322da990565e88b15bdfdd4c7abe903
feat(erp): 采购台账与采购退货界面改造

采购台账:
- 采购订单菜单/页面文案改为「采购台账」
- 表单新增采购类型、合同号、项目名称、付款日期(必填)、付款方式
- 支持选择销售订单 / 生产工单作为来源,自动带入单号与明细
- 明细新增产品编号(随物料选择带出),列表/详情展示
- 付款日期账期超 30 天标红并提示;销售订单新增收款日期及同样提醒

采购退货:
- 表单新增处理方式(退款/换货)、退款金额、合同号、附件上传
- 新增「提交审批」动作,接入采购 → 质量 → 仓库 BPM 流程
- 列表新增处理方式、退款金额、冲红单号、换货到货单号列
- 状态字典新增「审批中」

Co-Authored-By: Claude Code <noreply@anthropic.com>
已添加1个文件
已修改17个文件
732 ■■■■■ 文件已修改
src/api/erp/purchase/order/index.ts 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/erp/sale/order/index.ts 1 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/mes/wm/returnvendor/index.ts 44 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/packages/constants/src/biz-mes-enum.ts 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/packages/constants/src/dict-enum.ts 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/utils/index.ts 1 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/utils/overdue.ts 55 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/erp/purchase/order/data.ts 225 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/erp/purchase/order/detail/index.vue 28 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/erp/purchase/order/index.vue 31 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/erp/purchase/order/modules/form.vue 114 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/erp/purchase/order/modules/item-form.vue 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/erp/sale/order/data.ts 41 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/erp/sale/order/detail/index.vue 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/erp/sale/order/index.vue 18 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/wls/returnvendor/data.ts 93 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/wls/returnvendor/index.vue 27 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/wls/returnvendor/modules/form.vue 24 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/erp/purchase/order/index.ts
@@ -19,6 +19,16 @@
    discountPrice?: number; // ä¼˜æƒ é‡‘额,单位:元
    depositPrice?: number; // å®šé‡‘金额,单位:元
    accountId?: number; // ç»“算账户编号
    purchaseType?: number; // é‡‡è´­ç±»åž‹ï¼š1生产 2办公用品 3其他
    contractNo?: string; // åˆåŒå·ï¼ˆæ‰‹å·¥è¾“入,不自动生成)
    projectName?: string; // é¡¹ç›®åç§°ï¼ˆéžå¿…填)
    paymentDate?: Date | string; // ä»˜æ¬¾æ—¥æœŸ
    paymentMethod?: number; // ä»˜æ¬¾æ–¹å¼ï¼š1电汇 2汇票 3现金 4手机支付
    sourceType?: number; // æ¥æºç±»åž‹ï¼š1生产工单 3销售订单
    sourceId?: number; // æ¥æºå•编号
    sourceNo?: string; // æ¥æºå•号
    sourceTypeName?: string; // æ¥æºç±»åž‹åç§°
    productCodes?: string; // äº§å“ç¼–号列表(MDM ç‰©æ–™ç¼–码,逗号拼接)
    status?: number; // çŠ¶æ€
    remark?: string; // å¤‡æ³¨
    attachmentList?: { id: number; name?: string; url?: string }[]; // é™„件列表
@@ -39,6 +49,7 @@
    seq?: number; // å‰ç«¯è¡Œå·
    orderId?: number; // é‡‡è´­è®¢å•编号
    productId?: number; // äº§å“ç¼–号
    productCode?: string; // äº§å“ç¼–号(MDM ç‰©æ–™ç¼–码快照)
    productName?: string; // äº§å“åç§°
    productBarCode?: string; // äº§å“æ¡ç 
    productUnitId?: number; // äº§å“å•位编号
src/api/erp/sale/order/index.ts
@@ -12,6 +12,7 @@
    accountId?: number; // æ”¶æ¬¾è´¦æˆ·ç¼–号
    saleUserId?: number; // é”€å”®äººå‘˜ç¼–号
    orderTime: Date; // è®¢å•æ—¶é—´
    receiptDate?: Date | string; // æ”¶æ¬¾æ—¥æœŸ
    totalCount: number; // åˆè®¡æ•°é‡
    totalPrice: number; // åˆè®¡é‡‘额,单位:元
    status: number; // çŠ¶æ€
src/api/mes/wm/returnvendor/index.ts
@@ -16,9 +16,20 @@
    vendorNickname?: string; // ä¾›åº”商简称
    returnDate?: number | string; // é€€è´§æ—¥æœŸ
    returnReason?: string; // é€€è´§åŽŸå› 
    contractNo?: string; // é‡‡è´­åˆåŒå·ï¼ˆåªè¯»ï¼Œä»Žé‡‡è´­è®¢å•快照)
    handleType?: number; // å¤„理方式:1退款 2换货
    refundAmount?: number; // é€€æ¬¾é‡‘额
    redFlushPaymentId?: number; // è´Ÿæ•°å†²çº¢ä»˜æ¬¾å•编号
    redFlushPaymentNo?: string; // è´Ÿæ•°å†²çº¢ä»˜æ¬¾å•号
    replacementNoticeId?: number; // æ¢è´§åˆ°è´§é€šçŸ¥å•编号
    replacementNoticeCode?: string; // æ¢è´§åˆ°è´§é€šçŸ¥å•号
    processInstanceId?: string; // BPM æµç¨‹å®žä¾‹ç¼–号
    attachmentList?: { id: number; name?: string; url?: string }[]; // é™„件列表
    blobIds?: number[]; // é™„ä»¶ blobId åˆ—表(提交用)
    transportCode?: string; // è¿å•号
    transportTelephone?: string; // è”系电话
    status?: number; // å•据状态
    statusName?: string; // çŠ¶æ€åç§°
    remark?: string; // å¤‡æ³¨
    createTime?: Date; // åˆ›å»ºæ—¶é—´
  }
@@ -59,6 +70,39 @@
  return requestClient.put(`/mes/wm/return-vendor/submit?id=${id}`);
}
/** æäº¤ä¾›åº”商退货单审批(BPM:采购 -> è´¨é‡ -> ä»“库) */
export function submitReturnVendorApproval(
  id: number,
  processDefinitionKey = 'purchase_return_vendor_approve',
) {
  return requestClient.put('/mes/wm/return-vendor/submit-approval', null, {
    params: { id, processDefinitionKey },
  });
}
/** æŸ¥è¯¢ä¾›åº”商退货单附件 */
export function getReturnVendorAttachments(id: number) {
  return requestClient.get<
    { id: number; name?: string; url?: string }[]
  >('/mes/wm/return-vendor/attachments', { params: { id } });
}
/** ç”Ÿæˆé‡‡è´­é€€è´§è´Ÿæ•°å†²çº¢ä»˜æ¬¾å•(幂等) */
export function createReturnVendorRedFlush(id: number) {
  return requestClient.post<number>('/mes/wm/return-vendor/red-flush', null, {
    params: { id },
  });
}
/** ç”Ÿæˆæ¢è´§åˆ°è´§é€šçŸ¥å•(幂等) */
export function createReturnVendorReplacementNotice(id: number) {
  return requestClient.post<number>(
    '/mes/wm/return-vendor/replacement-notice',
    null,
    { params: { id } },
  );
}
/** æ‰§è¡Œæ‹£è´§ */
export function stockReturnVendor(id: number) {
  return requestClient.put(`/mes/wm/return-vendor/stock?id=${id}`);
src/packages/constants/src/biz-mes-enum.ts
@@ -379,12 +379,21 @@
/** MES ä¾›åº”商退货单状态枚举 */
export const MesWmReturnVendorStatusEnum = {
  PREPARE: MesOrderStatusConstants.DRAFT,
  /** å®¡æ‰¹ä¸­ï¼ˆBPM æµç¨‹æµè½¬ä¸­ï¼Œå¤ç”¨ CONFIRMED=1) */
  BPM_APPROVING: MesOrderStatusConstants.CONFIRMED,
  /** å¾…拣货(保留旧命名,值仍为 2) */
  APPROVING: MesOrderStatusConstants.APPROVING,
  APPROVED: MesOrderStatusConstants.APPROVED,
  FINISHED: MesOrderStatusConstants.FINISHED,
  CANCELED: MesOrderStatusConstants.CANCELLED,
} as const;
/** MES ä¾›åº”商退货单处理方式枚举 */
export const MesWmReturnVendorHandleTypeEnum = {
  REFUND: 1,
  REPLACEMENT: 2,
} as const;
/** MES å‘货通知单状态枚举 */
export const MesWmSalesNoticeStatusEnum = {
  PREPARE: MesOrderStatusConstants.DRAFT,
src/packages/constants/src/dict-enum.ts
@@ -142,6 +142,8 @@
const ERP_DICT = {
  ERP_AUDIT_STATUS: 'erp_audit_status', // ERP å®¡æ‰¹çŠ¶æ€
  ERP_STOCK_RECORD_BIZ_TYPE: 'erp_stock_record_biz_type', // åº“存明细的业务类型
  ERP_PURCHASE_TYPE: 'erp_purchase_type', // é‡‡è´­å°è´¦-采购类型:1生产 2办公用品 3其他
  ERP_PAYMENT_METHOD: 'erp_payment_method', // é‡‡è´­å°è´¦-付款方式:1电汇 2汇票 3现金 4手机支付
} as const;
/** ========== FINANCE - è´¢åŠ¡ç®¡ç†æ¨¡å— ========== */
@@ -291,6 +293,7 @@
  MES_WM_ARRIVAL_NOTICE_STATUS: 'mes_wm_arrival_notice_status', // MES åˆ°è´§é€šçŸ¥å•状态
  MES_WM_ITEM_RECEIPT_STATUS: 'mes_wm_item_receipt_status', // MES é‡‡è´­å…¥åº“单状态
  MES_WM_RETURN_VENDOR_STATUS: 'mes_wm_return_vendor_status', // MES ä¾›åº”商退货单状态
  MES_WM_RETURN_VENDOR_HANDLE_TYPE: 'mes_wm_return_vendor_handle_type', // MES ä¾›åº”商退货处理方式:1退款 2换货
  MES_WM_SALES_NOTICE_STATUS: 'mes_wm_sales_notice_status', // MES å‘货通知单状态
  MES_WM_RETURN_SALES_STATUS: 'mes_wm_return_sales_status', // MES é”€å”®é€€è´§å•状态
  MES_WM_RETURN_ISSUE_STATUS: 'mes_wm_return_issue_status', // MES ç”Ÿäº§é€€æ–™å•状态
src/utils/index.ts
@@ -1,6 +1,7 @@
import type { Recordable } from '@vben/types';
export * from './import-result';
export * from './overdue';
export * from './rangePickerProps';
export * from './routerHelper';
src/utils/overdue.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,55 @@
import dayjs from 'dayjs';
/** è´¦æœŸé»˜è®¤å¤©æ•°ï¼šä»˜æ¬¾æ—¥æœŸ/收款日期 è· ä¸šåŠ¡æ—¥æœŸ è¶…过该天数即视为超期 */
export const DEFAULT_ACCOUNT_PERIOD_DAYS = 30;
/**
 * è®¡ç®—账期天数(目标日期 - åŸºå‡†æ—¥æœŸï¼ŒæŒ‰è‡ªç„¶å¤©å–整)
 *
 * @param baseTime åŸºå‡†æ—¥æœŸï¼ˆé‡‡è´­è®¢å•日期 / é”€å”®è®¢å•日期)
 * @param targetTime ç›®æ ‡æ—¥æœŸï¼ˆä»˜æ¬¾æ—¥æœŸ / æ”¶æ¬¾æ—¥æœŸï¼‰
 * @returns è´¦æœŸå¤©æ•°ï¼›ä»»ä¸€æ—¥æœŸä¸ºç©ºæˆ–非法时返回 null
 */
export function getAccountPeriodDays(
  baseTime?: Date | null | string,
  targetTime?: Date | null | string,
): null | number {
  if (!baseTime || !targetTime) {
    return null;
  }
  const base = dayjs(baseTime);
  const target = dayjs(targetTime);
  if (!base.isValid() || !target.isValid()) {
    return null;
  }
  return target.startOf('day').diff(base.startOf('day'), 'day');
}
/**
 * æ˜¯å¦è¶…期
 *
 * @param baseTime åŸºå‡†æ—¥æœŸï¼ˆé‡‡è´­è®¢å•日期 / é”€å”®è®¢å•日期)
 * @param targetTime ç›®æ ‡æ—¥æœŸï¼ˆä»˜æ¬¾æ—¥æœŸ / æ”¶æ¬¾æ—¥æœŸï¼‰
 * @param days è´¦æœŸä¸Šé™ï¼Œé»˜è®¤ 30 å¤©
 */
export function isOverdue(
  baseTime?: Date | null | string,
  targetTime?: Date | null | string,
  days = DEFAULT_ACCOUNT_PERIOD_DAYS,
): boolean {
  const period = getAccountPeriodDays(baseTime, targetTime);
  return period !== null && period > days;
}
/** ç”Ÿæˆè¶…期提示文案 */
export function getOverdueTip(
  baseTime?: Date | null | string,
  targetTime?: Date | null | string,
  days = DEFAULT_ACCOUNT_PERIOD_DAYS,
): string {
  const period = getAccountPeriodDays(baseTime, targetTime);
  if (period === null) {
    return '';
  }
  return `账期 ${period} å¤©ï¼ˆè¶…过 ${days} å¤©ï¼‰`;
}
src/views/erp/purchase/order/data.ts
@@ -11,14 +11,34 @@
import { getAccountSimpleList } from '#/api/erp/finance/account';
import { getSupplierSimpleList } from '#/api/srm/supplier';
import { getSimpleUserList } from '#/api/system/user';
import { getRangePickerDefaultProps, getOverdueTip, withoutFixedColumns } from '#/utils';
import { MdmItemSelect } from '#/views/basicData/mdm/components';
import { getRangePickerDefaultProps, withoutFixedColumns } from '#/utils';
import { ErpSaleOrderSelect } from '#/views/erp/sale/order/components';
import { ProWorkOrderSelect } from '#/views/mes/pro/workorder/components';
/** è¡¨å•类型 */
export type FormType = 'create' | 'detail' | 'edit';
/** é‡‡è´­æ¥æºç±»åž‹ï¼š0 æ— æ¥æºã€1 ç”Ÿäº§å·¥å•、3 é”€å”®è®¢å•(与后端 ErpPurchaseOrderSourceTypeEnum å¯¹é½ï¼‰ */
export const PurchaseSourceTypeEnum = {
  NONE: 0,
  WORK_ORDER: 1,
  SALE_ORDER: 3,
} as const;
/** è¡¨å•来源联动回调 */
export interface FormSchemaHandlers {
  /** æ¥æºç±»åž‹å˜æ›´ï¼ˆç”¨äºŽæ¸…空已选来源单据) */
  onSourceTypeChange?: (sourceType: number | undefined) => void;
  /** æ¥æºå•据选中(用于带入采购明细) */
  onSourceOrderChange?: (sourceType: number, item: any) => void;
}
/** è¡¨å•的配置项 */
export function useFormSchema(formType: FormType): VbenFormSchema[] {
export function useFormSchema(
  formType: FormType,
  handlers?: FormSchemaHandlers,
): VbenFormSchema[] {
  return [
    {
      fieldName: 'id',
@@ -62,6 +82,127 @@
        valueField: 'id',
      },
      rules: 'required',
    },
    {
      fieldName: 'purchaseType',
      label: '采购类型',
      component: 'Select',
      componentProps: {
        placeholder: '请选择采购类型',
        allowClear: true,
        options: getDictOptions(DICT_TYPE.ERP_PURCHASE_TYPE, 'number'),
      },
    },
    {
      fieldName: 'contractNo',
      label: '合同号',
      component: 'Input',
      componentProps: {
        placeholder: '请输入合同号(手工输入,系统不自动生成)',
        maxlength: 64,
        allowClear: true,
      },
    },
    {
      fieldName: 'projectName',
      label: '项目名称',
      component: 'Input',
      componentProps: {
        placeholder: '请输入项目名称(非必填)',
        maxlength: 255,
        allowClear: true,
      },
    },
    {
      fieldName: 'paymentDate',
      label: '付款日期',
      component: 'DatePicker',
      componentProps: {
        placeholder: '请选择付款日期',
        showTime: false,
        format: 'YYYY-MM-DD',
        valueFormat: 'YYYY-MM-DD HH:mm:ss',
      },
      rules: 'required',
      help: (values: {
        orderTime?: Date | string;
        paymentDate?: Date | string;
      }) => {
        const tip = getOverdueTip(values.orderTime, values.paymentDate);
        return tip ? `⚠ ${tip},请确认付款计划` : '';
      },
    },
    {
      fieldName: 'paymentMethod',
      label: '付款方式',
      component: 'Select',
      componentProps: {
        placeholder: '请选择付款方式',
        allowClear: true,
        options: getDictOptions(DICT_TYPE.ERP_PAYMENT_METHOD, 'number'),
      },
    },
    {
      fieldName: 'sourceId',
      component: 'Input',
      dependencies: {
        triggerFields: [''],
        show: () => false,
      },
    },
    {
      fieldName: 'sourceNo',
      component: 'Input',
      dependencies: {
        triggerFields: [''],
        show: () => false,
      },
    },
    {
      fieldName: 'sourceType',
      label: '来源类型',
      component: 'Select',
      componentProps: {
        placeholder: '请选择来源类型',
        allowClear: true,
        options: [
          { label: '无来源', value: PurchaseSourceTypeEnum.NONE },
          { label: '生产工单', value: PurchaseSourceTypeEnum.WORK_ORDER },
          { label: '销售订单', value: PurchaseSourceTypeEnum.SALE_ORDER },
        ],
        onChange: (value: number | undefined) =>
          handlers?.onSourceTypeChange?.(value),
      },
    },
    {
      fieldName: 'sourceWorkOrderId',
      label: '生产工单',
      component: markRaw(ProWorkOrderSelect),
      componentProps: {
        placeholder: '请选择生产工单',
        onChange: (item: any) =>
          handlers?.onSourceOrderChange?.(PurchaseSourceTypeEnum.WORK_ORDER, item),
      },
      dependencies: {
        triggerFields: ['sourceType'],
        show: (values) =>
          values.sourceType === PurchaseSourceTypeEnum.WORK_ORDER,
      },
    },
    {
      fieldName: 'sourceSaleOrderId',
      label: '销售订单',
      component: markRaw(ErpSaleOrderSelect),
      componentProps: {
        placeholder: '请选择销售订单',
        onChange: (item: any) =>
          handlers?.onSourceOrderChange?.(PurchaseSourceTypeEnum.SALE_ORDER, item),
      },
      dependencies: {
        triggerFields: ['sourceType'],
        show: (values) =>
          values.sourceType === PurchaseSourceTypeEnum.SALE_ORDER,
      },
    },
    {
      component: 'InputNumber',
@@ -184,6 +325,12 @@
      slots: { default: 'productId' },
    },
    {
      field: 'productCode',
      title: '产品编号',
      minWidth: 140,
      showOverflow: 'tooltip',
    },
    {
      field: 'productUnitName',
      title: '单位',
      minWidth: 80,
@@ -291,6 +438,44 @@
      },
    },
    {
      fieldName: 'purchaseType',
      label: '采购类型',
      component: 'Select',
      componentProps: {
        options: getDictOptions(DICT_TYPE.ERP_PURCHASE_TYPE, 'number'),
        placeholder: '请选择采购类型',
        allowClear: true,
      },
    },
    {
      fieldName: 'contractNo',
      label: '合同号',
      component: 'Input',
      componentProps: {
        placeholder: '请输入合同号',
        allowClear: true,
      },
    },
    {
      fieldName: 'paymentMethod',
      label: '付款方式',
      component: 'Select',
      componentProps: {
        options: getDictOptions(DICT_TYPE.ERP_PAYMENT_METHOD, 'number'),
        placeholder: '请选择付款方式',
        allowClear: true,
      },
    },
    {
      fieldName: 'paymentDate',
      label: '付款日期',
      component: 'RangePicker',
      componentProps: {
        ...getRangePickerDefaultProps(),
        allowClear: true,
      },
    },
    {
      fieldName: 'creator',
      label: '创建人',
      component: 'ApiSelect',
@@ -362,6 +547,12 @@
      minWidth: 120,
    },
    {
      field: 'productCodes',
      title: '产品编号',
      showOverflow: 'tooltip',
      minWidth: 140,
    },
    {
      field: 'supplierName',
      title: '供应商',
      minWidth: 120,
@@ -373,6 +564,36 @@
      formatter: 'formatDate',
    },
    {
      field: 'purchaseType',
      title: '采购类型',
      minWidth: 100,
      cellRender: {
        name: 'CellDict',
        props: { type: DICT_TYPE.ERP_PURCHASE_TYPE },
      },
    },
    {
      field: 'contractNo',
      title: '合同号',
      minWidth: 140,
      showOverflow: 'tooltip',
    },
    {
      field: 'paymentDate',
      title: '付款日期',
      width: 140,
      slots: { default: 'paymentDate' },
    },
    {
      field: 'paymentMethod',
      title: '付款方式',
      minWidth: 100,
      cellRender: {
        name: 'CellDict',
        props: { type: DICT_TYPE.ERP_PAYMENT_METHOD },
      },
    },
    {
      field: 'creatorName',
      title: '创建人',
      minWidth: 120,
src/views/erp/purchase/order/detail/index.vue
@@ -43,14 +43,40 @@
  bordered: true,
  column: 2,
  schema: [
    { label: '采购订单号', field: 'no' },
    { label: '采购单号', field: 'no' },
    { label: '供应商名称', field: 'supplierName', render: textRender },
    {
      label: '采购类型',
      field: 'purchaseType',
      render: (val) =>
        val == null
          ? '-'
          : h(DictTag, { type: DICT_TYPE.ERP_PURCHASE_TYPE, value: val }),
    },
    { label: '合同号', field: 'contractNo', render: textRender },
    { label: '项目名称', field: 'projectName', render: textRender },
    {
      label: '订单时间',
      field: 'orderTime',
      render: (val) => formatDate(val) as string,
    },
    {
      label: '付款日期',
      field: 'paymentDate',
      render: (val) => (val ? (formatDate(val) as string) : '未填写'),
    },
    {
      label: '付款方式',
      field: 'paymentMethod',
      render: (val) =>
        val == null
          ? '-'
          : h(DictTag, { type: DICT_TYPE.ERP_PAYMENT_METHOD, value: val }),
    },
    { label: '来源类型', field: 'sourceTypeName', render: textRender },
    { label: '来源单号', field: 'sourceNo', render: textRender },
    { label: '产品编号', field: 'productCodes', render: textRender },
    {
      label: '合计数量',
      field: 'totalCount',
      render: (val) => erpCountInputFormatter(val) || '-',
src/views/erp/purchase/order/index.vue
@@ -8,7 +8,8 @@
import { Page, useVbenModal } from '@vben/common-ui';
import { downloadFileFromBlobPart, isEmpty } from '@vben/utils';
import { message, Tag } from 'ant-design-vue';
import { message, Tag, Tooltip } from 'ant-design-vue';
import dayjs from 'dayjs';
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
import {
@@ -19,6 +20,7 @@
  submitPurchaseOrder,
} from '#/api/erp/purchase/order';
import { $t } from '#/locales';
import { getOverdueTip, isOverdue } from '#/utils/overdue';
import { useGridColumns, useGridFormSchema } from './data';
import Form from './modules/form.vue';
@@ -47,7 +49,7 @@
/** å¯¼å‡ºè¡¨æ ¼ */
async function handleExport() {
  const data = await exportPurchaseOrder(await gridApi.formApi.getValues());
  downloadFileFromBlobPart({ fileName: '采购订单.xls', source: data });
  downloadFileFromBlobPart({ fileName: '采购台账.xls', source: data });
}
/** å¯¼å…¥é‡‡è´­è®¢å• */
@@ -88,7 +90,7 @@
  });
  try {
    await submitPurchaseOrder(row.id!);
    message.success(`采购订单 ${row.no} å·²æäº¤å®¡æ‰¹`);
    message.success(`采购台账 ${row.no} å·²æäº¤å®¡æ‰¹`);
    handleRefresh();
  } finally {
    hideLoading();
@@ -170,12 +172,12 @@
  <Page auto-content-height>
    <FormModal @success="handleRefresh" />
    <ImportModal @success="handleRefresh" />
    <Grid table-title="采购订单列表">
    <Grid table-title="采购台账列表">
      <template #toolbar-tools>
        <TableAction
          :actions="[
            {
              label: $t('ui.actionTitle.create', ['采购订单']),
              label: $t('ui.actionTitle.create', ['采购台账']),
              type: 'primary',
              icon: ACTION_ICON.ADD,
              auth: ['erp:purchase-order:create'],
@@ -210,6 +212,23 @@
          ]"
        />
      </template>
      <template #paymentDate="{ row }">
        <Tooltip
          v-if="row.paymentDate"
          :title="getOverdueTip(row.orderTime, row.paymentDate)"
        >
          <span
            :class="
              isOverdue(row.orderTime, row.paymentDate)
                ? 'font-medium text-red-500'
                : ''
            "
          >
            {{ dayjs(row.paymentDate).format('YYYY-MM-DD') }}
          </span>
        </Tooltip>
        <span v-else class="text-gray-400">未填写</span>
      </template>
      <template #inStatus="{ row }">
        <Tag v-if="row.inStatus === 0" color="default">未入库</Tag>
        <Tag v-else-if="row.inStatus === 1" color="warning">部分入库</Tag>
@@ -239,7 +258,7 @@
              auth: ['erp:purchase-order:update-status'],
              ifShow: () => row.status === 0,
              popConfirm: {
                title: `提交采购订单${row.no}审批吗?`,
                title: `提交采购台账${row.no}审批吗?`,
                confirm: handleSubmit.bind(null, row),
              },
            },
src/views/erp/purchase/order/modules/form.vue
@@ -17,9 +17,11 @@
  getPurchaseOrder,
  updatePurchaseOrder,
} from '#/api/erp/purchase/order';
import { getSaleOrderItemListByOrderId } from '#/api/erp/sale/order';
import { getWorkOrderBomItemListByWorkOrderId } from '#/api/mes/pro/workorder/bom';
import { $t } from '#/locales';
import { useFormSchema } from '../data';
import { PurchaseSourceTypeEnum, useFormSchema } from '../data';
import PurchaseOrderItemForm from './item-form.vue';
const emit = defineEmits(['success']);
@@ -29,13 +31,85 @@
const getTitle = computed(() => {
  if (formType.value === 'create') {
    return $t('ui.actionTitle.create', ['采购订单']);
    return $t('ui.actionTitle.create', ['采购台账']);
  } else if (formType.value === 'edit') {
    return $t('ui.actionTitle.edit', ['采购订单']);
    return $t('ui.actionTitle.edit', ['采购台账']);
  } else {
    return '采购订单详情';
    return '采购台账详情';
  }
});
/** æ¥æºç±»åž‹å˜æ›´ï¼šæ¸…空已选来源单据,避免来源单与类型不一致 */
function handleSourceTypeChange(sourceType: number | undefined) {
  formApi.setValues({
    sourceWorkOrderId: undefined,
    sourceSaleOrderId: undefined,
    sourceId: undefined,
    sourceNo: undefined,
  });
  // é€‰æ‹©ã€Œæ— æ¥æºã€æ—¶ä¸ä¿ç•™æ¥æºç±»åž‹å€¼
  if (!sourceType) {
    formApi.setFieldValue('sourceType', undefined);
  }
}
/** é€‰ä¸­æ¥æºå•据:回填来源信息并带入采购明细 */
async function handleSourceOrderChange(sourceType: number, item: any) {
  if (!item?.id || formType.value === 'detail') {
    return;
  }
  if (sourceType === PurchaseSourceTypeEnum.SALE_ORDER) {
    const saleItems = await getSaleOrderItemListByOrderId(item.id);
    if (saleItems?.length) {
      handleUpdateItems(
        saleItems.map((saleItem, index) => ({
          id: undefined,
          seq: index + 1,
          productId: saleItem.productId,
          productName: saleItem.productName,
          productUnitId: saleItem.productUnitId,
          productUnitName: saleItem.productUnitName,
          productPrice: saleItem.productPrice,
          count: saleItem.count,
          totalProductPrice: saleItem.totalProductPrice,
          taxPercent: saleItem.taxPercent ?? 0,
          taxPrice: saleItem.taxPrice,
          totalPrice: saleItem.totalPrice,
          remark: saleItem.remark,
          qcCheckFlag: false,
        })),
      );
    }
    await formApi.setFieldValue('sourceSaleOrderId', item.id);
  } else if (sourceType === PurchaseSourceTypeEnum.WORK_ORDER) {
    const bomItems = await getWorkOrderBomItemListByWorkOrderId(item.id);
    if (bomItems?.length) {
      handleUpdateItems(
        bomItems.map((bomItem, index) => ({
          id: undefined,
          seq: index + 1,
          productId: bomItem.itemId,
          productCode: bomItem.itemCode,
          productName: bomItem.itemName,
          productUnitId: bomItem.unitMeasureId,
          productUnitName: bomItem.unitMeasureName,
          productPrice: undefined,
          count: bomItem.quantity ?? 1,
          taxPercent: 0,
          remark: bomItem.remark,
          qcCheckFlag: false,
        })),
      );
      message.info('已按工单 BOM å¸¦å…¥ç‰©æ–™ï¼Œè¯·è¡¥å¡«å«ç¨Žå•ä»·');
    }
    await formApi.setFieldValue('sourceWorkOrderId', item.id);
  }
  await formApi.setValues({
    sourceType,
    sourceId: item.id,
    sourceNo: item.no ?? item.code,
  });
}
const [Form, formApi] = useVbenForm({
  commonConfig: {
@@ -46,7 +120,10 @@
  },
  wrapperClass: 'grid-cols-3',
  layout: 'vertical',
  schema: useFormSchema(formType.value),
  schema: useFormSchema(formType.value, {
    onSourceTypeChange: handleSourceTypeChange,
    onSourceOrderChange: handleSourceOrderChange,
  }),
  showDefaultActions: false,
  handleValuesChange: (values, changedFields) => {
    if (formData.value && changedFields.includes('discountPercent')) {
@@ -85,6 +162,13 @@
    ...item,
    id: undefined,
  }));
  // æ¥æºå•据选择器是前端辅助字段,提交时只需 sourceType/sourceId/sourceNo
  delete (data as any).sourceWorkOrderId;
  delete (data as any).sourceSaleOrderId;
  if (!data.sourceType) {
    data.sourceId = undefined;
    data.sourceNo = undefined;
  }
  await (formType.value === 'create'
    ? createPurchaseOrder(data)
    : updatePurchaseOrder(data));
@@ -139,7 +223,12 @@
    const data = modalApi.getData<{ formType: FormType; id?: number }>();
    formType.value = data.formType;
    formApi.setDisabled(formType.value === 'detail');
    formApi.updateSchema(useFormSchema(formType.value));
    formApi.updateSchema(
      useFormSchema(formType.value, {
        onSourceTypeChange: handleSourceTypeChange,
        onSourceOrderChange: handleSourceOrderChange,
      }),
    );
    if (!data || !data.id) {
      const accountList = await getAccountSimpleList();
      const defaultAccount = accountList.find((item) => item.defaultStatus);
@@ -152,6 +241,19 @@
    try {
      formData.value = await getPurchaseOrder(data.id);
      await formApi.setValues(formData.value);
      // å›žæ˜¾æ¥æºå•据选择器(后端只存 sourceId,前端按 sourceType æ˜ å°„到对应选择器)
      if (formData.value?.sourceId) {
        if (formData.value.sourceType === PurchaseSourceTypeEnum.WORK_ORDER) {
          await formApi.setFieldValue(
            'sourceWorkOrderId',
            formData.value.sourceId,
          );
        } else if (
          formData.value.sourceType === PurchaseSourceTypeEnum.SALE_ORDER
        ) {
          await formApi.setFieldValue('sourceSaleOrderId', formData.value.sourceId);
        }
      }
      if (formData.value?.attachmentList?.length) {
        const blobIds = formData.value.attachmentList.map((item) => ({
          uid: String(item.id),
src/views/erp/purchase/order/modules/item-form.vue
@@ -122,6 +122,7 @@
  const newRow = {
    id: undefined,
    productId: undefined,
    productCode: undefined,
    productUnitName: undefined,
    productPrice: undefined,
    count: 1,
@@ -154,6 +155,7 @@
    return;
  }
  row.productId = item.id;
  row.productCode = item.code;
  row.productUnitId = item.unitMeasureId;
  row.productUnitName = item.unitMeasureName;
  row.productName = item.name;
src/views/erp/sale/order/data.ts
@@ -6,7 +6,11 @@
import { z } from '#/adapter/form';
import { getAccountSimpleList } from '#/api/erp/finance/account';
import { getSimpleUserList } from '#/api/system/user';
import { getRangePickerDefaultProps, withoutFixedColumns } from '#/utils';
import {
  getOverdueTip,
  getRangePickerDefaultProps,
  withoutFixedColumns,
} from '#/utils';
import CrmCustomerSelect from '#/components/crm-customer-select.vue';
import { MdmItemSelect } from '#/views/basicData/mdm/components';
@@ -75,6 +79,24 @@
        valueFormat: 'YYYY-MM-DD HH:mm:ss',
      },
      rules: 'required',
    },
    {
      fieldName: 'receiptDate',
      label: '收款日期',
      component: 'DatePicker',
      componentProps: {
        placeholder: '请选择收款日期',
        showTime: false,
        format: 'YYYY-MM-DD',
        valueFormat: 'YYYY-MM-DD HH:mm:ss',
      },
      help: (values: {
        orderTime?: Date | string;
        receiptDate?: Date | string;
      }) => {
        const tip = getOverdueTip(values.orderTime, values.receiptDate);
        return tip ? `⚠ ${tip},请确认回款计划` : '';
      },
    },
    {
      label: '客户',
@@ -353,6 +375,17 @@
      },
    },
    {
      fieldName: 'receiptDate',
      label: '收款日期',
      component: 'RangePicker',
      componentProps: {
        ...getRangePickerDefaultProps(),
        showTime: false,
        format: 'YYYY-MM-DD',
        allowClear: true,
      },
    },
    {
      fieldName: 'supplierId',
      label: '客户',
      component: markRaw(CrmCustomerSelect),
@@ -470,6 +503,12 @@
      formatter: 'formatDate',
    },
    {
      field: 'receiptDate',
      title: '收款日期',
      width: 140,
      slots: { default: 'receiptDate' },
    },
    {
      field: 'creatorName',
      title: '创建人',
      minWidth: 120,
src/views/erp/sale/order/detail/index.vue
@@ -70,6 +70,11 @@
      render: (val) => formatDate(val) as string,
    },
    {
      label: '收款日期',
      field: 'receiptDate',
      render: (val) => (val ? (formatDate(val) as string) : '未填写'),
    },
    {
      label: '合计数量',
      field: 'totalCount',
      render: (val) => erpCountInputFormatter(val) || '-',
src/views/erp/sale/order/index.vue
@@ -24,6 +24,7 @@
import { generateFromSaleOrder } from '#/api/mes/wm/salesnotice';
import { $t } from '#/locales';
import { automaticApproval } from '#/store/audit-policy';
import { getOverdueTip, isOverdue } from '#/utils';
import { useGridColumns, useGridFormSchema } from './data';
import Form from './modules/form.vue';
@@ -329,6 +330,23 @@
        </a>
        <span v-else>-</span>
      </template>
      <template #receiptDate="{ row }">
        <Tooltip
          v-if="row.receiptDate"
          :title="getOverdueTip(row.orderTime, row.receiptDate)"
        >
          <span
            :class="
              isOverdue(row.orderTime, row.receiptDate)
                ? 'font-medium text-red-500'
                : ''
            "
          >
            {{ dayjs(row.receiptDate).format('YYYY-MM-DD') }}
          </span>
        </Tooltip>
        <span v-else class="text-gray-400">未填写</span>
      </template>
      <template #status="{ row }">
        <Tag v-if="row.status === 0" color="default">未提交</Tag>
        <Tag v-if="row.status === 15" color="processing">审批中</Tag>
src/views/wls/returnvendor/data.ts
@@ -106,8 +106,54 @@
          if (item?.supplierId && formApi) {
            formApi.setFieldValue('vendorId', item.supplierId);
            formApi.setFieldValue('vendorName', item.supplierName);
            formApi.setFieldValue('contractNo', item.contractNo ?? undefined);
          }
        },
      },
    },
    {
      fieldName: 'contractNo',
      label: '采购合同号',
      component: 'Input',
      componentProps: {
        placeholder: '选择采购订单后自动带入',
        disabled: true,
      },
    },
    {
      fieldName: 'handleType',
      label: '处理方式',
      component: 'Select',
      componentProps: {
        class: '!w-full',
        placeholder: '请选择处理方式',
        allowClear: true,
        options: [
          { label: '退款', value: 1 },
          { label: '换货', value: 2 },
        ],
        onChange: (value: number) => {
          if (value !== 1) {
            formApi?.setFieldValue('refundAmount', undefined);
          }
        },
      },
      rules: 'selectRequired',
      help: '退款:完成后自动生成负数冲红付款单;换货:完成后自动生成到货通知单,重新入库并质检。',
    },
    {
      fieldName: 'refundAmount',
      label: '退款金额',
      component: 'InputNumber',
      componentProps: {
        class: '!w-full',
        placeholder: '留空则按采购单价×退货数量自动计算',
        min: 0,
        precision: 2,
      },
      dependencies: {
        triggerFields: ['handleType'],
        show: (values) => values.handleType === 1,
      },
    },
    {
@@ -177,6 +223,20 @@
        rows: 3,
      },
    },
    {
      fieldName: 'blobIds',
      label: '附件',
      component: 'FileUpload',
      formItemClass: 'col-span-3',
      componentProps: {
        valueKey: 'id',
        maxNumber: 10,
        multiple: true,
        showDescription: !isHeaderReadonly(formType),
        disabled: isHeaderReadonly(formType),
      },
      help: '质量部发起退货时上传质检报告、不合格照片等附件。',
    },
  ];
}
@@ -241,6 +301,12 @@
      minWidth: 140,
    },
    {
      field: 'contractNo',
      title: '合同号',
      minWidth: 140,
      showOverflow: 'tooltip',
    },
    {
      field: 'vendorCode',
      title: '供应商编码',
      minWidth: 120,
@@ -257,6 +323,33 @@
      formatter: 'formatDate',
    },
    {
      field: 'handleType',
      title: '处理方式',
      minWidth: 100,
      cellRender: {
        name: 'CellDict',
        props: { type: DICT_TYPE.MES_WM_RETURN_VENDOR_HANDLE_TYPE },
      },
    },
    {
      field: 'refundAmount',
      title: '退款金额',
      minWidth: 120,
      formatter: 'formatAmount2',
    },
    {
      field: 'redFlushPaymentNo',
      title: '冲红单号',
      minWidth: 150,
      showOverflow: 'tooltip',
    },
    {
      field: 'replacementNoticeCode',
      title: '换货到货单号',
      minWidth: 150,
      showOverflow: 'tooltip',
    },
    {
      field: 'status',
      title: '单据状态',
      minWidth: 110,
src/views/wls/returnvendor/index.vue
@@ -14,6 +14,7 @@
  deleteReturnVendor,
  exportReturnVendor,
  getReturnVendorPage,
  submitReturnVendorApproval,
} from '#/api/mes/wm/returnvendor';
import { $t } from '#/locales';
@@ -43,6 +44,21 @@
/** ç¼–辑供应商退货单 */
function handleEdit(row: MesWmReturnVendorApi.ReturnVendor) {
  formModalApi.setData({ formType: 'update', id: row.id }).open();
}
/** æäº¤å®¡æ‰¹ï¼ˆBPM:采购 -> è´¨é‡ -> ä»“库) */
async function handleSubmitApproval(row: MesWmReturnVendorApi.ReturnVendor) {
  const hideLoading = message.loading({
    content: '提交审批中...',
    duration: 0,
  });
  try {
    await submitReturnVendorApproval(row.id!);
    message.success(`退货单 ${row.code} å·²æäº¤å®¡æ‰¹`);
    handleRefresh();
  } finally {
    hideLoading();
  }
}
/** æ‰§è¡Œæ‹£è´§ */
@@ -167,6 +183,16 @@
              },
            },
            {
              label: '提交审批',
              type: 'link',
              auth: ['mes:wm-return-vendor:submit-approval'],
              ifShow: row.status === MesWmReturnVendorStatusEnum.PREPARE,
              popConfirm: {
                title: `确认提交${row.code}审批?流程:采购 â†’ è´¨é‡ â†’ ä»“库`,
                confirm: handleSubmitApproval.bind(null, row),
              },
            },
            {
              label: '执行拣货',
              type: 'link',
              auth: ['mes:wm-return-vendor:update'],
@@ -186,6 +212,7 @@
              danger: true,
              auth: ['mes:wm-return-vendor:update'],
              ifShow:
                row.status === MesWmReturnVendorStatusEnum.BPM_APPROVING ||
                row.status === MesWmReturnVendorStatusEnum.APPROVING ||
                row.status === MesWmReturnVendorStatusEnum.APPROVED,
              popConfirm: {
src/views/wls/returnvendor/modules/form.vue
@@ -17,8 +17,9 @@
  createReturnVendor,
  finishReturnVendor,
  getReturnVendor,
  getReturnVendorAttachments,
  stockReturnVendor,
  submitReturnVendor,
  submitReturnVendorApproval,
  updateReturnVendor,
} from '#/api/mes/wm/returnvendor';
import { $t } from '#/locales';
@@ -97,8 +98,8 @@
      await updateReturnVendor({ ...formData.value, ...data });
      originalSnapshot.value = current;
    }
    await submitReturnVendor(formData.value.id);
    message.success('提交成功');
    await submitReturnVendorApproval(formData.value.id);
    message.success('提交成功,已进入采购→质量→仓库审批流程');
    await modalApi.close();
    emit('success');
  } finally {
@@ -203,6 +204,23 @@
          ...formData.value,
          returnDate: normalizeReturnDate(formData.value.returnDate),
        });
        // å›žæ˜¾é™„ä»¶
        const attachments =
          (formData.value.attachmentList?.length ?? 0) > 0
            ? formData.value.attachmentList
            : await getReturnVendorAttachments(data.id);
        if (attachments?.length) {
          await formApi.setFieldValue(
            'blobIds',
            attachments.map((item) => ({
              uid: String(item.id),
              name: item.name || '',
              url: item.url || '',
              status: 'done',
              id: item.id,
            })),
          );
        }
      } finally {
        modalApi.unlock();
      }