Files
mom-web/src/views/erp/sale/outwarehouse/SaleOutwarehouseForm.vue

509 lines
18 KiB
Vue
Raw Normal View History

2026-03-05 16:52:12 +08:00
<template>
2026-03-06 14:46:40 +08:00
<el-drawer
v-model="dialogVisible"
:title="dialogTitle"
direction="rtl"
size="100%"
:close-on-press-escape="true"
:destroy-on-close="true"
:append-to-body="true"
class="mobile-form-drawer"
>
<div class="mobile-form" v-loading="formLoading">
<el-form
ref="formRef"
:model="formData"
:rules="formRules"
label-position="top"
:disabled="disabled"
>
<!-- 基本信息 -->
<div class="mobile-form__section">
<div class="mobile-form__section-title">基本信息</div>
2026-03-05 16:52:12 +08:00
<el-form-item label="出库单号" prop="no">
<el-input disabled v-model="formData.no" placeholder="保存时自动生成" />
</el-form-item>
<el-form-item label="出库时间" prop="outTime">
2026-03-06 14:46:40 +08:00
<el-date-picker v-model="formData.outTime" type="datetime" value-format="YYYY-MM-DD HH:mm:ss" placeholder="选择出库时间" style="width: 100%" />
2026-03-05 16:52:12 +08:00
</el-form-item>
<el-form-item label="关联明细" prop="stockRecordIds">
<el-input v-model="formData.stockRecordText" readonly>
<template #append>
<el-button @click="openStockRecordList">
<Icon icon="ep:search" /> 选择
</el-button>
</template>
</el-input>
</el-form-item>
<el-form-item label="仓库" prop="warehouseId">
2026-03-06 14:46:40 +08:00
<el-select v-model="formData.warehouseId" clearable filterable placeholder="请选择仓库" style="width: 100%">
<el-option v-for="item in warehouseList" :key="item.id" :label="item.name" :value="item.id" />
2026-03-05 16:52:12 +08:00
</el-select>
</el-form-item>
<el-form-item label="客户" prop="customerId">
2026-03-06 14:46:40 +08:00
<el-select v-model="formData.customerId" clearable filterable placeholder="请选择客户" style="width: 100%">
<el-option v-for="item in customerList" :key="item.id" :label="item.name" :value="item.id" />
2026-03-05 16:52:12 +08:00
</el-select>
</el-form-item>
<el-form-item label="销售人员" prop="saleUserId">
2026-03-06 14:46:40 +08:00
<el-select v-model="formData.saleUserId" clearable filterable placeholder="请选择销售人员" style="width: 100%">
<el-option v-for="item in userList" :key="item.id" :label="item.nickname" :value="item.id" />
2026-03-05 16:52:12 +08:00
</el-select>
</el-form-item>
<el-form-item label="备注" prop="remark">
2026-03-06 14:46:40 +08:00
<el-input type="textarea" v-model="formData.remark" :rows="2" placeholder="请输入备注" />
2026-03-05 16:52:12 +08:00
</el-form-item>
<el-form-item label="附件" prop="fileUrl">
<UploadFile :is-show-tip="false" v-model="formData.fileUrl" :limit="1" />
</el-form-item>
2026-03-06 14:46:40 +08:00
</div>
<!-- 出库产品清单 -->
<div class="mobile-form__section">
<div class="mobile-form__section-title">出库产品清单</div>
<SaleOutwarehouseItemForm ref="itemFormRef" :items="formData.items" :disabled="disabled" />
</div>
<!-- 价格信息 -->
<div class="mobile-form__section">
<div class="mobile-form__section-title">价格信息</div>
2026-03-05 16:52:12 +08:00
<el-form-item label="优惠率(%" prop="discountPercent">
2026-03-06 14:46:40 +08:00
<el-input-number v-model="formData.discountPercent" controls-position="right" :min="0" :precision="2" placeholder="请输入优惠率" style="width: 100%" />
2026-03-05 16:52:12 +08:00
</el-form-item>
<el-form-item label="收款优惠" prop="discountPrice">
2026-03-06 14:46:40 +08:00
<el-input disabled v-model="formData.discountPrice" :formatter="erpPriceInputFormatter" />
2026-03-05 16:52:12 +08:00
</el-form-item>
<el-form-item label="优惠后金额">
2026-03-06 14:46:40 +08:00
<el-input disabled :model-value="formData.totalPrice - formData.otherPrice" :formatter="erpPriceInputFormatter" />
2026-03-05 16:52:12 +08:00
</el-form-item>
<el-form-item label="其它费用" prop="otherPrice">
2026-03-06 14:46:40 +08:00
<el-input-number v-model="formData.otherPrice" controls-position="right" :min="0" :precision="2" placeholder="请输入其它费用" style="width: 100%" />
2026-03-05 16:52:12 +08:00
</el-form-item>
<el-form-item label="结算账户" prop="accountId">
2026-03-06 14:46:40 +08:00
<el-select v-model="formData.accountId" clearable filterable placeholder="请选择结算账户" style="width: 100%">
<el-option v-for="item in accountList" :key="item.id" :label="item.name" :value="item.id" />
2026-03-05 16:52:12 +08:00
</el-select>
</el-form-item>
<el-form-item label="应收金额">
<el-input disabled v-model="formData.totalPrice" :formatter="erpPriceInputFormatter" />
</el-form-item>
2026-03-06 14:46:40 +08:00
<el-form-item label="总金额(反算单价)">
<el-input-number v-model="formData.reverseCalculationAmount" :min="0" :precision="4" style="width: 100%" placeholder="输入总金额反算单价" @change="handleReverseCalculation" />
2026-03-05 16:52:12 +08:00
</el-form-item>
2026-03-06 14:46:40 +08:00
</div>
</el-form>
</div>
<!-- 底部按钮 -->
2026-03-05 16:52:12 +08:00
<template #footer>
2026-03-06 14:46:40 +08:00
<div class="mobile-form__footer">
<el-button @click="dialogVisible = false" style="flex:1"> </el-button>
<el-button type="primary" @click="submitForm" :disabled="formLoading" v-if="!disabled" style="flex:2">
</el-button>
</div>
2026-03-05 16:52:12 +08:00
</template>
2026-03-06 14:46:40 +08:00
</el-drawer>
2026-03-05 16:52:12 +08:00
<!-- 库存批次选择弹窗 -->
<StockBatchSelectionDialog ref="stockBatchSelectionRef" @success="handleBatchSelectionChange" />
</template>
<script setup lang="ts">
import { SaleOutwarehouseApi, SaleOutwarehouseVO } from '@/api/erp/sale/outwarehouse'
import SaleOutwarehouseItemForm from './components/SaleOutwarehouseItemForm.vue'
import { CustomerApi, CustomerVO } from '@/api/erp/sale/customer'
import { AccountApi, AccountVO } from '@/api/erp/finance/account'
import { erpPriceInputFormatter, erpPriceMultiply } from '@/utils'
import * as UserApi from '@/api/system/user'
import { WarehouseApi, WarehouseVO } from '@/api/erp/stock/warehouse'
import { StockApi } from '@/api/erp/stock/stock/index'
import { ProductApi } from '@/api/erp/product/product'
import StockBatchSelectionDialog from './components/StockBatchSelectionDialog.vue'
import dayjs from 'dayjs'
/** ERP 销售出库表单 */
defineOptions({ name: 'SaleOutwarehouseForm' })
const { t } = useI18n() // 国际化
const message = useMessage() // 消息弹窗
const dialogVisible = ref(false) // 弹窗的是否展示
const dialogTitle = ref('') // 弹窗的标题
const formLoading = ref(false) // 表单的加载中1修改时的数据加载2提交的按钮禁用
const formType = ref('') // 表单的类型create - 新增update - 修改detail - 详情
const formData = ref({
id: undefined,
customerId: undefined,
accountId: undefined,
saleUserId: undefined,
outTime: undefined,
warehouseId: undefined,
remark: undefined,
fileUrl: '',
discountPercent: 0,
discountPrice: 0,
totalPrice: 0,
otherPrice: 0,
items: [],
no: undefined, // 出库单号,后端返回
stockRecordIds: [], // 关联的批次ID
stockRecordText: '', // 关联的批次显示文本
reverseCalculationAmount: undefined // 总金额反算字段
})
const formRules = reactive({
customerId: [{ required: true, message: '客户不能为空', trigger: 'blur' }],
// 移除仓库的必填验证,因为可能从多个仓库出库
outTime: [{ required: true, message: '出库时间不能为空', trigger: 'blur' }],
accountId: [{ required: true, message: '结算账户不能为空', trigger: 'blur' }]
})
const disabled = computed(() => formType.value === 'detail')
const formRef = ref() // 表单 Ref
const customerList = ref<CustomerVO[]>([]) // 客户列表
const accountList = ref<AccountVO[]>([]) // 账户列表
const userList = ref<UserApi.UserVO[]>([]) // 用户列表
const warehouseList = ref<WarehouseVO[]>([]) // 仓库列表
/** 子表的表单 */
const subTabsName = ref('item')
const itemFormRef = ref()
/** 打开【库存批次选择】弹窗 */
const stockBatchSelectionRef = ref() // 库存批次选择 Ref
const openStockRecordList = () => {
// 传入已选择的批次ID用于在列表中标记已选择的记录
const existingBatchIds = formData.value.items
.filter(item => item.batchId)
.map(item => item.batchId)
stockBatchSelectionRef.value.open(existingBatchIds)
}
/** 处理库存批次选择事件 */
const handleBatchSelectionChange = async (batches: any[]) => {
// 设置关联信息
formData.value.stockRecordIds = batches.map(batch => batch.id)
formData.value.stockRecordText = `已选择 ${batches.length} 个批次`
// 将选择的批次添加到出库单项,但避免重复添加
const existingBatchIds = formData.value.items
.filter(item => item.batchId)
.map(item => item.batchId)
// 过滤出未添加过的批次
const newBatches = batches.filter(batch => !existingBatchIds.includes(batch.id))
if (newBatches.length === 0) {
message.warning('所选批次已全部添加,请勿重复添加')
return
}
// 将新的批次添加到出库单项,并获取产品详细信息
const newItems = []
for (const batch of newBatches) {
let productInfo = null
let stockCount = 0
// 如果批次有产品ID获取产品详细信息
if (batch.productId) {
try {
productInfo = await ProductApi.getProduct(batch.productId)
// 获取库存数量
if (formData.value.warehouseId) {
stockCount = await StockApi.getStockCount(batch.productId, formData.value.warehouseId)
}
} catch (error) {
console.error('获取产品信息失败:', error)
}
}
const newItem = {
id: undefined,
warehouseId: formData.value.warehouseId || 1,
productId: batch.productId || undefined,
productName: productInfo?.name || batch.productName || `批次: ${batch.workOrderCode}`,
productUnitId: productInfo?.unitId || undefined,
productUnitName: productInfo?.unitName || '吨',
productBarCode: productInfo?.barCode || batch.workOrderCode,
productPrice: productInfo?.salePrice || 0,
count: batch.weight || 0, // 使用批次重量作为数量
stockCount: stockCount || 0, // 实际库存或批次重量
totalProductPrice: 0, // 需要计算
taxPercent: 0,
taxPrice: 0,
totalPrice: 0, // 需要计算
remark: `甜菊糖糖苷: ${batch.steviaGlycosidesPercent || 0}%`,
batchId: batch.id, // 关联的批次ID
batchCode: batch.workOrderCode // 批次编号
}
newItems.push(newItem)
}
// 添加到现有项中
formData.value.items = [...formData.value.items, ...newItems]
// 提示用户
if (newItems.length < batches.length) {
message.success(`已添加 ${newItems.length} 个新批次,${batches.length - newItems.length} 个批次已存在`)
} else {
message.success(`已添加 ${newItems.length} 个批次`)
}
}
/** 添加产品按钮 - 直接添加产品,不通过库存明细选择 */
const addProductDirectly = () => {
// 创建一个新的出库单项
const newItem = {
id: undefined,
warehouseId: formData.value.warehouseId, // 使用表单中选择的仓库
productId: undefined,
productName: '',
productUnitId: undefined,
productUnitName: '',
productBarCode: '',
productPrice: 0,
count: 1, // 默认数量为1
stockCount: 0,
totalProductPrice: 0,
taxPercent: 0,
taxPrice: 0,
totalPrice: 0,
remark: '',
stockRecordId: undefined // 不关联库存明细
}
// 添加到现有项中
formData.value.items = [...formData.value.items, newItem]
// 提示用户
message.success('已添加新产品行,请选择产品和设置数量')
}
/** 加载库存 */
const setStockCount = async (row: any) => {
if (!row.productId || !row.warehouseId) {
row.stockCount = 0
return
}
try {
// 使用产品ID和仓库ID获取具体仓库的库存
const count = await StockApi.getStockCount(row.productId, row.warehouseId)
row.stockCount = count || 0
console.log(`获取到产品${row.productId}在仓库${row.warehouseId}的库存: ${count}`)
} catch (error) {
console.error('获取库存失败:', error)
row.stockCount = 0
}
}
/** 反算单价功能 */
const handleReverseCalculation = () => {
const targetAmount = formData.value.reverseCalculationAmount
if (!targetAmount || targetAmount <= 0) {
return
}
const items = formData.value.items
if (!items || items.length === 0) {
message.warning('请先添加产品明细')
return
}
// 过滤出有数量的产品
const validItems = items.filter(item => item.count && item.count > 0)
if (validItems.length === 0) {
message.warning('请先设置产品数量')
return
}
// 计算目标净额(扣除优惠和其他费用)
const discountAmount = formData.value.discountPercent ?
erpPriceMultiply(targetAmount, formData.value.discountPercent / 100.0) : 0
const netAmount = targetAmount - discountAmount - (formData.value.otherPrice || 0)
if (netAmount <= 0) {
message.warning('扣除优惠和其他费用后金额不能为负数')
return
}
// 计算总权重(数量 * (1 + 税率)
let totalWeight = 0
validItems.forEach(item => {
const taxMultiplier = 1 + (item.taxPercent || 0) / 100.0
totalWeight += item.count * taxMultiplier
})
if (totalWeight === 0) {
message.warning('计算权重为零,请检查数量和税率设置')
return
}
// 按权重分配金额并反算单价
validItems.forEach(item => {
const taxMultiplier = 1 + (item.taxPercent || 0) / 100.0
const itemWeight = item.count * taxMultiplier
const itemTotalPrice = erpPriceMultiply(netAmount, itemWeight / totalWeight)
// 反算含税单价
const unitPriceWithTax = itemTotalPrice / item.count
// 计算不含税单价
item.productPrice = unitPriceWithTax / taxMultiplier
})
message.success(`已按总金额 ${targetAmount} 反算各产品单价`)
}
/** 计算 discountPrice、totalPrice 价格 */
watch(
() => formData.value,
(val) => {
if (!val) {
return
}
// 确保items存在且是数组
if (!val.items || !Array.isArray(val.items)) {
return
}
// 计算
const totalPrice = val.items.reduce((prev, curr) => prev + (curr.totalPrice || 0), 0)
const discountPrice =
val.discountPercent != null ? erpPriceMultiply(totalPrice, val.discountPercent / 100.0) : 0
formData.value.discountPrice = discountPrice
formData.value.totalPrice = totalPrice - discountPrice + (val.otherPrice || 0)
},
{ deep: true }
)
/** 打开弹窗 */
const open = async (type: string, id?: number) => {
dialogVisible.value = true
dialogTitle.value = t('action.' + type)
formType.value = type
resetForm()
// 新增时默认时间为此刻
if (!id) {
formData.value.outTime = dayjs().format('YYYY-MM-DD HH:mm:ss')
}
// 修改时,设置数据
if (id) {
formLoading.value = true
try {
const data = await SaleOutwarehouseApi.getSaleOutwarehouse(id)
// 处理后端返回的itemList字段转换为前端使用的items字段
if (data.itemList && Array.isArray(data.itemList)) {
data.items = data.itemList
delete data.itemList
}
formData.value = data
} finally {
formLoading.value = false
}
}
// 加载客户列表
customerList.value = await CustomerApi.getCustomerSimpleList()
// 加载用户列表
userList.value = await UserApi.getSimpleUserList()
// 加载账户列表
accountList.value = await AccountApi.getAccountSimpleList()
const defaultAccount = accountList.value.find((item) => item.defaultStatus)
if (defaultAccount) {
formData.value.accountId = defaultAccount.id
}
// 加载仓库列表
warehouseList.value = await WarehouseApi.getWarehouseSimpleList()
const defaultWarehouse = warehouseList.value.find((item) => item.defaultStatus)
if (defaultWarehouse) {
formData.value.warehouseId = defaultWarehouse.id
}
}
defineExpose({ open }) // 提供 open 方法,用于打开弹窗
/** 提交表单 */
const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
const submitForm = async () => {
// 校验表单
await formRef.value.validate()
await itemFormRef.value.validate()
// 提交请求
formLoading.value = true
try {
const data = formData.value as unknown as SaleOutwarehouseVO
// 确保 outTime 是字符串格式
if (data.outTime) {
if (typeof data.outTime !== 'string') {
data.outTime = dayjs(data.outTime).format('YYYY-MM-DD HH:mm:ss')
}
} else {
data.outTime = dayjs().format('YYYY-MM-DD HH:mm:ss')
}
console.log('提交的出库时间:', data.outTime, typeof data.outTime)
if (formType.value === 'create') {
await SaleOutwarehouseApi.createSaleOutwarehouse(data)
message.success(t('common.createSuccess'))
} else {
await SaleOutwarehouseApi.updateSaleOutwarehouse(data)
message.success(t('common.updateSuccess'))
}
dialogVisible.value = false
// 发送操作成功的事件
emit('success')
} finally {
formLoading.value = false
}
}
/** 重置表单 */
const resetForm = () => {
formData.value = {
id: undefined,
customerId: undefined,
accountId: undefined,
saleUserId: undefined,
outTime: undefined,
warehouseId: undefined,
remark: undefined,
fileUrl: undefined,
discountPercent: 0,
discountPrice: 0,
totalPrice: 0,
otherPrice: 0,
items: [],
stockRecordIds: [], // 关联的批次ID
stockRecordText: '', // 关联的批次显示文本
reverseCalculationAmount: undefined // 总金额反算字段
}
formRef.value?.resetFields()
}
</script>
2026-03-06 14:46:40 +08:00
<style lang="scss" scoped>
.mobile-form {
padding: 12px;
}
.mobile-form__section {
background: #fff;
border-radius: 8px;
padding: 14px;
margin-bottom: 12px;
}
.mobile-form__section-title {
font-size: 15px;
font-weight: 600;
color: #303133;
margin-bottom: 12px;
padding-bottom: 8px;
border-bottom: 1px solid #f0f0f0;
}
.mobile-form__footer {
display: flex;
gap: 12px;
padding: 0 12px;
}
</style>