feat(wms):完成 antd、ele 的 check 的迁移
parent
584370358e
commit
4cded0a674
|
|
@ -0,0 +1,483 @@
|
||||||
|
import type { VbenFormSchema } from '#/adapter/form';
|
||||||
|
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||||
|
import type { DescriptionItemSchema } from '#/components/description';
|
||||||
|
import type { NumberRangeValue } from '#/components/number-range-input';
|
||||||
|
|
||||||
|
import { h, markRaw } from 'vue';
|
||||||
|
|
||||||
|
import { DICT_TYPE } from '@vben/constants';
|
||||||
|
import { getDictOptions } from '@vben/hooks';
|
||||||
|
import { formatDate, formatDateTime } from '@vben/utils';
|
||||||
|
|
||||||
|
import { z } from '#/adapter/form';
|
||||||
|
import { getSimpleUserList } from '#/api/system/user';
|
||||||
|
import { DictTag } from '#/components/dict-tag';
|
||||||
|
import {
|
||||||
|
buildNumberRangeSchema,
|
||||||
|
NumberRangeInput,
|
||||||
|
} from '#/components/number-range-input';
|
||||||
|
import { getRangePickerDefaultProps } from '#/utils';
|
||||||
|
import { WmsWarehouseSelect } from '#/views/wms/md/warehouse/components';
|
||||||
|
import {
|
||||||
|
formatPrice,
|
||||||
|
formatQuantity,
|
||||||
|
formatSumPrice,
|
||||||
|
formatSumQuantity,
|
||||||
|
getLossClass,
|
||||||
|
PRICE_PRECISION,
|
||||||
|
QUANTITY_PRECISION,
|
||||||
|
roundPrice,
|
||||||
|
sumPrice,
|
||||||
|
sumQuantity,
|
||||||
|
} from '#/views/wms/utils/format';
|
||||||
|
|
||||||
|
/** 拆分数量/金额区间字段,适配后端 Min/Max 查询参数 */
|
||||||
|
function splitNumberRange(minFieldName: string, maxFieldName: string) {
|
||||||
|
return (
|
||||||
|
value: NumberRangeValue | undefined,
|
||||||
|
setValue: (fieldName: string, value: number | undefined) => void,
|
||||||
|
) => {
|
||||||
|
setValue(minFieldName, value?.[0]);
|
||||||
|
setValue(maxFieldName, value?.[1]);
|
||||||
|
return undefined;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 构建允许负数的区间搜索项,盘库盈亏数量需要支持盘亏 */
|
||||||
|
function buildSignedNumberRangeSchema(
|
||||||
|
label: string,
|
||||||
|
fieldName: string,
|
||||||
|
minFieldName: string,
|
||||||
|
maxFieldName: string,
|
||||||
|
precision: number,
|
||||||
|
): VbenFormSchema {
|
||||||
|
return {
|
||||||
|
component: markRaw(NumberRangeInput),
|
||||||
|
componentProps: {
|
||||||
|
precision,
|
||||||
|
},
|
||||||
|
fieldName,
|
||||||
|
label,
|
||||||
|
valueFormat: splitNumberRange(minFieldName, maxFieldName),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 计算单据盈亏金额 */
|
||||||
|
export function getOrderDifferencePrice(order: {
|
||||||
|
actualPrice?: number;
|
||||||
|
totalPrice?: number;
|
||||||
|
}) {
|
||||||
|
return roundPrice(Number(order.actualPrice || 0) - Number(order.totalPrice || 0));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 计算明细盈亏数量 */
|
||||||
|
export function getDetailDifferenceQuantity(detail: {
|
||||||
|
checkQuantity?: number;
|
||||||
|
quantity?: number;
|
||||||
|
}) {
|
||||||
|
return Number(detail.checkQuantity || 0) - Number(detail.quantity || 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 计算明细实际金额 */
|
||||||
|
export function getDetailActualPrice(detail: {
|
||||||
|
checkQuantity?: number;
|
||||||
|
price?: number;
|
||||||
|
}) {
|
||||||
|
if (
|
||||||
|
detail.checkQuantity === undefined ||
|
||||||
|
detail.checkQuantity === null ||
|
||||||
|
detail.price === undefined ||
|
||||||
|
detail.price === null
|
||||||
|
) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
return roundPrice(Number(detail.checkQuantity) * Number(detail.price));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 计算明细盈亏金额 */
|
||||||
|
export function getDetailDifferencePrice(detail: {
|
||||||
|
checkQuantity?: number;
|
||||||
|
price?: number;
|
||||||
|
quantity?: number;
|
||||||
|
}) {
|
||||||
|
if (detail.price === undefined || detail.price === null) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
return roundPrice(getDetailDifferenceQuantity(detail) * Number(detail.price));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 列表的搜索表单 */
|
||||||
|
export function useGridFormSchema(): VbenFormSchema[] {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
component: 'Input',
|
||||||
|
componentProps: {
|
||||||
|
allowClear: true,
|
||||||
|
placeholder: '请输入盘库单号',
|
||||||
|
},
|
||||||
|
fieldName: 'no',
|
||||||
|
label: '盘库单号',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
component: 'Select',
|
||||||
|
componentProps: {
|
||||||
|
allowClear: true,
|
||||||
|
options: getDictOptions(DICT_TYPE.WMS_ORDER_STATUS, 'number'),
|
||||||
|
placeholder: '请选择单据状态',
|
||||||
|
},
|
||||||
|
fieldName: 'status',
|
||||||
|
label: '单据状态',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
component: markRaw(WmsWarehouseSelect),
|
||||||
|
fieldName: 'warehouseId',
|
||||||
|
label: '仓库',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
component: 'RangePicker',
|
||||||
|
componentProps: {
|
||||||
|
...getRangePickerDefaultProps(),
|
||||||
|
allowClear: true,
|
||||||
|
},
|
||||||
|
fieldName: 'orderTime',
|
||||||
|
label: '单据日期',
|
||||||
|
},
|
||||||
|
buildSignedNumberRangeSchema(
|
||||||
|
'盈亏数量',
|
||||||
|
'totalQuantityRange',
|
||||||
|
'totalQuantityMin',
|
||||||
|
'totalQuantityMax',
|
||||||
|
QUANTITY_PRECISION,
|
||||||
|
),
|
||||||
|
buildNumberRangeSchema(
|
||||||
|
'总金额',
|
||||||
|
'totalPriceRange',
|
||||||
|
'totalPriceMin',
|
||||||
|
'totalPriceMax',
|
||||||
|
PRICE_PRECISION,
|
||||||
|
),
|
||||||
|
buildNumberRangeSchema(
|
||||||
|
'实际金额',
|
||||||
|
'actualPriceRange',
|
||||||
|
'actualPriceMin',
|
||||||
|
'actualPriceMax',
|
||||||
|
PRICE_PRECISION,
|
||||||
|
),
|
||||||
|
{
|
||||||
|
component: 'ApiSelect',
|
||||||
|
componentProps: {
|
||||||
|
allowClear: true,
|
||||||
|
api: getSimpleUserList,
|
||||||
|
labelField: 'nickname',
|
||||||
|
placeholder: '请选择创建用户',
|
||||||
|
showSearch: true,
|
||||||
|
valueField: 'id',
|
||||||
|
},
|
||||||
|
fieldName: 'creator',
|
||||||
|
label: '创建用户',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
component: 'ApiSelect',
|
||||||
|
componentProps: {
|
||||||
|
allowClear: true,
|
||||||
|
api: getSimpleUserList,
|
||||||
|
labelField: 'nickname',
|
||||||
|
placeholder: '请选择更新用户',
|
||||||
|
showSearch: true,
|
||||||
|
valueField: 'id',
|
||||||
|
},
|
||||||
|
fieldName: 'updater',
|
||||||
|
label: '更新用户',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
component: 'RangePicker',
|
||||||
|
componentProps: {
|
||||||
|
...getRangePickerDefaultProps(),
|
||||||
|
allowClear: true,
|
||||||
|
},
|
||||||
|
fieldName: 'createTime',
|
||||||
|
label: '创建时间',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
component: 'RangePicker',
|
||||||
|
componentProps: {
|
||||||
|
...getRangePickerDefaultProps(),
|
||||||
|
allowClear: true,
|
||||||
|
},
|
||||||
|
fieldName: 'updateTime',
|
||||||
|
label: '更新时间',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 列表表格列 */
|
||||||
|
export function useGridColumns(): VxeTableGridOptions['columns'] {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
fixed: 'left',
|
||||||
|
slots: { content: 'expand_content' },
|
||||||
|
type: 'expand',
|
||||||
|
width: 48,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'no',
|
||||||
|
fixed: 'left',
|
||||||
|
slots: { default: 'no' },
|
||||||
|
title: '单号',
|
||||||
|
width: 210,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
align: 'center',
|
||||||
|
cellRender: {
|
||||||
|
name: 'CellDict',
|
||||||
|
props: { type: DICT_TYPE.WMS_ORDER_STATUS },
|
||||||
|
},
|
||||||
|
field: 'status',
|
||||||
|
fixed: 'left',
|
||||||
|
title: '盘库状态',
|
||||||
|
width: 110,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'warehouseName',
|
||||||
|
minWidth: 180,
|
||||||
|
title: '仓库',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'quantityAmount',
|
||||||
|
minWidth: 200,
|
||||||
|
slots: { default: 'quantityAmount' },
|
||||||
|
title: '盈亏/金额(元)',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'operateInfo',
|
||||||
|
minWidth: 280,
|
||||||
|
slots: { default: 'operateInfo' },
|
||||||
|
title: '操作信息',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'remark',
|
||||||
|
minWidth: 160,
|
||||||
|
title: '备注',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'actions',
|
||||||
|
fixed: 'right',
|
||||||
|
slots: { default: 'actions' },
|
||||||
|
title: '操作',
|
||||||
|
width: 220,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 详情的字段 */
|
||||||
|
export function useDetailSchema(): DescriptionItemSchema[] {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
field: 'no',
|
||||||
|
label: '盘库单号',
|
||||||
|
render: (val) => val || '-',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'warehouseName',
|
||||||
|
label: '仓库',
|
||||||
|
render: (val) => val || '-',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'orderTime',
|
||||||
|
label: '单据日期',
|
||||||
|
render: (val) => formatDate(val, 'YYYY-MM-DD') || '-',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'status',
|
||||||
|
label: '单据状态',
|
||||||
|
render: (val) =>
|
||||||
|
val === undefined || val === null
|
||||||
|
? '-'
|
||||||
|
: h(DictTag, {
|
||||||
|
type: DICT_TYPE.WMS_ORDER_STATUS,
|
||||||
|
value: val,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'totalQuantity',
|
||||||
|
label: '盈亏数量',
|
||||||
|
render: (val) =>
|
||||||
|
h('span', { class: getLossClass(val) }, formatQuantity(val) || '-'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'totalPrice',
|
||||||
|
label: '总金额',
|
||||||
|
render: (val) => formatPrice(val) || '-',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'actualPrice',
|
||||||
|
label: '实际金额',
|
||||||
|
render: (val) => formatPrice(val) || '-',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'differencePrice',
|
||||||
|
label: '实际盈亏金额',
|
||||||
|
render: (_val, data) => {
|
||||||
|
const differencePrice = getOrderDifferencePrice(data || {});
|
||||||
|
return h(
|
||||||
|
'span',
|
||||||
|
{ class: getLossClass(differencePrice) },
|
||||||
|
formatPrice(differencePrice) || '-',
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'createTime',
|
||||||
|
label: '创建时间',
|
||||||
|
render: (val) => formatDateTime(val) || '-',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'creatorName',
|
||||||
|
label: '创建人',
|
||||||
|
render: (val, data) => val || data?.creator || '-',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'updateTime',
|
||||||
|
label: '更新时间',
|
||||||
|
render: (val) => formatDateTime(val) || '-',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'updaterName',
|
||||||
|
label: '更新人',
|
||||||
|
render: (val, data) => val || data?.updater || '-',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'remark',
|
||||||
|
label: '备注',
|
||||||
|
render: (val) => val || '-',
|
||||||
|
span: 2,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 表单的配置项 */
|
||||||
|
export function useFormSchema(): VbenFormSchema[] {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
component: 'Input',
|
||||||
|
dependencies: {
|
||||||
|
show: () => false,
|
||||||
|
triggerFields: [''],
|
||||||
|
},
|
||||||
|
fieldName: 'id',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
component: 'Input',
|
||||||
|
componentProps: {
|
||||||
|
maxLength: 64,
|
||||||
|
placeholder: '请输入盘库单号',
|
||||||
|
},
|
||||||
|
fieldName: 'no',
|
||||||
|
label: '盘库单号',
|
||||||
|
rules: z.string().min(1, '盘库单号不能为空').max(64),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
component: markRaw(WmsWarehouseSelect),
|
||||||
|
componentProps: {
|
||||||
|
disabled: true,
|
||||||
|
},
|
||||||
|
fieldName: 'warehouseId',
|
||||||
|
label: '仓库',
|
||||||
|
rules: 'required',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
component: 'DatePicker',
|
||||||
|
componentProps: {
|
||||||
|
class: 'w-full',
|
||||||
|
format: 'YYYY-MM-DD',
|
||||||
|
placeholder: '请选择单据日期',
|
||||||
|
valueFormat: 'x',
|
||||||
|
},
|
||||||
|
fieldName: 'orderTime',
|
||||||
|
label: '单据日期',
|
||||||
|
rules: 'required',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
component: 'Input',
|
||||||
|
componentProps: {
|
||||||
|
disabled: true,
|
||||||
|
},
|
||||||
|
fieldName: 'actualPrice',
|
||||||
|
label: '实际金额',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
component: 'Textarea',
|
||||||
|
componentProps: {
|
||||||
|
maxLength: 255,
|
||||||
|
placeholder: '请输入备注',
|
||||||
|
},
|
||||||
|
fieldName: 'remark',
|
||||||
|
formItemClass: 'col-span-2',
|
||||||
|
label: '备注',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 选择盘库仓库表单的配置项 */
|
||||||
|
export function useWarehouseFormSchema(
|
||||||
|
onWarehouseChange: (warehouse: unknown) => void,
|
||||||
|
): VbenFormSchema[] {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
component: markRaw(WmsWarehouseSelect),
|
||||||
|
componentProps: {
|
||||||
|
onChange: onWarehouseChange,
|
||||||
|
},
|
||||||
|
fieldName: 'warehouseId',
|
||||||
|
label: '仓库',
|
||||||
|
rules: 'required',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CheckOrderDetailFooterRow {
|
||||||
|
actualPrice?: number;
|
||||||
|
checkQuantity?: number;
|
||||||
|
differencePrice?: number;
|
||||||
|
differenceQuantity?: number;
|
||||||
|
quantity?: number;
|
||||||
|
}
|
||||||
|
type CheckOrderDetailFooterColumn = Pick<
|
||||||
|
NonNullable<NonNullable<VxeTableGridOptions['columns']>[number]>,
|
||||||
|
'field'
|
||||||
|
>;
|
||||||
|
|
||||||
|
/** 明细表格的合计行 */
|
||||||
|
export function getCheckDetailFooter({
|
||||||
|
columns,
|
||||||
|
data,
|
||||||
|
}: {
|
||||||
|
columns: CheckOrderDetailFooterColumn[];
|
||||||
|
data: CheckOrderDetailFooterRow[];
|
||||||
|
}) {
|
||||||
|
return [
|
||||||
|
columns.map((column, index) => {
|
||||||
|
if (index === 0) {
|
||||||
|
return '合计';
|
||||||
|
}
|
||||||
|
if (column.field === 'quantity') {
|
||||||
|
return formatSumQuantity(data, (detail) => detail.quantity);
|
||||||
|
}
|
||||||
|
if (column.field === 'checkQuantity') {
|
||||||
|
return formatSumQuantity(data, (detail) => detail.checkQuantity);
|
||||||
|
}
|
||||||
|
if (column.field === 'actualPrice') {
|
||||||
|
return formatSumPrice(data, (detail) => detail.actualPrice);
|
||||||
|
}
|
||||||
|
if (column.field === 'differenceQuantity') {
|
||||||
|
return formatQuantity(
|
||||||
|
sumQuantity(data, (detail) => detail.differenceQuantity),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (column.field === 'differencePrice') {
|
||||||
|
return formatPrice(sumPrice(data, (detail) => detail.differencePrice));
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,377 @@
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||||
|
import type { WmsCheckOrderApi } from '#/api/wms/order/check';
|
||||||
|
import type { WmsCheckOrderDetailApi } from '#/api/wms/order/check/detail';
|
||||||
|
|
||||||
|
import { reactive, ref } from 'vue';
|
||||||
|
|
||||||
|
import { DocAlert, Page, useVbenModal } from '@vben/common-ui';
|
||||||
|
import { downloadFileFromBlobPart, formatDateTime } from '@vben/utils';
|
||||||
|
|
||||||
|
import { message } from 'ant-design-vue';
|
||||||
|
|
||||||
|
import {
|
||||||
|
ACTION_ICON,
|
||||||
|
TableAction,
|
||||||
|
useVbenVxeGrid,
|
||||||
|
VxeColumn,
|
||||||
|
VxeTable,
|
||||||
|
} from '#/adapter/vxe-table';
|
||||||
|
import {
|
||||||
|
deleteCheckOrder,
|
||||||
|
exportCheckOrder,
|
||||||
|
getCheckOrderDetailListByOrderId,
|
||||||
|
getCheckOrderPage,
|
||||||
|
} from '#/api/wms/order/check';
|
||||||
|
import { $t } from '#/locales';
|
||||||
|
import {
|
||||||
|
OrderDeleteStatusList,
|
||||||
|
OrderStatusEnum,
|
||||||
|
OrderUpdateStatusList,
|
||||||
|
} from '#/views/wms/utils/constants';
|
||||||
|
import {
|
||||||
|
formatPrice,
|
||||||
|
formatQuantity,
|
||||||
|
getLossClass,
|
||||||
|
} from '#/views/wms/utils/format';
|
||||||
|
|
||||||
|
import {
|
||||||
|
getDetailActualPrice,
|
||||||
|
getDetailDifferencePrice,
|
||||||
|
getDetailDifferenceQuantity,
|
||||||
|
getOrderDifferencePrice,
|
||||||
|
useGridColumns,
|
||||||
|
useGridFormSchema,
|
||||||
|
} from './data';
|
||||||
|
import CheckOrderDetail from './modules/detail.vue';
|
||||||
|
import CheckOrderForm from './modules/form.vue';
|
||||||
|
import CheckOrderPrint from './modules/print.vue';
|
||||||
|
|
||||||
|
defineOptions({ name: 'WmsCheckOrder' });
|
||||||
|
|
||||||
|
const printRef = ref<InstanceType<typeof CheckOrderPrint>>();
|
||||||
|
const detailMap = reactive<Record<number, WmsCheckOrderDetailApi.CheckOrderDetail[]>>(
|
||||||
|
{},
|
||||||
|
);
|
||||||
|
|
||||||
|
const [FormModal, formModalApi] = useVbenModal({
|
||||||
|
connectedComponent: CheckOrderForm,
|
||||||
|
destroyOnClose: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const [DetailModal, detailModalApi] = useVbenModal({
|
||||||
|
connectedComponent: CheckOrderDetail,
|
||||||
|
destroyOnClose: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
/** 清空展开明细缓存 */
|
||||||
|
function clearDetailMap() {
|
||||||
|
for (const id of Object.keys(detailMap)) {
|
||||||
|
delete detailMap[Number(id)];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 刷新表格 */
|
||||||
|
function handleRefresh() {
|
||||||
|
clearDetailMap();
|
||||||
|
gridApi.query();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 创建盘库单 */
|
||||||
|
function handleCreate() {
|
||||||
|
formModalApi.setData({ type: 'create' }).open();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 编辑盘库单 */
|
||||||
|
function handleEdit(row: WmsCheckOrderApi.CheckOrder) {
|
||||||
|
formModalApi.setData({ id: row.id!, type: 'update' }).open();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 查看盘库单详情 */
|
||||||
|
function handleDetail(row: WmsCheckOrderApi.CheckOrder) {
|
||||||
|
detailModalApi.setData({ id: row.id! }).open();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 获取已展开行的明细 */
|
||||||
|
function getExpandedDetails(row: WmsCheckOrderApi.CheckOrder) {
|
||||||
|
return detailMap[row.id!] || [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 展开列表行时懒加载盘库明细 */
|
||||||
|
async function handleExpandChange(
|
||||||
|
row: WmsCheckOrderApi.CheckOrder,
|
||||||
|
expanded: boolean,
|
||||||
|
) {
|
||||||
|
if (!expanded) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
delete detailMap[row.id!];
|
||||||
|
detailMap[row.id!] = await getCheckOrderDetailListByOrderId(row.id!);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 判断盘库单是否可修改 */
|
||||||
|
function canUpdateCheckOrder(status?: number) {
|
||||||
|
return status !== undefined && OrderUpdateStatusList.includes(status);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 判断盘库单是否可删除 */
|
||||||
|
function canDeleteCheckOrder(status?: number) {
|
||||||
|
return status !== undefined && OrderDeleteStatusList.includes(status);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 获取修改按钮禁用提示 */
|
||||||
|
function getCheckOrderUpdateTip(status?: number) {
|
||||||
|
if (canUpdateCheckOrder(status)) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
if (status === OrderStatusEnum.FINISHED) {
|
||||||
|
return '已盘库,无法修改';
|
||||||
|
}
|
||||||
|
if (status === OrderStatusEnum.CANCELED) {
|
||||||
|
return '已作废,无法修改';
|
||||||
|
}
|
||||||
|
return '当前状态无法修改';
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 获取删除按钮禁用提示 */
|
||||||
|
function getCheckOrderDeleteTip(status?: number) {
|
||||||
|
if (canDeleteCheckOrder(status)) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
if (status === OrderStatusEnum.FINISHED) {
|
||||||
|
return '已盘库,无法删除';
|
||||||
|
}
|
||||||
|
return '当前状态无法删除';
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 删除盘库单 */
|
||||||
|
async function handleDelete(row: WmsCheckOrderApi.CheckOrder) {
|
||||||
|
const hideLoading = message.loading({
|
||||||
|
content: $t('ui.actionMessage.deleting', [row.no]),
|
||||||
|
duration: 0,
|
||||||
|
});
|
||||||
|
try {
|
||||||
|
await deleteCheckOrder(row.id!);
|
||||||
|
message.success($t('ui.actionMessage.deleteSuccess', [row.no]));
|
||||||
|
handleRefresh();
|
||||||
|
} finally {
|
||||||
|
hideLoading();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 导出盘库单 */
|
||||||
|
async function handleExport() {
|
||||||
|
const data = await exportCheckOrder(await gridApi.formApi.getValues());
|
||||||
|
downloadFileFromBlobPart({ fileName: '盘库单.xls', source: data });
|
||||||
|
}
|
||||||
|
|
||||||
|
const [Grid, gridApi] = useVbenVxeGrid({
|
||||||
|
formOptions: {
|
||||||
|
collapsed: true,
|
||||||
|
schema: useGridFormSchema(),
|
||||||
|
},
|
||||||
|
gridOptions: {
|
||||||
|
columns: useGridColumns(),
|
||||||
|
expandConfig: {
|
||||||
|
padding: true,
|
||||||
|
},
|
||||||
|
height: 'auto',
|
||||||
|
keepSource: true,
|
||||||
|
proxyConfig: {
|
||||||
|
ajax: {
|
||||||
|
query: async ({ page }, formValues) => {
|
||||||
|
return await getCheckOrderPage({
|
||||||
|
pageNo: page.currentPage,
|
||||||
|
pageSize: page.pageSize,
|
||||||
|
...formValues,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
rowConfig: {
|
||||||
|
isHover: true,
|
||||||
|
keyField: 'id',
|
||||||
|
},
|
||||||
|
toolbarConfig: {
|
||||||
|
refresh: true,
|
||||||
|
search: true,
|
||||||
|
},
|
||||||
|
} as VxeTableGridOptions<WmsCheckOrderApi.CheckOrder>,
|
||||||
|
gridEvents: {
|
||||||
|
toggleRowExpand: ({
|
||||||
|
expanded,
|
||||||
|
row,
|
||||||
|
}: {
|
||||||
|
expanded: boolean;
|
||||||
|
row: WmsCheckOrderApi.CheckOrder;
|
||||||
|
}) => {
|
||||||
|
handleExpandChange(row, expanded);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Page auto-content-height>
|
||||||
|
<template #doc>
|
||||||
|
<DocAlert title="【单据】盘库" url="https://doc.iocoder.cn/wms/order/check/" />
|
||||||
|
</template>
|
||||||
|
<FormModal @success="handleRefresh" />
|
||||||
|
<DetailModal />
|
||||||
|
<CheckOrderPrint ref="printRef" />
|
||||||
|
|
||||||
|
<Grid table-title="盘库单列表">
|
||||||
|
<template #expand_content="{ row }">
|
||||||
|
<VxeTable
|
||||||
|
:data="getExpandedDetails(row)"
|
||||||
|
border
|
||||||
|
:show-overflow="true"
|
||||||
|
size="small"
|
||||||
|
>
|
||||||
|
<VxeColumn title="商品信息" min-width="220">
|
||||||
|
<template #default="{ row: detail }">
|
||||||
|
<div>{{ detail.itemName || '-' }}</div>
|
||||||
|
<div v-if="detail.itemCode" class="text-xs text-gray-500">
|
||||||
|
商品编号:{{ detail.itemCode }}
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</VxeColumn>
|
||||||
|
<VxeColumn title="规格信息" min-width="220">
|
||||||
|
<template #default="{ row: detail }">
|
||||||
|
<div>{{ detail.skuName || '-' }}</div>
|
||||||
|
<div v-if="detail.skuCode" class="text-xs text-gray-500">
|
||||||
|
规格编号:{{ detail.skuCode }}
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</VxeColumn>
|
||||||
|
<VxeColumn title="账面数量" align="right" width="120">
|
||||||
|
<template #default="{ row: detail }">
|
||||||
|
{{ formatQuantity(detail.quantity) }}
|
||||||
|
</template>
|
||||||
|
</VxeColumn>
|
||||||
|
<VxeColumn title="实盘数量" align="right" width="120">
|
||||||
|
<template #default="{ row: detail }">
|
||||||
|
{{ formatQuantity(detail.checkQuantity) }}
|
||||||
|
</template>
|
||||||
|
</VxeColumn>
|
||||||
|
<VxeColumn title="单价(元)" align="right" width="120">
|
||||||
|
<template #default="{ row: detail }">
|
||||||
|
{{ formatPrice(detail.price) || '-' }}
|
||||||
|
</template>
|
||||||
|
</VxeColumn>
|
||||||
|
<VxeColumn title="实际金额(元)" align="right" width="140">
|
||||||
|
<template #default="{ row: detail }">
|
||||||
|
{{ formatPrice(getDetailActualPrice(detail)) || '-' }}
|
||||||
|
</template>
|
||||||
|
</VxeColumn>
|
||||||
|
<VxeColumn title="盈亏数量" align="right" width="120">
|
||||||
|
<template #default="{ row: detail }">
|
||||||
|
<span :class="getLossClass(getDetailDifferenceQuantity(detail))">
|
||||||
|
{{ formatQuantity(getDetailDifferenceQuantity(detail)) }}
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
</VxeColumn>
|
||||||
|
<VxeColumn title="实际盈亏金额(元)" align="right" width="160">
|
||||||
|
<template #default="{ row: detail }">
|
||||||
|
<span :class="getLossClass(getDetailDifferencePrice(detail))">
|
||||||
|
{{ formatPrice(getDetailDifferencePrice(detail)) || '-' }}
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
</VxeColumn>
|
||||||
|
</VxeTable>
|
||||||
|
</template>
|
||||||
|
<template #toolbar-tools>
|
||||||
|
<TableAction
|
||||||
|
:actions="[
|
||||||
|
{
|
||||||
|
label: $t('ui.actionTitle.create', ['盘库单']),
|
||||||
|
type: 'primary',
|
||||||
|
icon: ACTION_ICON.ADD,
|
||||||
|
auth: ['wms:check-order:create'],
|
||||||
|
onClick: handleCreate,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: $t('ui.actionTitle.export'),
|
||||||
|
type: 'primary',
|
||||||
|
icon: ACTION_ICON.DOWNLOAD,
|
||||||
|
auth: ['wms:check-order:export'],
|
||||||
|
onClick: handleExport,
|
||||||
|
},
|
||||||
|
]"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<template #no="{ row }">
|
||||||
|
<div>
|
||||||
|
单号:
|
||||||
|
<a class="text-primary" @click="handleDetail(row)">{{ row.no }}</a>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template #quantityAmount="{ row }">
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<span>盈亏数:</span>
|
||||||
|
<span :class="getLossClass(row.totalQuantity)">
|
||||||
|
{{ formatQuantity(row.totalQuantity) }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<span>总金额:</span>
|
||||||
|
<span>{{ formatPrice(row.totalPrice) }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<span>实际金额:</span>
|
||||||
|
<span>{{ formatPrice(row.actualPrice) }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<span>盈亏金额:</span>
|
||||||
|
<span :class="getLossClass(getOrderDifferencePrice(row))">
|
||||||
|
{{ formatPrice(getOrderDifferencePrice(row)) }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template #operateInfo="{ row }">
|
||||||
|
<div>
|
||||||
|
创建:{{ formatDateTime(row.createTime) || '-' }} /
|
||||||
|
{{ row.creatorName || row.creator || '-' }}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
更新:{{ formatDateTime(row.updateTime) || '-' }} /
|
||||||
|
{{ row.updaterName || row.updater || '-' }}
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template #actions="{ row }">
|
||||||
|
<TableAction
|
||||||
|
:actions="[
|
||||||
|
{
|
||||||
|
label: $t('common.edit'),
|
||||||
|
type: 'link',
|
||||||
|
icon: ACTION_ICON.EDIT,
|
||||||
|
disabled: !canUpdateCheckOrder(row.status),
|
||||||
|
tooltip: getCheckOrderUpdateTip(row.status),
|
||||||
|
auth: ['wms:check-order:update'],
|
||||||
|
onClick: handleEdit.bind(null, row),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: $t('common.delete'),
|
||||||
|
type: 'link',
|
||||||
|
danger: true,
|
||||||
|
icon: ACTION_ICON.DELETE,
|
||||||
|
disabled: !canDeleteCheckOrder(row.status),
|
||||||
|
tooltip: getCheckOrderDeleteTip(row.status),
|
||||||
|
auth: ['wms:check-order:delete'],
|
||||||
|
popConfirm: {
|
||||||
|
title: $t('ui.actionMessage.deleteConfirm', [row.no]),
|
||||||
|
confirm: handleDelete.bind(null, row),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '打印',
|
||||||
|
type: 'link',
|
||||||
|
auth: ['wms:check-order:query'],
|
||||||
|
onClick: () => printRef?.print(row.id!),
|
||||||
|
},
|
||||||
|
]"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</Grid>
|
||||||
|
</Page>
|
||||||
|
</template>
|
||||||
|
|
@ -0,0 +1,160 @@
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import type { WmsCheckOrderApi } from '#/api/wms/order/check';
|
||||||
|
import type { WmsCheckOrderDetailApi } from '#/api/wms/order/check/detail';
|
||||||
|
|
||||||
|
import { computed, ref } from 'vue';
|
||||||
|
|
||||||
|
import { useVbenModal } from '@vben/common-ui';
|
||||||
|
|
||||||
|
import { VxeColumn, VxeTable } from '#/adapter/vxe-table';
|
||||||
|
import {
|
||||||
|
getCheckOrder,
|
||||||
|
getCheckOrderDetailListByOrderId,
|
||||||
|
} from '#/api/wms/order/check';
|
||||||
|
import { useDescription } from '#/components/description';
|
||||||
|
import {
|
||||||
|
formatPrice,
|
||||||
|
formatQuantity,
|
||||||
|
getLossClass,
|
||||||
|
} from '#/views/wms/utils/format';
|
||||||
|
|
||||||
|
import {
|
||||||
|
getCheckDetailFooter,
|
||||||
|
getDetailActualPrice,
|
||||||
|
getDetailDifferencePrice,
|
||||||
|
getDetailDifferenceQuantity,
|
||||||
|
useDetailSchema,
|
||||||
|
} from '../data';
|
||||||
|
|
||||||
|
interface DetailRow extends WmsCheckOrderDetailApi.CheckOrderDetail {
|
||||||
|
actualPrice?: number;
|
||||||
|
differencePrice?: number;
|
||||||
|
differenceQuantity?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
defineOptions({ name: 'WmsCheckOrderDetail' });
|
||||||
|
|
||||||
|
const detailData = ref<WmsCheckOrderApi.CheckOrder>({});
|
||||||
|
|
||||||
|
/** 详情明细补齐实际金额和盈亏字段,便于表格与 footer 统一渲染 */
|
||||||
|
const detailRows = computed<DetailRow[]>(() =>
|
||||||
|
(detailData.value.details || []).map((detail) => ({
|
||||||
|
...detail,
|
||||||
|
actualPrice: getDetailActualPrice(detail),
|
||||||
|
differencePrice: getDetailDifferencePrice(detail),
|
||||||
|
differenceQuantity: getDetailDifferenceQuantity(detail),
|
||||||
|
})),
|
||||||
|
);
|
||||||
|
|
||||||
|
const [Descriptions] = useDescription({
|
||||||
|
bordered: true,
|
||||||
|
column: 2,
|
||||||
|
schema: useDetailSchema(),
|
||||||
|
useCard: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const [Modal, modalApi] = useVbenModal({
|
||||||
|
async onOpenChange(isOpen: boolean) {
|
||||||
|
if (!isOpen) {
|
||||||
|
detailData.value = {};
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const data = modalApi.getData<{ id?: number }>();
|
||||||
|
if (!data?.id) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
modalApi.lock();
|
||||||
|
try {
|
||||||
|
const order = await getCheckOrder(data.id);
|
||||||
|
const details =
|
||||||
|
order.details || (await getCheckOrderDetailListByOrderId(data.id));
|
||||||
|
detailData.value = { ...order, details };
|
||||||
|
} finally {
|
||||||
|
modalApi.unlock();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Modal
|
||||||
|
title="盘库单详情"
|
||||||
|
class="w-2/3"
|
||||||
|
:show-cancel-button="false"
|
||||||
|
:show-confirm-button="false"
|
||||||
|
>
|
||||||
|
<div class="mx-4 space-y-4">
|
||||||
|
<Descriptions :data="detailData" />
|
||||||
|
<VxeTable
|
||||||
|
:data="detailRows"
|
||||||
|
border
|
||||||
|
empty-text="暂无商品明细"
|
||||||
|
:footer-method="getCheckDetailFooter"
|
||||||
|
:show-overflow="true"
|
||||||
|
show-footer
|
||||||
|
size="small"
|
||||||
|
>
|
||||||
|
<VxeColumn title="商品信息" min-width="200">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<div>{{ row.itemName || '-' }}</div>
|
||||||
|
<div v-if="row.itemCode" class="text-xs text-gray-500">
|
||||||
|
商品编号:{{ row.itemCode }}
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</VxeColumn>
|
||||||
|
<VxeColumn title="规格信息" min-width="200">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<div>{{ row.skuName || '-' }}</div>
|
||||||
|
<div v-if="row.skuCode" class="text-xs text-gray-500">
|
||||||
|
规格编号:{{ row.skuCode }}
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</VxeColumn>
|
||||||
|
<VxeColumn field="quantity" title="账面数量" align="right" width="120">
|
||||||
|
<template #default="{ row }">
|
||||||
|
{{ formatQuantity(row.quantity) || '-' }}
|
||||||
|
</template>
|
||||||
|
</VxeColumn>
|
||||||
|
<VxeColumn title="单价(元)" align="right" width="120">
|
||||||
|
<template #default="{ row }">
|
||||||
|
{{ formatPrice(row.price) || '-' }}
|
||||||
|
</template>
|
||||||
|
</VxeColumn>
|
||||||
|
<VxeColumn field="checkQuantity" title="实盘数量" align="right" width="120">
|
||||||
|
<template #default="{ row }">
|
||||||
|
{{ formatQuantity(row.checkQuantity) || '-' }}
|
||||||
|
</template>
|
||||||
|
</VxeColumn>
|
||||||
|
<VxeColumn field="actualPrice" title="实际金额(元)" align="right" width="140">
|
||||||
|
<template #default="{ row }">
|
||||||
|
{{ formatPrice(row.actualPrice) || '-' }}
|
||||||
|
</template>
|
||||||
|
</VxeColumn>
|
||||||
|
<VxeColumn
|
||||||
|
field="differenceQuantity"
|
||||||
|
title="盈亏数量"
|
||||||
|
align="right"
|
||||||
|
width="120"
|
||||||
|
>
|
||||||
|
<template #default="{ row }">
|
||||||
|
<span :class="getLossClass(row.differenceQuantity)">
|
||||||
|
{{ formatQuantity(row.differenceQuantity) || '-' }}
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
</VxeColumn>
|
||||||
|
<VxeColumn
|
||||||
|
field="differencePrice"
|
||||||
|
title="实际盈亏金额(元)"
|
||||||
|
align="right"
|
||||||
|
width="160"
|
||||||
|
>
|
||||||
|
<template #default="{ row }">
|
||||||
|
<span :class="getLossClass(row.differencePrice)">
|
||||||
|
{{ formatPrice(row.differencePrice) || '-' }}
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
</VxeColumn>
|
||||||
|
</VxeTable>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
</template>
|
||||||
|
|
@ -0,0 +1,733 @@
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import type { VxeTableInstance } from '#/adapter/vxe-table';
|
||||||
|
import type { WmsInventoryApi } from '#/api/wms/inventory';
|
||||||
|
import type { WmsItemSkuApi } from '#/api/wms/md/item/sku';
|
||||||
|
import type { WmsWarehouseApi } from '#/api/wms/md/warehouse';
|
||||||
|
import type { WmsCheckOrderApi } from '#/api/wms/order/check';
|
||||||
|
import type { WmsCheckOrderDetailApi } from '#/api/wms/order/check/detail';
|
||||||
|
|
||||||
|
import { computed, nextTick, ref, watch } from 'vue';
|
||||||
|
|
||||||
|
import { confirm, useVbenModal } from '@vben/common-ui';
|
||||||
|
import { isEqual } from '@vben/utils';
|
||||||
|
|
||||||
|
import { InputNumber, message } from 'ant-design-vue';
|
||||||
|
|
||||||
|
import { useVbenForm } from '#/adapter/form';
|
||||||
|
import { TableAction, VxeColumn, VxeTable } from '#/adapter/vxe-table';
|
||||||
|
import { getInventoryList } from '#/api/wms/inventory';
|
||||||
|
import {
|
||||||
|
cancelCheckOrder,
|
||||||
|
completeCheckOrder,
|
||||||
|
createCheckOrder,
|
||||||
|
getCheckOrder,
|
||||||
|
getCheckOrderDetailListByOrderId,
|
||||||
|
updateCheckOrder,
|
||||||
|
} from '#/api/wms/order/check';
|
||||||
|
import { $t } from '#/locales';
|
||||||
|
import { WmsItemSkuSelect } from '#/views/wms/md/item/sku/components';
|
||||||
|
import {
|
||||||
|
OrderStatusEnum,
|
||||||
|
OrderUpdateStatusList,
|
||||||
|
} from '#/views/wms/utils/constants';
|
||||||
|
import {
|
||||||
|
dividePrice,
|
||||||
|
formatPrice,
|
||||||
|
formatQuantity,
|
||||||
|
getLossClass,
|
||||||
|
multiplyPrice,
|
||||||
|
PRICE_PRECISION,
|
||||||
|
QUANTITY_PRECISION,
|
||||||
|
sumPrice,
|
||||||
|
} from '#/views/wms/utils/format';
|
||||||
|
import { generateOrderNo } from '#/views/wms/utils/order';
|
||||||
|
|
||||||
|
import {
|
||||||
|
getCheckDetailFooter,
|
||||||
|
getDetailDifferencePrice,
|
||||||
|
getDetailDifferenceQuantity,
|
||||||
|
useFormSchema,
|
||||||
|
useWarehouseFormSchema,
|
||||||
|
} from '../data';
|
||||||
|
|
||||||
|
interface CheckInventoryRow extends WmsInventoryApi.Inventory {
|
||||||
|
availableQuantity?: number;
|
||||||
|
price?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface DetailRow extends WmsCheckOrderDetailApi.CheckOrderDetail {
|
||||||
|
actualPrice?: number;
|
||||||
|
differencePrice?: number;
|
||||||
|
differenceQuantity?: number;
|
||||||
|
seq: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
defineOptions({ name: 'WmsCheckOrderForm' });
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
success: [];
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const formData = ref<WmsCheckOrderApi.CheckOrder>({});
|
||||||
|
const formMode = ref('create');
|
||||||
|
const originalSubmitData = ref<WmsCheckOrderApi.CheckOrder>();
|
||||||
|
const details = ref<DetailRow[]>([]);
|
||||||
|
const detailTableRef = ref<VxeTableInstance>();
|
||||||
|
const skuSelectRef = ref<InstanceType<typeof WmsItemSkuSelect>>();
|
||||||
|
const selectingWarehouse = ref(false);
|
||||||
|
const warehouseName = ref<string>();
|
||||||
|
let detailSeq = 0; // 明细行可能还没有后端 id,使用本地序号作为 VXE 行操作的稳定标识
|
||||||
|
|
||||||
|
const getTitle = computed(() => {
|
||||||
|
return formMode.value === 'update'
|
||||||
|
? $t('ui.actionTitle.edit', ['盘库单'])
|
||||||
|
: $t('ui.actionTitle.create', ['盘库单']);
|
||||||
|
});
|
||||||
|
|
||||||
|
const modalTitle = computed(() => {
|
||||||
|
return selectingWarehouse.value ? '选择盘库仓库' : getTitle.value;
|
||||||
|
});
|
||||||
|
const modalClass = computed(() => {
|
||||||
|
return selectingWarehouse.value ? 'w-[420px]' : 'w-3/4';
|
||||||
|
});
|
||||||
|
|
||||||
|
const isPrepareOrder = computed(() => {
|
||||||
|
return (
|
||||||
|
!formData.value?.id ||
|
||||||
|
(formData.value.status !== undefined &&
|
||||||
|
OrderUpdateStatusList.includes(formData.value.status))
|
||||||
|
);
|
||||||
|
});
|
||||||
|
const isSavedPrepareOrder = computed(() => {
|
||||||
|
return (
|
||||||
|
!!formData.value?.id &&
|
||||||
|
formData.value.status !== undefined &&
|
||||||
|
OrderUpdateStatusList.includes(formData.value.status)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
/** 表单顶部实际金额来自明细合计,保持和 vue3 的只读汇总字段一致 */
|
||||||
|
const actualPrice = computed(() =>
|
||||||
|
sumPrice(details.value, (detail) => getDetailActualPrice(detail)),
|
||||||
|
);
|
||||||
|
|
||||||
|
const [Form, formApi] = useVbenForm({
|
||||||
|
commonConfig: {
|
||||||
|
componentProps: {
|
||||||
|
class: 'w-full',
|
||||||
|
},
|
||||||
|
labelWidth: 100,
|
||||||
|
},
|
||||||
|
layout: 'horizontal',
|
||||||
|
schema: useFormSchema(),
|
||||||
|
showDefaultActions: false,
|
||||||
|
wrapperClass: 'grid-cols-3',
|
||||||
|
});
|
||||||
|
|
||||||
|
const [WarehouseForm, warehouseFormApi] = useVbenForm({
|
||||||
|
commonConfig: {
|
||||||
|
componentProps: {
|
||||||
|
class: 'w-full',
|
||||||
|
},
|
||||||
|
labelWidth: 80,
|
||||||
|
},
|
||||||
|
layout: 'horizontal',
|
||||||
|
schema: useWarehouseFormSchema(handleWarehouseSelect),
|
||||||
|
showDefaultActions: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
/** 记录新增盘库单前置选择的仓库名称 */
|
||||||
|
function handleWarehouseSelect(warehouse: unknown) {
|
||||||
|
warehouseName.value = (warehouse as undefined | WmsWarehouseApi.Warehouse)
|
||||||
|
?.name;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 初始化新增盘库单草稿 */
|
||||||
|
async function initCreateForm(warehouse: {
|
||||||
|
warehouseId?: number;
|
||||||
|
warehouseName?: string;
|
||||||
|
}) {
|
||||||
|
formData.value = {
|
||||||
|
details: [],
|
||||||
|
no: generateOrderNo('PK'),
|
||||||
|
status: OrderStatusEnum.PREPARE,
|
||||||
|
warehouseId: warehouse.warehouseId,
|
||||||
|
warehouseName: warehouse.warehouseName,
|
||||||
|
};
|
||||||
|
setDetails([]);
|
||||||
|
await formApi.resetForm();
|
||||||
|
await formApi.setValues(formData.value);
|
||||||
|
await nextTick();
|
||||||
|
syncActualPriceField();
|
||||||
|
originalSubmitData.value = await buildSubmitData();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 确认前置选择仓库,进入主表单 */
|
||||||
|
async function handleWarehouseConfirm() {
|
||||||
|
const { valid } = await warehouseFormApi.validate();
|
||||||
|
if (!valid) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const values = (await warehouseFormApi.getValues()) as {
|
||||||
|
warehouseId?: number;
|
||||||
|
};
|
||||||
|
selectingWarehouse.value = false;
|
||||||
|
await nextTick();
|
||||||
|
await initCreateForm({
|
||||||
|
warehouseId: values.warehouseId,
|
||||||
|
warehouseName: warehouseName.value,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 同步表单顶部只读实际金额 */
|
||||||
|
function syncActualPriceField() {
|
||||||
|
void formApi.setFieldValue(
|
||||||
|
'actualPrice',
|
||||||
|
formatPrice(actualPrice.value) || '0.00',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(actualPrice, syncActualPriceField);
|
||||||
|
|
||||||
|
/** 计算明细实际金额 */
|
||||||
|
function getDetailActualPrice(detail: DetailRow) {
|
||||||
|
return detail.actualPrice ?? multiplyPrice(detail.checkQuantity, detail.price);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 刷新明细行的盈亏数据 */
|
||||||
|
function refreshDetailCalculatedFields(detail: DetailRow) {
|
||||||
|
detail.differenceQuantity = getDetailDifferenceQuantity(detail);
|
||||||
|
detail.differencePrice = getDetailDifferencePrice(detail);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 标准化明细行,补齐本地序号和计算字段 */
|
||||||
|
function normalizeDetail(
|
||||||
|
detail: WmsCheckOrderDetailApi.CheckOrderDetail & { actualPrice?: number },
|
||||||
|
): DetailRow {
|
||||||
|
detailSeq += 1;
|
||||||
|
const row: DetailRow = {
|
||||||
|
...detail,
|
||||||
|
actualPrice:
|
||||||
|
detail.actualPrice ?? multiplyPrice(detail.checkQuantity, detail.price),
|
||||||
|
seq: detailSeq,
|
||||||
|
};
|
||||||
|
refreshDetailCalculatedFields(row);
|
||||||
|
return row;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 根据库存构建盘库明细 */
|
||||||
|
function buildDetail(inventory: CheckInventoryRow): DetailRow {
|
||||||
|
return normalizeDetail({
|
||||||
|
actualPrice: multiplyPrice(inventory.availableQuantity, inventory.price),
|
||||||
|
availableQuantity: inventory.availableQuantity,
|
||||||
|
checkQuantity: inventory.availableQuantity,
|
||||||
|
id: undefined,
|
||||||
|
inventoryId: inventory.id,
|
||||||
|
itemCode: inventory.itemCode,
|
||||||
|
itemId: inventory.itemId,
|
||||||
|
itemName: inventory.itemName,
|
||||||
|
price: inventory.price,
|
||||||
|
quantity: inventory.availableQuantity,
|
||||||
|
skuCode: inventory.skuCode,
|
||||||
|
skuId: inventory.skuId,
|
||||||
|
skuName: inventory.skuName,
|
||||||
|
unit: inventory.unit,
|
||||||
|
warehouseId: inventory.warehouseId,
|
||||||
|
warehouseName: inventory.warehouseName,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 构建零库存盘库明细,用于盘点仓库内暂无库存的商品 */
|
||||||
|
function buildZeroInventoryDetail(sku: WmsItemSkuApi.ItemSku): DetailRow {
|
||||||
|
return normalizeDetail({
|
||||||
|
actualPrice: 0,
|
||||||
|
availableQuantity: 0,
|
||||||
|
checkQuantity: 0,
|
||||||
|
id: undefined,
|
||||||
|
inventoryId: undefined,
|
||||||
|
itemCode: sku.itemCode,
|
||||||
|
itemId: sku.itemId,
|
||||||
|
itemName: sku.itemName,
|
||||||
|
price: sku.costPrice,
|
||||||
|
quantity: 0,
|
||||||
|
skuCode: sku.code,
|
||||||
|
skuId: sku.id,
|
||||||
|
skuName: sku.name,
|
||||||
|
unit: sku.unit,
|
||||||
|
warehouseId: formData.value.warehouseId,
|
||||||
|
warehouseName: formData.value.warehouseName,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 设置盘库明细 */
|
||||||
|
function setDetails(list?: WmsCheckOrderDetailApi.CheckOrderDetail[]) {
|
||||||
|
detailSeq = 0;
|
||||||
|
details.value = (list || []).map((detail) => normalizeDetail(detail));
|
||||||
|
void refreshDetailFooter();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 刷新明细合计行 */
|
||||||
|
async function refreshDetailFooter() {
|
||||||
|
await nextTick();
|
||||||
|
await detailTableRef.value?.updateFooter();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 导入当前仓库的全部库存余额 */
|
||||||
|
async function handleImportAllInventory() {
|
||||||
|
if (!formData.value.warehouseId) {
|
||||||
|
message.warning('请先选择仓库');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (details.value.length > 0) {
|
||||||
|
await confirm('导入仓库库存会覆盖当前盘库明细,是否继续?');
|
||||||
|
}
|
||||||
|
modalApi.lock();
|
||||||
|
try {
|
||||||
|
const inventories = await loadWarehouseInventoryList();
|
||||||
|
details.value = inventories.map((inventory) =>
|
||||||
|
buildDetail({ ...inventory, availableQuantity: inventory.quantity }),
|
||||||
|
);
|
||||||
|
await refreshDetailFooter();
|
||||||
|
} finally {
|
||||||
|
modalApi.unlock();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 打开盘点商品添加弹窗,已导入/已添加 SKU 在选择器内禁选 */
|
||||||
|
function handleAddSkuInventory() {
|
||||||
|
if (!formData.value.warehouseId) {
|
||||||
|
message.warning('请先选择仓库');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
skuSelectRef.value?.open(getSelectedSkuIds(), {
|
||||||
|
multiple: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 选择商品 SKU */
|
||||||
|
async function handleSelectSku(skus: WmsItemSkuApi.ItemSku[]) {
|
||||||
|
if (skus.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
modalApi.lock();
|
||||||
|
try {
|
||||||
|
const warehouseInventoryMap = await getWarehouseInventoryMap();
|
||||||
|
const selectedSkuIds = new Set(getSelectedSkuIds());
|
||||||
|
let changed = false;
|
||||||
|
for (const sku of skus) {
|
||||||
|
if (!sku.id || selectedSkuIds.has(sku.id)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const inventory = warehouseInventoryMap.get(sku.id);
|
||||||
|
details.value.push(
|
||||||
|
inventory
|
||||||
|
? buildDetail({ ...inventory, availableQuantity: inventory.quantity })
|
||||||
|
: buildZeroInventoryDetail(sku),
|
||||||
|
);
|
||||||
|
selectedSkuIds.add(sku.id);
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
if (changed) {
|
||||||
|
await refreshDetailFooter();
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
modalApi.unlock();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 获得已导入/已添加 SKU 编号,避免重复盘点同一规格 */
|
||||||
|
function getSelectedSkuIds() {
|
||||||
|
return details.value
|
||||||
|
.map((detail) => detail.skuId)
|
||||||
|
.filter((id): id is number => id !== undefined);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 查询当前仓库全部库存余额 */
|
||||||
|
async function loadWarehouseInventoryList(): Promise<CheckInventoryRow[]> {
|
||||||
|
return (await getInventoryList({
|
||||||
|
warehouseId: formData.value.warehouseId!,
|
||||||
|
})) as CheckInventoryRow[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 获得当前仓库 SKU 对应库存余额,用于添加单个 SKU 时带入账面库存 */
|
||||||
|
async function getWarehouseInventoryMap(): Promise<Map<number, CheckInventoryRow>> {
|
||||||
|
const inventories = await loadWarehouseInventoryList();
|
||||||
|
return new Map(
|
||||||
|
inventories
|
||||||
|
.filter((inventory) => !!inventory.skuId)
|
||||||
|
.map((inventory) => [inventory.skuId!, inventory] as const),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 删除商品明细 */
|
||||||
|
function handleDeleteDetail(row: DetailRow) {
|
||||||
|
const index = details.value.findIndex((detail) => detail.seq === row.seq);
|
||||||
|
if (index !== -1) {
|
||||||
|
details.value.splice(index, 1);
|
||||||
|
void refreshDetailFooter();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 明细实盘数量变化 */
|
||||||
|
function handleDetailCheckQuantityChange(detail: DetailRow) {
|
||||||
|
if (detail.price !== undefined && detail.price !== null) {
|
||||||
|
detail.actualPrice = multiplyPrice(detail.checkQuantity, detail.price);
|
||||||
|
} else {
|
||||||
|
detail.price = dividePrice(detail.actualPrice, detail.checkQuantity);
|
||||||
|
}
|
||||||
|
refreshDetailCalculatedFields(detail);
|
||||||
|
void refreshDetailFooter();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 明细单价变化 */
|
||||||
|
function handleDetailPriceChange(detail: DetailRow) {
|
||||||
|
detail.actualPrice = multiplyPrice(detail.checkQuantity, detail.price);
|
||||||
|
refreshDetailCalculatedFields(detail);
|
||||||
|
void refreshDetailFooter();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 明细实际金额变化 */
|
||||||
|
function handleDetailActualPriceChange(detail: DetailRow) {
|
||||||
|
detail.price = dividePrice(detail.actualPrice, detail.checkQuantity);
|
||||||
|
refreshDetailCalculatedFields(detail);
|
||||||
|
void refreshDetailFooter();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 校验商品明细 */
|
||||||
|
function validateDetails(required = false) {
|
||||||
|
if (details.value.length === 0) {
|
||||||
|
if (required) {
|
||||||
|
message.error('至少包含一条盘库明细');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
for (let index = 0; index < details.value.length; index += 1) {
|
||||||
|
const detail = details.value[index]!;
|
||||||
|
if (
|
||||||
|
detail.checkQuantity === undefined ||
|
||||||
|
detail.checkQuantity === null ||
|
||||||
|
detail.checkQuantity < 0
|
||||||
|
) {
|
||||||
|
message.error(`第 ${index + 1} 行明细实盘数量不能小于 0`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 构建提交用的明细数据 */
|
||||||
|
function buildSubmitDetails() {
|
||||||
|
return details.value.map((row) => {
|
||||||
|
const {
|
||||||
|
actualPrice: _actualPrice,
|
||||||
|
availableQuantity: _availableQuantity,
|
||||||
|
differencePrice: _differencePrice,
|
||||||
|
differenceQuantity: _differenceQuantity,
|
||||||
|
seq: _seq,
|
||||||
|
...detail
|
||||||
|
} = row;
|
||||||
|
return detail;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 构建提交用的单据数据 */
|
||||||
|
async function buildSubmitData(): Promise<WmsCheckOrderApi.CheckOrder> {
|
||||||
|
const values = (await formApi.getValues()) as WmsCheckOrderApi.CheckOrder;
|
||||||
|
const {
|
||||||
|
actualPrice: _formActualPrice,
|
||||||
|
details: _formDetails,
|
||||||
|
totalPrice: _formTotalPrice,
|
||||||
|
totalQuantity: _formTotalQuantity,
|
||||||
|
...submitValues
|
||||||
|
} = values;
|
||||||
|
const {
|
||||||
|
actualPrice: _actualPrice,
|
||||||
|
details: _details,
|
||||||
|
totalPrice: _totalPrice,
|
||||||
|
totalQuantity: _totalQuantity,
|
||||||
|
...order
|
||||||
|
} = formData.value;
|
||||||
|
return {
|
||||||
|
...order,
|
||||||
|
...submitValues,
|
||||||
|
details: buildSubmitDetails(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 完成盘库 */
|
||||||
|
async function handleFormComplete() {
|
||||||
|
const { valid } = await formApi.validate();
|
||||||
|
if (!valid || !validateDetails(true) || !formData.value?.id) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await confirm('确认完成盘库?完成后将更新库存。');
|
||||||
|
modalApi.lock();
|
||||||
|
try {
|
||||||
|
const data = await buildSubmitData();
|
||||||
|
if (!isEqual(data, originalSubmitData.value)) {
|
||||||
|
await updateCheckOrder(data);
|
||||||
|
}
|
||||||
|
await completeCheckOrder(formData.value.id);
|
||||||
|
await modalApi.close();
|
||||||
|
emit('success');
|
||||||
|
message.success('盘库成功');
|
||||||
|
} finally {
|
||||||
|
modalApi.unlock();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 作废盘库单 */
|
||||||
|
async function handleFormCancel() {
|
||||||
|
if (!formData.value?.id) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await confirm('确认作废该盘库单?作废后不可恢复。');
|
||||||
|
modalApi.lock();
|
||||||
|
try {
|
||||||
|
await cancelCheckOrder(formData.value.id);
|
||||||
|
await modalApi.close();
|
||||||
|
emit('success');
|
||||||
|
message.success('作废成功');
|
||||||
|
} finally {
|
||||||
|
modalApi.unlock();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const [Modal, modalApi] = useVbenModal({
|
||||||
|
async onConfirm() {
|
||||||
|
if (selectingWarehouse.value) {
|
||||||
|
await handleWarehouseConfirm();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const { valid } = await formApi.validate();
|
||||||
|
if (!valid || !validateDetails(false) || !isPrepareOrder.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
modalApi.lock();
|
||||||
|
// 提交表单
|
||||||
|
const data = await buildSubmitData();
|
||||||
|
try {
|
||||||
|
await (formMode.value === 'update'
|
||||||
|
? updateCheckOrder(data)
|
||||||
|
: createCheckOrder(data));
|
||||||
|
// 关闭并提示
|
||||||
|
await modalApi.close();
|
||||||
|
emit('success');
|
||||||
|
message.success($t('ui.actionMessage.operationSuccess'));
|
||||||
|
} finally {
|
||||||
|
modalApi.unlock();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async onOpenChange(isOpen: boolean) {
|
||||||
|
if (!isOpen) {
|
||||||
|
formData.value = {};
|
||||||
|
originalSubmitData.value = undefined;
|
||||||
|
selectingWarehouse.value = false;
|
||||||
|
warehouseName.value = undefined;
|
||||||
|
setDetails([]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await formApi.resetForm();
|
||||||
|
const data = modalApi.getData<{
|
||||||
|
id?: number;
|
||||||
|
type?: string;
|
||||||
|
}>();
|
||||||
|
formMode.value = data?.type || (data?.id ? 'update' : 'create');
|
||||||
|
if (data?.id) {
|
||||||
|
modalApi.lock();
|
||||||
|
try {
|
||||||
|
// 加载数据
|
||||||
|
const order = await getCheckOrder(data.id);
|
||||||
|
const orderDetails =
|
||||||
|
order.details || (await getCheckOrderDetailListByOrderId(data.id));
|
||||||
|
formData.value = { ...order, details: orderDetails };
|
||||||
|
setDetails(orderDetails);
|
||||||
|
// 设置到 values
|
||||||
|
await formApi.setValues(formData.value);
|
||||||
|
await nextTick();
|
||||||
|
syncActualPriceField();
|
||||||
|
originalSubmitData.value = await buildSubmitData();
|
||||||
|
} finally {
|
||||||
|
modalApi.unlock();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 新增时先选择盘库仓库,再进入主表单
|
||||||
|
selectingWarehouse.value = true;
|
||||||
|
warehouseName.value = undefined;
|
||||||
|
formData.value = {};
|
||||||
|
setDetails([]);
|
||||||
|
await warehouseFormApi.resetForm();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Modal
|
||||||
|
:title="modalTitle"
|
||||||
|
:class="modalClass"
|
||||||
|
:show-confirm-button="selectingWarehouse || isPrepareOrder"
|
||||||
|
>
|
||||||
|
<template v-if="selectingWarehouse" #confirmText>开始盘库</template>
|
||||||
|
<WarehouseForm v-if="selectingWarehouse" class="mx-4" />
|
||||||
|
<div v-else class="mx-4">
|
||||||
|
<Form />
|
||||||
|
<div class="mt-4">
|
||||||
|
<div class="mb-3 flex items-center justify-between">
|
||||||
|
<span class="text-sm font-semibold">盘库明细</span>
|
||||||
|
<TableAction
|
||||||
|
:actions="[
|
||||||
|
{
|
||||||
|
label: '导入仓库库存',
|
||||||
|
disabled: !formData.warehouseId,
|
||||||
|
onClick: handleImportAllInventory,
|
||||||
|
type: 'primary',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '添加盘点商品',
|
||||||
|
disabled: !formData.warehouseId,
|
||||||
|
onClick: handleAddSkuInventory,
|
||||||
|
type: 'primary',
|
||||||
|
},
|
||||||
|
]"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<VxeTable
|
||||||
|
ref="detailTableRef"
|
||||||
|
:data="details"
|
||||||
|
border
|
||||||
|
empty-text="暂无商品明细"
|
||||||
|
:footer-method="getCheckDetailFooter"
|
||||||
|
:show-overflow="true"
|
||||||
|
show-footer
|
||||||
|
size="small"
|
||||||
|
>
|
||||||
|
<VxeColumn title="商品信息" min-width="210">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<div>{{ row.itemName || '-' }}</div>
|
||||||
|
<div v-if="row.itemCode" class="text-xs text-gray-500">
|
||||||
|
商品编号:{{ row.itemCode }}
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</VxeColumn>
|
||||||
|
<VxeColumn title="规格信息" min-width="210">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<div>{{ row.skuName || '-' }}</div>
|
||||||
|
<div v-if="row.skuCode" class="text-xs text-gray-500">
|
||||||
|
规格编号:{{ row.skuCode }}
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</VxeColumn>
|
||||||
|
<VxeColumn field="quantity" title="账面库存" align="right" width="120">
|
||||||
|
<template #default="{ row }">
|
||||||
|
{{ formatQuantity(row.quantity) || '-' }}
|
||||||
|
</template>
|
||||||
|
</VxeColumn>
|
||||||
|
<VxeColumn field="price" title="单价(元)" width="150">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<InputNumber
|
||||||
|
v-model:value="row.price"
|
||||||
|
:controls="false"
|
||||||
|
:min="0"
|
||||||
|
:precision="PRICE_PRECISION"
|
||||||
|
class="!w-full"
|
||||||
|
placeholder="单价"
|
||||||
|
@change="handleDetailPriceChange(row)"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</VxeColumn>
|
||||||
|
<VxeColumn field="checkQuantity" title="实际库存" width="150">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<InputNumber
|
||||||
|
v-model:value="row.checkQuantity"
|
||||||
|
:controls="false"
|
||||||
|
:min="0"
|
||||||
|
:precision="QUANTITY_PRECISION"
|
||||||
|
class="!w-full"
|
||||||
|
placeholder="数量"
|
||||||
|
@change="handleDetailCheckQuantityChange(row)"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</VxeColumn>
|
||||||
|
<VxeColumn field="actualPrice" title="实际金额(元)" width="150">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<InputNumber
|
||||||
|
v-model:value="row.actualPrice"
|
||||||
|
:controls="false"
|
||||||
|
:min="0"
|
||||||
|
:precision="PRICE_PRECISION"
|
||||||
|
class="!w-full"
|
||||||
|
placeholder="金额"
|
||||||
|
@change="handleDetailActualPriceChange(row)"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</VxeColumn>
|
||||||
|
<VxeColumn
|
||||||
|
field="differenceQuantity"
|
||||||
|
title="盈亏数"
|
||||||
|
align="right"
|
||||||
|
width="120"
|
||||||
|
>
|
||||||
|
<template #default="{ row }">
|
||||||
|
<span :class="getLossClass(row.differenceQuantity)">
|
||||||
|
{{ formatQuantity(row.differenceQuantity) }}
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
</VxeColumn>
|
||||||
|
<VxeColumn
|
||||||
|
field="differencePrice"
|
||||||
|
title="实际盈亏金额(元)"
|
||||||
|
align="right"
|
||||||
|
width="160"
|
||||||
|
>
|
||||||
|
<template #default="{ row }">
|
||||||
|
<span :class="getLossClass(row.differencePrice)">
|
||||||
|
{{ formatPrice(row.differencePrice) || '-' }}
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
</VxeColumn>
|
||||||
|
<VxeColumn title="操作" align="center" fixed="right" width="90">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<TableAction
|
||||||
|
:actions="[
|
||||||
|
{
|
||||||
|
label: '删除',
|
||||||
|
type: 'link',
|
||||||
|
danger: true,
|
||||||
|
onClick: handleDeleteDetail.bind(null, row),
|
||||||
|
},
|
||||||
|
]"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</VxeColumn>
|
||||||
|
</VxeTable>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<template #prepend-footer>
|
||||||
|
<div
|
||||||
|
v-if="!selectingWarehouse && isSavedPrepareOrder"
|
||||||
|
class="flex flex-auto items-center gap-2"
|
||||||
|
>
|
||||||
|
<TableAction
|
||||||
|
:actions="[
|
||||||
|
{
|
||||||
|
label: '完成盘库',
|
||||||
|
type: 'primary',
|
||||||
|
auth: ['wms:check-order:complete'],
|
||||||
|
onClick: handleFormComplete,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '作废',
|
||||||
|
type: 'primary',
|
||||||
|
danger: true,
|
||||||
|
auth: ['wms:check-order:cancel'],
|
||||||
|
onClick: handleFormCancel,
|
||||||
|
},
|
||||||
|
]"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<WmsItemSkuSelect ref="skuSelectRef" @change="handleSelectSku" />
|
||||||
|
</Modal>
|
||||||
|
</template>
|
||||||
|
|
@ -0,0 +1,296 @@
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import type { WmsCheckOrderApi } from '#/api/wms/order/check';
|
||||||
|
import type { WmsCheckOrderDetailApi } from '#/api/wms/order/check/detail';
|
||||||
|
|
||||||
|
import { computed, nextTick, ref } from 'vue';
|
||||||
|
|
||||||
|
import { Barcode, BarcodeFormatEnum } from '@vben/common-ui';
|
||||||
|
import { DICT_TYPE } from '@vben/constants';
|
||||||
|
import { getDictLabel } from '@vben/hooks';
|
||||||
|
import { formatDate, formatDateTime } from '@vben/utils';
|
||||||
|
|
||||||
|
import {
|
||||||
|
getCheckOrder,
|
||||||
|
getCheckOrderDetailListByOrderId,
|
||||||
|
} from '#/api/wms/order/check';
|
||||||
|
import {
|
||||||
|
formatPrice,
|
||||||
|
formatQuantity,
|
||||||
|
formatSumPrice,
|
||||||
|
formatSumQuantity,
|
||||||
|
getLossClass,
|
||||||
|
sumPrice,
|
||||||
|
sumQuantity,
|
||||||
|
} from '#/views/wms/utils/format';
|
||||||
|
|
||||||
|
import {
|
||||||
|
getDetailActualPrice,
|
||||||
|
getDetailDifferencePrice,
|
||||||
|
getDetailDifferenceQuantity,
|
||||||
|
getOrderDifferencePrice,
|
||||||
|
} from '../data';
|
||||||
|
|
||||||
|
interface PrintRow extends WmsCheckOrderDetailApi.CheckOrderDetail {
|
||||||
|
actualPrice?: number;
|
||||||
|
differencePrice?: number;
|
||||||
|
differenceQuantity?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
defineOptions({ name: 'WmsCheckOrderPrint' });
|
||||||
|
|
||||||
|
const printData = ref<WmsCheckOrderApi.CheckOrder>({});
|
||||||
|
const tableColumnCount = 8;
|
||||||
|
|
||||||
|
/** 打印明细补齐实际金额和盈亏字段,避免模板重复计算 */
|
||||||
|
const printRows = computed<PrintRow[]>(() =>
|
||||||
|
(printData.value.details || []).map((detail) => ({
|
||||||
|
...detail,
|
||||||
|
actualPrice: getDetailActualPrice(detail),
|
||||||
|
differencePrice: getDetailDifferencePrice(detail),
|
||||||
|
differenceQuantity: getDetailDifferenceQuantity(detail),
|
||||||
|
})),
|
||||||
|
);
|
||||||
|
|
||||||
|
/** 打印合计盈亏数量 */
|
||||||
|
const totalDifferenceQuantity = computed(() =>
|
||||||
|
sumQuantity(printRows.value, (detail) => detail.differenceQuantity),
|
||||||
|
);
|
||||||
|
/** 打印合计盈亏金额 */
|
||||||
|
const totalDifferencePrice = computed(() =>
|
||||||
|
sumPrice(printRows.value, (detail) => detail.differencePrice),
|
||||||
|
);
|
||||||
|
|
||||||
|
/** 等待条码和打印 DOM 完成绘制,避免浏览器打印到旧内容 */
|
||||||
|
function waitForPaint() {
|
||||||
|
return new Promise<void>((resolve) => {
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
requestAnimationFrame(() => resolve());
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 退出打印模式,恢复当前页面显示 */
|
||||||
|
function removePrintMode() {
|
||||||
|
document.body.classList.remove('wms-check-order-printing');
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 获取打印用字典文案,空值统一显示为横杠 */
|
||||||
|
function getPrintDictLabel(dictType: string, value?: number) {
|
||||||
|
if (value === undefined || value === null) {
|
||||||
|
return '-';
|
||||||
|
}
|
||||||
|
return getDictLabel(dictType, value) || '-';
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 打印盘库单:加载数据后只展示打印区域,再调用浏览器打印 */
|
||||||
|
async function print(id: number) {
|
||||||
|
const order = await getCheckOrder(id);
|
||||||
|
const details = order.details || (await getCheckOrderDetailListByOrderId(id));
|
||||||
|
printData.value = { ...order, details };
|
||||||
|
await nextTick();
|
||||||
|
await waitForPaint();
|
||||||
|
document.body.classList.add('wms-check-order-printing');
|
||||||
|
window.addEventListener('afterprint', removePrintMode, { once: true });
|
||||||
|
window.print();
|
||||||
|
}
|
||||||
|
|
||||||
|
defineExpose({ print });
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Teleport to="body">
|
||||||
|
<div
|
||||||
|
id="wmsCheckOrderPrint"
|
||||||
|
class="wms-check-order-print pointer-events-none fixed left-0 top-0 z-[-1] w-full bg-white text-[#303133] opacity-0"
|
||||||
|
>
|
||||||
|
<div class="relative mb-2">
|
||||||
|
<h2 class="m-0 text-center text-[1.5em] font-bold leading-[1.2]">
|
||||||
|
盘库单
|
||||||
|
</h2>
|
||||||
|
<div v-if="printData.no" class="absolute right-0 top-0">
|
||||||
|
<Barcode
|
||||||
|
:content="printData.no"
|
||||||
|
:display-value="false"
|
||||||
|
:format="BarcodeFormatEnum.CODE39"
|
||||||
|
:height="40"
|
||||||
|
:width="180"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3 grid grid-cols-3 gap-x-6 gap-y-2 text-sm leading-[1.5]">
|
||||||
|
<div>盘库单号:{{ printData.no || '-' }}</div>
|
||||||
|
<div>仓库:{{ printData.warehouseName || '-' }}</div>
|
||||||
|
<div>
|
||||||
|
盘库状态:{{ getPrintDictLabel(DICT_TYPE.WMS_ORDER_STATUS, printData.status) }}
|
||||||
|
</div>
|
||||||
|
<div>单据日期:{{ formatDate(printData.orderTime, 'YYYY-MM-DD') || '-' }}</div>
|
||||||
|
<div>
|
||||||
|
盈亏数量:
|
||||||
|
<span :class="getLossClass(printData.totalQuantity)">
|
||||||
|
{{ formatQuantity(printData.totalQuantity) || '-' }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div>总金额:{{ formatPrice(printData.totalPrice) || '-' }}</div>
|
||||||
|
<div>实际金额:{{ formatPrice(printData.actualPrice) || '-' }}</div>
|
||||||
|
<div>
|
||||||
|
实际盈亏金额:
|
||||||
|
<span :class="getLossClass(getOrderDifferencePrice(printData))">
|
||||||
|
{{ formatPrice(getOrderDifferencePrice(printData)) || '-' }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="col-span-3 grid grid-cols-2 gap-x-6">
|
||||||
|
<div>
|
||||||
|
创建:{{ formatDateTime(printData.createTime) || '-' }} /
|
||||||
|
{{ printData.creatorName || printData.creator || '-' }}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
更新:{{ formatDateTime(printData.updateTime) || '-' }} /
|
||||||
|
{{ printData.updaterName || printData.updater || '-' }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-span-3">备注:{{ printData.remark || '-' }}</div>
|
||||||
|
</div>
|
||||||
|
<table class="w-full border-collapse text-[13px] leading-[1.5]">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th class="border border-solid border-[#dcdfe6] bg-[#f5f7fa] p-2 text-left font-bold">
|
||||||
|
商品信息
|
||||||
|
</th>
|
||||||
|
<th class="border border-solid border-[#dcdfe6] bg-[#f5f7fa] p-2 text-left font-bold">
|
||||||
|
规格信息
|
||||||
|
</th>
|
||||||
|
<th class="border border-solid border-[#dcdfe6] bg-[#f5f7fa] p-2 text-left font-bold">
|
||||||
|
账面库存
|
||||||
|
</th>
|
||||||
|
<th class="border border-solid border-[#dcdfe6] bg-[#f5f7fa] p-2 text-left font-bold">
|
||||||
|
单价(元)
|
||||||
|
</th>
|
||||||
|
<th class="border border-solid border-[#dcdfe6] bg-[#f5f7fa] p-2 text-left font-bold">
|
||||||
|
实际库存
|
||||||
|
</th>
|
||||||
|
<th class="border border-solid border-[#dcdfe6] bg-[#f5f7fa] p-2 text-left font-bold">
|
||||||
|
实际金额(元)
|
||||||
|
</th>
|
||||||
|
<th class="border border-solid border-[#dcdfe6] bg-[#f5f7fa] p-2 text-left font-bold">
|
||||||
|
盈亏数
|
||||||
|
</th>
|
||||||
|
<th class="border border-solid border-[#dcdfe6] bg-[#f5f7fa] p-2 text-left font-bold">
|
||||||
|
实际盈亏金额(元)
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr v-for="detail in printRows" :key="detail.id || detail.skuId">
|
||||||
|
<td class="border border-solid border-[#dcdfe6] p-2">
|
||||||
|
<div>{{ detail.itemName || '-' }}</div>
|
||||||
|
<div v-if="detail.itemCode" class="text-xs">
|
||||||
|
编号:{{ detail.itemCode }}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td class="border border-solid border-[#dcdfe6] p-2">
|
||||||
|
<div>{{ detail.skuName || '-' }}</div>
|
||||||
|
<div v-if="detail.skuCode" class="text-xs">
|
||||||
|
编号:{{ detail.skuCode }}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td class="border border-solid border-[#dcdfe6] p-2 text-right">
|
||||||
|
{{ formatQuantity(detail.quantity) || '-' }}
|
||||||
|
</td>
|
||||||
|
<td class="border border-solid border-[#dcdfe6] p-2 text-right">
|
||||||
|
{{ formatPrice(detail.price) || '-' }}
|
||||||
|
</td>
|
||||||
|
<td class="border border-solid border-[#dcdfe6] p-2 text-right">
|
||||||
|
{{ formatQuantity(detail.checkQuantity) || '-' }}
|
||||||
|
</td>
|
||||||
|
<td class="border border-solid border-[#dcdfe6] p-2 text-right">
|
||||||
|
{{ formatPrice(detail.actualPrice) || '-' }}
|
||||||
|
</td>
|
||||||
|
<td class="border border-solid border-[#dcdfe6] p-2 text-right">
|
||||||
|
<span :class="getLossClass(detail.differenceQuantity)">
|
||||||
|
{{ formatQuantity(detail.differenceQuantity) || '-' }}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td class="border border-solid border-[#dcdfe6] p-2 text-right">
|
||||||
|
<span :class="getLossClass(detail.differencePrice)">
|
||||||
|
{{ formatPrice(detail.differencePrice) || '-' }}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr v-if="printRows.length > 0">
|
||||||
|
<td
|
||||||
|
class="border border-solid border-[#dcdfe6] bg-[#f5f7fa] p-2"
|
||||||
|
colspan="2"
|
||||||
|
>
|
||||||
|
合计
|
||||||
|
</td>
|
||||||
|
<td class="border border-solid border-[#dcdfe6] bg-[#f5f7fa] p-2 text-right">
|
||||||
|
{{ formatSumQuantity(printRows, (detail) => detail.quantity) }}
|
||||||
|
</td>
|
||||||
|
<td class="border border-solid border-[#dcdfe6] bg-[#f5f7fa] p-2 text-right"></td>
|
||||||
|
<td class="border border-solid border-[#dcdfe6] bg-[#f5f7fa] p-2 text-right">
|
||||||
|
{{ formatSumQuantity(printRows, (detail) => detail.checkQuantity) }}
|
||||||
|
</td>
|
||||||
|
<td class="border border-solid border-[#dcdfe6] bg-[#f5f7fa] p-2 text-right">
|
||||||
|
{{ formatSumPrice(printRows, (detail) => detail.actualPrice) }}
|
||||||
|
</td>
|
||||||
|
<td class="border border-solid border-[#dcdfe6] bg-[#f5f7fa] p-2 text-right">
|
||||||
|
<span :class="getLossClass(totalDifferenceQuantity)">
|
||||||
|
{{ formatQuantity(totalDifferenceQuantity) }}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td class="border border-solid border-[#dcdfe6] bg-[#f5f7fa] p-2 text-right">
|
||||||
|
<span :class="getLossClass(totalDifferencePrice)">
|
||||||
|
{{ formatPrice(totalDifferencePrice) }}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr v-if="printRows.length === 0">
|
||||||
|
<td
|
||||||
|
class="border border-solid border-[#dcdfe6] p-2 text-center"
|
||||||
|
:colspan="tableColumnCount"
|
||||||
|
>
|
||||||
|
暂无明细
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</Teleport>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
@page {
|
||||||
|
margin: 8mm 10mm;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media print {
|
||||||
|
:global(body.wms-check-order-printing) {
|
||||||
|
-webkit-print-color-adjust: exact;
|
||||||
|
margin: 0 !important;
|
||||||
|
padding: 0 !important;
|
||||||
|
print-color-adjust: exact;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(body.wms-check-order-printing *) {
|
||||||
|
visibility: hidden !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(body.wms-check-order-printing .wms-check-order-print),
|
||||||
|
:global(body.wms-check-order-printing .wms-check-order-print *) {
|
||||||
|
visibility: visible !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(body.wms-check-order-printing .wms-check-order-print) {
|
||||||
|
pointer-events: auto;
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
z-index: auto;
|
||||||
|
box-sizing: border-box;
|
||||||
|
width: 100%;
|
||||||
|
margin: 0 !important;
|
||||||
|
padding: 0 !important;
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -0,0 +1,483 @@
|
||||||
|
import type { VbenFormSchema } from '#/adapter/form';
|
||||||
|
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||||
|
import type { DescriptionItemSchema } from '#/components/description';
|
||||||
|
import type { NumberRangeValue } from '#/components/number-range-input';
|
||||||
|
|
||||||
|
import { h, markRaw } from 'vue';
|
||||||
|
|
||||||
|
import { DICT_TYPE } from '@vben/constants';
|
||||||
|
import { getDictOptions } from '@vben/hooks';
|
||||||
|
import { formatDate, formatDateTime } from '@vben/utils';
|
||||||
|
|
||||||
|
import { z } from '#/adapter/form';
|
||||||
|
import { getSimpleUserList } from '#/api/system/user';
|
||||||
|
import { DictTag } from '#/components/dict-tag';
|
||||||
|
import {
|
||||||
|
buildNumberRangeSchema,
|
||||||
|
NumberRangeInput,
|
||||||
|
} from '#/components/number-range-input';
|
||||||
|
import { getRangePickerDefaultProps } from '#/utils';
|
||||||
|
import { WmsWarehouseSelect } from '#/views/wms/md/warehouse/components';
|
||||||
|
import {
|
||||||
|
formatPrice,
|
||||||
|
formatQuantity,
|
||||||
|
formatSumPrice,
|
||||||
|
formatSumQuantity,
|
||||||
|
getLossClass,
|
||||||
|
PRICE_PRECISION,
|
||||||
|
QUANTITY_PRECISION,
|
||||||
|
roundPrice,
|
||||||
|
sumPrice,
|
||||||
|
sumQuantity,
|
||||||
|
} from '#/views/wms/utils/format';
|
||||||
|
|
||||||
|
/** 拆分数量/金额区间字段,适配后端 Min/Max 查询参数 */
|
||||||
|
function splitNumberRange(minFieldName: string, maxFieldName: string) {
|
||||||
|
return (
|
||||||
|
value: NumberRangeValue | undefined,
|
||||||
|
setValue: (fieldName: string, value: number | undefined) => void,
|
||||||
|
) => {
|
||||||
|
setValue(minFieldName, value?.[0]);
|
||||||
|
setValue(maxFieldName, value?.[1]);
|
||||||
|
return undefined;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 构建允许负数的区间搜索项,盘库盈亏数量需要支持盘亏 */
|
||||||
|
function buildSignedNumberRangeSchema(
|
||||||
|
label: string,
|
||||||
|
fieldName: string,
|
||||||
|
minFieldName: string,
|
||||||
|
maxFieldName: string,
|
||||||
|
precision: number,
|
||||||
|
): VbenFormSchema {
|
||||||
|
return {
|
||||||
|
component: markRaw(NumberRangeInput),
|
||||||
|
componentProps: {
|
||||||
|
precision,
|
||||||
|
},
|
||||||
|
fieldName,
|
||||||
|
label,
|
||||||
|
valueFormat: splitNumberRange(minFieldName, maxFieldName),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 计算单据盈亏金额 */
|
||||||
|
export function getOrderDifferencePrice(order: {
|
||||||
|
actualPrice?: number;
|
||||||
|
totalPrice?: number;
|
||||||
|
}) {
|
||||||
|
return roundPrice(Number(order.actualPrice || 0) - Number(order.totalPrice || 0));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 计算明细盈亏数量 */
|
||||||
|
export function getDetailDifferenceQuantity(detail: {
|
||||||
|
checkQuantity?: number;
|
||||||
|
quantity?: number;
|
||||||
|
}) {
|
||||||
|
return Number(detail.checkQuantity || 0) - Number(detail.quantity || 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 计算明细实际金额 */
|
||||||
|
export function getDetailActualPrice(detail: {
|
||||||
|
checkQuantity?: number;
|
||||||
|
price?: number;
|
||||||
|
}) {
|
||||||
|
if (
|
||||||
|
detail.checkQuantity === undefined ||
|
||||||
|
detail.checkQuantity === null ||
|
||||||
|
detail.price === undefined ||
|
||||||
|
detail.price === null
|
||||||
|
) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
return roundPrice(Number(detail.checkQuantity) * Number(detail.price));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 计算明细盈亏金额 */
|
||||||
|
export function getDetailDifferencePrice(detail: {
|
||||||
|
checkQuantity?: number;
|
||||||
|
price?: number;
|
||||||
|
quantity?: number;
|
||||||
|
}) {
|
||||||
|
if (detail.price === undefined || detail.price === null) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
return roundPrice(getDetailDifferenceQuantity(detail) * Number(detail.price));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 列表的搜索表单 */
|
||||||
|
export function useGridFormSchema(): VbenFormSchema[] {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
component: 'Input',
|
||||||
|
componentProps: {
|
||||||
|
clearable: true,
|
||||||
|
placeholder: '请输入盘库单号',
|
||||||
|
},
|
||||||
|
fieldName: 'no',
|
||||||
|
label: '盘库单号',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
component: 'Select',
|
||||||
|
componentProps: {
|
||||||
|
clearable: true,
|
||||||
|
options: getDictOptions(DICT_TYPE.WMS_ORDER_STATUS, 'number'),
|
||||||
|
placeholder: '请选择单据状态',
|
||||||
|
},
|
||||||
|
fieldName: 'status',
|
||||||
|
label: '单据状态',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
component: markRaw(WmsWarehouseSelect),
|
||||||
|
fieldName: 'warehouseId',
|
||||||
|
label: '仓库',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
component: 'RangePicker',
|
||||||
|
componentProps: {
|
||||||
|
...getRangePickerDefaultProps(),
|
||||||
|
clearable: true,
|
||||||
|
},
|
||||||
|
fieldName: 'orderTime',
|
||||||
|
label: '单据日期',
|
||||||
|
},
|
||||||
|
buildSignedNumberRangeSchema(
|
||||||
|
'盈亏数量',
|
||||||
|
'totalQuantityRange',
|
||||||
|
'totalQuantityMin',
|
||||||
|
'totalQuantityMax',
|
||||||
|
QUANTITY_PRECISION,
|
||||||
|
),
|
||||||
|
buildNumberRangeSchema(
|
||||||
|
'总金额',
|
||||||
|
'totalPriceRange',
|
||||||
|
'totalPriceMin',
|
||||||
|
'totalPriceMax',
|
||||||
|
PRICE_PRECISION,
|
||||||
|
),
|
||||||
|
buildNumberRangeSchema(
|
||||||
|
'实际金额',
|
||||||
|
'actualPriceRange',
|
||||||
|
'actualPriceMin',
|
||||||
|
'actualPriceMax',
|
||||||
|
PRICE_PRECISION,
|
||||||
|
),
|
||||||
|
{
|
||||||
|
component: 'ApiSelect',
|
||||||
|
componentProps: {
|
||||||
|
clearable: true,
|
||||||
|
api: getSimpleUserList,
|
||||||
|
labelField: 'nickname',
|
||||||
|
placeholder: '请选择创建用户',
|
||||||
|
filterable: true,
|
||||||
|
valueField: 'id',
|
||||||
|
},
|
||||||
|
fieldName: 'creator',
|
||||||
|
label: '创建用户',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
component: 'ApiSelect',
|
||||||
|
componentProps: {
|
||||||
|
clearable: true,
|
||||||
|
api: getSimpleUserList,
|
||||||
|
labelField: 'nickname',
|
||||||
|
placeholder: '请选择更新用户',
|
||||||
|
filterable: true,
|
||||||
|
valueField: 'id',
|
||||||
|
},
|
||||||
|
fieldName: 'updater',
|
||||||
|
label: '更新用户',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
component: 'RangePicker',
|
||||||
|
componentProps: {
|
||||||
|
...getRangePickerDefaultProps(),
|
||||||
|
clearable: true,
|
||||||
|
},
|
||||||
|
fieldName: 'createTime',
|
||||||
|
label: '创建时间',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
component: 'RangePicker',
|
||||||
|
componentProps: {
|
||||||
|
...getRangePickerDefaultProps(),
|
||||||
|
clearable: true,
|
||||||
|
},
|
||||||
|
fieldName: 'updateTime',
|
||||||
|
label: '更新时间',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 列表表格列 */
|
||||||
|
export function useGridColumns(): VxeTableGridOptions['columns'] {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
fixed: 'left',
|
||||||
|
slots: { content: 'expand_content' },
|
||||||
|
type: 'expand',
|
||||||
|
width: 48,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'no',
|
||||||
|
fixed: 'left',
|
||||||
|
slots: { default: 'no' },
|
||||||
|
title: '单号',
|
||||||
|
width: 210,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
align: 'center',
|
||||||
|
cellRender: {
|
||||||
|
name: 'CellDict',
|
||||||
|
props: { type: DICT_TYPE.WMS_ORDER_STATUS },
|
||||||
|
},
|
||||||
|
field: 'status',
|
||||||
|
fixed: 'left',
|
||||||
|
title: '盘库状态',
|
||||||
|
width: 110,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'warehouseName',
|
||||||
|
minWidth: 180,
|
||||||
|
title: '仓库',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'quantityAmount',
|
||||||
|
minWidth: 200,
|
||||||
|
slots: { default: 'quantityAmount' },
|
||||||
|
title: '盈亏/金额(元)',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'operateInfo',
|
||||||
|
minWidth: 280,
|
||||||
|
slots: { default: 'operateInfo' },
|
||||||
|
title: '操作信息',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'remark',
|
||||||
|
minWidth: 160,
|
||||||
|
title: '备注',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'actions',
|
||||||
|
fixed: 'right',
|
||||||
|
slots: { default: 'actions' },
|
||||||
|
title: '操作',
|
||||||
|
width: 220,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 详情的字段 */
|
||||||
|
export function useDetailSchema(): DescriptionItemSchema[] {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
field: 'no',
|
||||||
|
label: '盘库单号',
|
||||||
|
render: (val) => val || '-',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'warehouseName',
|
||||||
|
label: '仓库',
|
||||||
|
render: (val) => val || '-',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'orderTime',
|
||||||
|
label: '单据日期',
|
||||||
|
render: (val) => formatDate(val, 'YYYY-MM-DD') || '-',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'status',
|
||||||
|
label: '单据状态',
|
||||||
|
render: (val) =>
|
||||||
|
val === undefined || val === null
|
||||||
|
? '-'
|
||||||
|
: h(DictTag, {
|
||||||
|
type: DICT_TYPE.WMS_ORDER_STATUS,
|
||||||
|
value: val,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'totalQuantity',
|
||||||
|
label: '盈亏数量',
|
||||||
|
render: (val) =>
|
||||||
|
h('span', { class: getLossClass(val) }, formatQuantity(val) || '-'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'totalPrice',
|
||||||
|
label: '总金额',
|
||||||
|
render: (val) => formatPrice(val) || '-',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'actualPrice',
|
||||||
|
label: '实际金额',
|
||||||
|
render: (val) => formatPrice(val) || '-',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'differencePrice',
|
||||||
|
label: '实际盈亏金额',
|
||||||
|
render: (_val, data) => {
|
||||||
|
const differencePrice = getOrderDifferencePrice(data || {});
|
||||||
|
return h(
|
||||||
|
'span',
|
||||||
|
{ class: getLossClass(differencePrice) },
|
||||||
|
formatPrice(differencePrice) || '-',
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'createTime',
|
||||||
|
label: '创建时间',
|
||||||
|
render: (val) => formatDateTime(val) || '-',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'creatorName',
|
||||||
|
label: '创建人',
|
||||||
|
render: (val, data) => val || data?.creator || '-',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'updateTime',
|
||||||
|
label: '更新时间',
|
||||||
|
render: (val) => formatDateTime(val) || '-',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'updaterName',
|
||||||
|
label: '更新人',
|
||||||
|
render: (val, data) => val || data?.updater || '-',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'remark',
|
||||||
|
label: '备注',
|
||||||
|
render: (val) => val || '-',
|
||||||
|
span: 2,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 表单的配置项 */
|
||||||
|
export function useFormSchema(): VbenFormSchema[] {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
component: 'Input',
|
||||||
|
dependencies: {
|
||||||
|
show: () => false,
|
||||||
|
triggerFields: [''],
|
||||||
|
},
|
||||||
|
fieldName: 'id',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
component: 'Input',
|
||||||
|
componentProps: {
|
||||||
|
maxLength: 64,
|
||||||
|
placeholder: '请输入盘库单号',
|
||||||
|
},
|
||||||
|
fieldName: 'no',
|
||||||
|
label: '盘库单号',
|
||||||
|
rules: z.string().min(1, '盘库单号不能为空').max(64),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
component: markRaw(WmsWarehouseSelect),
|
||||||
|
componentProps: {
|
||||||
|
disabled: true,
|
||||||
|
},
|
||||||
|
fieldName: 'warehouseId',
|
||||||
|
label: '仓库',
|
||||||
|
rules: 'required',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
component: 'DatePicker',
|
||||||
|
componentProps: {
|
||||||
|
class: 'w-full',
|
||||||
|
format: 'YYYY-MM-DD',
|
||||||
|
placeholder: '请选择单据日期',
|
||||||
|
valueFormat: 'x',
|
||||||
|
},
|
||||||
|
fieldName: 'orderTime',
|
||||||
|
label: '单据日期',
|
||||||
|
rules: 'required',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
component: 'Input',
|
||||||
|
componentProps: {
|
||||||
|
disabled: true,
|
||||||
|
},
|
||||||
|
fieldName: 'actualPrice',
|
||||||
|
label: '实际金额',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
component: 'Textarea',
|
||||||
|
componentProps: {
|
||||||
|
maxLength: 255,
|
||||||
|
placeholder: '请输入备注',
|
||||||
|
},
|
||||||
|
fieldName: 'remark',
|
||||||
|
formItemClass: 'col-span-2',
|
||||||
|
label: '备注',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 选择盘库仓库表单的配置项 */
|
||||||
|
export function useWarehouseFormSchema(
|
||||||
|
onWarehouseChange: (warehouse: unknown) => void,
|
||||||
|
): VbenFormSchema[] {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
component: markRaw(WmsWarehouseSelect),
|
||||||
|
componentProps: {
|
||||||
|
onChange: onWarehouseChange,
|
||||||
|
},
|
||||||
|
fieldName: 'warehouseId',
|
||||||
|
label: '仓库',
|
||||||
|
rules: 'required',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CheckOrderDetailFooterRow {
|
||||||
|
actualPrice?: number;
|
||||||
|
checkQuantity?: number;
|
||||||
|
differencePrice?: number;
|
||||||
|
differenceQuantity?: number;
|
||||||
|
quantity?: number;
|
||||||
|
}
|
||||||
|
type CheckOrderDetailFooterColumn = Pick<
|
||||||
|
NonNullable<NonNullable<VxeTableGridOptions['columns']>[number]>,
|
||||||
|
'field'
|
||||||
|
>;
|
||||||
|
|
||||||
|
/** 明细表格的合计行 */
|
||||||
|
export function getCheckDetailFooter({
|
||||||
|
columns,
|
||||||
|
data,
|
||||||
|
}: {
|
||||||
|
columns: CheckOrderDetailFooterColumn[];
|
||||||
|
data: CheckOrderDetailFooterRow[];
|
||||||
|
}) {
|
||||||
|
return [
|
||||||
|
columns.map((column, index) => {
|
||||||
|
if (index === 0) {
|
||||||
|
return '合计';
|
||||||
|
}
|
||||||
|
if (column.field === 'quantity') {
|
||||||
|
return formatSumQuantity(data, (detail) => detail.quantity);
|
||||||
|
}
|
||||||
|
if (column.field === 'checkQuantity') {
|
||||||
|
return formatSumQuantity(data, (detail) => detail.checkQuantity);
|
||||||
|
}
|
||||||
|
if (column.field === 'actualPrice') {
|
||||||
|
return formatSumPrice(data, (detail) => detail.actualPrice);
|
||||||
|
}
|
||||||
|
if (column.field === 'differenceQuantity') {
|
||||||
|
return formatQuantity(
|
||||||
|
sumQuantity(data, (detail) => detail.differenceQuantity),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (column.field === 'differencePrice') {
|
||||||
|
return formatPrice(sumPrice(data, (detail) => detail.differencePrice));
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,378 @@
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||||
|
import type { WmsCheckOrderApi } from '#/api/wms/order/check';
|
||||||
|
import type { WmsCheckOrderDetailApi } from '#/api/wms/order/check/detail';
|
||||||
|
|
||||||
|
import { reactive, ref } from 'vue';
|
||||||
|
|
||||||
|
import { DocAlert, Page, useVbenModal } from '@vben/common-ui';
|
||||||
|
import { downloadFileFromBlobPart, formatDateTime } from '@vben/utils';
|
||||||
|
|
||||||
|
import { ElLoading, ElMessage } from 'element-plus';
|
||||||
|
|
||||||
|
import {
|
||||||
|
ACTION_ICON,
|
||||||
|
TableAction,
|
||||||
|
useVbenVxeGrid,
|
||||||
|
VxeColumn,
|
||||||
|
VxeTable,
|
||||||
|
} from '#/adapter/vxe-table';
|
||||||
|
import {
|
||||||
|
deleteCheckOrder,
|
||||||
|
exportCheckOrder,
|
||||||
|
getCheckOrderDetailListByOrderId,
|
||||||
|
getCheckOrderPage,
|
||||||
|
} from '#/api/wms/order/check';
|
||||||
|
import { $t } from '#/locales';
|
||||||
|
import {
|
||||||
|
OrderDeleteStatusList,
|
||||||
|
OrderStatusEnum,
|
||||||
|
OrderUpdateStatusList,
|
||||||
|
} from '#/views/wms/utils/constants';
|
||||||
|
import {
|
||||||
|
formatPrice,
|
||||||
|
formatQuantity,
|
||||||
|
getLossClass,
|
||||||
|
} from '#/views/wms/utils/format';
|
||||||
|
|
||||||
|
import {
|
||||||
|
getDetailActualPrice,
|
||||||
|
getDetailDifferencePrice,
|
||||||
|
getDetailDifferenceQuantity,
|
||||||
|
getOrderDifferencePrice,
|
||||||
|
useGridColumns,
|
||||||
|
useGridFormSchema,
|
||||||
|
} from './data';
|
||||||
|
import CheckOrderDetail from './modules/detail.vue';
|
||||||
|
import CheckOrderForm from './modules/form.vue';
|
||||||
|
import CheckOrderPrint from './modules/print.vue';
|
||||||
|
|
||||||
|
defineOptions({ name: 'WmsCheckOrder' });
|
||||||
|
|
||||||
|
const printRef = ref<InstanceType<typeof CheckOrderPrint>>();
|
||||||
|
const detailMap = reactive<Record<number, WmsCheckOrderDetailApi.CheckOrderDetail[]>>(
|
||||||
|
{},
|
||||||
|
);
|
||||||
|
|
||||||
|
const [FormModal, formModalApi] = useVbenModal({
|
||||||
|
connectedComponent: CheckOrderForm,
|
||||||
|
destroyOnClose: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const [DetailModal, detailModalApi] = useVbenModal({
|
||||||
|
connectedComponent: CheckOrderDetail,
|
||||||
|
destroyOnClose: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
/** 清空展开明细缓存 */
|
||||||
|
function clearDetailMap() {
|
||||||
|
for (const id of Object.keys(detailMap)) {
|
||||||
|
delete detailMap[Number(id)];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 刷新表格 */
|
||||||
|
function handleRefresh() {
|
||||||
|
clearDetailMap();
|
||||||
|
gridApi.query();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 创建盘库单 */
|
||||||
|
function handleCreate() {
|
||||||
|
formModalApi.setData({ type: 'create' }).open();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 编辑盘库单 */
|
||||||
|
function handleEdit(row: WmsCheckOrderApi.CheckOrder) {
|
||||||
|
formModalApi.setData({ id: row.id!, type: 'update' }).open();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 查看盘库单详情 */
|
||||||
|
function handleDetail(row: WmsCheckOrderApi.CheckOrder) {
|
||||||
|
detailModalApi.setData({ id: row.id! }).open();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 获取已展开行的明细 */
|
||||||
|
function getExpandedDetails(row: WmsCheckOrderApi.CheckOrder) {
|
||||||
|
return detailMap[row.id!] || [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 展开列表行时懒加载盘库明细 */
|
||||||
|
async function handleExpandChange(
|
||||||
|
row: WmsCheckOrderApi.CheckOrder,
|
||||||
|
expanded: boolean,
|
||||||
|
) {
|
||||||
|
if (!expanded) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
delete detailMap[row.id!];
|
||||||
|
detailMap[row.id!] = await getCheckOrderDetailListByOrderId(row.id!);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 判断盘库单是否可修改 */
|
||||||
|
function canUpdateCheckOrder(status?: number) {
|
||||||
|
return status !== undefined && OrderUpdateStatusList.includes(status);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 判断盘库单是否可删除 */
|
||||||
|
function canDeleteCheckOrder(status?: number) {
|
||||||
|
return status !== undefined && OrderDeleteStatusList.includes(status);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 获取修改按钮禁用提示 */
|
||||||
|
function getCheckOrderUpdateTip(status?: number) {
|
||||||
|
if (canUpdateCheckOrder(status)) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
if (status === OrderStatusEnum.FINISHED) {
|
||||||
|
return '已盘库,无法修改';
|
||||||
|
}
|
||||||
|
if (status === OrderStatusEnum.CANCELED) {
|
||||||
|
return '已作废,无法修改';
|
||||||
|
}
|
||||||
|
return '当前状态无法修改';
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 获取删除按钮禁用提示 */
|
||||||
|
function getCheckOrderDeleteTip(status?: number) {
|
||||||
|
if (canDeleteCheckOrder(status)) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
if (status === OrderStatusEnum.FINISHED) {
|
||||||
|
return '已盘库,无法删除';
|
||||||
|
}
|
||||||
|
return '当前状态无法删除';
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 删除盘库单 */
|
||||||
|
async function handleDelete(row: WmsCheckOrderApi.CheckOrder) {
|
||||||
|
const loadingInstance = ElLoading.service({
|
||||||
|
text: $t('ui.actionMessage.deleting', [row.no]),
|
||||||
|
});
|
||||||
|
try {
|
||||||
|
await deleteCheckOrder(row.id!);
|
||||||
|
ElMessage.success($t('ui.actionMessage.deleteSuccess', [row.no]));
|
||||||
|
handleRefresh();
|
||||||
|
} finally {
|
||||||
|
loadingInstance.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 导出盘库单 */
|
||||||
|
async function handleExport() {
|
||||||
|
const data = await exportCheckOrder(await gridApi.formApi.getValues());
|
||||||
|
downloadFileFromBlobPart({ fileName: '盘库单.xls', source: data });
|
||||||
|
}
|
||||||
|
|
||||||
|
const [Grid, gridApi] = useVbenVxeGrid({
|
||||||
|
formOptions: {
|
||||||
|
collapsed: true,
|
||||||
|
schema: useGridFormSchema(),
|
||||||
|
},
|
||||||
|
gridOptions: {
|
||||||
|
columns: useGridColumns(),
|
||||||
|
expandConfig: {
|
||||||
|
padding: true,
|
||||||
|
},
|
||||||
|
height: 'auto',
|
||||||
|
keepSource: true,
|
||||||
|
proxyConfig: {
|
||||||
|
ajax: {
|
||||||
|
query: async ({ page }, formValues) => {
|
||||||
|
return await getCheckOrderPage({
|
||||||
|
pageNo: page.currentPage,
|
||||||
|
pageSize: page.pageSize,
|
||||||
|
...formValues,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
rowConfig: {
|
||||||
|
isHover: true,
|
||||||
|
keyField: 'id',
|
||||||
|
},
|
||||||
|
toolbarConfig: {
|
||||||
|
refresh: true,
|
||||||
|
search: true,
|
||||||
|
},
|
||||||
|
} as VxeTableGridOptions<WmsCheckOrderApi.CheckOrder>,
|
||||||
|
gridEvents: {
|
||||||
|
toggleRowExpand: ({
|
||||||
|
expanded,
|
||||||
|
row,
|
||||||
|
}: {
|
||||||
|
expanded: boolean;
|
||||||
|
row: WmsCheckOrderApi.CheckOrder;
|
||||||
|
}) => {
|
||||||
|
handleExpandChange(row, expanded);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Page auto-content-height>
|
||||||
|
<template #doc>
|
||||||
|
<DocAlert title="【单据】盘库" url="https://doc.iocoder.cn/wms/order/check/" />
|
||||||
|
</template>
|
||||||
|
<FormModal @success="handleRefresh" />
|
||||||
|
<DetailModal />
|
||||||
|
<CheckOrderPrint ref="printRef" />
|
||||||
|
|
||||||
|
<Grid table-title="盘库单列表">
|
||||||
|
<template #expand_content="{ row }">
|
||||||
|
<VxeTable
|
||||||
|
:data="getExpandedDetails(row)"
|
||||||
|
border
|
||||||
|
:show-overflow="true"
|
||||||
|
size="small"
|
||||||
|
>
|
||||||
|
<VxeColumn title="商品信息" min-width="220">
|
||||||
|
<template #default="{ row: detail }">
|
||||||
|
<div>{{ detail.itemName || '-' }}</div>
|
||||||
|
<div v-if="detail.itemCode" class="text-xs text-gray-500">
|
||||||
|
商品编号:{{ detail.itemCode }}
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</VxeColumn>
|
||||||
|
<VxeColumn title="规格信息" min-width="220">
|
||||||
|
<template #default="{ row: detail }">
|
||||||
|
<div>{{ detail.skuName || '-' }}</div>
|
||||||
|
<div v-if="detail.skuCode" class="text-xs text-gray-500">
|
||||||
|
规格编号:{{ detail.skuCode }}
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</VxeColumn>
|
||||||
|
<VxeColumn title="账面数量" align="right" width="120">
|
||||||
|
<template #default="{ row: detail }">
|
||||||
|
{{ formatQuantity(detail.quantity) }}
|
||||||
|
</template>
|
||||||
|
</VxeColumn>
|
||||||
|
<VxeColumn title="实盘数量" align="right" width="120">
|
||||||
|
<template #default="{ row: detail }">
|
||||||
|
{{ formatQuantity(detail.checkQuantity) }}
|
||||||
|
</template>
|
||||||
|
</VxeColumn>
|
||||||
|
<VxeColumn title="单价(元)" align="right" width="120">
|
||||||
|
<template #default="{ row: detail }">
|
||||||
|
{{ formatPrice(detail.price) || '-' }}
|
||||||
|
</template>
|
||||||
|
</VxeColumn>
|
||||||
|
<VxeColumn title="实际金额(元)" align="right" width="140">
|
||||||
|
<template #default="{ row: detail }">
|
||||||
|
{{ formatPrice(getDetailActualPrice(detail)) || '-' }}
|
||||||
|
</template>
|
||||||
|
</VxeColumn>
|
||||||
|
<VxeColumn title="盈亏数量" align="right" width="120">
|
||||||
|
<template #default="{ row: detail }">
|
||||||
|
<span :class="getLossClass(getDetailDifferenceQuantity(detail))">
|
||||||
|
{{ formatQuantity(getDetailDifferenceQuantity(detail)) }}
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
</VxeColumn>
|
||||||
|
<VxeColumn title="实际盈亏金额(元)" align="right" width="160">
|
||||||
|
<template #default="{ row: detail }">
|
||||||
|
<span :class="getLossClass(getDetailDifferencePrice(detail))">
|
||||||
|
{{ formatPrice(getDetailDifferencePrice(detail)) || '-' }}
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
</VxeColumn>
|
||||||
|
</VxeTable>
|
||||||
|
</template>
|
||||||
|
<template #toolbar-tools>
|
||||||
|
<TableAction
|
||||||
|
:actions="[
|
||||||
|
{
|
||||||
|
label: $t('ui.actionTitle.create', ['盘库单']),
|
||||||
|
type: 'primary',
|
||||||
|
icon: ACTION_ICON.ADD,
|
||||||
|
auth: ['wms:check-order:create'],
|
||||||
|
onClick: handleCreate,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: $t('ui.actionTitle.export'),
|
||||||
|
type: 'primary',
|
||||||
|
icon: ACTION_ICON.DOWNLOAD,
|
||||||
|
auth: ['wms:check-order:export'],
|
||||||
|
onClick: handleExport,
|
||||||
|
},
|
||||||
|
]"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<template #no="{ row }">
|
||||||
|
<div>
|
||||||
|
单号:
|
||||||
|
<a class="text-primary" @click="handleDetail(row)">{{ row.no }}</a>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template #quantityAmount="{ row }">
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<span>盈亏数:</span>
|
||||||
|
<span :class="getLossClass(row.totalQuantity)">
|
||||||
|
{{ formatQuantity(row.totalQuantity) }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<span>总金额:</span>
|
||||||
|
<span>{{ formatPrice(row.totalPrice) }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<span>实际金额:</span>
|
||||||
|
<span>{{ formatPrice(row.actualPrice) }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<span>盈亏金额:</span>
|
||||||
|
<span :class="getLossClass(getOrderDifferencePrice(row))">
|
||||||
|
{{ formatPrice(getOrderDifferencePrice(row)) }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template #operateInfo="{ row }">
|
||||||
|
<div>
|
||||||
|
创建:{{ formatDateTime(row.createTime) || '-' }} /
|
||||||
|
{{ row.creatorName || row.creator || '-' }}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
更新:{{ formatDateTime(row.updateTime) || '-' }} /
|
||||||
|
{{ row.updaterName || row.updater || '-' }}
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template #actions="{ row }">
|
||||||
|
<TableAction
|
||||||
|
:actions="[
|
||||||
|
{
|
||||||
|
label: $t('common.edit'),
|
||||||
|
type: 'primary',
|
||||||
|
link: true,
|
||||||
|
icon: ACTION_ICON.EDIT,
|
||||||
|
disabled: !canUpdateCheckOrder(row.status),
|
||||||
|
tooltip: getCheckOrderUpdateTip(row.status),
|
||||||
|
auth: ['wms:check-order:update'],
|
||||||
|
onClick: handleEdit.bind(null, row),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: $t('common.delete'),
|
||||||
|
type: 'danger',
|
||||||
|
link: true,
|
||||||
|
icon: ACTION_ICON.DELETE,
|
||||||
|
disabled: !canDeleteCheckOrder(row.status),
|
||||||
|
tooltip: getCheckOrderDeleteTip(row.status),
|
||||||
|
auth: ['wms:check-order:delete'],
|
||||||
|
popConfirm: {
|
||||||
|
title: $t('ui.actionMessage.deleteConfirm', [row.no]),
|
||||||
|
confirm: handleDelete.bind(null, row),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '打印',
|
||||||
|
type: 'primary',
|
||||||
|
link: true,
|
||||||
|
auth: ['wms:check-order:query'],
|
||||||
|
onClick: () => printRef?.print(row.id!),
|
||||||
|
},
|
||||||
|
]"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</Grid>
|
||||||
|
</Page>
|
||||||
|
</template>
|
||||||
|
|
@ -0,0 +1,159 @@
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import type { WmsCheckOrderApi } from '#/api/wms/order/check';
|
||||||
|
import type { WmsCheckOrderDetailApi } from '#/api/wms/order/check/detail';
|
||||||
|
|
||||||
|
import { computed, ref } from 'vue';
|
||||||
|
|
||||||
|
import { useVbenModal } from '@vben/common-ui';
|
||||||
|
|
||||||
|
import { VxeColumn, VxeTable } from '#/adapter/vxe-table';
|
||||||
|
import {
|
||||||
|
getCheckOrder,
|
||||||
|
getCheckOrderDetailListByOrderId,
|
||||||
|
} from '#/api/wms/order/check';
|
||||||
|
import { useDescription } from '#/components/description';
|
||||||
|
import {
|
||||||
|
formatPrice,
|
||||||
|
formatQuantity,
|
||||||
|
getLossClass,
|
||||||
|
} from '#/views/wms/utils/format';
|
||||||
|
|
||||||
|
import {
|
||||||
|
getCheckDetailFooter,
|
||||||
|
getDetailActualPrice,
|
||||||
|
getDetailDifferencePrice,
|
||||||
|
getDetailDifferenceQuantity,
|
||||||
|
useDetailSchema,
|
||||||
|
} from '../data';
|
||||||
|
|
||||||
|
interface DetailRow extends WmsCheckOrderDetailApi.CheckOrderDetail {
|
||||||
|
actualPrice?: number;
|
||||||
|
differencePrice?: number;
|
||||||
|
differenceQuantity?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
defineOptions({ name: 'WmsCheckOrderDetail' });
|
||||||
|
|
||||||
|
const detailData = ref<WmsCheckOrderApi.CheckOrder>({});
|
||||||
|
|
||||||
|
/** 详情明细补齐实际金额和盈亏字段,便于表格与 footer 统一渲染 */
|
||||||
|
const detailRows = computed<DetailRow[]>(() =>
|
||||||
|
(detailData.value.details || []).map((detail) => ({
|
||||||
|
...detail,
|
||||||
|
actualPrice: getDetailActualPrice(detail),
|
||||||
|
differencePrice: getDetailDifferencePrice(detail),
|
||||||
|
differenceQuantity: getDetailDifferenceQuantity(detail),
|
||||||
|
})),
|
||||||
|
);
|
||||||
|
|
||||||
|
const [Descriptions] = useDescription({
|
||||||
|
border: true,
|
||||||
|
column: 2,
|
||||||
|
schema: useDetailSchema(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const [Modal, modalApi] = useVbenModal({
|
||||||
|
async onOpenChange(isOpen: boolean) {
|
||||||
|
if (!isOpen) {
|
||||||
|
detailData.value = {};
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const data = modalApi.getData<{ id?: number }>();
|
||||||
|
if (!data?.id) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
modalApi.lock();
|
||||||
|
try {
|
||||||
|
const order = await getCheckOrder(data.id);
|
||||||
|
const details =
|
||||||
|
order.details || (await getCheckOrderDetailListByOrderId(data.id));
|
||||||
|
detailData.value = { ...order, details };
|
||||||
|
} finally {
|
||||||
|
modalApi.unlock();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Modal
|
||||||
|
title="盘库单详情"
|
||||||
|
class="w-2/3"
|
||||||
|
:show-cancel-button="false"
|
||||||
|
:show-confirm-button="false"
|
||||||
|
>
|
||||||
|
<div class="mx-4 space-y-4">
|
||||||
|
<Descriptions :data="detailData" />
|
||||||
|
<VxeTable
|
||||||
|
:data="detailRows"
|
||||||
|
border
|
||||||
|
empty-text="暂无商品明细"
|
||||||
|
:footer-method="getCheckDetailFooter"
|
||||||
|
:show-overflow="true"
|
||||||
|
show-footer
|
||||||
|
size="small"
|
||||||
|
>
|
||||||
|
<VxeColumn title="商品信息" min-width="200">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<div>{{ row.itemName || '-' }}</div>
|
||||||
|
<div v-if="row.itemCode" class="text-xs text-gray-500">
|
||||||
|
商品编号:{{ row.itemCode }}
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</VxeColumn>
|
||||||
|
<VxeColumn title="规格信息" min-width="200">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<div>{{ row.skuName || '-' }}</div>
|
||||||
|
<div v-if="row.skuCode" class="text-xs text-gray-500">
|
||||||
|
规格编号:{{ row.skuCode }}
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</VxeColumn>
|
||||||
|
<VxeColumn field="quantity" title="账面数量" align="right" width="120">
|
||||||
|
<template #default="{ row }">
|
||||||
|
{{ formatQuantity(row.quantity) || '-' }}
|
||||||
|
</template>
|
||||||
|
</VxeColumn>
|
||||||
|
<VxeColumn title="单价(元)" align="right" width="120">
|
||||||
|
<template #default="{ row }">
|
||||||
|
{{ formatPrice(row.price) || '-' }}
|
||||||
|
</template>
|
||||||
|
</VxeColumn>
|
||||||
|
<VxeColumn field="checkQuantity" title="实盘数量" align="right" width="120">
|
||||||
|
<template #default="{ row }">
|
||||||
|
{{ formatQuantity(row.checkQuantity) || '-' }}
|
||||||
|
</template>
|
||||||
|
</VxeColumn>
|
||||||
|
<VxeColumn field="actualPrice" title="实际金额(元)" align="right" width="140">
|
||||||
|
<template #default="{ row }">
|
||||||
|
{{ formatPrice(row.actualPrice) || '-' }}
|
||||||
|
</template>
|
||||||
|
</VxeColumn>
|
||||||
|
<VxeColumn
|
||||||
|
field="differenceQuantity"
|
||||||
|
title="盈亏数量"
|
||||||
|
align="right"
|
||||||
|
width="120"
|
||||||
|
>
|
||||||
|
<template #default="{ row }">
|
||||||
|
<span :class="getLossClass(row.differenceQuantity)">
|
||||||
|
{{ formatQuantity(row.differenceQuantity) || '-' }}
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
</VxeColumn>
|
||||||
|
<VxeColumn
|
||||||
|
field="differencePrice"
|
||||||
|
title="实际盈亏金额(元)"
|
||||||
|
align="right"
|
||||||
|
width="160"
|
||||||
|
>
|
||||||
|
<template #default="{ row }">
|
||||||
|
<span :class="getLossClass(row.differencePrice)">
|
||||||
|
{{ formatPrice(row.differencePrice) || '-' }}
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
</VxeColumn>
|
||||||
|
</VxeTable>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
</template>
|
||||||
|
|
@ -0,0 +1,732 @@
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import type { VxeTableInstance } from '#/adapter/vxe-table';
|
||||||
|
import type { WmsInventoryApi } from '#/api/wms/inventory';
|
||||||
|
import type { WmsItemSkuApi } from '#/api/wms/md/item/sku';
|
||||||
|
import type { WmsWarehouseApi } from '#/api/wms/md/warehouse';
|
||||||
|
import type { WmsCheckOrderApi } from '#/api/wms/order/check';
|
||||||
|
import type { WmsCheckOrderDetailApi } from '#/api/wms/order/check/detail';
|
||||||
|
|
||||||
|
import { computed, nextTick, ref, watch } from 'vue';
|
||||||
|
|
||||||
|
import { confirm, useVbenModal } from '@vben/common-ui';
|
||||||
|
import { isEqual } from '@vben/utils';
|
||||||
|
|
||||||
|
import { ElInputNumber, ElMessage } from 'element-plus';
|
||||||
|
|
||||||
|
import { useVbenForm } from '#/adapter/form';
|
||||||
|
import { TableAction, VxeColumn, VxeTable } from '#/adapter/vxe-table';
|
||||||
|
import { getInventoryList } from '#/api/wms/inventory';
|
||||||
|
import {
|
||||||
|
cancelCheckOrder,
|
||||||
|
completeCheckOrder,
|
||||||
|
createCheckOrder,
|
||||||
|
getCheckOrder,
|
||||||
|
getCheckOrderDetailListByOrderId,
|
||||||
|
updateCheckOrder,
|
||||||
|
} from '#/api/wms/order/check';
|
||||||
|
import { $t } from '#/locales';
|
||||||
|
import { WmsItemSkuSelect } from '#/views/wms/md/item/sku/components';
|
||||||
|
import {
|
||||||
|
OrderStatusEnum,
|
||||||
|
OrderUpdateStatusList,
|
||||||
|
} from '#/views/wms/utils/constants';
|
||||||
|
import {
|
||||||
|
dividePrice,
|
||||||
|
formatPrice,
|
||||||
|
formatQuantity,
|
||||||
|
getLossClass,
|
||||||
|
multiplyPrice,
|
||||||
|
PRICE_PRECISION,
|
||||||
|
QUANTITY_PRECISION,
|
||||||
|
sumPrice,
|
||||||
|
} from '#/views/wms/utils/format';
|
||||||
|
import { generateOrderNo } from '#/views/wms/utils/order';
|
||||||
|
|
||||||
|
import {
|
||||||
|
getCheckDetailFooter,
|
||||||
|
getDetailDifferencePrice,
|
||||||
|
getDetailDifferenceQuantity,
|
||||||
|
useFormSchema,
|
||||||
|
useWarehouseFormSchema,
|
||||||
|
} from '../data';
|
||||||
|
|
||||||
|
interface CheckInventoryRow extends WmsInventoryApi.Inventory {
|
||||||
|
availableQuantity?: number;
|
||||||
|
price?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface DetailRow extends WmsCheckOrderDetailApi.CheckOrderDetail {
|
||||||
|
actualPrice?: number;
|
||||||
|
differencePrice?: number;
|
||||||
|
differenceQuantity?: number;
|
||||||
|
seq: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
defineOptions({ name: 'WmsCheckOrderForm' });
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
success: [];
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const formData = ref<WmsCheckOrderApi.CheckOrder>({});
|
||||||
|
const formMode = ref('create');
|
||||||
|
const originalSubmitData = ref<WmsCheckOrderApi.CheckOrder>();
|
||||||
|
const details = ref<DetailRow[]>([]);
|
||||||
|
const detailTableRef = ref<VxeTableInstance>();
|
||||||
|
const skuSelectRef = ref<InstanceType<typeof WmsItemSkuSelect>>();
|
||||||
|
const selectingWarehouse = ref(false);
|
||||||
|
const warehouseName = ref<string>();
|
||||||
|
let detailSeq = 0; // 明细行可能还没有后端 id,使用本地序号作为 VXE 行操作的稳定标识
|
||||||
|
|
||||||
|
const getTitle = computed(() => {
|
||||||
|
return formMode.value === 'update'
|
||||||
|
? $t('ui.actionTitle.edit', ['盘库单'])
|
||||||
|
: $t('ui.actionTitle.create', ['盘库单']);
|
||||||
|
});
|
||||||
|
|
||||||
|
const modalTitle = computed(() => {
|
||||||
|
return selectingWarehouse.value ? '选择盘库仓库' : getTitle.value;
|
||||||
|
});
|
||||||
|
const modalClass = computed(() => {
|
||||||
|
return selectingWarehouse.value ? 'w-[420px]' : 'w-3/4';
|
||||||
|
});
|
||||||
|
|
||||||
|
const isPrepareOrder = computed(() => {
|
||||||
|
return (
|
||||||
|
!formData.value?.id ||
|
||||||
|
(formData.value.status !== undefined &&
|
||||||
|
OrderUpdateStatusList.includes(formData.value.status))
|
||||||
|
);
|
||||||
|
});
|
||||||
|
const isSavedPrepareOrder = computed(() => {
|
||||||
|
return (
|
||||||
|
!!formData.value?.id &&
|
||||||
|
formData.value.status !== undefined &&
|
||||||
|
OrderUpdateStatusList.includes(formData.value.status)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
/** 表单顶部实际金额来自明细合计,保持和 vue3 的只读汇总字段一致 */
|
||||||
|
const actualPrice = computed(() =>
|
||||||
|
sumPrice(details.value, (detail) => getDetailActualPrice(detail)),
|
||||||
|
);
|
||||||
|
|
||||||
|
const [Form, formApi] = useVbenForm({
|
||||||
|
commonConfig: {
|
||||||
|
componentProps: {
|
||||||
|
class: 'w-full',
|
||||||
|
},
|
||||||
|
labelWidth: 100,
|
||||||
|
},
|
||||||
|
layout: 'horizontal',
|
||||||
|
schema: useFormSchema(),
|
||||||
|
showDefaultActions: false,
|
||||||
|
wrapperClass: 'grid-cols-3',
|
||||||
|
});
|
||||||
|
|
||||||
|
const [WarehouseForm, warehouseFormApi] = useVbenForm({
|
||||||
|
commonConfig: {
|
||||||
|
componentProps: {
|
||||||
|
class: 'w-full',
|
||||||
|
},
|
||||||
|
labelWidth: 80,
|
||||||
|
},
|
||||||
|
layout: 'horizontal',
|
||||||
|
schema: useWarehouseFormSchema(handleWarehouseSelect),
|
||||||
|
showDefaultActions: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
/** 记录新增盘库单前置选择的仓库名称 */
|
||||||
|
function handleWarehouseSelect(warehouse: unknown) {
|
||||||
|
warehouseName.value = (warehouse as undefined | WmsWarehouseApi.Warehouse)
|
||||||
|
?.name;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 初始化新增盘库单草稿 */
|
||||||
|
async function initCreateForm(warehouse: {
|
||||||
|
warehouseId?: number;
|
||||||
|
warehouseName?: string;
|
||||||
|
}) {
|
||||||
|
formData.value = {
|
||||||
|
details: [],
|
||||||
|
no: generateOrderNo('PK'),
|
||||||
|
status: OrderStatusEnum.PREPARE,
|
||||||
|
warehouseId: warehouse.warehouseId,
|
||||||
|
warehouseName: warehouse.warehouseName,
|
||||||
|
};
|
||||||
|
setDetails([]);
|
||||||
|
await formApi.resetForm();
|
||||||
|
await formApi.setValues(formData.value);
|
||||||
|
await nextTick();
|
||||||
|
syncActualPriceField();
|
||||||
|
originalSubmitData.value = await buildSubmitData();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 确认前置选择仓库,进入主表单 */
|
||||||
|
async function handleWarehouseConfirm() {
|
||||||
|
const { valid } = await warehouseFormApi.validate();
|
||||||
|
if (!valid) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const values = (await warehouseFormApi.getValues()) as {
|
||||||
|
warehouseId?: number;
|
||||||
|
};
|
||||||
|
selectingWarehouse.value = false;
|
||||||
|
await nextTick();
|
||||||
|
await initCreateForm({
|
||||||
|
warehouseId: values.warehouseId,
|
||||||
|
warehouseName: warehouseName.value,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 同步表单顶部只读实际金额 */
|
||||||
|
function syncActualPriceField() {
|
||||||
|
void formApi.setFieldValue(
|
||||||
|
'actualPrice',
|
||||||
|
formatPrice(actualPrice.value) || '0.00',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(actualPrice, syncActualPriceField);
|
||||||
|
|
||||||
|
/** 计算明细实际金额 */
|
||||||
|
function getDetailActualPrice(detail: DetailRow) {
|
||||||
|
return detail.actualPrice ?? multiplyPrice(detail.checkQuantity, detail.price);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 刷新明细行的盈亏数据 */
|
||||||
|
function refreshDetailCalculatedFields(detail: DetailRow) {
|
||||||
|
detail.differenceQuantity = getDetailDifferenceQuantity(detail);
|
||||||
|
detail.differencePrice = getDetailDifferencePrice(detail);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 标准化明细行,补齐本地序号和计算字段 */
|
||||||
|
function normalizeDetail(
|
||||||
|
detail: WmsCheckOrderDetailApi.CheckOrderDetail & { actualPrice?: number },
|
||||||
|
): DetailRow {
|
||||||
|
detailSeq += 1;
|
||||||
|
const row: DetailRow = {
|
||||||
|
...detail,
|
||||||
|
actualPrice:
|
||||||
|
detail.actualPrice ?? multiplyPrice(detail.checkQuantity, detail.price),
|
||||||
|
seq: detailSeq,
|
||||||
|
};
|
||||||
|
refreshDetailCalculatedFields(row);
|
||||||
|
return row;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 根据库存构建盘库明细 */
|
||||||
|
function buildDetail(inventory: CheckInventoryRow): DetailRow {
|
||||||
|
return normalizeDetail({
|
||||||
|
actualPrice: multiplyPrice(inventory.availableQuantity, inventory.price),
|
||||||
|
availableQuantity: inventory.availableQuantity,
|
||||||
|
checkQuantity: inventory.availableQuantity,
|
||||||
|
id: undefined,
|
||||||
|
inventoryId: inventory.id,
|
||||||
|
itemCode: inventory.itemCode,
|
||||||
|
itemId: inventory.itemId,
|
||||||
|
itemName: inventory.itemName,
|
||||||
|
price: inventory.price,
|
||||||
|
quantity: inventory.availableQuantity,
|
||||||
|
skuCode: inventory.skuCode,
|
||||||
|
skuId: inventory.skuId,
|
||||||
|
skuName: inventory.skuName,
|
||||||
|
unit: inventory.unit,
|
||||||
|
warehouseId: inventory.warehouseId,
|
||||||
|
warehouseName: inventory.warehouseName,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 构建零库存盘库明细,用于盘点仓库内暂无库存的商品 */
|
||||||
|
function buildZeroInventoryDetail(sku: WmsItemSkuApi.ItemSku): DetailRow {
|
||||||
|
return normalizeDetail({
|
||||||
|
actualPrice: 0,
|
||||||
|
availableQuantity: 0,
|
||||||
|
checkQuantity: 0,
|
||||||
|
id: undefined,
|
||||||
|
inventoryId: undefined,
|
||||||
|
itemCode: sku.itemCode,
|
||||||
|
itemId: sku.itemId,
|
||||||
|
itemName: sku.itemName,
|
||||||
|
price: sku.costPrice,
|
||||||
|
quantity: 0,
|
||||||
|
skuCode: sku.code,
|
||||||
|
skuId: sku.id,
|
||||||
|
skuName: sku.name,
|
||||||
|
unit: sku.unit,
|
||||||
|
warehouseId: formData.value.warehouseId,
|
||||||
|
warehouseName: formData.value.warehouseName,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 设置盘库明细 */
|
||||||
|
function setDetails(list?: WmsCheckOrderDetailApi.CheckOrderDetail[]) {
|
||||||
|
detailSeq = 0;
|
||||||
|
details.value = (list || []).map((detail) => normalizeDetail(detail));
|
||||||
|
void refreshDetailFooter();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 刷新明细合计行 */
|
||||||
|
async function refreshDetailFooter() {
|
||||||
|
await nextTick();
|
||||||
|
await detailTableRef.value?.updateFooter();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 导入当前仓库的全部库存余额 */
|
||||||
|
async function handleImportAllInventory() {
|
||||||
|
if (!formData.value.warehouseId) {
|
||||||
|
ElMessage.warning('请先选择仓库');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (details.value.length > 0) {
|
||||||
|
await confirm('导入仓库库存会覆盖当前盘库明细,是否继续?');
|
||||||
|
}
|
||||||
|
modalApi.lock();
|
||||||
|
try {
|
||||||
|
const inventories = await loadWarehouseInventoryList();
|
||||||
|
details.value = inventories.map((inventory) =>
|
||||||
|
buildDetail({ ...inventory, availableQuantity: inventory.quantity }),
|
||||||
|
);
|
||||||
|
await refreshDetailFooter();
|
||||||
|
} finally {
|
||||||
|
modalApi.unlock();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 打开盘点商品添加弹窗,已导入/已添加 SKU 在选择器内禁选 */
|
||||||
|
function handleAddSkuInventory() {
|
||||||
|
if (!formData.value.warehouseId) {
|
||||||
|
ElMessage.warning('请先选择仓库');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
skuSelectRef.value?.open(getSelectedSkuIds(), {
|
||||||
|
multiple: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 选择商品 SKU */
|
||||||
|
async function handleSelectSku(skus: WmsItemSkuApi.ItemSku[]) {
|
||||||
|
if (skus.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
modalApi.lock();
|
||||||
|
try {
|
||||||
|
const warehouseInventoryMap = await getWarehouseInventoryMap();
|
||||||
|
const selectedSkuIds = new Set(getSelectedSkuIds());
|
||||||
|
let changed = false;
|
||||||
|
for (const sku of skus) {
|
||||||
|
if (!sku.id || selectedSkuIds.has(sku.id)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const inventory = warehouseInventoryMap.get(sku.id);
|
||||||
|
details.value.push(
|
||||||
|
inventory
|
||||||
|
? buildDetail({ ...inventory, availableQuantity: inventory.quantity })
|
||||||
|
: buildZeroInventoryDetail(sku),
|
||||||
|
);
|
||||||
|
selectedSkuIds.add(sku.id);
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
if (changed) {
|
||||||
|
await refreshDetailFooter();
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
modalApi.unlock();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 获得已导入/已添加 SKU 编号,避免重复盘点同一规格 */
|
||||||
|
function getSelectedSkuIds() {
|
||||||
|
return details.value
|
||||||
|
.map((detail) => detail.skuId)
|
||||||
|
.filter((id): id is number => id !== undefined);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 查询当前仓库全部库存余额 */
|
||||||
|
async function loadWarehouseInventoryList(): Promise<CheckInventoryRow[]> {
|
||||||
|
return (await getInventoryList({
|
||||||
|
warehouseId: formData.value.warehouseId!,
|
||||||
|
})) as CheckInventoryRow[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 获得当前仓库 SKU 对应库存余额,用于添加单个 SKU 时带入账面库存 */
|
||||||
|
async function getWarehouseInventoryMap(): Promise<Map<number, CheckInventoryRow>> {
|
||||||
|
const inventories = await loadWarehouseInventoryList();
|
||||||
|
return new Map(
|
||||||
|
inventories
|
||||||
|
.filter((inventory) => !!inventory.skuId)
|
||||||
|
.map((inventory) => [inventory.skuId!, inventory] as const),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 删除商品明细 */
|
||||||
|
function handleDeleteDetail(row: DetailRow) {
|
||||||
|
const index = details.value.findIndex((detail) => detail.seq === row.seq);
|
||||||
|
if (index !== -1) {
|
||||||
|
details.value.splice(index, 1);
|
||||||
|
void refreshDetailFooter();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 明细实盘数量变化 */
|
||||||
|
function handleDetailCheckQuantityChange(detail: DetailRow) {
|
||||||
|
if (detail.price !== undefined && detail.price !== null) {
|
||||||
|
detail.actualPrice = multiplyPrice(detail.checkQuantity, detail.price);
|
||||||
|
} else {
|
||||||
|
detail.price = dividePrice(detail.actualPrice, detail.checkQuantity);
|
||||||
|
}
|
||||||
|
refreshDetailCalculatedFields(detail);
|
||||||
|
void refreshDetailFooter();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 明细单价变化 */
|
||||||
|
function handleDetailPriceChange(detail: DetailRow) {
|
||||||
|
detail.actualPrice = multiplyPrice(detail.checkQuantity, detail.price);
|
||||||
|
refreshDetailCalculatedFields(detail);
|
||||||
|
void refreshDetailFooter();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 明细实际金额变化 */
|
||||||
|
function handleDetailActualPriceChange(detail: DetailRow) {
|
||||||
|
detail.price = dividePrice(detail.actualPrice, detail.checkQuantity);
|
||||||
|
refreshDetailCalculatedFields(detail);
|
||||||
|
void refreshDetailFooter();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 校验商品明细 */
|
||||||
|
function validateDetails(required = false) {
|
||||||
|
if (details.value.length === 0) {
|
||||||
|
if (required) {
|
||||||
|
ElMessage.error('至少包含一条盘库明细');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
for (let index = 0; index < details.value.length; index += 1) {
|
||||||
|
const detail = details.value[index]!;
|
||||||
|
if (
|
||||||
|
detail.checkQuantity === undefined ||
|
||||||
|
detail.checkQuantity === null ||
|
||||||
|
detail.checkQuantity < 0
|
||||||
|
) {
|
||||||
|
ElMessage.error(`第 ${index + 1} 行明细实盘数量不能小于 0`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 构建提交用的明细数据 */
|
||||||
|
function buildSubmitDetails() {
|
||||||
|
return details.value.map((row) => {
|
||||||
|
const {
|
||||||
|
actualPrice: _actualPrice,
|
||||||
|
availableQuantity: _availableQuantity,
|
||||||
|
differencePrice: _differencePrice,
|
||||||
|
differenceQuantity: _differenceQuantity,
|
||||||
|
seq: _seq,
|
||||||
|
...detail
|
||||||
|
} = row;
|
||||||
|
return detail;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 构建提交用的单据数据 */
|
||||||
|
async function buildSubmitData(): Promise<WmsCheckOrderApi.CheckOrder> {
|
||||||
|
const values = (await formApi.getValues()) as WmsCheckOrderApi.CheckOrder;
|
||||||
|
const {
|
||||||
|
actualPrice: _formActualPrice,
|
||||||
|
details: _formDetails,
|
||||||
|
totalPrice: _formTotalPrice,
|
||||||
|
totalQuantity: _formTotalQuantity,
|
||||||
|
...submitValues
|
||||||
|
} = values;
|
||||||
|
const {
|
||||||
|
actualPrice: _actualPrice,
|
||||||
|
details: _details,
|
||||||
|
totalPrice: _totalPrice,
|
||||||
|
totalQuantity: _totalQuantity,
|
||||||
|
...order
|
||||||
|
} = formData.value;
|
||||||
|
return {
|
||||||
|
...order,
|
||||||
|
...submitValues,
|
||||||
|
details: buildSubmitDetails(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 完成盘库 */
|
||||||
|
async function handleFormComplete() {
|
||||||
|
const { valid } = await formApi.validate();
|
||||||
|
if (!valid || !validateDetails(true) || !formData.value?.id) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await confirm('确认完成盘库?完成后将更新库存。');
|
||||||
|
modalApi.lock();
|
||||||
|
try {
|
||||||
|
const data = await buildSubmitData();
|
||||||
|
if (!isEqual(data, originalSubmitData.value)) {
|
||||||
|
await updateCheckOrder(data);
|
||||||
|
}
|
||||||
|
await completeCheckOrder(formData.value.id);
|
||||||
|
await modalApi.close();
|
||||||
|
emit('success');
|
||||||
|
ElMessage.success('盘库成功');
|
||||||
|
} finally {
|
||||||
|
modalApi.unlock();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 作废盘库单 */
|
||||||
|
async function handleFormCancel() {
|
||||||
|
if (!formData.value?.id) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await confirm('确认作废该盘库单?作废后不可恢复。');
|
||||||
|
modalApi.lock();
|
||||||
|
try {
|
||||||
|
await cancelCheckOrder(formData.value.id);
|
||||||
|
await modalApi.close();
|
||||||
|
emit('success');
|
||||||
|
ElMessage.success('作废成功');
|
||||||
|
} finally {
|
||||||
|
modalApi.unlock();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const [Modal, modalApi] = useVbenModal({
|
||||||
|
async onConfirm() {
|
||||||
|
if (selectingWarehouse.value) {
|
||||||
|
await handleWarehouseConfirm();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const { valid } = await formApi.validate();
|
||||||
|
if (!valid || !validateDetails(false) || !isPrepareOrder.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
modalApi.lock();
|
||||||
|
// 提交表单
|
||||||
|
const data = await buildSubmitData();
|
||||||
|
try {
|
||||||
|
await (formMode.value === 'update'
|
||||||
|
? updateCheckOrder(data)
|
||||||
|
: createCheckOrder(data));
|
||||||
|
// 关闭并提示
|
||||||
|
await modalApi.close();
|
||||||
|
emit('success');
|
||||||
|
ElMessage.success($t('ui.actionMessage.operationSuccess'));
|
||||||
|
} finally {
|
||||||
|
modalApi.unlock();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async onOpenChange(isOpen: boolean) {
|
||||||
|
if (!isOpen) {
|
||||||
|
formData.value = {};
|
||||||
|
originalSubmitData.value = undefined;
|
||||||
|
selectingWarehouse.value = false;
|
||||||
|
warehouseName.value = undefined;
|
||||||
|
setDetails([]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await formApi.resetForm();
|
||||||
|
const data = modalApi.getData<{
|
||||||
|
id?: number;
|
||||||
|
type?: string;
|
||||||
|
}>();
|
||||||
|
formMode.value = data?.type || (data?.id ? 'update' : 'create');
|
||||||
|
if (data?.id) {
|
||||||
|
modalApi.lock();
|
||||||
|
try {
|
||||||
|
// 加载数据
|
||||||
|
const order = await getCheckOrder(data.id);
|
||||||
|
const orderDetails =
|
||||||
|
order.details || (await getCheckOrderDetailListByOrderId(data.id));
|
||||||
|
formData.value = { ...order, details: orderDetails };
|
||||||
|
setDetails(orderDetails);
|
||||||
|
// 设置到 values
|
||||||
|
await formApi.setValues(formData.value);
|
||||||
|
await nextTick();
|
||||||
|
syncActualPriceField();
|
||||||
|
originalSubmitData.value = await buildSubmitData();
|
||||||
|
} finally {
|
||||||
|
modalApi.unlock();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 新增时先选择盘库仓库,再进入主表单
|
||||||
|
selectingWarehouse.value = true;
|
||||||
|
warehouseName.value = undefined;
|
||||||
|
formData.value = {};
|
||||||
|
setDetails([]);
|
||||||
|
await warehouseFormApi.resetForm();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Modal
|
||||||
|
:title="modalTitle"
|
||||||
|
:class="modalClass"
|
||||||
|
:show-confirm-button="selectingWarehouse || isPrepareOrder"
|
||||||
|
>
|
||||||
|
<template v-if="selectingWarehouse" #confirmText>开始盘库</template>
|
||||||
|
<WarehouseForm v-if="selectingWarehouse" class="mx-4" />
|
||||||
|
<div v-else class="mx-4">
|
||||||
|
<Form />
|
||||||
|
<div class="mt-4">
|
||||||
|
<div class="mb-3 flex items-center justify-between">
|
||||||
|
<span class="text-sm font-semibold">盘库明细</span>
|
||||||
|
<TableAction
|
||||||
|
:actions="[
|
||||||
|
{
|
||||||
|
label: '导入仓库库存',
|
||||||
|
disabled: !formData.warehouseId,
|
||||||
|
onClick: handleImportAllInventory,
|
||||||
|
type: 'primary',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '添加盘点商品',
|
||||||
|
disabled: !formData.warehouseId,
|
||||||
|
onClick: handleAddSkuInventory,
|
||||||
|
type: 'primary',
|
||||||
|
},
|
||||||
|
]"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<VxeTable
|
||||||
|
ref="detailTableRef"
|
||||||
|
:data="details"
|
||||||
|
border
|
||||||
|
empty-text="暂无商品明细"
|
||||||
|
:footer-method="getCheckDetailFooter"
|
||||||
|
:show-overflow="true"
|
||||||
|
show-footer
|
||||||
|
size="small"
|
||||||
|
>
|
||||||
|
<VxeColumn title="商品信息" min-width="210">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<div>{{ row.itemName || '-' }}</div>
|
||||||
|
<div v-if="row.itemCode" class="text-xs text-gray-500">
|
||||||
|
商品编号:{{ row.itemCode }}
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</VxeColumn>
|
||||||
|
<VxeColumn title="规格信息" min-width="210">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<div>{{ row.skuName || '-' }}</div>
|
||||||
|
<div v-if="row.skuCode" class="text-xs text-gray-500">
|
||||||
|
规格编号:{{ row.skuCode }}
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</VxeColumn>
|
||||||
|
<VxeColumn field="quantity" title="账面库存" align="right" width="120">
|
||||||
|
<template #default="{ row }">
|
||||||
|
{{ formatQuantity(row.quantity) || '-' }}
|
||||||
|
</template>
|
||||||
|
</VxeColumn>
|
||||||
|
<VxeColumn field="price" title="单价(元)" width="150">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<ElInputNumber
|
||||||
|
v-model="row.price"
|
||||||
|
:controls="false"
|
||||||
|
:min="0"
|
||||||
|
:precision="PRICE_PRECISION"
|
||||||
|
class="!w-full"
|
||||||
|
placeholder="单价"
|
||||||
|
@change="handleDetailPriceChange(row)"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</VxeColumn>
|
||||||
|
<VxeColumn field="checkQuantity" title="实际库存" width="150">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<ElInputNumber
|
||||||
|
v-model="row.checkQuantity"
|
||||||
|
:controls="false"
|
||||||
|
:min="0"
|
||||||
|
:precision="QUANTITY_PRECISION"
|
||||||
|
class="!w-full"
|
||||||
|
placeholder="数量"
|
||||||
|
@change="handleDetailCheckQuantityChange(row)"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</VxeColumn>
|
||||||
|
<VxeColumn field="actualPrice" title="实际金额(元)" width="150">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<ElInputNumber
|
||||||
|
v-model="row.actualPrice"
|
||||||
|
:controls="false"
|
||||||
|
:min="0"
|
||||||
|
:precision="PRICE_PRECISION"
|
||||||
|
class="!w-full"
|
||||||
|
placeholder="金额"
|
||||||
|
@change="handleDetailActualPriceChange(row)"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</VxeColumn>
|
||||||
|
<VxeColumn
|
||||||
|
field="differenceQuantity"
|
||||||
|
title="盈亏数"
|
||||||
|
align="right"
|
||||||
|
width="120"
|
||||||
|
>
|
||||||
|
<template #default="{ row }">
|
||||||
|
<span :class="getLossClass(row.differenceQuantity)">
|
||||||
|
{{ formatQuantity(row.differenceQuantity) }}
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
</VxeColumn>
|
||||||
|
<VxeColumn
|
||||||
|
field="differencePrice"
|
||||||
|
title="实际盈亏金额(元)"
|
||||||
|
align="right"
|
||||||
|
width="160"
|
||||||
|
>
|
||||||
|
<template #default="{ row }">
|
||||||
|
<span :class="getLossClass(row.differencePrice)">
|
||||||
|
{{ formatPrice(row.differencePrice) || '-' }}
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
</VxeColumn>
|
||||||
|
<VxeColumn title="操作" align="center" fixed="right" width="90">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<TableAction
|
||||||
|
:actions="[
|
||||||
|
{
|
||||||
|
label: '删除',
|
||||||
|
type: 'danger',
|
||||||
|
link: true,
|
||||||
|
onClick: handleDeleteDetail.bind(null, row),
|
||||||
|
},
|
||||||
|
]"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</VxeColumn>
|
||||||
|
</VxeTable>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<template #prepend-footer>
|
||||||
|
<div
|
||||||
|
v-if="!selectingWarehouse && isSavedPrepareOrder"
|
||||||
|
class="flex flex-auto items-center gap-2"
|
||||||
|
>
|
||||||
|
<TableAction
|
||||||
|
:actions="[
|
||||||
|
{
|
||||||
|
label: '完成盘库',
|
||||||
|
type: 'primary',
|
||||||
|
auth: ['wms:check-order:complete'],
|
||||||
|
onClick: handleFormComplete,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '作废',
|
||||||
|
type: 'danger',
|
||||||
|
auth: ['wms:check-order:cancel'],
|
||||||
|
onClick: handleFormCancel,
|
||||||
|
},
|
||||||
|
]"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<WmsItemSkuSelect ref="skuSelectRef" @change="handleSelectSku" />
|
||||||
|
</Modal>
|
||||||
|
</template>
|
||||||
|
|
@ -0,0 +1,296 @@
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import type { WmsCheckOrderApi } from '#/api/wms/order/check';
|
||||||
|
import type { WmsCheckOrderDetailApi } from '#/api/wms/order/check/detail';
|
||||||
|
|
||||||
|
import { computed, nextTick, ref } from 'vue';
|
||||||
|
|
||||||
|
import { Barcode, BarcodeFormatEnum } from '@vben/common-ui';
|
||||||
|
import { DICT_TYPE } from '@vben/constants';
|
||||||
|
import { getDictLabel } from '@vben/hooks';
|
||||||
|
import { formatDate, formatDateTime } from '@vben/utils';
|
||||||
|
|
||||||
|
import {
|
||||||
|
getCheckOrder,
|
||||||
|
getCheckOrderDetailListByOrderId,
|
||||||
|
} from '#/api/wms/order/check';
|
||||||
|
import {
|
||||||
|
formatPrice,
|
||||||
|
formatQuantity,
|
||||||
|
formatSumPrice,
|
||||||
|
formatSumQuantity,
|
||||||
|
getLossClass,
|
||||||
|
sumPrice,
|
||||||
|
sumQuantity,
|
||||||
|
} from '#/views/wms/utils/format';
|
||||||
|
|
||||||
|
import {
|
||||||
|
getDetailActualPrice,
|
||||||
|
getDetailDifferencePrice,
|
||||||
|
getDetailDifferenceQuantity,
|
||||||
|
getOrderDifferencePrice,
|
||||||
|
} from '../data';
|
||||||
|
|
||||||
|
interface PrintRow extends WmsCheckOrderDetailApi.CheckOrderDetail {
|
||||||
|
actualPrice?: number;
|
||||||
|
differencePrice?: number;
|
||||||
|
differenceQuantity?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
defineOptions({ name: 'WmsCheckOrderPrint' });
|
||||||
|
|
||||||
|
const printData = ref<WmsCheckOrderApi.CheckOrder>({});
|
||||||
|
const tableColumnCount = 8;
|
||||||
|
|
||||||
|
/** 打印明细补齐实际金额和盈亏字段,避免模板重复计算 */
|
||||||
|
const printRows = computed<PrintRow[]>(() =>
|
||||||
|
(printData.value.details || []).map((detail) => ({
|
||||||
|
...detail,
|
||||||
|
actualPrice: getDetailActualPrice(detail),
|
||||||
|
differencePrice: getDetailDifferencePrice(detail),
|
||||||
|
differenceQuantity: getDetailDifferenceQuantity(detail),
|
||||||
|
})),
|
||||||
|
);
|
||||||
|
|
||||||
|
/** 打印合计盈亏数量 */
|
||||||
|
const totalDifferenceQuantity = computed(() =>
|
||||||
|
sumQuantity(printRows.value, (detail) => detail.differenceQuantity),
|
||||||
|
);
|
||||||
|
/** 打印合计盈亏金额 */
|
||||||
|
const totalDifferencePrice = computed(() =>
|
||||||
|
sumPrice(printRows.value, (detail) => detail.differencePrice),
|
||||||
|
);
|
||||||
|
|
||||||
|
/** 等待条码和打印 DOM 完成绘制,避免浏览器打印到旧内容 */
|
||||||
|
function waitForPaint() {
|
||||||
|
return new Promise<void>((resolve) => {
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
requestAnimationFrame(() => resolve());
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 退出打印模式,恢复当前页面显示 */
|
||||||
|
function removePrintMode() {
|
||||||
|
document.body.classList.remove('wms-check-order-printing');
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 获取打印用字典文案,空值统一显示为横杠 */
|
||||||
|
function getPrintDictLabel(dictType: string, value?: number) {
|
||||||
|
if (value === undefined || value === null) {
|
||||||
|
return '-';
|
||||||
|
}
|
||||||
|
return getDictLabel(dictType, value) || '-';
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 打印盘库单:加载数据后只展示打印区域,再调用浏览器打印 */
|
||||||
|
async function print(id: number) {
|
||||||
|
const order = await getCheckOrder(id);
|
||||||
|
const details = order.details || (await getCheckOrderDetailListByOrderId(id));
|
||||||
|
printData.value = { ...order, details };
|
||||||
|
await nextTick();
|
||||||
|
await waitForPaint();
|
||||||
|
document.body.classList.add('wms-check-order-printing');
|
||||||
|
window.addEventListener('afterprint', removePrintMode, { once: true });
|
||||||
|
window.print();
|
||||||
|
}
|
||||||
|
|
||||||
|
defineExpose({ print });
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Teleport to="body">
|
||||||
|
<div
|
||||||
|
id="wmsCheckOrderPrint"
|
||||||
|
class="wms-check-order-print pointer-events-none fixed left-0 top-0 z-[-1] w-full bg-white text-[#303133] opacity-0"
|
||||||
|
>
|
||||||
|
<div class="relative mb-2">
|
||||||
|
<h2 class="m-0 text-center text-[1.5em] font-bold leading-[1.2]">
|
||||||
|
盘库单
|
||||||
|
</h2>
|
||||||
|
<div v-if="printData.no" class="absolute right-0 top-0">
|
||||||
|
<Barcode
|
||||||
|
:content="printData.no"
|
||||||
|
:display-value="false"
|
||||||
|
:format="BarcodeFormatEnum.CODE39"
|
||||||
|
:height="40"
|
||||||
|
:width="180"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3 grid grid-cols-3 gap-x-6 gap-y-2 text-sm leading-[1.5]">
|
||||||
|
<div>盘库单号:{{ printData.no || '-' }}</div>
|
||||||
|
<div>仓库:{{ printData.warehouseName || '-' }}</div>
|
||||||
|
<div>
|
||||||
|
盘库状态:{{ getPrintDictLabel(DICT_TYPE.WMS_ORDER_STATUS, printData.status) }}
|
||||||
|
</div>
|
||||||
|
<div>单据日期:{{ formatDate(printData.orderTime, 'YYYY-MM-DD') || '-' }}</div>
|
||||||
|
<div>
|
||||||
|
盈亏数量:
|
||||||
|
<span :class="getLossClass(printData.totalQuantity)">
|
||||||
|
{{ formatQuantity(printData.totalQuantity) || '-' }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div>总金额:{{ formatPrice(printData.totalPrice) || '-' }}</div>
|
||||||
|
<div>实际金额:{{ formatPrice(printData.actualPrice) || '-' }}</div>
|
||||||
|
<div>
|
||||||
|
实际盈亏金额:
|
||||||
|
<span :class="getLossClass(getOrderDifferencePrice(printData))">
|
||||||
|
{{ formatPrice(getOrderDifferencePrice(printData)) || '-' }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="col-span-3 grid grid-cols-2 gap-x-6">
|
||||||
|
<div>
|
||||||
|
创建:{{ formatDateTime(printData.createTime) || '-' }} /
|
||||||
|
{{ printData.creatorName || printData.creator || '-' }}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
更新:{{ formatDateTime(printData.updateTime) || '-' }} /
|
||||||
|
{{ printData.updaterName || printData.updater || '-' }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-span-3">备注:{{ printData.remark || '-' }}</div>
|
||||||
|
</div>
|
||||||
|
<table class="w-full border-collapse text-[13px] leading-[1.5]">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th class="border border-solid border-[#dcdfe6] bg-[#f5f7fa] p-2 text-left font-bold">
|
||||||
|
商品信息
|
||||||
|
</th>
|
||||||
|
<th class="border border-solid border-[#dcdfe6] bg-[#f5f7fa] p-2 text-left font-bold">
|
||||||
|
规格信息
|
||||||
|
</th>
|
||||||
|
<th class="border border-solid border-[#dcdfe6] bg-[#f5f7fa] p-2 text-left font-bold">
|
||||||
|
账面库存
|
||||||
|
</th>
|
||||||
|
<th class="border border-solid border-[#dcdfe6] bg-[#f5f7fa] p-2 text-left font-bold">
|
||||||
|
单价(元)
|
||||||
|
</th>
|
||||||
|
<th class="border border-solid border-[#dcdfe6] bg-[#f5f7fa] p-2 text-left font-bold">
|
||||||
|
实际库存
|
||||||
|
</th>
|
||||||
|
<th class="border border-solid border-[#dcdfe6] bg-[#f5f7fa] p-2 text-left font-bold">
|
||||||
|
实际金额(元)
|
||||||
|
</th>
|
||||||
|
<th class="border border-solid border-[#dcdfe6] bg-[#f5f7fa] p-2 text-left font-bold">
|
||||||
|
盈亏数
|
||||||
|
</th>
|
||||||
|
<th class="border border-solid border-[#dcdfe6] bg-[#f5f7fa] p-2 text-left font-bold">
|
||||||
|
实际盈亏金额(元)
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr v-for="detail in printRows" :key="detail.id || detail.skuId">
|
||||||
|
<td class="border border-solid border-[#dcdfe6] p-2">
|
||||||
|
<div>{{ detail.itemName || '-' }}</div>
|
||||||
|
<div v-if="detail.itemCode" class="text-xs">
|
||||||
|
编号:{{ detail.itemCode }}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td class="border border-solid border-[#dcdfe6] p-2">
|
||||||
|
<div>{{ detail.skuName || '-' }}</div>
|
||||||
|
<div v-if="detail.skuCode" class="text-xs">
|
||||||
|
编号:{{ detail.skuCode }}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td class="border border-solid border-[#dcdfe6] p-2 text-right">
|
||||||
|
{{ formatQuantity(detail.quantity) || '-' }}
|
||||||
|
</td>
|
||||||
|
<td class="border border-solid border-[#dcdfe6] p-2 text-right">
|
||||||
|
{{ formatPrice(detail.price) || '-' }}
|
||||||
|
</td>
|
||||||
|
<td class="border border-solid border-[#dcdfe6] p-2 text-right">
|
||||||
|
{{ formatQuantity(detail.checkQuantity) || '-' }}
|
||||||
|
</td>
|
||||||
|
<td class="border border-solid border-[#dcdfe6] p-2 text-right">
|
||||||
|
{{ formatPrice(detail.actualPrice) || '-' }}
|
||||||
|
</td>
|
||||||
|
<td class="border border-solid border-[#dcdfe6] p-2 text-right">
|
||||||
|
<span :class="getLossClass(detail.differenceQuantity)">
|
||||||
|
{{ formatQuantity(detail.differenceQuantity) || '-' }}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td class="border border-solid border-[#dcdfe6] p-2 text-right">
|
||||||
|
<span :class="getLossClass(detail.differencePrice)">
|
||||||
|
{{ formatPrice(detail.differencePrice) || '-' }}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr v-if="printRows.length > 0">
|
||||||
|
<td
|
||||||
|
class="border border-solid border-[#dcdfe6] bg-[#f5f7fa] p-2"
|
||||||
|
colspan="2"
|
||||||
|
>
|
||||||
|
合计
|
||||||
|
</td>
|
||||||
|
<td class="border border-solid border-[#dcdfe6] bg-[#f5f7fa] p-2 text-right">
|
||||||
|
{{ formatSumQuantity(printRows, (detail) => detail.quantity) }}
|
||||||
|
</td>
|
||||||
|
<td class="border border-solid border-[#dcdfe6] bg-[#f5f7fa] p-2 text-right"></td>
|
||||||
|
<td class="border border-solid border-[#dcdfe6] bg-[#f5f7fa] p-2 text-right">
|
||||||
|
{{ formatSumQuantity(printRows, (detail) => detail.checkQuantity) }}
|
||||||
|
</td>
|
||||||
|
<td class="border border-solid border-[#dcdfe6] bg-[#f5f7fa] p-2 text-right">
|
||||||
|
{{ formatSumPrice(printRows, (detail) => detail.actualPrice) }}
|
||||||
|
</td>
|
||||||
|
<td class="border border-solid border-[#dcdfe6] bg-[#f5f7fa] p-2 text-right">
|
||||||
|
<span :class="getLossClass(totalDifferenceQuantity)">
|
||||||
|
{{ formatQuantity(totalDifferenceQuantity) }}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td class="border border-solid border-[#dcdfe6] bg-[#f5f7fa] p-2 text-right">
|
||||||
|
<span :class="getLossClass(totalDifferencePrice)">
|
||||||
|
{{ formatPrice(totalDifferencePrice) }}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr v-if="printRows.length === 0">
|
||||||
|
<td
|
||||||
|
class="border border-solid border-[#dcdfe6] p-2 text-center"
|
||||||
|
:colspan="tableColumnCount"
|
||||||
|
>
|
||||||
|
暂无明细
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</Teleport>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
@page {
|
||||||
|
margin: 8mm 10mm;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media print {
|
||||||
|
:global(body.wms-check-order-printing) {
|
||||||
|
-webkit-print-color-adjust: exact;
|
||||||
|
margin: 0 !important;
|
||||||
|
padding: 0 !important;
|
||||||
|
print-color-adjust: exact;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(body.wms-check-order-printing *) {
|
||||||
|
visibility: hidden !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(body.wms-check-order-printing .wms-check-order-print),
|
||||||
|
:global(body.wms-check-order-printing .wms-check-order-print *) {
|
||||||
|
visibility: visible !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(body.wms-check-order-printing .wms-check-order-print) {
|
||||||
|
pointer-events: auto;
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
z-index: auto;
|
||||||
|
box-sizing: border-box;
|
||||||
|
width: 100%;
|
||||||
|
margin: 0 !important;
|
||||||
|
padding: 0 !important;
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
Loading…
Reference in New Issue