feat(wms):出库管理,调整合计金额、数量的字段与交互。(前端负责展示,后端负责计算)

pull/878/head
YunaiV 2026-05-14 09:07:26 +08:00
parent 6f96d004a9
commit df013ac69c
8 changed files with 180 additions and 96 deletions

View File

@ -13,6 +13,7 @@ export interface ShipmentOrderDetailVO {
warehouseName?: string
quantity?: number
availableQuantity?: number
amount?: number
price?: number
totalPrice?: number
createTime?: Date
}

View File

@ -15,7 +15,7 @@ export interface ShipmentOrderVO {
warehouseId?: number
warehouseName?: string
totalQuantity?: number
totalAmount?: number
totalPrice?: number
details?: ShipmentOrderDetailVO[]
createTime?: Date
creator?: string

View File

@ -198,9 +198,11 @@ import MerchantSelect from '@/views/wms/md/merchant/components/MerchantSelect.vu
import WarehouseSelect from '@/views/wms/md/warehouse/components/WarehouseSelect.vue'
import { OrderStatusEnum, OrderUpdateStatusList } from '@/views/wms/utils/constants'
import {
dividePrice,
formatPrice,
formatSumPrice,
formatSumQuantity,
multiplyPrice,
PRICE_PRECISION,
QUANTITY_PRECISION,
sumPrice
@ -296,24 +298,6 @@ const normalizeDetails = (details: ReceiptOrderDetailVO[]) =>
totalPrice: detail.totalPrice ?? multiplyPrice(detail.quantity, detail.price)
}))
const roundPrice = (value: number) => {
return Number.isFinite(value) ? Number(value.toFixed(PRICE_PRECISION)) : undefined
}
const multiplyPrice = (quantity?: number, price?: number) => {
if (!quantity || price === undefined || price === null) {
return undefined
}
return roundPrice(Number(quantity) * Number(price))
}
const dividePrice = (totalPrice?: number, quantity?: number) => {
if (totalPrice === undefined || totalPrice === null || !quantity) {
return undefined
}
return roundPrice(Number(totalPrice) / Number(quantity))
}
/** 添加商品 */
const handleAddDetail = () => {
skuSelectRef.value?.open(getSelectedSkuIds())

View File

@ -37,7 +37,7 @@
{{ formatQuantity(detailData.totalQuantity) || '-' }}
</el-descriptions-item>
<el-descriptions-item label="总金额">
{{ formatPrice(detailData.totalAmount) || '-' }}
{{ formatPrice(detailData.totalPrice) || '-' }}
</el-descriptions-item>
<el-descriptions-item label="创建时间">
{{ formatNullableDate(detailData.createTime) }}
@ -80,14 +80,14 @@
</template>
</el-table-column>
<el-table-column align="center" label="单位" prop="unit" width="100" />
<el-table-column align="right" label="单价" prop="unitPrice" width="140">
<el-table-column align="right" label="单价" prop="price" width="140">
<template #default="scope">
{{ formatPrice(scope.row.unitPrice) || '-' }}
{{ formatPrice(scope.row.price) || '-' }}
</template>
</el-table-column>
<el-table-column align="right" label="总价" prop="amount" width="140">
<el-table-column align="right" label="总价" prop="totalPrice" width="140">
<template #default="scope">
{{ formatPrice(scope.row.amount) || '-' }}
{{ formatPrice(scope.row.totalPrice) || '-' }}
</template>
</el-table-column>
</el-table>
@ -111,7 +111,7 @@ import {
defineOptions({ name: 'WmsShipmentOrderDetail' })
interface DetailRow extends ShipmentOrderDetailVO {
unitPrice?: number
totalPrice?: number
}
const loading = ref(false) //
@ -120,9 +120,9 @@ const detailData = ref<ShipmentOrderVO>({}) // 详情数据
const detailRows = computed<DetailRow[]>(() =>
(detailData.value.details || []).map((detail) => ({
...detail,
unitPrice:
detail.amount != null && detail.quantity
? Number(detail.amount) / Number(detail.quantity)
totalPrice:
detail.price != null && detail.quantity
? Number(detail.price) * Number(detail.quantity)
: undefined
}))
)
@ -135,8 +135,11 @@ const getSummaries = ({ columns, data }: { columns: any[]; data: DetailRow[] })
if (column.property === 'quantity') {
return formatSumQuantity(data, (detail) => detail.quantity)
}
if (column.property === 'amount') {
return formatSumPrice(data, (detail) => detail.amount)
if (column.property === 'price') {
return formatSumPrice(data, (detail) => detail.price)
}
if (column.property === 'totalPrice') {
return formatSumPrice(data, (detail) => detail.totalPrice)
}
return ''
})

View File

@ -56,7 +56,7 @@
<el-input v-model="formData.bizOrderNo" maxlength="64" placeholder="请输入业务单号" />
</el-form-item>
</el-col>
<el-col :span="8">
<el-col :span="16">
<el-form-item label="备注" prop="remark">
<el-input
v-model="formData.remark"
@ -67,23 +67,6 @@
/>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="总数量" prop="totalQuantity">
<el-input :model-value="formatQuantity(totalQuantity)" disabled />
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="总金额" prop="totalAmount">
<el-input-number
v-model="formData.totalAmount"
:controls="false"
:min="0"
:precision="PRICE_PRECISION"
class="!w-1/1"
placeholder="请输入总金额"
/>
</el-form-item>
</el-col>
</el-row>
<div class="mb-12px flex items-center justify-between">
@ -101,7 +84,13 @@
</span>
</el-tooltip>
</div>
<el-table :data="formData.details" border empty-text="">
<el-table
:data="formData.details"
:summary-method="getDetailSummaries"
border
empty-text="暂无商品明细"
show-summary
>
<el-table-column label="商品信息" min-width="220">
<template #default="scope">
<div>{{ scope.row.itemName || '-' }}</div>
@ -123,7 +112,7 @@
{{ formatQuantity(scope.row.availableQuantity) || '-' }}
</template>
</el-table-column>
<el-table-column label="出库数量" width="160">
<el-table-column label="出库数量" prop="quantity" width="160">
<template #default="scope">
<el-input-number
v-model="scope.row.quantity"
@ -132,19 +121,33 @@
:precision="QUANTITY_PRECISION"
class="!w-1/1"
placeholder="数量"
@change="handleDetailQuantityChange(scope.row)"
/>
</template>
</el-table-column>
<el-table-column label="金额(元)" width="160">
<el-table-column label="单价(元)" prop="price" width="160">
<template #default="scope">
<el-input-number
v-model="scope.row.amount"
v-model="scope.row.price"
:controls="false"
:min="0"
:precision="PRICE_PRECISION"
class="!w-1/1"
placeholder="单价"
@change="handleDetailPriceChange(scope.row)"
/>
</template>
</el-table-column>
<el-table-column label="金额(元)" prop="totalPrice" width="160">
<template #default="scope">
<el-input-number
v-model="scope.row.totalPrice"
:controls="false"
:min="0"
:precision="PRICE_PRECISION"
class="!w-1/1"
placeholder="金额"
@change="handleDetailAmountChange"
@change="handleDetailTotalPriceChange(scope.row)"
/>
</template>
</el-table-column>
@ -205,11 +208,15 @@ import MerchantSelect from '@/views/wms/md/merchant/components/MerchantSelect.vu
import WarehouseSelect from '@/views/wms/md/warehouse/components/WarehouseSelect.vue'
import { OrderStatusEnum, OrderUpdateStatusList } from '@/views/wms/utils/constants'
import {
dividePrice,
formatPrice,
formatQuantity,
formatSumPrice,
formatSumQuantity,
multiplyPrice,
PRICE_PRECISION,
QUANTITY_PRECISION,
sumPrice,
sumQuantity
sumPrice
} from '@/views/wms/utils/format'
import { generateOrderNo } from '@/views/wms/utils/order'
@ -233,8 +240,6 @@ const formData = ref<ShipmentOrderVO>({
bizOrderNo: undefined,
merchantId: undefined,
warehouseId: undefined,
totalQuantity: 0,
totalAmount: 0,
remark: undefined,
details: []
})
@ -242,15 +247,12 @@ const formRules = reactive<FormRules>({
no: [{ required: true, message: '出库单号不能为空', trigger: 'blur' }],
type: [{ required: true, message: '出库类型不能为空', trigger: 'change' }],
orderTime: [{ required: true, message: '单据日期不能为空', trigger: 'change' }],
warehouseId: [{ required: true, message: '仓库不能为空', trigger: 'change' }],
totalQuantity: [{ required: true, message: '出库数量不能为空', trigger: 'change' }],
totalAmount: [{ required: true, message: '总金额不能为空', trigger: 'blur' }]
warehouseId: [{ required: true, message: '仓库不能为空', trigger: 'change' }]
})
const formRef = ref() // Ref
const inventorySelectRef = ref() // Ref
const totalQuantity = computed(() => sumQuantity(formData.value.details || [], (detail) => detail.quantity))
const detailTotalAmount = computed(() => sumPrice(formData.value.details || [], (detail) => detail.amount))
const detailPriceSum = computed(() => sumPrice(formData.value.details || [], (detail) => detail.price))
const isPrepareOrder = computed(
() =>
!formData.value.id ||
@ -276,7 +278,7 @@ const open = async (type: string, id?: number) => {
const order = await ShipmentOrderApi.getShipmentOrder(id)
formData.value = {
...order,
details: order.details || []
details: normalizeDetails(order.details || [])
}
} finally {
formLoading.value = false
@ -300,9 +302,16 @@ const buildDetail = (inventory: InventorySelectRow): ShipmentOrderDetailVO => ({
warehouseName: inventory.warehouseName,
quantity: undefined,
availableQuantity: inventory.availableQuantity,
amount: undefined
price: undefined,
totalPrice: undefined
})
const normalizeDetails = (details: ShipmentOrderDetailVO[]) =>
details.map((detail) => ({
...detail,
totalPrice: detail.totalPrice ?? multiplyPrice(detail.quantity, detail.price)
}))
/** 添加商品 */
const handleAddDetail = () => {
inventorySelectRef.value?.open()
@ -320,7 +329,6 @@ const handleSelectInventory = (inventories: InventorySelectRow[]) => {
}
formData.value.details!.push(buildDetail(inventory))
})
refreshTotalAmount()
}
/** 判断库存是否已选择 */
@ -336,24 +344,49 @@ const isInventorySelected = (inventory: InventorySelectRow) => {
/** 删除明细 */
const handleDeleteDetail = (index: number) => {
formData.value.details?.splice(index, 1)
refreshTotalAmount()
}
/** 仓库变化 */
const handleWarehouseChange = () => {
formData.value.details = []
refreshTotalAmount()
}
/** 明细数量变化 */
const handleDetailQuantityChange = (detail: ShipmentOrderDetailVO) => {
if (detail.price !== undefined && detail.price !== null) {
detail.totalPrice = multiplyPrice(detail.quantity, detail.price)
return
}
detail.price = dividePrice(detail.totalPrice, detail.quantity)
}
/** 明细单价变化 */
const handleDetailPriceChange = (detail: ShipmentOrderDetailVO) => {
detail.totalPrice = multiplyPrice(detail.quantity, detail.price)
}
/** 明细金额变化 */
const handleDetailAmountChange = () => {
refreshTotalAmount()
const handleDetailTotalPriceChange = (detail: ShipmentOrderDetailVO) => {
detail.price = dividePrice(detail.totalPrice, detail.quantity)
}
/** 汇总明细金额 */
const refreshTotalAmount = () => {
formData.value.totalAmount = detailTotalAmount.value
}
/** 明细合计 */
const getDetailSummaries = ({ columns, data }: { columns: any[]; data: ShipmentOrderDetailVO[] }) =>
columns.map((column, index) => {
if (index === 0) {
return '合计'
}
if (column.property === 'quantity') {
return formatSumQuantity(data, (detail) => detail.quantity)
}
if (column.property === 'price') {
return formatPrice(detailPriceSum.value)
}
if (column.property === 'totalPrice') {
return formatSumPrice(data, (detail) => detail.totalPrice)
}
return ''
})
/** 校验明细 */
const validateDetails = (required: boolean) => {
@ -383,13 +416,13 @@ const validateDetails = (required: boolean) => {
}
/** 构建提交数据 */
const buildSubmitData = () =>
({
...formData.value,
totalQuantity: totalQuantity.value,
totalAmount: formData.value.totalAmount,
details: formData.value.details || []
}) as ShipmentOrderVO
const buildSubmitData = () => {
const { totalQuantity: _totalQuantity, totalPrice: _totalPrice, details, ...order } = formData.value
return {
...order,
details: (details || []).map(({ totalPrice: _rowTotalPrice, ...detail }) => detail)
} as ShipmentOrderVO
}
/** 提交表单 */
const emit = defineEmits(['success']) // success
@ -467,8 +500,6 @@ const resetForm = () => {
bizOrderNo: undefined,
merchantId: undefined,
warehouseId: undefined,
totalQuantity: 0,
totalAmount: 0,
remark: undefined,
details: []
}

View File

@ -15,7 +15,7 @@
<div>客户{{ printData.merchantName || '-' }}</div>
<div>业务单号{{ printData.bizOrderNo || '-' }}</div>
<div>总数量{{ formatQuantity(printData.totalQuantity) || '-' }}</div>
<div>总金额{{ formatPrice(printData.totalAmount) || '-' }}</div>
<div>总金额{{ formatPrice(printData.totalPrice) || '-' }}</div>
<div>创建时间{{ formatNullableDate(printData.createTime) }}</div>
<div class="col-span-3">备注{{ printData.remark || '-' }}</div>
</div>
@ -25,11 +25,12 @@
<th class="border border-solid border-#dcdfe6 bg-#f5f7fa p-8px text-left">商品信息</th>
<th class="border border-solid border-#dcdfe6 bg-#f5f7fa p-8px text-left">规格信息</th>
<th class="border border-solid border-#dcdfe6 bg-#f5f7fa p-8px text-left">数量</th>
<th class="border border-solid border-#dcdfe6 bg-#f5f7fa p-8px text-left">单价()</th>
<th class="border border-solid border-#dcdfe6 bg-#f5f7fa p-8px text-left">金额()</th>
</tr>
</thead>
<tbody>
<tr v-for="detail in printData.details || []" :key="detail.id">
<tr v-for="detail in printRows" :key="detail.id">
<td class="border border-solid border-#dcdfe6 p-8px">
<div>{{ detail.itemName || '-' }}</div>
<div v-if="detail.itemCode" class="text-12px">{{ detail.itemCode }}</div>
@ -42,10 +43,25 @@
{{ formatQuantity(detail.quantity) || '-' }}
</td>
<td class="border border-solid border-#dcdfe6 p-8px text-right">
{{ formatPrice(detail.amount) || '-' }}
{{ formatPrice(detail.price) || '-' }}
</td>
<td class="border border-solid border-#dcdfe6 p-8px text-right">
{{ formatPrice(detail.totalPrice) || '-' }}
</td>
</tr>
<tr v-if="!printData.details?.length">
<tr v-if="printRows.length">
<td class="border border-solid border-#dcdfe6 bg-#f5f7fa p-8px" colspan="2">合计</td>
<td class="border border-solid border-#dcdfe6 bg-#f5f7fa p-8px text-right">
{{ formatSumQuantity(printRows, (detail) => detail.quantity) }}
</td>
<td class="border border-solid border-#dcdfe6 bg-#f5f7fa p-8px text-right">
{{ formatSumPrice(printRows, (detail) => detail.price) }}
</td>
<td class="border border-solid border-#dcdfe6 bg-#f5f7fa p-8px text-right">
{{ formatSumPrice(printRows, (detail) => detail.totalPrice) }}
</td>
</tr>
<tr v-if="!printRows.length">
<td
class="border border-solid border-#dcdfe6 p-8px text-center"
:colspan="tableColumnCount"
@ -63,14 +79,15 @@
import { formatNullableDate } from '@/utils/formatTime'
import { DICT_TYPE, getDictLabel } from '@/utils/dict'
import { ShipmentOrderApi, ShipmentOrderVO } from '@/api/wms/order/shipment'
import { formatPrice, formatQuantity } from '@/views/wms/utils/format'
import { ShipmentOrderDetailVO } from '@/api/wms/order/shipment/detail'
import { formatPrice, formatQuantity, formatSumPrice, formatSumQuantity } from '@/views/wms/utils/format'
/** WMS 出库单打印 */
defineOptions({ name: 'WmsShipmentOrderPrint' })
const printData = ref<ShipmentOrderVO>({}) //
const printButtonRef = ref<HTMLButtonElement>() //
const tableColumnCount = 4
const tableColumnCount = 5
const printObj = ref({
id: 'wmsShipmentOrderPrint',
popTitle: '&nbsp',
@ -79,6 +96,20 @@ const printObj = ref({
zIndex: 20003
})
interface PrintRow extends ShipmentOrderDetailVO {
totalPrice?: number
}
const printRows = computed<PrintRow[]>(() =>
(printData.value.details || []).map((detail) => ({
...detail,
totalPrice:
detail.price != null && detail.quantity
? Number((Number(detail.price) * Number(detail.quantity)).toFixed(2))
: undefined
}))
)
/** 打印出库单 */
const print = async (id: number) => {
printData.value = await ShipmentOrderApi.getShipmentOrder(id)

View File

@ -80,10 +80,10 @@
/>
</div>
</el-form-item>
<el-form-item label="总金额" prop="totalAmountMin">
<el-form-item label="总金额" prop="totalPriceMin">
<div class="flex w-240px items-center gap-8px">
<el-input-number
v-model="queryParams.totalAmountMin"
v-model="queryParams.totalPriceMin"
:controls="false"
:min="0"
:precision="PRICE_PRECISION"
@ -92,7 +92,7 @@
/>
<span></span>
<el-input-number
v-model="queryParams.totalAmountMax"
v-model="queryParams.totalPriceMax"
:controls="false"
:min="0"
:precision="PRICE_PRECISION"
@ -242,9 +242,14 @@
{{ formatQuantity(detailScope.row.quantity) }}
</template>
</el-table-column>
<el-table-column align="right" label="单价(元)" width="120">
<template #default="detailScope">
{{ formatPrice(detailScope.row.price) || '-' }}
</template>
</el-table-column>
<el-table-column align="right" label="金额(元)" width="120">
<template #default="detailScope">
{{ formatPrice(detailScope.row.amount) || '-' }}
{{ formatPrice(getDetailTotalPrice(detailScope.row)) || '-' }}
</template>
</el-table-column>
</el-table>
@ -305,7 +310,7 @@
</div>
<div class="flex items-center justify-between">
<span>金额</span>
<span>{{ formatPrice(scope.row.totalAmount) }}</span>
<span>{{ formatPrice(scope.row.totalPrice) }}</span>
</div>
</template>
</el-table-column>
@ -460,8 +465,8 @@ const getDefaultQueryParams = () => ({
orderTime: undefined as string[] | undefined,
totalQuantityMin: undefined as number | undefined,
totalQuantityMax: undefined as number | undefined,
totalAmountMin: undefined as number | undefined,
totalAmountMax: undefined as number | undefined,
totalPriceMin: undefined as number | undefined,
totalPriceMax: undefined as number | undefined,
type: undefined as number | undefined,
bizOrderNo: undefined as string | undefined,
creator: undefined as number | undefined,
@ -532,6 +537,14 @@ const handleWarehouseChange = () => {
handleQuery()
}
/** 计算明细金额 */
const getDetailTotalPrice = (detail: ShipmentOrderDetailVO) => {
if (!detail.quantity || detail.price === undefined || detail.price === null) {
return undefined
}
return Number(detail.quantity) * Number(detail.price)
}
/** 展开明细 */
const handleExpandChange = async (row: ShipmentOrderVO) => {
if (!row.id || detailMap[row.id]) {

View File

@ -48,6 +48,27 @@ export const formatPrice = (value?: number | string | null) => {
return decimalValue === undefined ? '' : decimalValue.toFixed(PRICE_PRECISION)
}
/** 金额四舍五入 */
export const roundPrice = (value: number) => {
return Number.isFinite(value) ? Number(value.toFixed(PRICE_PRECISION)) : undefined
}
/** 数量 * 单价,计算金额 */
export const multiplyPrice = (quantity?: number, price?: number) => {
if (!quantity || price === undefined || price === null) {
return undefined
}
return roundPrice(Number(quantity) * Number(price))
}
/** 金额 / 数量,反算单价 */
export const dividePrice = (totalPrice?: number, quantity?: number) => {
if (totalPrice === undefined || totalPrice === null || !quantity) {
return undefined
}
return roundPrice(Number(totalPrice) / Number(quantity))
}
/** 汇总数量 */
export const sumQuantity = <T>(list: T[], getter: (item: T) => DecimalValue) => {
return sumDecimal(list, getter)