import type { VbenFormApi, VbenFormSchema } from '#/adapter/form';
|
import type { MdmItemModelApi } from '#/api/mdm/item-model';
|
|
import { getItem as getMdmItem } from '#/api/mdm/item';
|
import { getItemModelListByItem } from '#/api/mdm/item-model';
|
|
/** 录入单位选项(含到物料主单位的换算比率) */
|
export interface ItemEnterUnitOption {
|
id: number;
|
name: string;
|
rate: number;
|
}
|
|
/** 型号下拉选项:ApiSelect 会把 id/model 映射成 value/label,其余字段原样保留 */
|
export type ItemModelSelectOption = Pick<
|
MdmItemModelApi.ItemModel,
|
'unitMeasureId' | 'unitMeasureName' | 'unitMeasureRate'
|
>;
|
|
/** 出库/入库单行表单中与双单位换算相关的字段 */
|
export interface LineUnitFormValues {
|
enterQuantity?: number;
|
enterUnitMeasureId?: number;
|
enterUnitMeasureName?: string;
|
enterUnitRate?: number;
|
itemId?: number;
|
itemModelId?: number;
|
itemPrimaryUnitId?: number;
|
itemPrimaryUnitName?: string;
|
quantityMax?: number;
|
}
|
|
/** 双单位换算用的临时表单字段(只供前端换算与展示,提交前必须剔除) */
|
const LINE_TEMP_FIELDS = [
|
'itemPrimaryUnitId',
|
'itemPrimaryUnitName',
|
'enterUnitMeasureName',
|
'enterUnitRate',
|
] as const;
|
|
/** 剔除双单位换算用的临时字段,得到可直接提交的行数据 */
|
export function omitLineTempFields<T extends object>(values: T): T {
|
const data = { ...values } as Record<string, unknown>;
|
for (const field of LINE_TEMP_FIELDS) {
|
delete data[field];
|
}
|
return data as T;
|
}
|
|
/** 换算比率是否有效(必须大于 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 interface LineUnitHandlers {
|
/** 是否按物料主单位录入(未选录入单位、或录入单位就是物料主单位) */
|
isPrimaryUnitEnter: (values: LineUnitFormValues) => boolean;
|
/** 切换物料:重新记录物料主单位,并清空型号、录入单位与数量(口径都随物料变化) */
|
handleItemChange: (itemId?: number) => Promise<void>;
|
/** 选择型号:默认按型号计量单位录入,并记录换算比率 */
|
handleModelChange: (
|
option: ItemModelSelectOption | undefined,
|
values: LineUnitFormValues,
|
) => Promise<void>;
|
/** 切换录入单位:记录换算比率并立即重算主单位数量;改回主单位时清掉录入数量 */
|
handleEnterUnitChange: (
|
value: number | undefined,
|
option: { label?: string; rate?: number } | undefined,
|
values: LineUnitFormValues,
|
) => Promise<void>;
|
/** 录入数量变化:按录入单位与换算比率试算主单位数量,供用户核对换算结果 */
|
handleEnterQuantityChange: (
|
value: number | undefined,
|
values: LineUnitFormValues,
|
) => Promise<void>;
|
}
|
|
/**
|
* 生成行表单的双单位录入联动方法
|
*
|
* @param quantityField 物料主单位口径的数量字段名(出库为 quantity,入库为 receivedQuantity)
|
*/
|
export function useLineUnitHandlers(
|
formApi?: VbenFormApi,
|
quantityField = 'quantity',
|
): LineUnitHandlers {
|
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,
|
[quantityField]: 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 ? { [quantityField]: 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)
|
? { [quantityField]: undefined }
|
: {
|
[quantityField]: roundQuantity(
|
values.enterQuantity * option.rate,
|
),
|
}),
|
});
|
}
|
|
async function handleEnterQuantityChange(
|
value: number | undefined,
|
values: LineUnitFormValues,
|
) {
|
if (isPrimaryUnitEnter(values) || !isValidRate(values.enterUnitRate)) {
|
return;
|
}
|
await formApi?.setFieldValue(
|
quantityField,
|
value == null ? undefined : roundQuantity(value * values.enterUnitRate),
|
);
|
}
|
|
return {
|
handleEnterQuantityChange,
|
handleEnterUnitChange,
|
handleItemChange,
|
handleModelChange,
|
isPrimaryUnitEnter,
|
};
|
}
|
|
/** 行表单「物料型号 / 录入单位 / 录入数量 / 主单位数量」相关字段的通用配置 */
|
export interface ItemUnitSchemaOptions {
|
/** 物料主单位口径的数量字段名(默认 quantity) */
|
quantityField?: string;
|
/** 物料主单位口径的数量字段标签(默认「出库数量」) */
|
quantityLabel?: string;
|
}
|
|
/**
|
* 生成行表单的双单位字段组(型号、录入单位、录入数量、主单位数量及换算用隐藏字段)
|
*
|
* 主单位数量字段在按辅单位录入时由系统换算、不可手工修改;按主单位录入时手工填写并受库存上限约束
|
*/
|
export function buildItemUnitSchema(
|
formApi?: VbenFormApi,
|
options: ItemUnitSchemaOptions = {},
|
): VbenFormSchema[] {
|
const quantityField = options.quantityField ?? 'quantity';
|
const quantityLabel = options.quantityLabel ?? '出库数量';
|
const handlers = useLineUnitHandlers(formApi, quantityField);
|
|
return [
|
// 物料主单位随所选物料变化,但表单 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) =>
|
handlers.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 },
|
) => handlers.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',
|
'quantityMax',
|
'enterUnitRate',
|
],
|
show: (values) => !handlers.isPrimaryUnitEnter(values),
|
// 返回 null 表示不追加规则(回退基础 rules)
|
rules: (values) =>
|
handlers.isPrimaryUnitEnter(values) ? null : 'required',
|
componentProps: (values) => ({
|
placeholder: values.enterUnitMeasureName
|
? `请填写按【${values.enterUnitMeasureName}】计的数量`
|
: '请按录入单位填写数量',
|
// 按辅单位录入时,可录入上限为「库存上限 ÷ 换算比率」
|
max:
|
values.quantityMax != null && isValidRate(values.enterUnitRate)
|
? Math.floor((values.quantityMax / values.enterUnitRate) * 1e6) /
|
1e6
|
: undefined,
|
// 录入数量变化时立即试算主单位数量
|
onChange: (value: number | undefined) =>
|
handlers.handleEnterQuantityChange(value, values),
|
}),
|
},
|
},
|
{
|
fieldName: quantityField,
|
label: quantityLabel,
|
component: 'InputNumber',
|
componentProps: {
|
class: '!w-full',
|
min: 0.01,
|
precision: 6,
|
},
|
dependencies: {
|
triggerFields: [
|
'enterUnitMeasureId',
|
'itemPrimaryUnitId',
|
'itemPrimaryUnitName',
|
'quantityMax',
|
],
|
// 按主单位录入时手工填写、必填;按辅单位录入时为系统换算结果,无需校验
|
rules: (values) =>
|
handlers.isPrimaryUnitEnter(values) ? 'required' : null,
|
componentProps: (values) => {
|
const primary = handlers.isPrimaryUnitEnter(values);
|
return {
|
disabled: !primary,
|
max: values.quantityMax,
|
placeholder: primary
|
? `请按物料主单位${values.itemPrimaryUnitName ? `【${values.itemPrimaryUnitName}】` : ''}填写数量`
|
: '按录入单位与换算比率自动换算',
|
};
|
},
|
},
|
},
|
];
|
}
|