| | |
| | | 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'; |
| | | |
| | |
| | | |
| | | 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, |
| | |
| | | { |
| | | 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); |
| | | }, |
| | | }, |
| | |
| | | { |
| | | fieldName: 'vendorId', |
| | | label: '供应商', |
| | | component: markRaw(ErpSupplierSelect), |
| | | component: markRaw(SrmSupplierSelect), |
| | | componentProps: { |
| | | placeholder: '请选择供应商', |
| | | }, |
| | |
| | | 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', |
| | |
| | | ]; |
| | | } |
| | | |
| | | /** 录入单位选项(含到物料主单位的换算比率) */ |
| | | 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', |
| | |
| | | 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: { |
| | |
| | | component: markRaw(MdItemSelect), |
| | | componentProps: { |
| | | placeholder: '请选择物料', |
| | | // 切换物料后,型号、录入单位与数量都要重新选 |
| | | onChange: (item?: MesMdItemApi.Item) => handleItemChange(item?.id), |
| | | }, |
| | | rules: 'selectRequired', |
| | | dependencies: { |
| | |
| | | }), |
| | | }, |
| | | }, |
| | | // 物料主单位随所选物料变化,但表单 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: '入库数量', |
| | |
| | | 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', |