Merge remote-tracking branch 'origin/dev_新疆_新高电力' into dev_新疆_新高电力
已修改8个文件
230 ■■■■■ 文件已修改
src/api/aftersales/return.ts 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/crm/receivable/index.ts 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/aftersales/return/detail.vue 139 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/aftersales/return/index.vue 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/crm/backlog/modules/receivable-audit-list.vue 26 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/crm/receivable/detail/index.vue 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/crm/receivable/index.vue 35 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/workbench/components/TaskListPanel.vue 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/aftersales/return.ts
@@ -33,6 +33,7 @@
    defectCategory?: string;
    totalReturnPrice?: number;
    actualRefundPrice?: number;
    refundVoucherNo?: string;
    status?: number;
    auditUserId?: number;
    auditTime?: string;
@@ -63,6 +64,16 @@
  });
}
/** 确认线下退款或换货已经实际完成,附凭证号。 */
export function completeRefund(data: { id: number; actualRefundPrice: number; voucherNo: string }) {
  return requestClient.put<boolean>('/aftersales/return/complete-refund', data);
}
/** 取消尚未入库的售后退货申请,并同步取消关联 MES 单据。 */
export function cancelReturn(id: number) {
  return requestClient.put<boolean>('/aftersales/return/cancel', null, { params: { id } });
}
/** 获取退货申请详情 */
export function getReturn(id: number) {
  return requestClient.get<AftersalesReturnApi.Return>(
src/api/crm/receivable/index.ts
@@ -17,7 +17,7 @@
    invoiceId?: number;
    invoice?: InvoiceInfo;
    auditStatus: number;
    processInstanceId: number;
    processInstanceId?: string;
    returnTime: Date;
    returnType: number;
    price: number;
@@ -98,10 +98,10 @@
  return requestClient.download('/crm/receivable/export-excel', { params });
}
/** 更新回款的状态(审核/反审核) */
export function updateReceivableStatus(id: number, status: number) {
  return requestClient.put('/crm/receivable/update-status', null, {
    params: { id, status },
/** 提交回款 BPM 审批 */
export function submitReceivable(id: number) {
  return requestClient.put('/crm/receivable/submit', null, {
    params: { id },
  });
}
src/views/aftersales/return/detail.vue
@@ -7,31 +7,76 @@
import { useRoute, useRouter } from 'vue-router';
import { Page } from '@vben/common-ui';
import { useAccess } from '@vben/access';
import { AftersaleReturnStatusEnum } from '@vben/constants';
import { Button, Card, Descriptions, message, Popconfirm, Select, Tag } from 'ant-design-vue';
import { Button, Card, Descriptions, Input, InputNumber, message, Modal, Popconfirm, Select, Tag } from 'ant-design-vue';
import { useVbenForm } from '#/adapter/form';
import { approveReturn, createReturn, getReturn } from '#/api/aftersales/return';
import { approveReturn, cancelReturn, completeRefund, createReturn, getReturn } from '#/api/aftersales/return';
import { getTicket } from '#/api/aftersales/ticket';
import { AftersalesTicketSelect } from '#/views/aftersales/ticket/components';
import { getSaleOrder } from '#/api/erp/sale/order';
import { getSaleOrderItemListByOrderId } from '#/api/erp/sale/order';
defineOptions({ name: 'AftersalesReturnDetail' });
const route = useRoute();
const router = useRouter();
const { hasAccessByCodes } = useAccess();
const loading = ref(false);
const submitting = ref(false);
const returnData = ref<AftersalesReturnApi.Return>();
const ticketData = ref<AftersalesTicketApi.Ticket>();
const saleOrderItems = ref<ErpSaleOrderApi.SaleOrderItem[]>([]);
const requestedQuantities = ref<Record<number, number>>({});
function initializeRequestedQuantities() {
  requestedQuantities.value = Object.fromEntries(
    (ticketData.value?.items || []).filter((item) => item.id).map((item) => [item.id!, item.returnQuantity || 0]),
  );
}
const returnId = computed(() => route.params.id as string);
const ticketIdFromQuery = computed(() => Number(route.query.ticketId || 0));
const isCreate = computed(() => returnId.value === 'new');
const isDraft = computed(() => returnData.value?.status === AftersaleReturnStatusEnum.DRAFT);
const canConfirmRefund = computed(() => returnData.value?.status === AftersaleReturnStatusEnum.INBOUND
  || (returnData.value?.returnType === 3 && returnData.value?.status === AftersaleReturnStatusEnum.SYNCED_MES));
const canCancelReturn = computed(() => returnData.value?.status === AftersaleReturnStatusEnum.DRAFT
  || returnData.value?.status === AftersaleReturnStatusEnum.SYNCED_MES
  || returnData.value?.status === AftersaleReturnStatusEnum.QC_CHECKING
  || returnData.value?.status === AftersaleReturnStatusEnum.QC_DONE);
const refundModalOpen = ref(false);
const refundSubmitting = ref(false);
const actualRefundPrice = ref<number>(0);
const refundVoucherNo = ref('');
function openRefundModal() {
  actualRefundPrice.value = returnData.value?.returnType === 2 ? 0 : (returnData.value?.totalReturnPrice || 0);
  refundVoucherNo.value = '';
  refundModalOpen.value = true;
}
async function handleConfirmRefund() {
  if (!returnData.value?.id || !refundVoucherNo.value.trim()) {
    message.warning('请填写退款流水号或换货单号');
    return;
  }
  refundSubmitting.value = true;
  try {
    await completeRefund({
      id: returnData.value.id,
      actualRefundPrice: actualRefundPrice.value || 0,
      voucherNo: refundVoucherNo.value.trim(),
    });
    refundModalOpen.value = false;
    message.success('退款/换货完成,工单已完结');
    await loadReturn();
  } finally {
    refundSubmitting.value = false;
  }
}
const statusMap: Record<number, { color: string; label: string }> = {
  [AftersaleReturnStatusEnum.DRAFT]: { color: 'default', label: '草稿' },
@@ -121,9 +166,9 @@
  loading.value = true;
  try {
    ticketData.value = await getTicket(ticketIdFromQuery.value);
    initializeRequestedQuantities();
    if (ticketData.value?.saleOrderId) {
      const order = await getSaleOrder(ticketData.value.saleOrderId);
      saleOrderItems.value = (order as Record<string, unknown>).items as ErpSaleOrderApi.SaleOrderItem[] || [];
      saleOrderItems.value = await getSaleOrderItemListByOrderId(ticketData.value.saleOrderId);
    }
  } finally {
    loading.value = false;
@@ -135,9 +180,9 @@
  loading.value = true;
  try {
    ticketData.value = await getTicket(item.id as number);
    initializeRequestedQuantities();
    if (ticketData.value?.saleOrderId) {
      const order = await getSaleOrder(ticketData.value.saleOrderId);
      saleOrderItems.value = (order as Record<string, unknown>).items as ErpSaleOrderApi.SaleOrderItem[] || [];
      saleOrderItems.value = await getSaleOrderItemListByOrderId(ticketData.value.saleOrderId);
    }
  } finally {
    loading.value = false;
@@ -147,6 +192,11 @@
const needReturnItems = computed(() =>
  (ticketData.value?.items || []).filter((item) => item.needReturn),
);
function findSaleOrderItem(item: AftersalesTicketApi.TicketItem) {
  return saleOrderItems.value.find((orderItem) => orderItem.id === item.saleOrderItemId)
    || saleOrderItems.value.find((orderItem) => orderItem.productId === item.itemId);
}
async function handleCreateReturn() {
  const { valid } = await createFormApi.validate();
@@ -160,6 +210,20 @@
  submitting.value = true;
  try {
    const formValues = await createFormApi.getValues();
    const sourceItems = formValues.returnType === 3 ? (ticketData.value?.items || []) : needReturnItems.value;
    const selectedItems = sourceItems.filter((item) => (requestedQuantities.value[item.id!] || 0) > 0);
    if (!selectedItems.length) {
      message.warning('请为本次申请填写至少一项正数数量;普通退货只能选择工单标记为需退货的物料');
      return;
    }
    if (selectedItems.some((item) => (requestedQuantities.value[item.id!] || 0) > (item.saleQuantity || 0))) {
      message.warning('申请数量不能超过工单销售数量');
      return;
    }
    if (selectedItems.some((item) => !findSaleOrderItem(item))) {
      message.warning('销售订单明细与工单物料不匹配,请先核对工单关联的销售订单');
      return;
    }
    const newId = await createReturn({
      ticketId: ticketData.value!.id!,
      customerId: ticketData.value!.customerId!,
@@ -168,10 +232,10 @@
      returnReason: formValues.returnReason as string,
      defectCategory: formValues.defectCategory as string | undefined,
      remark: formValues.remark as string | undefined,
      items: needReturnItems.value.map((item) => {
        const orderItem = saleOrderItems.value.find((oi) => oi.productId === item.itemId);
      items: selectedItems.map((item) => {
        const orderItem = findSaleOrderItem(item);
        const unitPrice = orderItem?.productPrice || 0;
        const returnQuantity = item.returnQuantity || 0;
        const returnQuantity = requestedQuantities.value[item.id!] || 0;
        return {
          ticketItemId: item.id,
          itemId: item.itemId,
@@ -196,7 +260,14 @@
async function handleApprove() {
  await approveReturn(returnData.value!.id!);
  message.success('审核通过,已同步MES创建退货单');
  message.success(returnData.value?.returnType === 3 ? '审核通过,等待退款确认' : '审核通过,已同步MES创建退货单');
  await loadReturn();
}
async function handleCancel() {
  if (!returnData.value?.id) return;
  await cancelReturn(returnData.value.id);
  message.success('退货申请已取消,售后工单已恢复处理中');
  await loadReturn();
}
@@ -215,6 +286,15 @@
<template>
  <Page auto-content-height :loading="loading">
    <Modal v-model:open="refundModalOpen" title="确认外部退款/换货已完成" :confirm-loading="refundSubmitting" @ok="handleConfirmRefund">
      <div class="flex flex-col gap-3">
        <p>仅在财务退款到账或换货实际完成后确认。确认后工单将自动完结。</p>
        <div>实际退款金额(换货可填 0)</div>
        <InputNumber v-model:value="actualRefundPrice" :min="0" :max="returnData?.totalReturnPrice" class="w-full" />
        <div>退款流水号或换货单号</div>
        <Input v-model:value="refundVoucherNo" :maxlength="100" placeholder="填写可追溯的凭证号" />
      </div>
    </Modal>
    <template #title>
      <div class="flex items-center gap-3">
        <Button @click="goBack">返回</Button>
@@ -235,7 +315,8 @@
        </Descriptions>
      </Card>
      <Card v-if="needReturnItems.length" title="退货明细" class="mb-4">
      <Card v-if="ticketData.items?.length" title="工单物料明细" class="mb-4">
        <p class="mb-2 text-sm text-gray-500">填写本次申请数量。普通退货仅提交标记为“需退货”的物料;仅退款可选择任意工单物料。</p>
        <div class="overflow-x-auto">
          <table class="w-full border text-sm">
            <thead class="bg-gray-50">
@@ -243,15 +324,19 @@
                <th class="border p-2 text-left">物料编码</th>
                <th class="border p-2 text-left">物料名称</th>
                <th class="border p-2 text-left">批次号</th>
                <th class="border p-2 text-right">退货数量</th>
                <th class="border p-2 text-center">需退货</th>
                <th class="border p-2 text-right">本次申请数量</th>
              </tr>
            </thead>
            <tbody>
              <tr v-for="item in needReturnItems" :key="item.id">
              <tr v-for="item in ticketData.items" :key="item.id">
                <td class="border p-2">{{ item.itemCode }}</td>
                <td class="border p-2">{{ item.itemName }}</td>
                <td class="border p-2">{{ item.batchCode || '-' }}</td>
                <td class="border p-2 text-right">{{ item.returnQuantity || '-' }}</td>
                <td class="border p-2 text-center">{{ item.needReturn ? '是' : '否' }}</td>
                <td class="border p-2 text-right">
                  <InputNumber v-model:value="requestedQuantities[item.id!]" :min="0" :max="item.returnQuantity && item.returnQuantity > 0 ? Math.min(item.returnQuantity, item.saleQuantity || 0) : (item.saleQuantity || 0)" class="w-28" />
                </td>
              </tr>
            </tbody>
          </table>
@@ -285,7 +370,8 @@
          <Descriptions.Item label="退货单号">{{ returnData.no }}</Descriptions.Item>
          <Descriptions.Item label="状态">
            <Tag :color="statusMap[returnData.status!]?.color">
              {{ statusMap[returnData.status!]?.label }}
              {{ returnData.returnType === 3 && returnData.status === AftersaleReturnStatusEnum.SYNCED_MES
                ? '待退款确认' : statusMap[returnData.status!]?.label }}
            </Tag>
          </Descriptions.Item>
          <Descriptions.Item label="客户">{{ returnData.customerName || '-' }}</Descriptions.Item>
@@ -301,6 +387,13 @@
          <Descriptions.Item label="MES退货单号">
            {{ returnData.mesReturnCode || '-' }}
          </Descriptions.Item>
          <Descriptions.Item v-if="returnData.status === AftersaleReturnStatusEnum.SYNCED_MES
            || returnData.status === AftersaleReturnStatusEnum.QC_DONE
            || returnData.status === AftersaleReturnStatusEnum.INBOUND" label="下一步" :span="3">
            {{ returnData.status === AftersaleReturnStatusEnum.INBOUND ? '等待外部退款或换货完成,再录入金额与凭证号'
              : returnData.status === AftersaleReturnStatusEnum.QC_DONE ? '在 MES 销售退货单完成执行与上架'
                : returnData.returnType === 3 ? '等待外部退款完成并录入凭证' : '在 MES 销售退货单中提交、检验、上架;入库后再确认退款或换货' }}
          </Descriptions.Item>
          <Descriptions.Item label="退货原因" :span="3">
            {{ returnData.returnReason || '-' }}
          </Descriptions.Item>
@@ -311,6 +404,7 @@
          <Descriptions.Item label="实退金额">
            {{ returnData.actualRefundPrice ? `¥${returnData.actualRefundPrice}` : '-' }}
          </Descriptions.Item>
          <Descriptions.Item label="退款/换货凭证">{{ returnData.refundVoucherNo || '-' }}</Descriptions.Item>
          <Descriptions.Item label="质检结论">{{ returnData.qualityResult || '-' }}</Descriptions.Item>
          <Descriptions.Item label="入库时间">{{ returnData.returnInboundTime || '-' }}</Descriptions.Item>
          <Descriptions.Item label="退款时间">{{ returnData.refundTime || '-' }}</Descriptions.Item>
@@ -349,13 +443,24 @@
    </template>
    <template #extra>
      <div v-if="!isCreate && isDraft" class="flex gap-2">
      <div v-if="!isCreate" class="flex gap-2">
        <Popconfirm
          v-if="isDraft && hasAccessByCodes(['aftersales:return:approve'])"
          title="确认审核通过该退货申请?审核后将自动同步MES创建退货单。"
          @confirm="handleApprove"
        >
          <Button type="primary">审核通过</Button>
        </Popconfirm>
        <Popconfirm
          v-if="canCancelReturn && hasAccessByCodes(['aftersales:return:cancel'])"
          title="确认取消退货申请?未入库的 MES 单据会同步取消,工单恢复处理中。"
          @confirm="handleCancel"
        >
          <Button danger>取消申请</Button>
        </Popconfirm>
        <Button v-if="canConfirmRefund && hasAccessByCodes(['aftersales:return:refund'])" type="primary" @click="openRefundModal">
          确认外部退款/换货完成
        </Button>
      </div>
    </template>
  </Page>
src/views/aftersales/return/index.vue
@@ -117,7 +117,7 @@
      <template #status="{ row }">
        <Tag v-if="row.status === 0">草稿</Tag>
        <Tag v-else-if="row.status === 10" color="processing">销售审核中</Tag>
        <Tag v-else-if="row.status === 20" color="processing">已同步MES</Tag>
        <Tag v-else-if="row.status === 20" color="processing">{{ row.returnType === 3 ? '待退款确认' : '已同步MES' }}</Tag>
        <Tag v-else-if="row.status === 30" color="warning">检验中</Tag>
        <Tag v-else-if="row.status === 40" color="warning">检验完成</Tag>
        <Tag v-else-if="row.status === 50" color="success">已入库</Tag>
src/views/crm/backlog/modules/receivable-audit-list.vue
@@ -5,28 +5,20 @@
import { useRouter } from 'vue-router';
import { Button, message } from 'ant-design-vue';
import { Button } from 'ant-design-vue';
import { useVbenVxeGrid } from '#/adapter/vxe-table';
import { getReceivablePage, updateReceivableStatus } from '#/api/crm/receivable';
import { getReceivablePage } from '#/api/crm/receivable';
import { useGridColumns } from '#/views/crm/receivable/data';
import { AUDIT_STATUS } from '../data';
const { push } = useRouter();
/** 审核 */
async function handleAudit(row: CrmReceivableApi.Receivable) {
  const hideLoading = message.loading({
    content: '审核中...',
    duration: 0,
  });
  try {
    await updateReceivableStatus(row.id!, 20);
    message.success('审核成功');
    gridApi.query();
  } finally {
    hideLoading();
/** 审批操作在 BPM 流程详情中办理。 */
function handleAudit(row: CrmReceivableApi.Receivable) {
  if (row.processInstanceId) {
    push({ name: 'BpmProcessInstanceDetail', query: { id: row.processInstanceId } });
  }
}
@@ -50,7 +42,7 @@
    schema: [
      {
        fieldName: 'auditStatus',
        label: '合同状态',
        label: '回款状态',
        component: 'Select',
        componentProps: {
          allowClear: true,
@@ -106,11 +98,11 @@
    </template>
    <template #actions="{ row }">
      <Button
        v-if="row.auditStatus === 10"
        v-if="row.auditStatus === 10 && row.processInstanceId"
        type="link"
        @click="handleAudit(row)"
      >
        审核
        查看审批
      </Button>
    </template>
  </Grid>
src/views/crm/receivable/detail/index.vue
@@ -99,7 +99,8 @@
            type: 'primary',
            icon: ACTION_ICON.EDIT,
            auth: ['crm:receivable:update'],
            ifShow: permissionListRef?.validateWrite,
            ifShow: permissionListRef?.validateWrite &&
              (receivable.auditStatus === 0 || receivable.auditStatus === 30),
            onClick: handleEdit,
          },
        ]"
src/views/crm/receivable/index.vue
@@ -15,7 +15,7 @@
  deleteReceivable,
  exportReceivable,
  getReceivablePage,
  updateReceivableStatus,
  submitReceivable,
} from '#/api/crm/receivable';
import { $t } from '#/locales';
@@ -76,18 +76,15 @@
  }
}
/** 更新回款状态(审核/反审核) */
async function handleUpdateStatus(
  row: CrmReceivableApi.Receivable,
  status: number,
) {
/** 提交回款审批 */
async function handleSubmit(row: CrmReceivableApi.Receivable) {
  const hideLoading = message.loading({
    content: '更新状态中...',
    content: '正在提交审批...',
    duration: 0,
  });
  try {
    await updateReceivableStatus(row.id!, status);
    message.success('更新状态成功');
    await submitReceivable(row.id!);
    message.success('提交审批成功');
    handleRefresh();
  } finally {
    hideLoading();
@@ -216,24 +213,18 @@
              type: 'link',
              icon: ACTION_ICON.EDIT,
              auth: ['crm:receivable:update'],
              ifShow: row.auditStatus === 10,
              ifShow: row.auditStatus === 0 || row.auditStatus === 30,
              onClick: handleEdit.bind(null, row),
            },
            {
              label: '审核',
              label: '提交审批',
              type: 'link',
              icon: ACTION_ICON.AUDIT,
              auth: ['crm:receivable:update'],
              ifShow: row.auditStatus === 10,
              onClick: handleUpdateStatus.bind(null, row, 20),
            },
            {
              label: '反审核',
              type: 'link',
              icon: ACTION_ICON.VIEW,
              auth: ['crm:receivable:update'],
              ifShow: row.auditStatus === 20,
              onClick: handleUpdateStatus.bind(null, row, 10),
              ifShow: row.auditStatus === 0 || row.auditStatus === 30,
              popConfirm: {
                title: '确认提交回款审批?提交后需由流程审批人处理。',
                confirm: handleSubmit.bind(null, row),
              },
            },
            {
              label: $t('common.delete'),
src/views/mes/workbench/components/TaskListPanel.vue
@@ -55,7 +55,7 @@
const pauseLoading = ref(false);
const pauseRules = {
  reason: [{ required: true, message: '请输入停工原因', trigger: 'blur' }],
  reason: [{ required: true, message: '请输入停工原因', trigger: 'blur' as const }],
};
function handlePause(item: PendingTask, event: Event) {
@@ -160,7 +160,7 @@
                复工
              </Button>
              <Button
                v-else
                v-else-if="item.status === 1 || item.status === 2"
                type="primary"
                danger
                size="small"