src/views/wls/itemreceipt/data.ts
@@ -1,10 +1,13 @@
import type { VbenFormApi, VbenFormSchema } from '#/adapter/form';
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { MdmItemModelApi } from '#/api/mdm/item-model';
import type { MesMdItemApi } from '#/api/mes/md/item';
import type { MesWmArrivalNoticeApi } from '#/api/mes/wm/arrivalnotice';
import type { MesWmArrivalNoticeLineApi } from '#/api/mes/wm/arrivalnotice/line';
import type { MesWmItemReceiptApi } from '#/api/mes/wm/itemreceipt';
import type { MesWmItemReceiptDetailApi } from '#/api/mes/wm/itemreceipt/detail';
import type { MesWmItemReceiptLineApi } from '#/api/mes/wm/itemreceipt/line';
import type { SrmSupplierApi } from '#/api/srm/supplier';
import { h, markRaw } from 'vue';
@@ -16,10 +19,12 @@
import { Button } from 'ant-design-vue';
import { getItem as getMdmItem } from '#/api/mdm/item';
import { getItemModelListByItem } from '#/api/mdm/item-model';
import { generateAutoCode } from '#/api/mes/md/autocode/record';
import { getRangePickerDefaultProps } from '#/utils';
import { MdItemSelect } from '#/views/mes/md/item/components';
import { ErpSupplierSelect } from '#/views/erp/purchase/supplier/components';
import { SrmSupplierSelect } from '#/views/srm/supplier/components';
import {
  WmArrivalNoticeLineSelect,
  WmArrivalNoticeSelect,
@@ -132,10 +137,10 @@
    {
      fieldName: 'vendorId',
      label: '供应商',
      component: markRaw(ErpSupplierSelect),
      component: markRaw(SrmSupplierSelect),
      componentProps: {
        placeholder: '请选择供应商',
        onChange: async (supplier?: any) => {
        onChange: async (supplier?: SrmSupplierApi.SupplierVO) => {
          await formApi?.setFieldValue('vendorName', supplier?.name);
        },
      },
@@ -193,7 +198,7 @@
    {
      fieldName: 'vendorId',
      label: '供应商',
      component: markRaw(ErpSupplierSelect),
      component: markRaw(SrmSupplierSelect),
      componentProps: {
        placeholder: '请选择供应商',
      },
@@ -285,9 +290,24 @@
      minWidth: 120,
    },
    {
      field: 'itemModel',
      title: '物料型号',
      minWidth: 120,
    },
    {
      field: 'unitMeasureName',
      title: '单位',
      width: 80,
      title: '主计量单位',
      width: 100,
    },
    {
      field: 'enterUnitMeasureName',
      title: '录入单位',
      width: 90,
    },
    {
      field: 'enterQuantity',
      title: '录入数量',
      width: 100,
    },
    {
      field: 'receivedQuantity',
@@ -312,11 +332,221 @@
  ];
}
/** 录入单位选项(含到物料主单位的换算比率) */
export interface ItemEnterUnitOption {
  id: number;
  name: string;
  rate: number;
}
/** 型号下拉选项:ApiSelect 会把 id/model 映射成 value/label,其余字段原样保留 */
type ItemModelSelectOption = Pick<
  MdmItemModelApi.ItemModel,
  'unitMeasureId' | 'unitMeasureName' | 'unitMeasureRate'
>;
/** 入库单行表单中与双单位换算相关的字段 */
interface LineUnitFormValues {
  enterQuantity?: number;
  enterUnitMeasureId?: number;
  enterUnitMeasureName?: string;
  enterUnitRate?: number;
  itemId?: number;
  itemModelId?: number;
  itemPrimaryUnitId?: number;
  itemPrimaryUnitName?: string;
}
/** 双单位换算用的临时表单字段(只供前端换算与展示,提交前必须剔除) */
const LINE_TEMP_FIELDS = [
  'itemPrimaryUnitId',
  'itemPrimaryUnitName',
  'enterUnitMeasureName',
  'enterUnitRate',
] as const;
/** 剔除双单位换算用的临时字段,得到可直接提交的行数据 */
export function omitLineTempFields(
  values: Record<string, unknown>,
): MesWmItemReceiptLineApi.ItemReceiptLine {
  const data = { ...values };
  for (const field of LINE_TEMP_FIELDS) {
    delete data[field];
  }
  return data as MesWmItemReceiptLineApi.ItemReceiptLine;
}
/** 换算比率是否有效(必须大于 0) */
function isValidRate(rate?: number): rate is number {
  return typeof rate === 'number' && rate > 0;
}
/** 换算结果按 6 位小数展示(最终以后端换算结果为准) */
function roundQuantity(quantity: number): number {
  return Math.round(quantity * 1e6) / 1e6;
}
/**
 * 查询物料可选的录入单位:主单位 + 辅单位
 *
 * 选定型号时只保留「物料主单位」与「型号计量单位」两种口径,与后端换算规则保持一致
 */
export async function getItemEnterUnits(params: {
  itemId?: number;
  itemModelId?: number;
}): Promise<ItemEnterUnitOption[]> {
  const itemId = params.itemId;
  if (!itemId) {
    return [];
  }
  const item = await getMdmItem(itemId);
  if (!item?.unitMeasureId) {
    return [];
  }
  // 主单位始终可选,按主单位录入时不做换算
  const primaryUnit: ItemEnterUnitOption = {
    id: item.unitMeasureId,
    name: item.unitMeasureName || '物料主单位',
    rate: 1,
  };
  const unitOptions: ItemEnterUnitOption[] = [primaryUnit];
  const pushUnit = (id?: number, name?: string, rate?: number) => {
    if (
      !id ||
      id === primaryUnit.id ||
      !isValidRate(rate) ||
      unitOptions.some((unit) => unit.id === id)
    ) {
      return;
    }
    unitOptions.push({ id, name: name || '辅单位', rate });
  };
  if (params.itemModelId) {
    const models = await getItemModelListByItem(itemId);
    const modelOption = models?.find((option) => option.id === params.itemModelId);
    pushUnit(
      modelOption?.unitMeasureId,
      modelOption?.unitMeasureName,
      modelOption?.unitMeasureRate,
    );
    return unitOptions;
  }
  // 未选型号时,物料主数据上配置了换算比率的辅单位可选
  pushUnit(item.unitMeasureId2, item.unitMeasureName2, item.unitMeasureRate1);
  pushUnit(item.unitMeasureId3, item.unitMeasureName3, item.unitMeasureRate2);
  return unitOptions;
}
/** 查询某个录入单位到物料主单位的换算比率(编辑回显时补齐前端换算上下文) */
export async function getEnterUnitRate(
  itemId?: number,
  itemModelId?: number,
  enterUnitMeasureId?: number,
): Promise<number | undefined> {
  if (!itemId || !enterUnitMeasureId) {
    return undefined;
  }
  const unitOptions = await getItemEnterUnits({ itemId, itemModelId });
  return unitOptions.find((unit) => unit.id === enterUnitMeasureId)?.rate;
}
/** 入库单行新增/修改的表单 */
export function useLineFormSchema(
  hasNotice: boolean,
  formApi?: VbenFormApi,
): VbenFormSchema[] {
  /** 是否按物料主单位录入(未选录入单位、或录入单位就是物料主单位) */
  function isPrimaryUnitEnter(values: LineUnitFormValues) {
    return (
      !values.enterUnitMeasureId ||
      values.enterUnitMeasureId === values.itemPrimaryUnitId
    );
  }
  /** 切换物料:重新记录物料主单位,并清空型号、录入单位与数量(口径都随物料变化) */
  async function handleItemChange(itemId?: number) {
    // 先清空,避免上一个物料的型号与单位口径残留
    await formApi?.setValues({
      itemId,
      itemModelId: undefined,
      itemPrimaryUnitId: undefined,
      itemPrimaryUnitName: undefined,
      enterUnitMeasureId: undefined,
      enterUnitMeasureName: undefined,
      enterUnitRate: undefined,
      enterQuantity: undefined,
      receivedQuantity: undefined,
    });
    if (!itemId) {
      return;
    }
    const item = await getMdmItem(itemId);
    await formApi?.setValues({
      itemPrimaryUnitId: item?.unitMeasureId,
      itemPrimaryUnitName: item?.unitMeasureName,
    });
  }
  /** 选择型号:默认按型号计量单位录入,并记录换算比率 */
  async function handleModelChange(
    option: ItemModelSelectOption | undefined,
    values: LineUnitFormValues,
  ) {
    const enterUnitMeasureId =
      option?.unitMeasureId && option.unitMeasureId !== values.itemPrimaryUnitId
        ? option.unitMeasureId
        : undefined;
    await formApi?.setValues({
      enterUnitMeasureId,
      enterUnitMeasureName: enterUnitMeasureId
        ? option?.unitMeasureName
        : undefined,
      enterUnitRate: enterUnitMeasureId ? option?.unitMeasureRate : undefined,
      enterQuantity: undefined,
      // 切到型号单位录入后入库数量由系统换算,先清掉上一次口径的数值
      ...(enterUnitMeasureId ? { receivedQuantity: undefined } : {}),
    });
  }
  /** 切换录入单位:记录换算比率并立即重算入库数量;改回主单位时清掉录入数量 */
  async function handleEnterUnitChange(
    value: number | undefined,
    option: { label?: string; rate?: number } | undefined,
    values: LineUnitFormValues,
  ) {
    if (!value || value === values.itemPrimaryUnitId) {
      await formApi?.setValues({
        enterUnitMeasureName: undefined,
        enterUnitRate: undefined,
        enterQuantity: undefined,
      });
      return;
    }
    await formApi?.setValues({
      enterUnitMeasureName: option?.label,
      enterUnitRate: option?.rate,
      ...(values.enterQuantity == null || !isValidRate(option?.rate)
        ? { receivedQuantity: undefined }
        : {
            receivedQuantity: roundQuantity(values.enterQuantity * option.rate),
          }),
    });
  }
  /** 录入数量变化:按录入单位与换算比率试算入库数量,供用户核对换算结果 */
  async function handleEnterQuantityChange(
    value: number | undefined,
    values: LineUnitFormValues,
  ) {
    if (isPrimaryUnitEnter(values) || !isValidRate(values.enterUnitRate)) {
      return;
    }
    await formApi?.setFieldValue(
      'receivedQuantity',
      value == null ? undefined : roundQuantity(value * values.enterUnitRate),
    );
  }
  return [
    {
      fieldName: 'noticeId',
@@ -331,14 +561,17 @@
      label: '到货通知单行',
      component: markRaw(WmArrivalNoticeLineSelect),
      componentProps: {
        // 选择到货通知单行后,自动回填物料和入库数量
        // 选择到货通知单行后,自动回填物料与入库数量
        onChange: async (
          line?: MesWmArrivalNoticeLineApi.ArrivalNoticeLine,
        ) => {
          await formApi?.setValues({
            itemId: line?.itemId,
            receivedQuantity: line?.arrivalQuantity,
          });
          await handleItemChange(line?.itemId);
          if (line?.arrivalQuantity != null) {
            await formApi?.setFieldValue(
              'receivedQuantity',
              line.arrivalQuantity,
            );
          }
        },
      },
      dependencies: {
@@ -355,6 +588,8 @@
      component: markRaw(MdItemSelect),
      componentProps: {
        placeholder: '请选择物料',
        // 切换物料后,型号、录入单位与数量都要重新选
        onChange: (item?: MesMdItemApi.Item) => handleItemChange(item?.id),
      },
      rules: 'selectRequired',
      dependencies: {
@@ -365,6 +600,121 @@
        }),
      },
    },
    // 物料主单位随所选物料变化,但表单 props 只能静态声明,故用隐藏字段承载
    {
      fieldName: 'itemPrimaryUnitId',
      component: 'Input',
      dependencies: {
        triggerFields: [''],
        show: () => false,
      },
    },
    {
      fieldName: 'itemPrimaryUnitName',
      component: 'Input',
      dependencies: {
        triggerFields: [''],
        show: () => false,
      },
    },
    {
      fieldName: 'itemModelId',
      label: '物料型号',
      component: 'ApiSelect',
      componentProps: {
        labelField: 'model',
        valueField: 'id',
        allowClear: true,
        api: (params: { itemId?: number }) =>
          getItemModelListByItem(params.itemId!),
        shouldFetch: (params: { itemId?: number }) => !!params?.itemId,
        placeholder: '请先选择物料',
      },
      dependencies: {
        triggerFields: ['itemId'],
        componentProps: (values) => ({
          disabled: !values.itemId,
          params: { itemId: values.itemId },
          placeholder: values.itemId ? '请选择物料型号' : '请先选择物料',
          // 选定型号后默认按其计量单位录入
          onChange: (option?: ItemModelSelectOption) =>
            handleModelChange(option, values),
        }),
      },
    },
    {
      fieldName: 'enterUnitMeasureId',
      label: '录入单位',
      component: 'ApiSelect',
      componentProps: {
        allowClear: true,
        labelField: 'name',
        valueField: 'id',
        api: (params: { itemId?: number; itemModelId?: number }) =>
          getItemEnterUnits(params),
        shouldFetch: (params: { itemId?: number }) => !!params?.itemId,
        placeholder: '默认为物料主计量单位',
      },
      dependencies: {
        triggerFields: ['itemId', 'itemModelId', 'itemPrimaryUnitName'],
        componentProps: (values) => ({
          disabled: !values.itemId,
          params: { itemId: values.itemId, itemModelId: values.itemModelId },
          placeholder: values.itemId
            ? `默认按物料主单位${values.itemPrimaryUnitName ? `【${values.itemPrimaryUnitName}】` : ''}录入`
            : '请先选择物料',
          onChange: (
            value: number | undefined,
            option?: { label?: string; rate?: number },
          ) => handleEnterUnitChange(value, option, values),
        }),
      },
    },
    // 录入单位名称与换算比率随所选单位变化,同样用隐藏字段承载
    {
      fieldName: 'enterUnitMeasureName',
      component: 'Input',
      dependencies: {
        triggerFields: [''],
        show: () => false,
      },
    },
    {
      fieldName: 'enterUnitRate',
      component: 'Input',
      dependencies: {
        triggerFields: [''],
        show: () => false,
      },
    },
    {
      fieldName: 'enterQuantity',
      label: '录入数量',
      component: 'InputNumber',
      componentProps: {
        class: '!w-full',
        min: 0,
        precision: 6,
      },
      dependencies: {
        triggerFields: [
          'enterUnitMeasureId',
          'enterUnitMeasureName',
          'itemPrimaryUnitId',
        ],
        show: (values) => !isPrimaryUnitEnter(values),
        // 返回 null 表示不追加规则(回退基础 rules)
        rules: (values) => (isPrimaryUnitEnter(values) ? null : 'required'),
        componentProps: (values) => ({
          placeholder: values.enterUnitMeasureName
            ? `请填写按【${values.enterUnitMeasureName}】计的数量`
            : '请按录入单位填写数量',
          // 录入数量变化时立即试算入库数量
          onChange: (value: number | undefined) =>
            handleEnterQuantityChange(value, values),
        }),
      },
    },
    {
      fieldName: 'receivedQuantity',
      label: '入库数量',
@@ -372,10 +722,26 @@
      componentProps: {
        class: '!w-full',
        min: 0,
        placeholder: '请输入入库数量',
        precision: 2,
        precision: 6,
      },
      rules: 'required',
      dependencies: {
        triggerFields: [
          'enterUnitMeasureId',
          'itemPrimaryUnitId',
          'itemPrimaryUnitName',
        ],
        // 按主单位录入时手工填写、必填;按辅单位录入时为系统换算结果,无需校验
        rules: (values) => (isPrimaryUnitEnter(values) ? 'required' : null),
        componentProps: (values) => {
          const primary = isPrimaryUnitEnter(values);
          return {
            disabled: !primary,
            placeholder: primary
              ? `请按物料主单位${values.itemPrimaryUnitName ? `【${values.itemPrimaryUnitName}】` : ''}填写数量`
              : '按录入单位与换算比率自动换算',
          };
        },
      },
    },
    {
      fieldName: 'productionDate',