云
13 小时以前 21599300cc73ff7384b74cc5e4ac2cbd82bdc293
feat(wls): 更新销售退货功能支持关联销售出库单

- 移除产品销售、退货销售、发货通知单中的质检相关字段
- 重构退货销售单,新增关联销售出库单选择功能
- 新增退货销售单行表单,支持通过出库单行自动带入物料和批次信息
- 添加退货销售单行列表组件,支持添加和编辑退货行
- 实现销售出库单和出库行选择对话框组件
- 更新退货销售单API接口,添加可退销售出库单查询功能
- 修改退货销售单表格列,增加销售出库单号显示
- 调整物料规格型号和单位字段命名规范
- 移除退货销售单的质检执行按钮和相关逻辑
已修改11个文件
已添加5个文件
1072 ■■■■■ 文件已修改
src/api/mes/wm/returnsales/index.ts 44 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/mes/wm/returnsales/line/index.ts 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/wls/productsales/data.ts 24 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/wls/returnsales/components/index.ts 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/wls/returnsales/components/line-select-dialog.vue 192 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/wls/returnsales/components/line-select.vue 180 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/wls/returnsales/components/select-dialog.vue 209 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/wls/returnsales/components/select.vue 152 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/wls/returnsales/data.ts 153 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/wls/returnsales/index.vue 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/wls/returnsales/modules/detail-form.vue 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/wls/returnsales/modules/form.vue 1 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/wls/returnsales/modules/line-form.vue 45 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/wls/returnsales/modules/line-list.vue 20 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/wls/salesnotice/components/line-select-dialog.vue 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/wls/salesnotice/data.ts 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/mes/wm/returnsales/index.ts
@@ -12,11 +12,40 @@
    clientId?: number; // å®¢æˆ·ç¼–号
    clientCode?: string; // å®¢æˆ·ç¼–码
    clientName?: string; // å®¢æˆ·åç§°
    productSalesId?: number; // é”€å”®å‡ºåº“单编号
    productSalesCode?: string; // é”€å”®å‡ºåº“单号
    returnDate?: number; // é€€è´§æ—¥æœŸ
    returnReason?: string; // é€€è´§åŽŸå› 
    status?: number; // å•据状态
    remark?: string; // å¤‡æ³¨
    createTime?: Date; // åˆ›å»ºæ—¶é—´
  }
  /** å¯é€€é”€å”®å‡ºåº“单 */
  export interface ReturnableProductSales {
    id?: number; // é”€å”®å‡ºåº“单编号
    code?: string; // å‡ºåº“单号
    name?: string; // å‡ºåº“单名称
    clientId?: number; // å®¢æˆ·ç¼–号
    clientName?: string; // å®¢æˆ·åç§°
    salesDate?: number; // å‡ºåº“日期
    outboundTime?: number; // å®žé™…出库时间
  }
  /** å¯é€€é”€å”®å‡ºåº“单行 */
  export interface ReturnableProductSalesLine {
    productSalesLineId?: number; // é”€å”®å‡ºåº“单行编号
    salesId?: number; // é”€å”®å‡ºåº“单编号
    itemId?: number; // ç‰©æ–™ç¼–号
    itemCode?: string; // ç‰©æ–™ç¼–码
    itemName?: string; // ç‰©æ–™åç§°
    itemSpecification?: string; // è§„格型号
    itemUnit?: string; // è®¡é‡å•位
    batchId?: number; // æ‰¹æ¬¡ç¼–号
    batchCode?: string; // æ‰¹æ¬¡å·
    outboundQuantity?: number; // å‡ºåº“数量
    returnedQuantity?: number; // å·²é€€æ•°é‡
    returnableQuantity?: number; // å¯é€€æ•°é‡
  }
}
@@ -28,6 +57,21 @@
  );
}
/** æŸ¥è¯¢å¯é€€é”€å”®å‡ºåº“单分页(仅已完成且存在可退行) */
export function getReturnableProductSalesPage(params: PageParam) {
  return requestClient.get<
    PageResult<MesWmReturnSalesApi.ReturnableProductSales>
  >('/mes/wm/return-sales/returnable-product-sales-page', { params });
}
/** æŸ¥è¯¢é”€å”®å‡ºåº“单可退行列表 */
export function getReturnableProductSalesLineList(productSalesId: number) {
  return requestClient.get<MesWmReturnSalesApi.ReturnableProductSalesLine[]>(
    '/mes/wm/return-sales/returnable-product-sales-line-list',
    { params: { productSalesId } },
  );
}
/** æŸ¥è¯¢é”€å”®é€€è´§å•详情 */
export function getReturnSales(id: number) {
  return requestClient.get<MesWmReturnSalesApi.ReturnSales>(
src/api/mes/wm/returnsales/line/index.ts
@@ -7,11 +7,12 @@
  export interface ReturnSalesLine {
    id?: number; // è¡Œç¼–号
    returnId?: number; // é€€è´§å•编号
    productSalesLineId?: number; // é”€å”®å‡ºåº“单行编号
    itemId?: number; // ç‰©æ–™ç¼–号
    itemCode?: string; // ç‰©æ–™ç¼–码
    itemName?: string; // ç‰©æ–™åç§°
    specification?: string; // è§„格型号
    unitMeasureName?: string; // å•位
    itemSpecification?: string; // è§„格型号
    itemUnit?: string; // å•位
    quantity?: number; // é€€è´§æ•°é‡
    batchId?: number; // æ‰¹æ¬¡ç¼–号
    batchCode?: string; // æ‰¹æ¬¡å·
src/views/wls/productsales/data.ts
@@ -19,7 +19,6 @@
import { Button } from 'ant-design-vue';
import { z } from '#/adapter/form';
import CrmCustomerSelect from '#/components/crm-customer-select.vue';
import { generateAutoCode } from '#/api/mes/md/autocode/record';
import { getRangePickerDefaultProps } from '#/utils';
@@ -391,15 +390,6 @@
      minWidth: 120,
    },
    {
      field: 'oqcCheckFlag',
      title: '是否校验',
      width: 100,
      cellRender: {
        name: 'CellDict',
        props: { type: DICT_TYPE.INFRA_BOOLEAN_STRING },
      },
    },
    {
      field: 'remark',
      title: '备注',
      minWidth: 150,
@@ -436,12 +426,11 @@
      label: '发货通知单行',
      component: markRaw(WmSalesNoticeLineSelect),
      componentProps: {
        // é€‰æ‹©å‘货通知单行后,自动回填物料、数量、批次、是否检验
        // é€‰æ‹©å‘货通知单行后,自动回填物料、数量、批次
        onChange: async (line?: MesWmSalesNoticeLineApi.SalesNoticeLine) => {
          await formApi?.setValues({
            batchCode: line?.batchCode,
            itemId: line?.itemId,
            oqcCheckFlag: line?.oqcCheckFlag ?? false,
            quantity: line?.quantity,
          });
        },
@@ -489,17 +478,6 @@
        precision: 2,
      },
      rules: 'required',
    },
    {
      fieldName: 'oqcCheckFlag',
      label: '是否校验',
      component: 'RadioGroup',
      componentProps: {
        buttonStyle: 'solid',
        optionType: 'button',
        options: getDictOptions(DICT_TYPE.INFRA_BOOLEAN_STRING, 'boolean'),
      },
      rules: z.boolean().default(false),
    },
    {
      fieldName: 'remark',
src/views/wls/returnsales/components/index.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,2 @@
export { default as WmReturnSalesProductSalesLineSelect } from './line-select.vue';
export { default as WmReturnSalesProductSalesSelect } from './select.vue';
src/views/wls/returnsales/components/line-select-dialog.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,192 @@
<script lang="ts" setup>
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { MesWmReturnSalesApi } from '#/api/mes/wm/returnsales';
import { nextTick, ref } from 'vue';
import { message, Modal } from 'ant-design-vue';
import { useVbenVxeGrid } from '#/adapter/vxe-table';
import { getReturnableProductSalesLineList } from '#/api/mes/wm/returnsales';
const emit = defineEmits<{
  selected: [rows: MesWmReturnSalesApi.ReturnableProductSalesLine[]];
}>();
const open = ref(false); // å¼¹çª—是否打开
const productSalesId = ref<number>(); // æ‰€å±žé”€å”®å‡ºåº“单编号
const selectedRows = ref<MesWmReturnSalesApi.ReturnableProductSalesLine[]>([]); // å·²é€‰è¡Œ
const preSelectedId = ref<number>(); // é¢„选行编号
/** è¡¨æ ¼å­—段 */
function useGridColumns(): VxeTableGridOptions<MesWmReturnSalesApi.ReturnableProductSalesLine>['columns'] {
  return [
    {
      type: 'radio',
      width: 50,
    },
    {
      field: 'itemCode',
      title: '物料编码',
      minWidth: 120,
    },
    {
      field: 'itemName',
      title: '物料名称',
      minWidth: 140,
    },
    {
      field: 'itemSpecification',
      title: '规格型号',
      minWidth: 120,
    },
    {
      field: 'itemUnit',
      title: '单位',
      width: 80,
    },
    {
      field: 'batchCode',
      title: '批次号',
      minWidth: 140,
    },
    {
      field: 'outboundQuantity',
      title: '出库数量',
      width: 110,
    },
    {
      field: 'returnedQuantity',
      title: '已退数量',
      width: 110,
    },
    {
      field: 'returnableQuantity',
      title: '可退数量',
      width: 110,
    },
  ];
}
/** å¤„理单选切换 */
function handleRadioChange(row: MesWmReturnSalesApi.ReturnableProductSalesLine) {
  selectedRows.value = [row];
}
/** å¤„理行双击:选中并确认 */
async function handleCellDblclick({
  row,
}: {
  row: MesWmReturnSalesApi.ReturnableProductSalesLine;
}) {
  selectedRows.value = [row];
  await gridApi.grid.setRadioRow(row);
  handleConfirm();
}
/** å›žæ˜¾é¢„选行 */
async function applyPreSelection() {
  if (!preSelectedId.value) {
    return;
  }
  const rows =
    gridApi.grid.getData() as MesWmReturnSalesApi.ReturnableProductSalesLine[];
  for (const row of rows) {
    if (row.productSalesLineId === preSelectedId.value) {
      await gridApi.grid.setRadioRow(row);
      selectedRows.value = [row];
      return;
    }
  }
}
const [Grid, gridApi] = useVbenVxeGrid({
  gridOptions: {
    columns: useGridColumns(),
    height: 460,
    keepSource: true,
    pagerConfig: {
      enabled: false,
    },
    radioConfig: {
      highlight: true,
      trigger: 'row',
    },
    proxyConfig: {
      ajax: {
        query: async () => {
          if (!productSalesId.value) {
            return { list: [], total: 0 };
          }
          const list = await getReturnableProductSalesLineList(
            productSalesId.value,
          );
          return { list, total: list.length };
        },
      },
    },
    rowConfig: {
      keyField: 'productSalesLineId',
      isHover: true,
    },
    toolbarConfig: {
      refresh: true,
    },
  } as VxeTableGridOptions<MesWmReturnSalesApi.ReturnableProductSalesLine>,
  gridEvents: {
    cellDblclick: handleCellDblclick,
    radioChange: ({
      row,
    }: {
      row: MesWmReturnSalesApi.ReturnableProductSalesLine;
    }) => {
      handleRadioChange(row);
    },
  },
});
/** æ‰“开行选择弹窗 */
async function openModal(id: number | undefined, selectedId?: number) {
  open.value = true;
  productSalesId.value = id;
  preSelectedId.value = selectedId;
  selectedRows.value = [];
  await nextTick();
  await gridApi.grid.clearRadioRow();
  await gridApi.query();
  await nextTick();
  await applyPreSelection();
}
/** å…³é—­å¼¹çª— */
async function closeModal() {
  open.value = false;
  selectedRows.value = [];
  await gridApi.grid.clearRadioRow();
}
/** ç¡®è®¤é€‰æ‹©è¡Œ */
function handleConfirm() {
  if (selectedRows.value.length === 0) {
    message.warning('请选择一条数据');
    return;
  }
  emit('selected', [selectedRows.value[0]!]);
  open.value = false;
}
defineExpose({ open: openModal });
</script>
<template>
  <Modal
    v-model:open="open"
    title="可退出库行选择"
    width="70%"
    :destroy-on-close="true"
    @cancel="closeModal"
    @ok="handleConfirm"
  >
    <Grid table-title="可退出库行列表" />
  </Modal>
</template>
src/views/wls/returnsales/components/line-select.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,180 @@
<script lang="ts" setup>
import type { MesWmReturnSalesApi } from '#/api/mes/wm/returnsales';
import { computed, ref, useAttrs, watch } from 'vue';
import { IconifyIcon } from '@vben/icons';
import { Input, Tooltip } from 'ant-design-vue';
import { getProductSalesLine } from '#/api/mes/wm/productsales/line';
import WmReturnSalesProductSalesLineSelectDialog from './line-select-dialog.vue';
defineOptions({
  name: 'WmReturnSalesProductSalesLineSelect',
  inheritAttrs: false,
});
const props = withDefaults(
  defineProps<{
    allowClear?: boolean;
    disabled?: boolean;
    modelValue?: number;
    placeholder?: string;
    productSalesId?: number; // æ‰€å±žé”€å”®å‡ºåº“单编号
  }>(),
  {
    allowClear: true,
    disabled: false,
    modelValue: undefined,
    placeholder: '请选择出库行',
    productSalesId: undefined,
  },
);
const emit = defineEmits<{
  change: [item: MesWmReturnSalesApi.ReturnableProductSalesLine | undefined];
  'update:modelValue': [value: number | undefined];
}>();
const attrs = useAttrs();
const dialogRef = ref<
  InstanceType<typeof WmReturnSalesProductSalesLineSelectDialog>
>();
const hovering = ref(false);
const selectedItem = ref<MesWmReturnSalesApi.ReturnableProductSalesLine>();
const displayLabel = computed(() => {
  const item = selectedItem.value;
  if (!item) {
    return '';
  }
  return `${item.itemCode ?? ''} - ${item.itemName ?? ''}`;
});
const showClear = computed(
  () =>
    props.allowClear &&
    !props.disabled &&
    hovering.value &&
    props.modelValue !== undefined &&
    props.modelValue !== null,
);
/** æ ¹æ®ç¼–号单条查询出库行信息(用于编辑回显) */
async function resolveItemById(id: number | undefined) {
  if (!id) {
    selectedItem.value = undefined;
    return;
  }
  if (selectedItem.value?.productSalesLineId === id) {
    return;
  }
  try {
    const line = await getProductSalesLine(id);
    selectedItem.value = line
      ? {
          batchCode: line.batchCode,
          batchId: line.batchId,
          itemCode: line.itemCode,
          itemId: line.itemId,
          itemName: line.itemName,
          itemSpecification: line.specification,
          itemUnit: line.unitMeasureName,
          outboundQuantity: line.quantity,
          productSalesLineId: line.id,
          salesId: line.salesId,
        }
      : undefined;
  } catch {
    selectedItem.value = undefined;
  }
}
watch(() => props.modelValue, resolveItemById, { immediate: true });
/** productSalesId å˜åŒ–时清空选中(关联的出库行已失效) */
watch(
  () => props.productSalesId,
  () => {
    selectedItem.value = undefined;
    emit('update:modelValue', undefined);
    emit('change', undefined);
  },
);
/** æ¸…空已选行 */
function clearSelected() {
  selectedItem.value = undefined;
  emit('update:modelValue', undefined);
  emit('change', undefined);
}
/** æ‰“开行选择弹窗 */
function handleClick(event: MouseEvent) {
  if (props.disabled || !props.productSalesId) {
    return;
  }
  const target = event.target as HTMLElement;
  if (showClear.value && target.closest('.ant-input-suffix')) {
    event.stopPropagation();
    clearSelected();
    return;
  }
  dialogRef.value?.open(props.productSalesId, props.modelValue ?? undefined);
}
/** å¼¹çª—选中回调 */
function handleSelected(
  rows: MesWmReturnSalesApi.ReturnableProductSalesLine[],
) {
  const item = rows[0];
  if (!item) {
    return;
  }
  selectedItem.value = item;
  emit('update:modelValue', item.productSalesLineId);
  emit('change', item);
}
</script>
<template>
  <div
    v-bind="attrs"
    class="w-full"
    :class="disabled ? 'cursor-not-allowed' : 'cursor-pointer'"
    @click="handleClick"
    @mouseenter="hovering = true"
    @mouseleave="hovering = false"
  >
    <Tooltip :mouse-enter-delay="0.5" :open="selectedItem ? undefined : false">
      <template #title>
        <div v-if="selectedItem" class="leading-6">
          <div>物料编码:{{ selectedItem.itemCode || '-' }}</div>
          <div>物料名称:{{ selectedItem.itemName || '-' }}</div>
          <div>规格型号:{{ selectedItem.itemSpecification || '-' }}</div>
          <div>批次号:{{ selectedItem.batchCode || '-' }}</div>
          <div>出库数量:{{ selectedItem.outboundQuantity ?? '-' }}</div>
        </div>
      </template>
      <Input
        :disabled="disabled"
        :placeholder="placeholder"
        :value="displayLabel"
        readonly
      >
        <template #suffix>
          <IconifyIcon
            class="size-4"
            :icon="showClear ? 'lucide:circle-x' : 'lucide:search'"
          />
        </template>
      </Input>
    </Tooltip>
  </div>
  <WmReturnSalesProductSalesLineSelectDialog
    ref="dialogRef"
    @selected="handleSelected"
  />
</template>
src/views/wls/returnsales/components/select-dialog.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,209 @@
<script lang="ts" setup>
import type { VbenFormSchema } from '#/adapter/form';
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { MesWmReturnSalesApi } from '#/api/mes/wm/returnsales';
import { markRaw, nextTick, ref } from 'vue';
import { message, Modal } from 'ant-design-vue';
import { useVbenVxeGrid } from '#/adapter/vxe-table';
import { getReturnableProductSalesPage } from '#/api/mes/wm/returnsales';
import CrmCustomerSelect from '#/components/crm-customer-select.vue';
const emit = defineEmits<{
  selected: [rows: MesWmReturnSalesApi.ReturnableProductSales[]];
}>();
const open = ref(false); // å¼¹çª—是否打开
const selectedRows = ref<MesWmReturnSalesApi.ReturnableProductSales[]>([]); // å·²é€‰å‡ºåº“单
const preSelectedId = ref<number>(); // é¢„选出库单编号
/** æœç´¢è¡¨å• */
function useSearchSchema(): VbenFormSchema[] {
  return [
    {
      fieldName: 'code',
      label: '出库单号',
      component: 'Input',
      componentProps: {
        allowClear: true,
        placeholder: '请输入出库单号',
      },
    },
    {
      fieldName: 'name',
      label: '出库单名称',
      component: 'Input',
      componentProps: {
        allowClear: true,
        placeholder: '请输入出库单名称',
      },
    },
    {
      fieldName: 'clientId',
      label: '客户',
      component: markRaw(CrmCustomerSelect),
      componentProps: {
        placeholder: '请选择客户',
      },
    },
  ];
}
/** è¡¨æ ¼å­—段 */
function useGridColumns(): VxeTableGridOptions<MesWmReturnSalesApi.ReturnableProductSales>['columns'] {
  return [
    {
      type: 'radio',
      width: 50,
    },
    {
      field: 'code',
      title: '出库单号',
      minWidth: 160,
    },
    {
      field: 'name',
      title: '出库单名称',
      minWidth: 150,
    },
    {
      field: 'clientName',
      title: '客户名称',
      minWidth: 120,
    },
    {
      field: 'salesDate',
      title: '出库日期',
      width: 120,
      formatter: 'formatDate',
    },
    {
      field: 'outboundTime',
      title: '实际出库时间',
      minWidth: 160,
      formatter: 'formatDateTime',
    },
  ];
}
/** å¤„理单选切换 */
function handleRadioChange(row: MesWmReturnSalesApi.ReturnableProductSales) {
  selectedRows.value = [row];
}
/** å¤„理行双击:选中并确认 */
async function handleCellDblclick({
  row,
}: {
  row: MesWmReturnSalesApi.ReturnableProductSales;
}) {
  selectedRows.value = [row];
  await gridApi.grid.setRadioRow(row);
  handleConfirm();
}
/** å›žæ˜¾é¢„选出库单 */
async function applyPreSelection() {
  if (!preSelectedId.value) {
    return;
  }
  const rows = gridApi.grid.getData() as MesWmReturnSalesApi.ReturnableProductSales[];
  for (const row of rows) {
    if (row.id === preSelectedId.value) {
      await gridApi.grid.setRadioRow(row);
      selectedRows.value = [row];
      return;
    }
  }
}
const [Grid, gridApi] = useVbenVxeGrid({
  formOptions: {
    schema: useSearchSchema(),
  },
  gridOptions: {
    columns: useGridColumns(),
    height: 520,
    keepSource: true,
    radioConfig: {
      highlight: true,
      trigger: 'row',
    },
    proxyConfig: {
      ajax: {
        query: async ({ page }, formValues) => {
          return await getReturnableProductSalesPage({
            pageNo: page.currentPage,
            pageSize: page.pageSize,
            ...formValues,
          });
        },
      },
    },
    rowConfig: {
      keyField: 'id',
      isHover: true,
    },
    toolbarConfig: {
      refresh: true,
      search: true,
    },
  } as VxeTableGridOptions<MesWmReturnSalesApi.ReturnableProductSales>,
  gridEvents: {
    cellDblclick: handleCellDblclick,
    radioChange: ({ row }: { row: MesWmReturnSalesApi.ReturnableProductSales }) => {
      handleRadioChange(row);
    },
  },
});
/** é‡ç½®æŸ¥è¯¢å’Œé€‰æ‹©çŠ¶æ€ */
async function resetQueryState() {
  selectedRows.value = [];
  await gridApi.grid.clearRadioRow();
  await gridApi.formApi.resetForm();
}
/** æ‰“开出库单选择弹窗 */
async function openModal(selectedId?: number) {
  open.value = true;
  preSelectedId.value = selectedId;
  await resetQueryState();
  await gridApi.query();
  await nextTick();
  await applyPreSelection();
}
/** å…³é—­å‡ºåº“单选择弹窗 */
async function closeModal() {
  open.value = false;
  await resetQueryState();
}
/** ç¡®è®¤é€‰æ‹©å‡ºåº“单 */
function handleConfirm() {
  if (selectedRows.value.length === 0) {
    message.warning('请选择一条数据');
    return;
  }
  emit('selected', [selectedRows.value[0]!]);
  open.value = false;
}
defineExpose({ open: openModal });
</script>
<template>
  <Modal
    v-model:open="open"
    title="销售出库单选择"
    width="70%"
    :destroy-on-close="true"
    @cancel="closeModal"
    @ok="handleConfirm"
  >
    <Grid table-title="可退销售出库单列表" />
  </Modal>
</template>
src/views/wls/returnsales/components/select.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,152 @@
<script lang="ts" setup>
import type { MesWmReturnSalesApi } from '#/api/mes/wm/returnsales';
import { computed, ref, useAttrs, watch } from 'vue';
import { IconifyIcon } from '@vben/icons';
import { Input, Tooltip } from 'ant-design-vue';
import { getProductSales } from '#/api/mes/wm/productsales';
import WmReturnSalesProductSalesSelectDialog from './select-dialog.vue';
defineOptions({ name: 'WmReturnSalesProductSalesSelect', inheritAttrs: false });
const props = withDefaults(
  defineProps<{
    allowClear?: boolean;
    disabled?: boolean;
    modelValue?: number;
    placeholder?: string;
  }>(),
  {
    allowClear: true,
    disabled: false,
    modelValue: undefined,
    placeholder: '请选择销售出库单',
  },
);
const emit = defineEmits<{
  change: [item: MesWmReturnSalesApi.ReturnableProductSales | undefined];
  'update:modelValue': [value: number | undefined];
}>();
const attrs = useAttrs();
const dialogRef = ref<
  InstanceType<typeof WmReturnSalesProductSalesSelectDialog>
>();
const hovering = ref(false);
const selectedItem = ref<MesWmReturnSalesApi.ReturnableProductSales>();
const displayLabel = computed(() => selectedItem.value?.code ?? '');
const showClear = computed(
  () =>
    props.allowClear &&
    !props.disabled &&
    hovering.value &&
    props.modelValue !== undefined &&
    props.modelValue !== null,
);
/** æ ¹æ®ç¼–号单条查询出库单信息(用于编辑回显) */
async function resolveItemById(id: number | undefined) {
  if (!id) {
    selectedItem.value = undefined;
    return;
  }
  if (selectedItem.value?.id === id) {
    return;
  }
  // å·²é€€å®Œçš„出库单不在「可退」列表中,这里直接按编号回显
  try {
    const productSales = await getProductSales(id);
    selectedItem.value = productSales
      ? {
          clientId: productSales.clientId,
          clientName: productSales.clientName,
          code: productSales.code,
          id: productSales.id,
          name: productSales.name,
          salesDate: productSales.salesDate,
        }
      : undefined;
  } catch {
    selectedItem.value = undefined;
  }
}
watch(() => props.modelValue, resolveItemById, { immediate: true });
/** æ¸…空已选出库单 */
function clearSelected() {
  selectedItem.value = undefined;
  emit('update:modelValue', undefined);
  emit('change', undefined);
}
/** æ‰“开出库单选择弹窗 */
function handleClick(event: MouseEvent) {
  if (props.disabled) {
    return;
  }
  const target = event.target as HTMLElement;
  if (showClear.value && target.closest('.ant-input-suffix')) {
    event.stopPropagation();
    clearSelected();
    return;
  }
  dialogRef.value?.open(props.modelValue ?? undefined);
}
/** å¼¹çª—选中回调 */
function handleSelected(rows: MesWmReturnSalesApi.ReturnableProductSales[]) {
  const item = rows[0];
  if (!item) {
    return;
  }
  selectedItem.value = item;
  emit('update:modelValue', item.id);
  emit('change', item);
}
</script>
<template>
  <div
    v-bind="attrs"
    class="w-full"
    :class="disabled ? 'cursor-not-allowed' : 'cursor-pointer'"
    @click="handleClick"
    @mouseenter="hovering = true"
    @mouseleave="hovering = false"
  >
    <Tooltip :mouse-enter-delay="0.5" :open="selectedItem ? undefined : false">
      <template #title>
        <div v-if="selectedItem" class="leading-6">
          <div>出库单号:{{ selectedItem.code || '-' }}</div>
          <div>出库单名称:{{ selectedItem.name || '-' }}</div>
          <div>客户:{{ selectedItem.clientName || '-' }}</div>
        </div>
      </template>
      <Input
        :disabled="disabled"
        :placeholder="placeholder"
        :value="displayLabel"
        readonly
      >
        <template #suffix>
          <IconifyIcon
            class="size-4"
            :icon="showClear ? 'lucide:circle-x' : 'lucide:search'"
          />
        </template>
      </Input>
    </Tooltip>
  </div>
  <WmReturnSalesProductSalesSelectDialog
    ref="dialogRef"
    @selected="handleSelected"
  />
</template>
src/views/wls/returnsales/data.ts
@@ -11,16 +11,20 @@
import { Button } from 'ant-design-vue';
import { z } from '#/adapter/form';
import CrmCustomerSelect from '#/components/crm-customer-select.vue';
import { generateAutoCode } from '#/api/mes/md/autocode/record';
import { MdItemSelect } from '#/views/mes/md/item/components';
import { MdmItemSelect } from '#/views/basicData/mdm/components';
import { WmBatchSelect } from '#/views/wls/batch/components';
import {
  WmWarehouseAreaSelect,
  WmWarehouseLocationSelect,
  WmWarehouseSelect,
} from '#/views/wls/warehouse/components';
import {
  WmReturnSalesProductSalesLineSelect,
  WmReturnSalesProductSalesSelect,
} from './components';
/** è¡¨å•类型 */
export type FormType = 'create' | 'detail' | 'finish' | 'stock' | 'update';
@@ -85,6 +89,20 @@
        placeholder: '请输入退货单名称',
      },
      rules: 'required',
    },
    {
      fieldName: 'productSalesId',
      label: '销售出库单',
      component: markRaw(WmReturnSalesProductSalesSelect),
      componentProps: {
        disabled: isHeaderReadonly(formType),
        // é€‰æ‹©å‡ºåº“单后自动带出客户
        onChange: async (
          productSales?: MesWmReturnSalesApi.ReturnableProductSales,
        ) => {
          await formApi?.setFieldValue('clientId', productSales?.clientId);
        },
      },
    },
    {
      fieldName: 'clientId',
@@ -188,6 +206,11 @@
      minWidth: 150,
    },
    {
      field: 'productSalesCode',
      title: '销售出库单号',
      minWidth: 160,
    },
    {
      field: 'clientCode',
      title: '客户编码',
      minWidth: 120,
@@ -248,12 +271,12 @@
      minWidth: 140,
    },
    {
      field: 'specification',
      field: 'itemSpecification',
      title: '规格型号',
      minWidth: 120,
    },
    {
      field: 'unitMeasureName',
      field: 'itemUnit',
      title: '单位',
      width: 80,
    },
@@ -266,24 +289,6 @@
      field: 'batchCode',
      title: '批次号',
      minWidth: 120,
    },
    {
      field: 'rqcCheckFlag',
      title: '是否需要质检',
      width: 120,
      cellRender: {
        name: 'CellDict',
        props: { type: DICT_TYPE.INFRA_BOOLEAN_STRING },
      },
    },
    {
      field: 'qualityStatus',
      title: '质量状态',
      width: 100,
      cellRender: {
        name: 'CellDict',
        props: { type: DICT_TYPE.MES_WM_QUALITY_STATUS },
      },
    },
    ...(editable || stockable
      ? [
@@ -299,28 +304,52 @@
}
/** é€€è´§å•行新增/修改的表单 */
export function useLineFormSchema(clientId?: number): VbenFormSchema[] {
export function useLineFormSchema(
  options: { clientId?: number; productSalesId?: number } = {},
  formApi?: VbenFormApi,
): VbenFormSchema[] {
  const { clientId, productSalesId } = options;
  const hasProductSales = !!productSalesId; // å…³è”了销售出库单时,物料/批次由出库行带入
  return [
    {
      fieldName: 'productSalesLineId',
      label: '出库行',
      component: markRaw(WmReturnSalesProductSalesLineSelect),
      componentProps: {
        // é€‰æ‹©å‡ºåº“行后自动带入物料、批次与可退数量
        onChange: async (
          line?: MesWmReturnSalesApi.ReturnableProductSalesLine,
        ) => {
          await formApi?.setValues({
            batchCode: line?.batchCode,
            itemId: line?.itemId,
            quantity: line?.returnableQuantity,
            returnableQuantity: line?.returnableQuantity,
          });
        },
        productSalesId,
      },
      rules: hasProductSales ? 'selectRequired' : undefined,
      dependencies: {
        triggerFields: [''],
        if: () => hasProductSales,
      },
    },
    {
      fieldName: 'itemId',
      label: '物料',
      component: markRaw(MdItemSelect),
      component: markRaw(MdmItemSelect),
      componentProps: {
        placeholder: '请选择物料',
      },
      rules: 'selectRequired',
      dependencies: {
        triggerFields: ['productSalesLineId'],
        componentProps: (values) => ({
          disabled: hasProductSales || !!values.productSalesLineId,
          placeholder: '请选择物料',
        }),
    },
    {
      fieldName: 'quantity',
      label: '退货数量',
      component: 'InputNumber',
      componentProps: {
        class: '!w-full',
        min: 0,
        placeholder: '请输入退货数量',
        precision: 2,
      },
      rules: 'required',
    },
    {
      fieldName: 'batchId',
@@ -333,6 +362,7 @@
      },
      dependencies: {
        triggerFields: ['itemId'],
        if: () => !hasProductSales,
        componentProps: (values) => ({
          clientId,
          disabled: true,
@@ -342,10 +372,50 @@
      },
    },
    {
      fieldName: 'rqcCheckFlag',
      label: '需要质检',
      component: 'Switch',
      rules: z.boolean().default(false),
      fieldName: 'batchCode',
      label: '批次号',
      component: 'Input',
      componentProps: {
        disabled: true,
        placeholder: '由出库行带入',
      },
      dependencies: {
        triggerFields: [''],
        if: () => hasProductSales,
      },
    },
    {
      fieldName: 'returnableQuantity',
      label: '可退数量',
      component: 'Input',
      componentProps: {
        disabled: true,
        placeholder: '选择出库行后自动带出',
      },
      dependencies: {
        triggerFields: [''],
        if: () => hasProductSales,
      },
    },
    {
      fieldName: 'quantity',
      label: '退货数量',
      component: 'InputNumber',
      componentProps: {
        class: '!w-full',
        min: 0,
        placeholder: '请输入退货数量',
        precision: 2,
      },
      rules: 'required',
      dependencies: {
        triggerFields: ['returnableQuantity'],
        componentProps: (values) => ({
          max: hasProductSales
            ? (values.returnableQuantity as number | undefined)
            : undefined,
        }),
      },
    },
    {
      fieldName: 'remark',
@@ -409,7 +479,7 @@
    {
      fieldName: 'itemId',
      label: '物料',
      component: markRaw(MdItemSelect),
      component: markRaw(MdmItemSelect),
      componentProps: {
        disabled: true,
      },
@@ -471,7 +541,8 @@
      label: '批次号',
      component: 'Input',
      componentProps: {
        placeholder: '请输入批次号',
        disabled: true,
        placeholder: '由退货行带入',
      },
    },
    {
src/views/wls/returnsales/index.vue
@@ -55,11 +55,6 @@
  formModalApi.setData({ formType: 'finish', id: row.id }).open();
}
/** æç¤ºå‰å¾€è´¨æ£€æ¨¡å— */
function handleQc() {
  message.info('请前往【质量管理 - é€€è´§æ£€éªŒï¼ˆRQC)】中进行退货检验操作');
}
/** åˆ é™¤é”€å”®é€€è´§å• */
async function handleDelete(row: MesWmReturnSalesApi.ReturnSales) {
  const hideLoading = message.loading({
@@ -172,12 +167,6 @@
              },
            },
            {
              label: '执行质检',
              type: 'link',
              ifShow: row.status === MesWmReturnSalesStatusEnum.CONFIRMED,
              onClick: handleQc,
            },
            {
              label: '执行退货',
              type: 'link',
              auth: ['mes:wm-return-sales:finish'],
@@ -197,7 +186,6 @@
              danger: true,
              auth: ['mes:wm-return-sales:cancel'],
              ifShow:
                row.status === MesWmReturnSalesStatusEnum.CONFIRMED ||
                row.status === MesWmReturnSalesStatusEnum.APPROVING ||
                row.status === MesWmReturnSalesStatusEnum.APPROVED,
              popConfirm: {
src/views/wls/returnsales/modules/detail-form.vue
@@ -74,6 +74,7 @@
    formApi.setState({ schema: useDetailFormSchema(formApi) });
    // åŠ è½½æ•°æ®
    const data = modalApi.getData<{
      batchCode?: string;
      detailId?: number;
      itemId?: number;
      lineId: number;
@@ -91,7 +92,10 @@
        modalApi.unlock();
      }
    } else if (data.itemId) {
      await formApi.setFieldValue('itemId', data.itemId);
      await formApi.setValues({
        batchCode: data.batchCode,
        itemId: data.itemId,
      });
    }
  },
});
src/views/wls/returnsales/modules/form.vue
@@ -197,6 +197,7 @@
        <LineList
          :client-id="formData.clientId"
          :form-type="formType"
          :product-sales-id="formData.productSalesId"
          :return-id="formData.id"
        />
      </div>
src/views/wls/returnsales/modules/line-form.vue
@@ -8,6 +8,7 @@
import { message } from 'ant-design-vue';
import { useVbenForm } from '#/adapter/form';
import { getReturnableProductSalesLineList } from '#/api/mes/wm/returnsales';
import {
  createReturnSalesLine,
  getReturnSalesLine,
@@ -20,6 +21,7 @@
const emit = defineEmits(['success']);
const formData = ref<MesWmReturnSalesLineApi.ReturnSalesLine>();
const returnId = ref<number>(); // æ‰€å±žé€€è´§å•编号
const productSalesId = ref<number>(); // å…³è”的销售出库单编号
const getTitle = computed(() => {
  return formData.value?.id
@@ -40,6 +42,39 @@
  showDefaultActions: false,
  wrapperClass: 'grid-cols-3',
});
/**
 * è®¡ç®—当前行可填写的最大数量:
 * å¯é€€è¡Œåˆ—表中的可退数量不含本单占用,需加上本行已有数量
 */
async function loadReturnableQuantity(
  line: MesWmReturnSalesLineApi.ReturnSalesLine,
) {
  if (!productSalesId.value) {
    return;
  }
  const current = Number(line.quantity ?? 0);
  await formApi.setFieldValue('returnableQuantity', current);
  if (!line.productSalesLineId) {
    return;
  }
  try {
    const lines = await getReturnableProductSalesLineList(
      productSalesId.value,
    );
    const matched = lines.find(
      (item) => item.productSalesLineId === line.productSalesLineId,
    );
    if (matched) {
      await formApi.setFieldValue(
        'returnableQuantity',
        Number(matched.returnableQuantity ?? 0) + current,
      );
    }
  } catch {
    // å‡ºåº“单已不可退时保持本行数量作为上限
  }
}
const [Modal, modalApi] = useVbenModal({
  async onConfirm() {
@@ -73,10 +108,17 @@
    const data = modalApi.getData<{
      clientId?: number;
      id?: number;
      productSalesId?: number;
      returnId: number;
    }>();
    returnId.value = data.returnId;
    formApi.setState({ schema: useLineFormSchema(data.clientId) });
    productSalesId.value = data.productSalesId;
    formApi.setState({
      schema: useLineFormSchema(
        { clientId: data.clientId, productSalesId: data.productSalesId },
        formApi,
      ),
    });
    if (!data.id) {
      return;
    }
@@ -85,6 +127,7 @@
      formData.value = await getReturnSalesLine(data.id);
      // è®¾ç½®åˆ° values
      await formApi.setValues(formData.value);
      await loadReturnableQuantity(formData.value);
    } finally {
      modalApi.unlock();
    }
src/views/wls/returnsales/modules/line-list.vue
@@ -28,6 +28,7 @@
const props = defineProps<{
  clientId?: number;
  formType: FormType;
  productSalesId?: number;
  returnId: number;
}>();
@@ -63,6 +64,7 @@
  lineFormModalApi
    .setData({
      clientId: props.clientId,
      productSalesId: props.productSalesId,
      returnId: props.returnId,
    })
    .open();
@@ -74,6 +76,7 @@
    .setData({
      clientId: props.clientId,
      id: row.id,
      productSalesId: props.productSalesId,
      returnId: props.returnId,
    })
    .open();
@@ -96,13 +99,18 @@
/** ä¸Šæž¶ï¼šç›´æŽ¥æ‰“开明细创建表单 */
function handleStock(row: MesWmReturnSalesLineApi.ReturnSalesLine) {
  openDetailForm(row.id!, row.itemId);
  openDetailForm(row.id!, row.itemId, row.batchCode);
}
/** æ‰“开上架明细表单 */
function openDetailForm(lineId: number, itemId?: number, detailId?: number) {
function openDetailForm(
  lineId: number,
  itemId?: number,
  batchCode?: string,
  detailId?: number,
) {
  detailFormModalApi
    .setData({ detailId, itemId, lineId, returnId: props.returnId })
    .setData({ batchCode, detailId, itemId, lineId, returnId: props.returnId })
    .open();
}
@@ -185,7 +193,7 @@
        <TableAction
          :actions="[
            {
              label: '添加物料',
              label: '添加退货行',
              type: 'primary',
              icon: ACTION_ICON.ADD,
              onClick: handleCreate,
@@ -197,7 +205,9 @@
        <DetailList
          :details="getExpandedDetails(row)"
          :form-type="formType"
          @edit="(detailId) => openDetailForm(row.id!, row.itemId, detailId)"
          @edit="
            (detailId) => openDetailForm(row.id!, row.itemId, row.batchCode, detailId)
          "
          @refresh="loadLineDetails(row.id!)"
        />
      </template>
src/views/wls/salesnotice/components/line-select-dialog.vue
@@ -4,8 +4,6 @@
import { nextTick, ref } from 'vue';
import { DICT_TYPE } from '@vben/constants';
import { message, Modal } from 'ant-design-vue';
import { useVbenVxeGrid } from '#/adapter/vxe-table';
@@ -57,15 +55,6 @@
      field: 'batchCode',
      title: '批次号',
      minWidth: 120,
    },
    {
      field: 'oqcCheckFlag',
      title: '是否检验',
      width: 90,
      cellRender: {
        name: 'CellDict',
        props: { type: DICT_TYPE.INFRA_BOOLEAN_STRING },
      },
    },
  ];
}
src/views/wls/salesnotice/data.ts
@@ -13,7 +13,6 @@
import { Button } from 'ant-design-vue';
import { z } from '#/adapter/form';
import { generateAutoCode } from '#/api/mes/md/autocode/record';
import CrmCustomerSelect from '#/components/crm-customer-select.vue';
import {
@@ -298,15 +297,6 @@
      minWidth: 120,
    },
    {
      field: 'oqcCheckFlag',
      title: '是否检验',
      width: 90,
      cellRender: {
        name: 'CellDict',
        props: { type: DICT_TYPE.INFRA_BOOLEAN_STRING },
      },
    },
    {
      field: 'remark',
      title: '备注',
      minWidth: 120,
@@ -439,12 +429,6 @@
        disabled: true,
        placeholder: '选择物料/批次后自动带出',
      },
    },
    {
      fieldName: 'oqcCheckFlag',
      label: '是否检验',
      component: 'Switch',
      rules: z.boolean().default(true),
    },
    {
      fieldName: 'remark',