feat(@vben/web-antd): erp-sale-out 新增销售出库管理功能,包括出库列表、表单及相关操作
							parent
							
								
									e84df50f7b
								
							
						
					
					
						commit
						765bb2ef5a
					
				|  | @ -8,6 +8,7 @@ export namespace ErpSaleOrderApi { | |||
|     id?: number; // 订单工单编号
 | ||||
|     no: string; // 销售订单号
 | ||||
|     customerId: number; // 客户编号
 | ||||
|     accountId?: number; // 收款账户编号
 | ||||
|     orderTime: Date; // 订单时间
 | ||||
|     totalCount: number; // 合计数量
 | ||||
|     totalPrice: number; // 合计金额,单位:元
 | ||||
|  |  | |||
|  | @ -2,17 +2,45 @@ import type { PageParam, PageResult } from '@vben/request'; | |||
| 
 | ||||
| import { requestClient } from '#/api/request'; | ||||
| 
 | ||||
| namespace ErpSaleOutApi { | ||||
| export namespace ErpSaleOutApi { | ||||
|   /** 销售出库信息 */ | ||||
|   export interface SaleOut { | ||||
|     id?: number; // 销售出库编号
 | ||||
|     no: string; // 销售出库号
 | ||||
|     customerId: number; // 客户编号
 | ||||
|     outTime: Date; // 出库时间
 | ||||
|     totalCount: number; // 合计数量
 | ||||
|     totalPrice: number; // 合计金额,单位:元
 | ||||
|     status: number; // 状态
 | ||||
|     remark: string; // 备注
 | ||||
|     no?: string; // 销售出库号
 | ||||
|     customerId?: number; // 客户编号
 | ||||
|     saleUserId?: number; // 客户编号
 | ||||
|     outTime?: Date; // 出库时间
 | ||||
|     totalCount?: number; // 合计数量
 | ||||
|     totalPrice?: number; // 合计金额,单位:元
 | ||||
|     status?: number; // 状态
 | ||||
|     remark?: string; // 备注
 | ||||
|     discountPercent?: number; // 折扣百分比
 | ||||
|     discountPrice?: number; // 折扣金额
 | ||||
|     otherPrice?: number; // 其他费用
 | ||||
|     totalProductPrice?: number; // 合计商品金额
 | ||||
|     taxPrice?: number; // 合计税额
 | ||||
|     totalTaxPrice?: number; // 合计税额
 | ||||
|     fileUrl?: string; // 附件地址
 | ||||
|     items?: SaleOutItem[]; | ||||
|   } | ||||
|   export interface SaleOutItem { | ||||
|     count?: number; | ||||
|     id?: number; | ||||
|     orderItemId?: number; | ||||
|     productBarCode?: string; | ||||
|     productId?: number; | ||||
|     productName: string; | ||||
|     productPrice: number; | ||||
|     productUnitId?: number; | ||||
|     productUnitName?: string; | ||||
|     totalProductPrice?: number; | ||||
|     remark: string; | ||||
|     stockCount?: number; | ||||
|     taxPercent?: number; | ||||
|     taxPrice?: number; | ||||
|     totalPrice?: number; | ||||
|     warehouseId?: number; | ||||
|     outCount?: number; | ||||
|   } | ||||
| 
 | ||||
|   /** 销售出库分页查询参数 */ | ||||
|  |  | |||
|  | @ -0,0 +1,591 @@ | |||
| import type { VbenFormSchema } from '#/adapter/form'; | ||||
| import type { VxeTableGridOptions } from '#/adapter/vxe-table'; | ||||
| 
 | ||||
| import { erpNumberFormatter, erpPriceInputFormatter } from '@vben/utils'; | ||||
| 
 | ||||
| import { z } from '#/adapter/form'; | ||||
| import { getAccountSimpleList } from '#/api/erp/finance/account'; | ||||
| import { getProductSimpleList } from '#/api/erp/product/product'; | ||||
| import { getCustomerSimpleList } from '#/api/erp/sale/customer'; | ||||
| import { getWarehouseSimpleList } from '#/api/erp/stock/warehouse'; | ||||
| import { getSimpleUserList } from '#/api/system/user'; | ||||
| import { DICT_TYPE, getDictOptions } from '#/utils'; | ||||
| 
 | ||||
| /** 表单的配置项 */ | ||||
| export function useFormSchema(formType: string): VbenFormSchema[] { | ||||
|   return [ | ||||
|     { | ||||
|       component: 'Input', | ||||
|       componentProps: { | ||||
|         style: { display: 'none' }, | ||||
|       }, | ||||
|       fieldName: 'id', | ||||
|       label: 'ID', | ||||
|       hideLabel: true, | ||||
|       formItemClass: 'hidden', | ||||
|     }, | ||||
|     { | ||||
|       component: 'Input', | ||||
|       componentProps: { | ||||
|         placeholder: '系统自动生成', | ||||
|         disabled: true, | ||||
|       }, | ||||
|       fieldName: 'no', | ||||
|       label: '出库单号', | ||||
|     }, | ||||
|     { | ||||
|       component: 'ApiSelect', | ||||
|       componentProps: { | ||||
|         disabled: true, | ||||
|         placeholder: '请选择客户', | ||||
|         allowClear: true, | ||||
|         showSearch: true, | ||||
|         api: getCustomerSimpleList, | ||||
|         fieldNames: { | ||||
|           label: 'name', | ||||
|           value: 'id', | ||||
|         }, | ||||
|       }, | ||||
|       fieldName: 'customerId', | ||||
|       label: '客户', | ||||
|       rules: 'required', | ||||
|     }, | ||||
|     { | ||||
|       fieldName: 'orderNo', | ||||
|       label: '关联订单', | ||||
|       component: 'Input', | ||||
|       formItemClass: 'col-span-1', | ||||
|       rules: 'required', | ||||
|       componentProps: { | ||||
|         placeholder: '请选择关联订单', | ||||
|         disabled: formType === 'detail', | ||||
|       }, | ||||
|     }, | ||||
|     { | ||||
|       component: 'DatePicker', | ||||
|       componentProps: { | ||||
|         disabled: formType === 'detail', | ||||
|         placeholder: '选择出库时间', | ||||
|         showTime: true, | ||||
|         format: 'YYYY-MM-DD HH:mm:ss', | ||||
|         valueFormat: 'x', | ||||
|         style: { width: '100%' }, | ||||
|       }, | ||||
|       fieldName: 'outTime', | ||||
|       label: '出库时间', | ||||
|       rules: 'required', | ||||
|     }, | ||||
|     { | ||||
|       component: 'Textarea', | ||||
|       componentProps: { | ||||
|         placeholder: '请输入备注', | ||||
|         disabled: formType === 'detail', | ||||
|         autoSize: { minRows: 1, maxRows: 1 }, | ||||
|         class: 'w-full', | ||||
|       }, | ||||
|       fieldName: 'remark', | ||||
|       formItemClass: 'col-span-2', | ||||
|       label: '备注', | ||||
|     }, | ||||
|     { | ||||
|       component: 'FileUpload', | ||||
|       componentProps: { | ||||
|         disabled: formType === 'detail', | ||||
|         maxNumber: 1, | ||||
|         maxSize: 10, | ||||
|         accept: [ | ||||
|           'pdf', | ||||
|           'doc', | ||||
|           'docx', | ||||
|           'xls', | ||||
|           'xlsx', | ||||
|           'txt', | ||||
|           'jpg', | ||||
|           'jpeg', | ||||
|           'png', | ||||
|         ], | ||||
|         showDescription: true, | ||||
|       }, | ||||
|       fieldName: 'fileUrl', | ||||
|       label: '附件', | ||||
|       formItemClass: 'col-span-3', | ||||
|     }, | ||||
|     { | ||||
|       fieldName: 'product', | ||||
|       label: '产品清单', | ||||
|       component: 'Input', | ||||
|       formItemClass: 'col-span-3', | ||||
|     }, | ||||
|     { | ||||
|       component: 'InputNumber', | ||||
|       fieldName: 'discountPercent', | ||||
|       componentProps: { | ||||
|         placeholder: '优惠率', | ||||
|         min: 0, | ||||
|         max: 100, | ||||
|         disabled: true, | ||||
|         precision: 2, | ||||
|         style: { width: '100%' }, | ||||
|       }, | ||||
| 
 | ||||
|       label: '优惠率(%)', | ||||
|     }, | ||||
|     { | ||||
|       component: 'InputNumber', | ||||
|       componentProps: { | ||||
|         placeholder: '付款优惠', | ||||
|         precision: 2, | ||||
|         formatter: erpPriceInputFormatter, | ||||
|         disabled: true, | ||||
|         style: { width: '100%' }, | ||||
|       }, | ||||
|       fieldName: 'discountPrice', | ||||
|       label: '付款优惠', | ||||
|     }, | ||||
|     { | ||||
|       component: 'InputNumber', | ||||
|       componentProps: { | ||||
|         placeholder: '优惠后金额', | ||||
|         precision: 2, | ||||
|         formatter: erpPriceInputFormatter, | ||||
|         disabled: true, | ||||
|         style: { width: '100%' }, | ||||
|       }, | ||||
|       fieldName: 'discountedPrice', | ||||
|       label: '优惠后金额', | ||||
|     }, | ||||
|     { | ||||
|       component: 'InputNumber', | ||||
|       componentProps: { | ||||
|         disabled: formType === 'detail', | ||||
|         placeholder: '请输入其他费用', | ||||
|         precision: 2, | ||||
|         formatter: erpPriceInputFormatter, | ||||
|         style: { width: '100%' }, | ||||
|       }, | ||||
|       fieldName: 'otherPrice', | ||||
|       label: '其他费用', | ||||
|     }, | ||||
|     { | ||||
|       component: 'ApiSelect', | ||||
|       componentProps: { | ||||
|         placeholder: '请选择结算账户', | ||||
|         disabled: true, | ||||
|         allowClear: true, | ||||
|         showSearch: true, | ||||
|         api: getAccountSimpleList, | ||||
|         fieldNames: { | ||||
|           label: 'name', | ||||
|           value: 'id', | ||||
|         }, | ||||
|       }, | ||||
|       fieldName: 'accountId', | ||||
|       label: '结算账户', | ||||
|     }, | ||||
|     { | ||||
|       component: 'InputNumber', | ||||
|       componentProps: { | ||||
|         precision: 2, | ||||
|         style: { width: '100%' }, | ||||
|         disabled: true, | ||||
|         min: 0, | ||||
|       }, | ||||
|       fieldName: 'totalPrice', | ||||
|       label: '应收金额', | ||||
|       rules: z.number().min(0).optional(), | ||||
|     }, | ||||
|   ]; | ||||
| } | ||||
| 
 | ||||
| /** 采购订单项表格列定义 */ | ||||
| export function useSaleOutItemTableColumns(): VxeTableGridOptions['columns'] { | ||||
|   return [ | ||||
|     { type: 'seq', title: '序号', minWidth: 50, fixed: 'left' }, | ||||
|     { | ||||
|       field: 'warehouseId', | ||||
|       title: '仓库名称', | ||||
|       minWidth: 200, | ||||
|       slots: { default: 'warehouseId' }, | ||||
|     }, | ||||
|     { | ||||
|       field: 'productId', | ||||
|       title: '产品名称', | ||||
|       minWidth: 200, | ||||
|       slots: { default: 'productId' }, | ||||
|     }, | ||||
|     { | ||||
|       field: 'stockCount', | ||||
|       title: '仓库库存', | ||||
|       minWidth: 80, | ||||
|     }, | ||||
|     { | ||||
|       field: 'productBarCode', | ||||
|       title: '条码', | ||||
|       minWidth: 120, | ||||
|     }, | ||||
|     { | ||||
|       field: 'productUnitName', | ||||
|       title: '单位', | ||||
|       minWidth: 80, | ||||
|     }, | ||||
|     { | ||||
|       field: 'totalCount', | ||||
|       title: '原数量', | ||||
|       minWidth: 120, | ||||
|     }, | ||||
|     { | ||||
|       field: 'outCount', | ||||
|       title: '已出库数量', | ||||
|       minWidth: 120, | ||||
|     }, | ||||
|     { | ||||
|       field: 'count', | ||||
|       title: '数量', | ||||
|       minWidth: 120, | ||||
|       fixed: 'right', | ||||
|       slots: { default: 'count' }, | ||||
|     }, | ||||
|     { | ||||
|       field: 'productPrice', | ||||
|       title: '产品单价', | ||||
|       fixed: 'right', | ||||
|       minWidth: 120, | ||||
|       slots: { default: 'productPrice' }, | ||||
|     }, | ||||
|     { | ||||
|       field: 'totalProductPrice', | ||||
|       fixed: 'right', | ||||
|       title: '产品金额', | ||||
|       minWidth: 120, | ||||
|       formatter: 'formatAmount2', | ||||
|     }, | ||||
|     { | ||||
|       fixed: 'right', | ||||
|       field: 'taxPercent', | ||||
|       title: '税率(%)', | ||||
|       minWidth: 100, | ||||
|     }, | ||||
|     { | ||||
|       fixed: 'right', | ||||
|       field: 'taxPrice', | ||||
|       title: '税额', | ||||
|       minWidth: 120, | ||||
|       formatter: 'formatAmount2', | ||||
|     }, | ||||
|     { | ||||
|       field: 'totalPrice', | ||||
|       fixed: 'right', | ||||
|       title: '合计金额', | ||||
|       minWidth: 120, | ||||
|       formatter: 'formatAmount2', | ||||
|     }, | ||||
|   ]; | ||||
| } | ||||
| 
 | ||||
| /** 列表的搜索表单 */ | ||||
| export function useGridFormSchema(): VbenFormSchema[] { | ||||
|   return [ | ||||
|     { | ||||
|       fieldName: 'no', | ||||
|       label: '出库单号', | ||||
|       component: 'Input', | ||||
|       componentProps: { | ||||
|         placeholder: '请输入出库单号', | ||||
|         allowClear: true, | ||||
|       }, | ||||
|     }, | ||||
|     { | ||||
|       fieldName: 'productId', | ||||
|       label: '产品', | ||||
|       component: 'ApiSelect', | ||||
|       componentProps: { | ||||
|         placeholder: '请选择产品', | ||||
|         allowClear: true, | ||||
|         showSearch: true, | ||||
|         api: getProductSimpleList, | ||||
|         fieldNames: { | ||||
|           label: 'name', | ||||
|           value: 'id', | ||||
|         }, | ||||
|       }, | ||||
|     }, | ||||
|     { | ||||
|       fieldName: 'outTime', | ||||
|       label: '出库时间', | ||||
|       component: 'RangePicker', | ||||
|       componentProps: { | ||||
|         placeholder: ['开始时间', '结束时间'], | ||||
|         showTime: true, | ||||
|         format: 'YYYY-MM-DD HH:mm:ss', | ||||
|         valueFormat: 'YYYY-MM-DD HH:mm:ss', | ||||
|       }, | ||||
|     }, | ||||
|     { | ||||
|       fieldName: 'customerId', | ||||
|       label: '客户', | ||||
|       component: 'ApiSelect', | ||||
|       componentProps: { | ||||
|         placeholder: '请选择客户', | ||||
|         allowClear: true, | ||||
|         showSearch: true, | ||||
|         api: getCustomerSimpleList, | ||||
|         fieldNames: { | ||||
|           label: 'name', | ||||
|           value: 'id', | ||||
|         }, | ||||
|       }, | ||||
|     }, | ||||
|     { | ||||
|       fieldName: 'warehouseId', | ||||
|       label: '仓库', | ||||
|       component: 'ApiSelect', | ||||
|       componentProps: { | ||||
|         placeholder: '请选择仓库', | ||||
|         allowClear: true, | ||||
|         showSearch: true, | ||||
|         api: getWarehouseSimpleList, | ||||
|         labelField: 'name', | ||||
|         valueField: 'id', | ||||
|         filterOption: false, | ||||
|       }, | ||||
|     }, | ||||
|     { | ||||
|       fieldName: 'creator', | ||||
|       label: '创建人', | ||||
|       component: 'ApiSelect', | ||||
|       componentProps: { | ||||
|         placeholder: '请选择创建人', | ||||
|         allowClear: true, | ||||
|         showSearch: true, | ||||
|         api: getSimpleUserList, | ||||
|         fieldNames: { | ||||
|           label: 'nickname', | ||||
|           value: 'id', | ||||
|         }, | ||||
|       }, | ||||
|     }, | ||||
|     { | ||||
|       fieldName: 'orderNo', | ||||
|       label: '关联订单', | ||||
|       component: 'Input', | ||||
|       componentProps: { | ||||
|         placeholder: '请输入关联订单号', | ||||
|         allowClear: true, | ||||
|       }, | ||||
|     }, | ||||
|     { | ||||
|       fieldName: 'receiptStatus', | ||||
|       label: '收款状态', | ||||
|       component: 'Select', | ||||
|       componentProps: { | ||||
|         options: [ | ||||
|           { label: '未收款', value: 0 }, | ||||
|           { label: '部分收款', value: 1 }, | ||||
|           { label: '全部收款', value: 2 }, | ||||
|         ], | ||||
|         placeholder: '请选择收款状态', | ||||
|         allowClear: true, | ||||
|       }, | ||||
|     }, | ||||
|     { | ||||
|       fieldName: 'status', | ||||
|       label: '状态', | ||||
|       component: 'Select', | ||||
|       componentProps: { | ||||
|         placeholder: '请选择状态', | ||||
|         allowClear: true, | ||||
|         options: getDictOptions(DICT_TYPE.ERP_AUDIT_STATUS, 'number'), | ||||
|       }, | ||||
|     }, | ||||
|     { | ||||
|       fieldName: 'remark', | ||||
|       label: '备注', | ||||
|       component: 'Input', | ||||
|       componentProps: { | ||||
|         placeholder: '请输入备注', | ||||
|         allowClear: true, | ||||
|       }, | ||||
|     }, | ||||
|   ]; | ||||
| } | ||||
| 
 | ||||
| /** 列表的字段 */ | ||||
| export function useGridColumns(): VxeTableGridOptions['columns'] { | ||||
|   return [ | ||||
|     { | ||||
|       type: 'checkbox', | ||||
|       width: 50, | ||||
|       fixed: 'left', | ||||
|     }, | ||||
|     { | ||||
|       field: 'no', | ||||
|       title: '出库单号', | ||||
|       width: 200, | ||||
|       fixed: 'left', | ||||
|     }, | ||||
|     { | ||||
|       field: 'productNames', | ||||
|       title: '产品信息', | ||||
|       showOverflow: 'tooltip', | ||||
|       minWidth: 120, | ||||
|     }, | ||||
|     { | ||||
|       field: 'customerName', | ||||
|       title: '客户', | ||||
|       minWidth: 120, | ||||
|     }, | ||||
|     { | ||||
|       field: 'outTime', | ||||
|       title: '出库时间', | ||||
|       width: 160, | ||||
|       formatter: 'formatDate', | ||||
|     }, | ||||
|     { | ||||
|       field: 'creatorName', | ||||
|       title: '创建人', | ||||
|       minWidth: 120, | ||||
|     }, | ||||
|     { | ||||
|       field: 'totalCount', | ||||
|       title: '总数量', | ||||
|       minWidth: 120, | ||||
|     }, | ||||
|     { | ||||
|       field: 'totalPrice', | ||||
|       title: '应收金额', | ||||
|       formatter: 'formatNumber', | ||||
|       minWidth: 120, | ||||
|     }, | ||||
|     { | ||||
|       field: 'receiptPrice', | ||||
|       title: '已收金额', | ||||
|       formatter: 'formatNumber', | ||||
|       minWidth: 120, | ||||
|     }, | ||||
|     { | ||||
|       field: 'unReceiptPrice', | ||||
|       title: '未收金额', | ||||
|       formatter: ({ row }) => { | ||||
|         return `${erpNumberFormatter(row.totalPrice - row.receiptPrice, 2)}元`; | ||||
|       }, | ||||
|       minWidth: 120, | ||||
|     }, | ||||
|     { | ||||
|       field: 'status', | ||||
|       title: '状态', | ||||
|       minWidth: 120, | ||||
|       cellRender: { | ||||
|         name: 'CellDict', | ||||
|         props: { type: DICT_TYPE.ERP_AUDIT_STATUS }, | ||||
|       }, | ||||
|     }, | ||||
|     { | ||||
|       title: '操作', | ||||
|       minWidth: 250, | ||||
|       fixed: 'right', | ||||
|       slots: { default: 'actions' }, | ||||
|     }, | ||||
|   ]; | ||||
| } | ||||
| /** 列表的搜索表单 */ | ||||
| export function useOrderGridFormSchema(): VbenFormSchema[] { | ||||
|   return [ | ||||
|     { | ||||
|       fieldName: 'no', | ||||
|       label: '订单单号', | ||||
|       component: 'Input', | ||||
|       componentProps: { | ||||
|         placeholder: '请输入订单单号', | ||||
|         allowClear: true, | ||||
|         disabled: true, | ||||
|       }, | ||||
|     }, | ||||
|     { | ||||
|       fieldName: 'productId', | ||||
|       label: '产品', | ||||
|       component: 'ApiSelect', | ||||
|       componentProps: { | ||||
|         placeholder: '请选择产品', | ||||
|         allowClear: true, | ||||
|         showSearch: true, | ||||
|         api: getProductSimpleList, | ||||
|         fieldNames: { | ||||
|           label: 'name', | ||||
|           value: 'id', | ||||
|         }, | ||||
|       }, | ||||
|     }, | ||||
|     { | ||||
|       fieldName: 'orderTime', | ||||
|       label: '订单时间', | ||||
|       component: 'RangePicker', | ||||
|       componentProps: { | ||||
|         placeholder: ['开始时间', '结束时间'], | ||||
|         showTime: true, | ||||
|         format: 'YYYY-MM-DD HH:mm:ss', | ||||
|         valueFormat: 'YYYY-MM-DD HH:mm:ss', | ||||
|       }, | ||||
|     }, | ||||
|   ]; | ||||
| } | ||||
| 
 | ||||
| /** 列表的字段 */ | ||||
| export function useOrderGridColumns(): VxeTableGridOptions['columns'] { | ||||
|   return [ | ||||
|     { | ||||
|       type: 'radio', | ||||
|       width: 50, | ||||
|       fixed: 'left', | ||||
|     }, | ||||
|     { | ||||
|       field: 'no', | ||||
|       title: '订单单号', | ||||
|       width: 200, | ||||
|       fixed: 'left', | ||||
|     }, | ||||
|     { | ||||
|       field: 'productNames', | ||||
|       title: '产品信息', | ||||
|       showOverflow: 'tooltip', | ||||
|       minWidth: 120, | ||||
|     }, | ||||
|     { | ||||
|       field: 'customerName', | ||||
|       title: '客户', | ||||
|       minWidth: 120, | ||||
|     }, | ||||
|     { | ||||
|       field: 'orderTime', | ||||
|       title: '订单时间', | ||||
|       width: 160, | ||||
|       formatter: 'formatDate', | ||||
|     }, | ||||
|     { | ||||
|       field: 'creatorName', | ||||
|       title: '创建人', | ||||
|       minWidth: 120, | ||||
|     }, | ||||
|     { | ||||
|       field: 'totalCount', | ||||
|       title: '总数量', | ||||
|       minWidth: 120, | ||||
|     }, | ||||
|     { | ||||
|       field: 'outCount', | ||||
|       title: '出库数量', | ||||
|       minWidth: 120, | ||||
|     }, | ||||
|     { | ||||
|       field: 'totalProductPrice', | ||||
|       title: '金额合计', | ||||
|       formatter: 'formatNumber', | ||||
|       minWidth: 120, | ||||
|     }, | ||||
|     { | ||||
|       field: 'totalPrice', | ||||
|       title: '含税金额', | ||||
|       formatter: 'formatNumber', | ||||
|       minWidth: 120, | ||||
|     }, | ||||
|   ]; | ||||
| } | ||||
|  | @ -1,34 +1,239 @@ | |||
| <script lang="ts" setup> | ||||
| import { DocAlert, Page } from '@vben/common-ui'; | ||||
| import type { VxeTableGridOptions } from '#/adapter/vxe-table'; | ||||
| import type { ErpSaleOutApi } from '#/api/erp/sale/out'; | ||||
| 
 | ||||
| import { Button } from 'ant-design-vue'; | ||||
| import { ref } from 'vue'; | ||||
| 
 | ||||
| import { DocAlert, Page, useVbenModal } from '@vben/common-ui'; | ||||
| import { downloadFileFromBlobPart, isEmpty } from '@vben/utils'; | ||||
| 
 | ||||
| import { message } from 'ant-design-vue'; | ||||
| 
 | ||||
| import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table'; | ||||
| import { | ||||
|   deleteSaleOut, | ||||
|   exportSaleOut, | ||||
|   getSaleOutPage, | ||||
|   updateSaleOutStatus, | ||||
| } from '#/api/erp/sale/out'; | ||||
| import { $t } from '#/locales'; | ||||
| 
 | ||||
| import { useGridColumns, useGridFormSchema } from './data'; | ||||
| import SaleOutForm from './modules/sale-out-form.vue'; | ||||
| 
 | ||||
| /** ERP 销售出库列表 */ | ||||
| defineOptions({ name: 'ErpSaleOut' }); | ||||
| 
 | ||||
| const [FormModal, formModalApi] = useVbenModal({ | ||||
|   connectedComponent: SaleOutForm, | ||||
|   destroyOnClose: true, | ||||
| }); | ||||
| 
 | ||||
| /** 刷新表格 */ | ||||
| function onRefresh() { | ||||
|   gridApi.query(); | ||||
| } | ||||
| 
 | ||||
| const checkedIds = ref<number[]>([]); | ||||
| function handleRowCheckboxChange({ | ||||
|   records, | ||||
| }: { | ||||
|   records: ErpSaleOutApi.SaleOut[]; | ||||
| }) { | ||||
|   checkedIds.value = records.map((item) => item.id!); | ||||
| } | ||||
| 
 | ||||
| /** 详情 */ | ||||
| function handleDetail(row: ErpSaleOutApi.SaleOut) { | ||||
|   formModalApi.setData({ type: 'detail', id: row.id }).open(); | ||||
| } | ||||
| 
 | ||||
| /** 新增 */ | ||||
| function handleCreate() { | ||||
|   formModalApi.setData({ type: 'create' }).open(); | ||||
| } | ||||
| 
 | ||||
| /** 编辑 */ | ||||
| function handleEdit(row: ErpSaleOutApi.SaleOut) { | ||||
|   formModalApi.setData({ type: 'update', id: row.id }).open(); | ||||
| } | ||||
| 
 | ||||
| /** 删除 */ | ||||
| async function handleDelete(ids: number[]) { | ||||
|   const hideLoading = message.loading({ | ||||
|     content: $t('ui.actionMessage.deleting'), | ||||
|     duration: 0, | ||||
|     key: 'action_process_msg', | ||||
|   }); | ||||
|   try { | ||||
|     await deleteSaleOut(ids); | ||||
|     message.success({ | ||||
|       content: $t('ui.actionMessage.deleteSuccess'), | ||||
|       key: 'action_process_msg', | ||||
|     }); | ||||
|     onRefresh(); | ||||
|   } catch { | ||||
|     // 处理错误 | ||||
|   } finally { | ||||
|     hideLoading(); | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| /** 审批/反审批操作 */ | ||||
| function handleUpdateStatus(row: ErpSaleOutApi.SaleOut, status: number) { | ||||
|   const hideLoading = message.loading({ | ||||
|     content: `确定${status === 20 ? '审批' : '反审批'}该订单吗?`, | ||||
|     duration: 0, | ||||
|     key: 'action_process_msg', | ||||
|   }); | ||||
|   updateSaleOutStatus({ id: row.id!, status }) | ||||
|     .then(() => { | ||||
|       message.success({ | ||||
|         content: `${status === 20 ? '审批' : '反审批'}成功`, | ||||
|         key: 'action_process_msg', | ||||
|       }); | ||||
|       onRefresh(); | ||||
|     }) | ||||
|     .catch(() => { | ||||
|       // 处理错误 | ||||
|     }) | ||||
|     .finally(() => { | ||||
|       hideLoading(); | ||||
|     }); | ||||
| } | ||||
| 
 | ||||
| /** 导出 */ | ||||
| async function handleExport() { | ||||
|   const data = await exportSaleOut(await gridApi.formApi.getValues()); | ||||
|   downloadFileFromBlobPart({ fileName: '销售出库.xls', source: data }); | ||||
| } | ||||
| 
 | ||||
| const [Grid, gridApi] = useVbenVxeGrid({ | ||||
|   formOptions: { | ||||
|     schema: useGridFormSchema(), | ||||
|   }, | ||||
|   gridOptions: { | ||||
|     columns: useGridColumns(), | ||||
|     height: 'auto', | ||||
|     keepSource: true, | ||||
|     proxyConfig: { | ||||
|       ajax: { | ||||
|         query: async ({ page }, formValues) => { | ||||
|           return await getSaleOutPage({ | ||||
|             pageNo: page.currentPage, | ||||
|             pageSize: page.pageSize, | ||||
|             ...formValues, | ||||
|           }); | ||||
|         }, | ||||
|       }, | ||||
|     }, | ||||
|     rowConfig: { | ||||
|       keyField: 'id', | ||||
|       isHover: true, | ||||
|     }, | ||||
|     toolbarConfig: { | ||||
|       refresh: true, | ||||
|       search: true, | ||||
|     }, | ||||
|   } as VxeTableGridOptions<ErpSaleOutApi.SaleOut>, | ||||
|   gridEvents: { | ||||
|     checkboxAll: handleRowCheckboxChange, | ||||
|     checkboxChange: handleRowCheckboxChange, | ||||
|   }, | ||||
| }); | ||||
| </script> | ||||
| 
 | ||||
| <template> | ||||
|   <Page> | ||||
|   <Page auto-content-height> | ||||
|     <template #doc> | ||||
|       <DocAlert | ||||
|         title="【销售】销售订单、出库、退货" | ||||
|         url="https://doc.iocoder.cn/erp/sale/" | ||||
|       /> | ||||
|     </template> | ||||
|     <Button | ||||
|       danger | ||||
|       type="link" | ||||
|       target="_blank" | ||||
|       href="https://github.com/yudaocode/yudao-ui-admin-vue3" | ||||
|     > | ||||
|       该功能支持 Vue3 + element-plus 版本! | ||||
|     </Button> | ||||
|     <br /> | ||||
|     <Button | ||||
|       type="link" | ||||
|       target="_blank" | ||||
|       href="https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/erp/sale/out/index" | ||||
|     > | ||||
|       可参考 | ||||
|       https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/erp/sale/out/index | ||||
|       代码,pull request 贡献给我们! | ||||
|     </Button> | ||||
|     <FormModal @success="onRefresh" /> | ||||
| 
 | ||||
|     <Grid table-title="销售出库列表"> | ||||
|       <template #toolbar-tools> | ||||
|         <TableAction | ||||
|           :actions="[ | ||||
|             { | ||||
|               label: $t('ui.actionTitle.create', ['销售出库']), | ||||
|               type: 'primary', | ||||
|               icon: ACTION_ICON.ADD, | ||||
|               auth: ['erp:sale-out:create'], | ||||
|               onClick: handleCreate, | ||||
|             }, | ||||
|             { | ||||
|               label: $t('ui.actionTitle.export'), | ||||
|               type: 'primary', | ||||
|               icon: ACTION_ICON.DOWNLOAD, | ||||
|               auth: ['erp:sale-out:export'], | ||||
|               onClick: handleExport, | ||||
|             }, | ||||
|             { | ||||
|               label: '批量删除', | ||||
|               type: 'primary', | ||||
|               danger: true, | ||||
|               disabled: isEmpty(checkedIds), | ||||
|               icon: ACTION_ICON.DELETE, | ||||
|               auth: ['erp:sale-out:delete'], | ||||
|               popConfirm: { | ||||
|                 disabled: isEmpty(checkedIds), | ||||
|                 title: `是否删除所选中数据?`, | ||||
|                 confirm: () => { | ||||
|                   handleDelete(checkedIds); | ||||
|                 }, | ||||
|               }, | ||||
|             }, | ||||
|           ]" | ||||
|         /> | ||||
|       </template> | ||||
|       <template #actions="{ row }"> | ||||
|         <TableAction | ||||
|           :actions="[ | ||||
|             { | ||||
|               label: $t('common.detail'), | ||||
|               type: 'link', | ||||
|               icon: ACTION_ICON.VIEW, | ||||
|               auth: ['erp:sale-out:query'], | ||||
|               onClick: handleDetail.bind(null, row), | ||||
|             }, | ||||
|             { | ||||
|               label: $t('common.edit'), | ||||
|               type: 'link', | ||||
|               icon: ACTION_ICON.EDIT, | ||||
|               auth: ['erp:sale-out:update'], | ||||
|               ifShow: () => row.status !== 20, | ||||
|               onClick: handleEdit.bind(null, row), | ||||
|             }, | ||||
|             { | ||||
|               label: row.status === 10 ? '审批' : '反审批', | ||||
|               type: 'link', | ||||
|               auth: ['erp:sale-out:update-status'], | ||||
|               popConfirm: { | ||||
|                 title: `确认${row.status === 10 ? '审批' : '反审批'}${row.no}吗?`, | ||||
|                 confirm: handleUpdateStatus.bind( | ||||
|                   null, | ||||
|                   row, | ||||
|                   row.status === 10 ? 20 : 10, | ||||
|                 ), | ||||
|               }, | ||||
|             }, | ||||
|             { | ||||
|               label: $t('common.delete'), | ||||
|               type: 'link', | ||||
|               danger: true, | ||||
|               color: 'error', | ||||
|               auth: ['erp:sale-out:delete'], | ||||
|               popConfirm: { | ||||
|                 title: $t('ui.actionMessage.deleteConfirm', [row.no]), | ||||
|                 confirm: () => handleDelete([row.id!]), | ||||
|               }, | ||||
|             }, | ||||
|           ]" | ||||
|         /> | ||||
|       </template> | ||||
|     </Grid> | ||||
|   </Page> | ||||
| </template> | ||||
|  |  | |||
|  | @ -0,0 +1,310 @@ | |||
| <script lang="ts" setup> | ||||
| import type { ErpSaleOrderApi } from '#/api/erp/sale/order'; | ||||
| import type { ErpSaleOutApi } from '#/api/erp/sale/out'; | ||||
| 
 | ||||
| import { computed, nextTick, ref, watch } from 'vue'; | ||||
| 
 | ||||
| import { useVbenModal } from '@vben/common-ui'; | ||||
| 
 | ||||
| import { message } from 'ant-design-vue'; | ||||
| 
 | ||||
| import { useVbenForm } from '#/adapter/form'; | ||||
| import { createSaleOut, getSaleOut, updateSaleOut } from '#/api/erp/sale/out'; | ||||
| 
 | ||||
| import { useFormSchema } from '../data'; | ||||
| import SaleOutItemForm from './sale-out-item-form.vue'; | ||||
| import SelectSaleOrderForm from './select-sale-order-form.vue'; | ||||
| 
 | ||||
| const emit = defineEmits(['success']); | ||||
| const formData = ref< | ||||
|   ErpSaleOutApi.SaleOut & { | ||||
|     accountId?: number; | ||||
|     customerId?: number; | ||||
|     discountedPrice?: number; | ||||
|     discountPercent?: number; | ||||
|     fileUrl?: string; | ||||
|     order?: ErpSaleOrderApi.SaleOrder; | ||||
|     orderId?: number; | ||||
|     orderNo?: string; | ||||
|   } | ||||
| >({ | ||||
|   id: undefined, | ||||
|   no: undefined, | ||||
|   accountId: undefined, | ||||
|   outTime: undefined, | ||||
|   remark: undefined, | ||||
|   fileUrl: undefined, | ||||
|   discountPercent: 0, | ||||
|   customerId: undefined, | ||||
|   discountPrice: 0, | ||||
|   totalPrice: 0, | ||||
|   otherPrice: 0, | ||||
|   discountedPrice: 0, | ||||
|   items: [], | ||||
| }); | ||||
| const formType = ref(''); | ||||
| const itemFormRef = ref(); | ||||
| 
 | ||||
| const getTitle = computed(() => { | ||||
|   if (formType.value === 'create') return '添加销售出库'; | ||||
|   if (formType.value === 'update') return '编辑销售出库'; | ||||
|   return '销售出库详情'; | ||||
| }); | ||||
| 
 | ||||
| const [Form, formApi] = useVbenForm({ | ||||
|   commonConfig: { | ||||
|     componentProps: { | ||||
|       class: 'w-full', | ||||
|     }, | ||||
|     labelWidth: 120, | ||||
|   }, | ||||
|   wrapperClass: 'grid-cols-3', | ||||
|   layout: 'vertical', | ||||
|   schema: useFormSchema(formType.value), | ||||
|   showDefaultActions: false, | ||||
|   handleValuesChange: (values, changedFields) => { | ||||
|     if (formData.value && changedFields.includes('otherPrice')) { | ||||
|       formData.value.otherPrice = values.otherPrice; | ||||
|       formData.value.totalPrice = | ||||
|         (formData.value.discountedPrice || 0) + | ||||
|         (formData.value.otherPrice || 0); | ||||
| 
 | ||||
|       formApi.setValues(formData.value, false); | ||||
|     } | ||||
|   }, | ||||
| }); | ||||
| 
 | ||||
| // 更新销售出库项 | ||||
| const handleUpdateItems = async (items: ErpSaleOutApi.SaleOutItem[]) => { | ||||
|   if (formData.value) { | ||||
|     const data = await formApi.getValues(); | ||||
|     formData.value = { ...data, items }; | ||||
|     formApi.setValues(formData.value, false); | ||||
|   } | ||||
| }; | ||||
| 
 | ||||
| // 选择采购订单 | ||||
| const handleUpdateOrder = (order: ErpSaleOrderApi.SaleOrder) => { | ||||
|   formData.value = { | ||||
|     ...formData.value, | ||||
|     orderId: order.id, | ||||
|     orderNo: order.no!, | ||||
|     customerId: order.customerId!, | ||||
|     accountId: order.accountId!, | ||||
|     remark: order.remark!, | ||||
|     discountPercent: order.discountPercent!, | ||||
|     fileUrl: order.fileUrl!, | ||||
|   }; | ||||
|   // 将订单项设置到入库单项 | ||||
|   order.items!.forEach((item: any) => { | ||||
|     item.totalCount = item.count; | ||||
|     item.count = item.totalCount - item.outCount; | ||||
|     item.orderItemId = item.id; | ||||
|     item.id = undefined; | ||||
|   }); | ||||
|   formData.value.items = order.items!.filter( | ||||
|     (item) => item.count && item.count > 0, | ||||
|   ) as ErpSaleOutApi.SaleOutItem[]; | ||||
| 
 | ||||
|   formApi.setValues(formData.value, false); | ||||
| }; | ||||
| 
 | ||||
| watch( | ||||
|   () => formData.value.items!, | ||||
|   (newItems: ErpSaleOutApi.SaleOutItem[]) => { | ||||
|     if (newItems && newItems.length > 0) { | ||||
|       // 计算每个产品的总价、税额和总价 | ||||
|       newItems.forEach((item) => { | ||||
|         item.totalProductPrice = (item.productPrice || 0) * (item.count || 0); | ||||
|         item.taxPrice = | ||||
|           (item.totalProductPrice || 0) * ((item.taxPercent || 0) / 100); | ||||
|         item.totalPrice = (item.totalProductPrice || 0) + (item.taxPrice || 0); | ||||
|       }); | ||||
|       // 计算总价 | ||||
|       const totalPrice = newItems.reduce((sum, item) => { | ||||
|         return sum + (item.totalProductPrice || 0) + (item.taxPrice || 0); | ||||
|       }, 0); | ||||
|       formData.value.totalPrice = totalPrice; | ||||
|     } else { | ||||
|       formData.value.totalPrice = 0; | ||||
|     } | ||||
|     // 优惠金额 | ||||
|     formData.value.discountPrice = | ||||
|       ((formData.value.totalPrice || 0) * | ||||
|         (formData.value.discountPercent || 0)) / | ||||
|       100; | ||||
|     // 优惠后价格 | ||||
|     formData.value.discountedPrice = | ||||
|       formData.value.totalPrice - formData.value.discountPrice; | ||||
| 
 | ||||
|     // 计算最终价格(包含其他费用) | ||||
|     formData.value.totalPrice = | ||||
|       formData.value.discountedPrice + (formData.value.otherPrice || 0); | ||||
|     formApi.setValues(formData.value, false); | ||||
|   }, | ||||
|   { immediate: true }, | ||||
| ); | ||||
| 
 | ||||
| /** | ||||
|  * 创建或更新销售出库 | ||||
|  */ | ||||
| const [Modal, modalApi] = useVbenModal({ | ||||
|   async onConfirm() { | ||||
|     const { valid } = await formApi.validate(); | ||||
|     if (!valid) { | ||||
|       return; | ||||
|     } | ||||
|     await nextTick(); | ||||
| 
 | ||||
|     const itemFormInstance = Array.isArray(itemFormRef.value) | ||||
|       ? itemFormRef.value[0] | ||||
|       : itemFormRef.value; | ||||
|     if (itemFormInstance && typeof itemFormInstance.validate === 'function') { | ||||
|       try { | ||||
|         const isValid = await itemFormInstance.validate(); | ||||
|         if (!isValid) { | ||||
|           message.error('子表单验证失败'); | ||||
|           return; | ||||
|         } | ||||
|       } catch (error: any) { | ||||
|         message.error(error.message || '子表单验证失败'); | ||||
|         return; | ||||
|       } | ||||
|     } else { | ||||
|       message.error('子表单验证方法不存在'); | ||||
|       return; | ||||
|     } | ||||
| 
 | ||||
|     // 验证产品清单不能为空 | ||||
|     if (!formData.value?.items || formData.value.items.length === 0) { | ||||
|       message.error('产品清单不能为空,请至少添加一个产品'); | ||||
|       return; | ||||
|     } | ||||
| 
 | ||||
|     modalApi.lock(); | ||||
|     // 提交表单 | ||||
|     const data = (await formApi.getValues()) as ErpSaleOutApi.SaleOut; | ||||
|     data.items = formData.value?.items?.map((item) => ({ | ||||
|       ...item, | ||||
|       // 解决新增销售出库报错 | ||||
|       id: undefined, | ||||
|     })); | ||||
|     try { | ||||
|       await (formType.value === 'create' | ||||
|         ? createSaleOut(data) | ||||
|         : updateSaleOut(data)); | ||||
|       // 关闭并提示 | ||||
|       await modalApi.close(); | ||||
|       emit('success'); | ||||
|       message.success(formType.value === 'create' ? '新增成功' : '更新成功'); | ||||
|     } finally { | ||||
|       modalApi.unlock(); | ||||
|     } | ||||
|   }, | ||||
|   async onOpenChange(isOpen: boolean) { | ||||
|     if (!isOpen) { | ||||
|       formData.value = { | ||||
|         id: undefined, | ||||
|         no: undefined, | ||||
|         accountId: undefined, | ||||
|         outTime: undefined, | ||||
|         remark: undefined, | ||||
|         fileUrl: undefined, | ||||
|         discountPercent: 0, | ||||
|         customerId: undefined, | ||||
|         discountPrice: 0, | ||||
|         totalPrice: 0, | ||||
|         otherPrice: 0, | ||||
|         items: [], | ||||
|       }; | ||||
|       formApi.setValues(formData.value, false); | ||||
|       return; | ||||
|     } | ||||
|     // 加载数据 | ||||
|     const data = modalApi.getData<{ id?: number; type: string }>(); | ||||
|     if (!data) { | ||||
|       return; | ||||
|     } | ||||
|     formType.value = data.type; | ||||
|     formApi.updateSchema(useFormSchema(formType.value)); | ||||
| 
 | ||||
|     if (!data.id) { | ||||
|       // 初始化空的表单数据 | ||||
|       formData.value = { | ||||
|         id: undefined, | ||||
|         no: undefined, | ||||
|         accountId: undefined, | ||||
|         outTime: undefined, | ||||
|         remark: undefined, | ||||
|         fileUrl: undefined, | ||||
|         discountPercent: 0, | ||||
|         customerId: undefined, | ||||
|         discountPrice: 0, | ||||
|         totalPrice: 0, | ||||
|         otherPrice: 0, | ||||
|         items: [], | ||||
|       } as unknown as ErpSaleOutApi.SaleOut; | ||||
|       await nextTick(); | ||||
|       const itemFormInstance = Array.isArray(itemFormRef.value) | ||||
|         ? itemFormRef.value[0] | ||||
|         : itemFormRef.value; | ||||
|       if (itemFormInstance && typeof itemFormInstance.init === 'function') { | ||||
|         itemFormInstance.init([]); | ||||
|       } | ||||
|       return; | ||||
|     } | ||||
| 
 | ||||
|     modalApi.lock(); | ||||
|     try { | ||||
|       formData.value = await getSaleOut(data.id); | ||||
| 
 | ||||
|       // 设置到 values | ||||
|       await formApi.setValues(formData.value, false); | ||||
|       // 初始化子表单 | ||||
|       await nextTick(); | ||||
|       const itemFormInstance = Array.isArray(itemFormRef.value) | ||||
|         ? itemFormRef.value[0] | ||||
|         : itemFormRef.value; | ||||
|       if (itemFormInstance && typeof itemFormInstance.init === 'function') { | ||||
|         itemFormInstance.init(formData.value.items || []); | ||||
|       } | ||||
|     } finally { | ||||
|       modalApi.unlock(); | ||||
|     } | ||||
|   }, | ||||
| }); | ||||
| 
 | ||||
| defineExpose({ modalApi }); | ||||
| </script> | ||||
| 
 | ||||
| <template> | ||||
|   <Modal | ||||
|     v-bind="$attrs" | ||||
|     :title="getTitle" | ||||
|     class="w-2/3" | ||||
|     :closable="true" | ||||
|     :mask-closable="true" | ||||
|     :show-confirm-button="formType !== 'detail'" | ||||
|   > | ||||
|     <Form class="mx-3"> | ||||
|       <template #product="slotProps"> | ||||
|         <SaleOutItemForm | ||||
|           v-bind="slotProps" | ||||
|           ref="itemFormRef" | ||||
|           class="w-full" | ||||
|           :items="formData?.items ?? []" | ||||
|           :disabled="formType === 'detail'" | ||||
|           @update:items="handleUpdateItems" | ||||
|         /> | ||||
|       </template> | ||||
|       <template #orderNo="slotProps"> | ||||
|         <SelectSaleOrderForm | ||||
|           :modal="Modal" | ||||
|           v-bind="slotProps" | ||||
|           :order-no="formData?.orderNo" | ||||
|           @update:order="handleUpdateOrder" | ||||
|         /> | ||||
|       </template> | ||||
|     </Form> | ||||
|   </Modal> | ||||
| </template> | ||||
|  | @ -0,0 +1,255 @@ | |||
| <script lang="ts" setup> | ||||
| import type { ErpSaleOutApi } from '#/api/erp/sale/out'; | ||||
| 
 | ||||
| import { nextTick, onMounted, ref, watch } from 'vue'; | ||||
| 
 | ||||
| import { erpPriceMultiply } from '@vben/utils'; | ||||
| 
 | ||||
| import { InputNumber, Select } from 'ant-design-vue'; | ||||
| 
 | ||||
| import { useVbenVxeGrid } from '#/adapter/vxe-table'; | ||||
| import { getProductSimpleList } from '#/api/erp/product/product'; | ||||
| import { getWarehouseStockCount } from '#/api/erp/stock/stock'; | ||||
| import { getWarehouseSimpleList } from '#/api/erp/stock/warehouse'; | ||||
| 
 | ||||
| import { useSaleOutItemTableColumns } from '../data'; | ||||
| 
 | ||||
| const props = withDefaults(defineProps<Props>(), { | ||||
|   items: () => [], | ||||
|   disabled: false, | ||||
| }); | ||||
| 
 | ||||
| const emit = defineEmits(['update:items', 'update:totalPrice']); | ||||
| 
 | ||||
| interface Props { | ||||
|   items?: ErpSaleOutApi.SaleOutItem[]; | ||||
|   disabled?: boolean; | ||||
| } | ||||
| 
 | ||||
| const tableData = ref<ErpSaleOutApi.SaleOutItem[]>([]); | ||||
| const productOptions = ref<any[]>([]); | ||||
| const warehouseOptions = ref<any[]>([]); | ||||
| 
 | ||||
| /** 表格配置 */ | ||||
| const [Grid, gridApi] = useVbenVxeGrid({ | ||||
|   gridOptions: { | ||||
|     editConfig: { | ||||
|       trigger: 'click', | ||||
|       mode: 'cell', | ||||
|     }, | ||||
|     columns: useSaleOutItemTableColumns(), | ||||
|     data: tableData.value, | ||||
|     border: true, | ||||
|     showOverflow: true, | ||||
|     autoResize: true, | ||||
|     minHeight: 250, | ||||
|     keepSource: true, | ||||
|     rowConfig: { | ||||
|       keyField: 'id', | ||||
|     }, | ||||
|     pagerConfig: { | ||||
|       enabled: false, | ||||
|     }, | ||||
|     toolbarConfig: { | ||||
|       enabled: false, | ||||
|     }, | ||||
|   }, | ||||
| }); | ||||
| 
 | ||||
| /** 监听外部传入的列数据 */ | ||||
| watch( | ||||
|   () => props.items, | ||||
|   async (items) => { | ||||
|     if (!items) { | ||||
|       return; | ||||
|     } | ||||
|     await nextTick(); | ||||
|     tableData.value = [...items]; | ||||
|     await nextTick(); | ||||
|     gridApi.grid.reloadData(tableData.value); | ||||
|   }, | ||||
|   { | ||||
|     immediate: true, | ||||
|   }, | ||||
| ); | ||||
| 
 | ||||
| /** 初始化 */ | ||||
| onMounted(async () => { | ||||
|   productOptions.value = await getProductSimpleList(); | ||||
|   warehouseOptions.value = await getWarehouseSimpleList(); | ||||
| }); | ||||
| 
 | ||||
| function handlePriceChange(row: any) { | ||||
|   if (row.productPrice && row.count) { | ||||
|     row.totalProductPrice = erpPriceMultiply(row.productPrice, row.count) ?? 0; | ||||
|     row.taxPrice = | ||||
|       erpPriceMultiply(row.totalProductPrice, (row.taxPercent || 0) / 100) ?? 0; | ||||
|     row.totalPrice = row.totalProductPrice + row.taxPrice; | ||||
|   } | ||||
|   handleUpdateValue(row); | ||||
| } | ||||
| 
 | ||||
| const handleWarehouseChange = async (row: ErpSaleOutApi.SaleOutItem) => { | ||||
|   const warehouseId = row.warehouseId; | ||||
|   const stockCount = await getWarehouseStockCount({ | ||||
|     productId: row.productId!, | ||||
|     warehouseId: warehouseId!, | ||||
|   }); | ||||
|   row.stockCount = stockCount || 0; | ||||
|   handleUpdateValue(row); | ||||
| }; | ||||
| 
 | ||||
| function handleUpdateValue(row: any) { | ||||
|   const index = tableData.value.findIndex((item) => item.id === row.id); | ||||
|   if (index === -1) { | ||||
|     tableData.value.push(row); | ||||
|   } else { | ||||
|     tableData.value[index] = row; | ||||
|   } | ||||
|   emit('update:items', [...tableData.value]); | ||||
| } | ||||
| 
 | ||||
| const getSummaries = (): { | ||||
|   count: number; | ||||
|   productName: string; | ||||
|   taxPrice: number; | ||||
|   totalPrice: number; | ||||
|   totalProductPrice: number; | ||||
| } => { | ||||
|   const count = tableData.value.reduce( | ||||
|     (sum, item) => sum + (item.count || 0), | ||||
|     0, | ||||
|   ); | ||||
|   const totalProductPrice = tableData.value.reduce( | ||||
|     (sum, item) => sum + (item.totalProductPrice || 0), | ||||
|     0, | ||||
|   ); | ||||
|   const taxPrice = tableData.value.reduce( | ||||
|     (sum, item) => sum + (item.taxPrice || 0), | ||||
|     0, | ||||
|   ); | ||||
|   const totalPrice = tableData.value.reduce( | ||||
|     (sum, item) => sum + (item.totalPrice || 0), | ||||
|     0, | ||||
|   ); | ||||
|   return { | ||||
|     productName: '合计', | ||||
|     count, | ||||
|     totalProductPrice, | ||||
|     taxPrice, | ||||
|     totalPrice, | ||||
|   }; | ||||
| }; | ||||
| 
 | ||||
| const validate = async (): Promise<boolean> => { | ||||
|   try { | ||||
|     for (let i = 0; i < tableData.value.length; i++) { | ||||
|       const item = tableData.value[i]; | ||||
|       if (item) { | ||||
|         if (!item.warehouseId) { | ||||
|           throw new Error(`第 ${i + 1} 行:仓库不能为空`); | ||||
|         } | ||||
|         if (!item.count || item.count <= 0) { | ||||
|           throw new Error(`第 ${i + 1} 行:产品数量不能为空`); | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|     return true; | ||||
|   } catch (error) { | ||||
|     console.error('验证失败:', error); | ||||
|     throw error; | ||||
|   } | ||||
| }; | ||||
| 
 | ||||
| const getData = (): ErpSaleOutApi.SaleOutItem[] => tableData.value; | ||||
| const init = (items: ErpSaleOutApi.SaleOutItem[] | undefined): void => { | ||||
|   tableData.value = | ||||
|     items && items.length > 0 | ||||
|       ? items.map((item) => { | ||||
|           const newItem = { ...item }; | ||||
|           if (newItem.productPrice && newItem.count) { | ||||
|             newItem.totalProductPrice = | ||||
|               erpPriceMultiply(newItem.productPrice, newItem.count) ?? 0; | ||||
|             newItem.taxPrice = | ||||
|               erpPriceMultiply( | ||||
|                 newItem.totalProductPrice, | ||||
|                 (newItem.taxPercent || 0) / 100, | ||||
|               ) ?? 0; | ||||
|             newItem.totalPrice = newItem.totalProductPrice + newItem.taxPrice; | ||||
|           } | ||||
|           return newItem; | ||||
|         }) | ||||
|       : []; | ||||
|   nextTick(() => { | ||||
|     gridApi.grid.reloadData(tableData.value); | ||||
|   }); | ||||
| }; | ||||
| 
 | ||||
| defineExpose({ | ||||
|   validate, | ||||
|   getData, | ||||
|   init, | ||||
| }); | ||||
| </script> | ||||
| 
 | ||||
| <template> | ||||
|   <Grid class="w-full"> | ||||
|     <template #warehouseId="{ row }"> | ||||
|       <Select | ||||
|         v-model:value="row.warehouseId" | ||||
|         :options="warehouseOptions" | ||||
|         :field-names="{ label: 'name', value: 'id' }" | ||||
|         placeholder="请选择仓库" | ||||
|         :disabled="disabled" | ||||
|         show-search | ||||
|         class="w-full" | ||||
|         @change="handleWarehouseChange(row)" | ||||
|       /> | ||||
|     </template> | ||||
|     <template #productId="{ row }"> | ||||
|       <Select | ||||
|         disabled | ||||
|         v-model:value="row.productId" | ||||
|         :options="productOptions" | ||||
|         :field-names="{ label: 'name', value: 'id' }" | ||||
|         style="width: 100%" | ||||
|         placeholder="请选择产品" | ||||
|         show-search | ||||
|       /> | ||||
|     </template> | ||||
| 
 | ||||
|     <template #count="{ row }"> | ||||
|       <InputNumber | ||||
|         v-if="!disabled" | ||||
|         v-model:value="row.count" | ||||
|         :min="0" | ||||
|         :precision="2" | ||||
|         @change="handlePriceChange(row)" | ||||
|       /> | ||||
|       <span v-else>{{ row.count || '-' }}</span> | ||||
|     </template> | ||||
|     <template #productPrice="{ row }"> | ||||
|       <InputNumber | ||||
|         disabled | ||||
|         v-model:value="row.productPrice" | ||||
|         :min="0" | ||||
|         :precision="2" | ||||
|         @change="handlePriceChange(row)" | ||||
|       /> | ||||
|     </template> | ||||
| 
 | ||||
|     <template #bottom> | ||||
|       <div class="border-border bg-muted mt-2 rounded border p-2"> | ||||
|         <div class="text-muted-foreground flex justify-between text-sm"> | ||||
|           <span class="text-foreground font-medium">合计:</span> | ||||
|           <div class="flex space-x-4"> | ||||
|             <span>数量:{{ getSummaries().count }}</span> | ||||
|             <span>金额:{{ getSummaries().totalProductPrice }}</span> | ||||
|             <span>税额:{{ getSummaries().taxPrice }}</span> | ||||
|             <span>税额合计:{{ getSummaries().totalPrice }}</span> | ||||
|           </div> | ||||
|         </div> | ||||
|       </div> | ||||
|     </template> | ||||
|   </Grid> | ||||
| </template> | ||||
|  | @ -0,0 +1,69 @@ | |||
| <script lang="ts" setup> | ||||
| import type { ErpSaleOrderApi } from '#/api/erp/sale/order'; | ||||
| 
 | ||||
| import { ref } from 'vue'; | ||||
| 
 | ||||
| import { IconifyIcon } from '@vben/icons'; | ||||
| 
 | ||||
| import { Input, message, Modal } from 'ant-design-vue'; | ||||
| 
 | ||||
| import SelectSaleOrderGrid from './select-sale-order-grid.vue'; | ||||
| 
 | ||||
| defineProps({ | ||||
|   orderNo: { | ||||
|     type: String, | ||||
|     default: () => undefined, | ||||
|   }, | ||||
|   disabled: { | ||||
|     type: Boolean, | ||||
|     default: false, | ||||
|   }, | ||||
| }); | ||||
| const emit = defineEmits<{ | ||||
|   'update:order': [order: ErpSaleOrderApi.SaleOrder]; | ||||
| }>(); | ||||
| const order = ref<ErpSaleOrderApi.SaleOrder>(); | ||||
| const open = ref<boolean>(false); | ||||
| const handleSelectOrder = (selectorder: ErpSaleOrderApi.SaleOrder) => { | ||||
|   order.value = selectorder; | ||||
| }; | ||||
| 
 | ||||
| const handleOk = () => { | ||||
|   if (!order.value) { | ||||
|     message.warning('请选择一个采购订单'); | ||||
|     return; | ||||
|   } | ||||
|   emit('update:order', order.value); | ||||
|   open.value = false; | ||||
| }; | ||||
| </script> | ||||
| 
 | ||||
| <template> | ||||
|   <Input | ||||
|     v-bind="$attrs" | ||||
|     readonly | ||||
|     :value="orderNo" | ||||
|     :disabled="disabled" | ||||
|     @click="() => !disabled && (open = true)" | ||||
|   > | ||||
|     <template #addonAfter> | ||||
|       <div> | ||||
|         <IconifyIcon | ||||
|           class="h-full w-6 cursor-pointer" | ||||
|           icon="ant-design:setting-outlined" | ||||
|           :style="{ cursor: disabled ? 'not-allowed' : 'pointer' }" | ||||
|           @click="() => !disabled && (open = true)" | ||||
|         /> | ||||
|       </div> | ||||
|     </template> | ||||
|   </Input> | ||||
|   <Modal | ||||
|     v-model:open="open" | ||||
|     title="选择关联订单" | ||||
|     class="!w-[50vw]" | ||||
|     :show-confirm-button="true" | ||||
|     @ok="handleOk" | ||||
|   > | ||||
|     <SelectSaleOrderGrid @select-row="handleSelectOrder" /> | ||||
|   </Modal> | ||||
| </template> | ||||
|  | @ -0,0 +1,54 @@ | |||
| <script lang="ts" setup> | ||||
| import type { VxeTableGridOptions } from '#/adapter/vxe-table'; | ||||
| import type { ErpSaleOrderApi } from '#/api/erp/sale/order'; | ||||
| 
 | ||||
| import { useVbenVxeGrid } from '#/adapter/vxe-table'; | ||||
| import { getSaleOrderPage } from '#/api/erp/sale/order'; | ||||
| 
 | ||||
| import { useOrderGridColumns, useOrderGridFormSchema } from '../data'; | ||||
| 
 | ||||
| const emit = defineEmits(['selectRow']); | ||||
| const [Grid] = useVbenVxeGrid({ | ||||
|   formOptions: { | ||||
|     schema: useOrderGridFormSchema(), | ||||
|   }, | ||||
|   gridOptions: { | ||||
|     columns: useOrderGridColumns(), | ||||
|     height: 'auto', | ||||
|     keepSource: true, | ||||
|     proxyConfig: { | ||||
|       ajax: { | ||||
|         query: async ({ page }, formValues) => { | ||||
|           return await getSaleOrderPage({ | ||||
|             pageNo: page.currentPage, | ||||
|             pageSize: page.pageSize, | ||||
|             outEnable: true, | ||||
|             ...formValues, | ||||
|           }); | ||||
|         }, | ||||
|       }, | ||||
|     }, | ||||
|     rowConfig: { | ||||
|       keyField: 'id', | ||||
|       isHover: true, | ||||
|     }, | ||||
|     radioConfig: { | ||||
|       trigger: 'row', | ||||
|       highlight: true, | ||||
|     }, | ||||
|     toolbarConfig: { | ||||
|       refresh: true, | ||||
|       search: true, | ||||
|     }, | ||||
|   } as VxeTableGridOptions<ErpSaleOrderApi.SaleOrder>, | ||||
|   gridEvents: { | ||||
|     radioChange: ({ row }: { row: ErpSaleOrderApi.SaleOrder }) => { | ||||
|       emit('selectRow', row); | ||||
|     }, | ||||
|   }, | ||||
| }); | ||||
| </script> | ||||
| 
 | ||||
| <template> | ||||
|   <Grid class="max-h-[600px]" table-title="销售订单列表(仅展示可出库)" /> | ||||
| </template> | ||||
		Loading…
	
		Reference in New Issue
	
	 xuzhiqiang
						xuzhiqiang