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

534 lines
22 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<!-- 移动端布局 -->
<el-drawer v-if="isMobile" 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>
<el-form-item label="出库单号" prop="no">
<el-input disabled v-model="formData.no" placeholder="保存时自动生成" />
</el-form-item>
<el-form-item label="出库时间" prop="outTime">
<el-date-picker v-model="formData.outTime" type="datetime" value-format="YYYY-MM-DD HH:mm:ss" placeholder="选择出库时间" style="width: 100%" />
</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">
<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" />
</el-select>
</el-form-item>
<el-form-item label="客户" prop="customerId">
<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" />
</el-select>
</el-form-item>
<el-form-item label="销售人员" prop="saleUserId">
<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" />
</el-select>
</el-form-item>
<el-form-item label="备注" prop="remark">
<el-input type="textarea" v-model="formData.remark" :rows="2" placeholder="请输入备注" />
</el-form-item>
<el-form-item label="附件" prop="fileUrl">
<UploadFile :is-show-tip="false" v-model="formData.fileUrl" :limit="1" />
</el-form-item>
</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>
<el-form-item label="优惠率(%" prop="discountPercent">
<el-input-number v-model="formData.discountPercent" controls-position="right" :min="0" :precision="2" placeholder="请输入优惠率" style="width: 100%" />
</el-form-item>
<el-form-item label="收款优惠" prop="discountPrice">
<el-input disabled v-model="formData.discountPrice" :formatter="erpPriceInputFormatter" />
</el-form-item>
<el-form-item label="优惠后金额">
<el-input disabled :model-value="formData.totalPrice - formData.otherPrice" :formatter="erpPriceInputFormatter" />
</el-form-item>
<el-form-item label="其它费用" prop="otherPrice">
<el-input-number v-model="formData.otherPrice" controls-position="right" :min="0" :precision="2" placeholder="请输入其它费用" style="width: 100%" />
</el-form-item>
<el-form-item label="结算账户" prop="accountId">
<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" />
</el-select>
</el-form-item>
<el-form-item label="应收金额">
<el-input disabled v-model="formData.totalPrice" :formatter="erpPriceInputFormatter" />
</el-form-item>
<el-form-item label="总金额(反算单价)">
<el-input-number v-model="formData.reverseCalculationAmount" :min="0" :precision="4" style="width: 100%" placeholder="输入总金额反算单价" @change="handleReverseCalculation" />
</el-form-item>
</div>
</el-form>
</div>
<!-- 底部按钮 -->
<template #footer>
<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>
</template>
</el-drawer>
<!-- PC端布局 -->
<Dialog v-else :title="dialogTitle" v-model="dialogVisible" width="1440">
<el-form ref="formRef" :model="formData" :rules="formRules" label-width="100px" v-loading="formLoading" :disabled="disabled">
<el-row :gutter="20">
<el-col :span="8"><el-form-item label="出库单号" prop="no"><el-input disabled v-model="formData.no" placeholder="保存时自动生成" /></el-form-item></el-col>
<el-col :span="8"><el-form-item label="出库时间" prop="outTime"><el-date-picker v-model="formData.outTime" type="datetime" value-format="YYYY-MM-DD HH:mm:ss" placeholder="选择出库时间" class="!w-1/1" /></el-form-item></el-col>
<el-col :span="8"><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-col>
<el-col :span="8"><el-form-item label="仓库" prop="warehouseId"><el-select v-model="formData.warehouseId" clearable filterable placeholder="请选择仓库" class="!w-1/1"><el-option v-for="item in warehouseList" :key="item.id" :label="item.name" :value="item.id" /></el-select></el-form-item></el-col>
<el-col :span="8"><el-form-item label="客户" prop="customerId"><el-select v-model="formData.customerId" clearable filterable placeholder="请选择客户" class="!w-1/1"><el-option v-for="item in customerList" :key="item.id" :label="item.name" :value="item.id" /></el-select></el-form-item></el-col>
<el-col :span="8"><el-form-item label="销售人员" prop="saleUserId"><el-select v-model="formData.saleUserId" clearable filterable placeholder="请选择销售人员" class="!w-1/1"><el-option v-for="item in userList" :key="item.id" :label="item.nickname" :value="item.id" /></el-select></el-form-item></el-col>
<el-col :span="16"><el-form-item label="备注" prop="remark"><el-input type="textarea" v-model="formData.remark" :rows="1" placeholder="请输入备注" /></el-form-item></el-col>
<el-col :span="8"><el-form-item label="附件" prop="fileUrl"><UploadFile :is-show-tip="false" v-model="formData.fileUrl" :limit="1" /></el-form-item></el-col>
</el-row>
<ContentWrap>
<el-tabs v-model="subTabsName" class="-mt-15px -mb-10px">
<el-tab-pane label="出库产品清单" name="item"><SaleOutwarehouseItemForm ref="itemFormRef" :items="formData.items" :disabled="disabled" /></el-tab-pane>
</el-tabs>
</ContentWrap>
<el-row :gutter="20">
<el-col :span="8"><el-form-item label="优惠率(%" prop="discountPercent"><el-input-number v-model="formData.discountPercent" controls-position="right" :min="0" :precision="2" placeholder="请输入优惠率" class="!w-1/1" /></el-form-item></el-col>
<el-col :span="8"><el-form-item label="收款优惠" prop="discountPrice"><el-input disabled v-model="formData.discountPrice" :formatter="erpPriceInputFormatter" /></el-form-item></el-col>
<el-col :span="8"><el-form-item label="优惠后金额"><el-input disabled :model-value="formData.totalPrice - formData.otherPrice" :formatter="erpPriceInputFormatter" /></el-form-item></el-col>
<el-col :span="8"><el-form-item label="其它费用" prop="otherPrice"><el-input-number v-model="formData.otherPrice" controls-position="right" :min="0" :precision="2" placeholder="请输入其它费用" class="!w-1/1" /></el-form-item></el-col>
<el-col :span="8"><el-form-item label="结算账户" prop="accountId"><el-select v-model="formData.accountId" clearable filterable placeholder="请选择结算账户" class="!w-1/1"><el-option v-for="item in accountList" :key="item.id" :label="item.name" :value="item.id" /></el-select></el-form-item></el-col>
<el-col :span="8"><el-form-item label="应收金额"><el-input disabled v-model="formData.totalPrice" :formatter="erpPriceInputFormatter" /></el-form-item></el-col>
<el-col :span="8"><el-form-item label="总金额"><el-input-number v-model="formData.reverseCalculationAmount" :min="0" :precision="4" class="!w-1/1" placeholder="输入总金额反算单价" @change="handleReverseCalculation" /></el-form-item></el-col>
</el-row>
</el-form>
<template #footer>
<el-button @click="submitForm" type="primary" :disabled="formLoading" v-if="!disabled"> </el-button>
<el-button @click="dialogVisible = false"> </el-button>
</template>
</Dialog>
<!-- 库存批次选择弹窗 -->
<StockBatchSelectionDialog ref="stockBatchSelectionRef" @success="handleBatchSelectionChange" />
</template>
<script setup lang="ts">
import { ref, reactive, computed, watch } from 'vue'
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'
import { useWindowSize } from '@vueuse/core'
/** ERP 销售出库表单 */
defineOptions({ name: 'SaleOutwarehouseForm' })
const { width } = useWindowSize()
const isMobile = computed(() => width.value < 768)
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>
<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>