Commit 7dd60816 by qinjianhui

feat: 快速创建出库单功能开发

parent 8bdf60a3
...@@ -13,6 +13,7 @@ Vue3 + Element Plus + Pinia + Axios + TypeScript + Vite ...@@ -13,6 +13,7 @@ Vue3 + Element Plus + Pinia + Axios + TypeScript + Vite
- 当有上下两个子表时,使用 `src/components/splitDiv/splitDiv.vue` 组件,使用时注意传递对应的 props 和插槽 - 当有上下两个子表时,使用 `src/components/splitDiv/splitDiv.vue` 组件,使用时注意传递对应的 props 和插槽
- 表格相关布局时,使用 `src/components/TableView.vue` 组件,使用时注意传递对应的 props 和插槽 - 表格相关布局时,使用 `src/components/TableView.vue` 组件,使用时注意传递对应的 props 和插槽
- `ElDialog`组件中`footer`插槽都放在中间位置 - `ElDialog`组件中`footer`插槽都放在中间位置
- 写入表格`columns`时,如果需要对齐,使用`align`属性,数字相关的列, `align`属性值为`right`,长度一致的列, `align`属性值为`center`, 长度不一致的列, `align`属性值为`left`
## 接口相关 ## 接口相关
......
<template>
<ElDialog
v-model="newDialogVisible"
title="创建出库单"
width="80%"
:close-on-click-modal="false"
>
<div class="dialog-form">
<ElForm
ref="editFormRef"
:model="editForm"
:rules="rules"
inline
label-width="90px"
>
<ElFormItem label="出库单号" prop="account">
<ElInput v-model.trim="editForm.outNo" clearable disabled />
</ElFormItem>
<ElFormItem label="工厂:" prop="factoryCode">
<span>{{ editForm.factoryCode }}</span>
</ElFormItem>
<ElFormItem label="仓库" prop="warehouseId" required>
<ElSelect
v-model="editForm.warehouseId"
clearable
disabled
placeholder="请选择仓库"
style="width: 160px"
>
<ElOption
v-for="item in warehouseList"
:key="item.id"
:label="item.name"
:value="item.id"
></ElOption>
</ElSelect>
</ElFormItem>
<ElFormItem label="备注" prop="remark" style="width: 45%">
<ElInput
v-model.trim="editForm.remark"
placeholder="请输入备注"
clearable
/>
</ElFormItem>
</ElForm>
<ElTable
size="small"
:data="otherPurchaseData"
height="500px"
border
@selection-change="productSelectionChange"
>
<ElTableColumn
type="selection"
width="70"
header-align="center"
align="center"
></ElTableColumn>
<ElTableColumn
show-overflow-tooltip
width="60"
align="center"
label="序号"
type="index"
></ElTableColumn>
<ElTableColumn
show-overflow-tooltip
align="center"
width="100"
label="SKU图片"
prop="skuImage"
>
<template #default="{ row }">
<ImageView :src="row.skuImage" width="40px" height="40px" />
</template>
</ElTableColumn>
<ElTableColumn
show-overflow-tooltip
align="center"
label="库存SKU"
prop="warehouseSku"
/>
<ElTableColumn
show-overflow-tooltip
align="center"
label="商品名称"
prop="skuName"
/>
<ElTableColumn
show-overflow-tooltip
align="center"
label="款号"
prop="productNo"
/>
<ElTableColumn
show-overflow-tooltip
align="center"
label="可用库存数量"
prop="usableInventory"
/>
<ElTableColumn align="center" label="出库数量" prop="outCount">
<template #default="{ row }">
<el-input
v-model.number="row.outCount"
placeholder="出库数量"
style="width: 120px"
clearable
size="small"
@input="setCostPrice(row)"
></el-input>
</template>
</ElTableColumn>
<ElTableColumn
width="80"
align="center"
label="币种"
prop="currencyName"
/>
<ElTableColumn
width="100"
align="center"
label="成本价"
prop="costPrice"
/>
<ElTableColumn
align="center"
width="100"
label="总成本"
prop="totalPrice"
/>
<ElTableColumn align="center" label="库位" prop="locationCode">
<template #default="{ row }">
<span v-if="row.locationCode">{{ row.locationCode }}</span>
<ElSelect
v-else
v-model="row.locationId"
clearable
placeholder="请输入库位"
style="width: 120px"
filterable
@change="handleLocationChange(row.locationId, row)"
>
<ElOption
v-for="item in locationList"
:key="item.locationId"
:label="item.locationCode"
:value="item.locationId"
></ElOption>
</ElSelect>
</template>
</ElTableColumn>
<ElTableColumn
show-overflow-tooltip
align="center"
label="所属客户"
prop="userMark"
/>
<ElTableColumn
show-overflow-tooltip
align="center"
width="240"
label="备注"
prop="remark"
>
<template #default="{ row }">
<ElInput v-model.trim="row.remark" clearable size="small" />
</template>
</ElTableColumn>
</ElTable>
</div>
<template #footer>
<div class="product-dialog-footer">
<div>
<el-button
type="danger"
size="small"
@click="deleteOtherWarehousing()"
>
删除
</el-button>
</div>
<div>
<el-button
size="small"
style="margin-left: 10px"
@click="newDialogVisible = false"
>
取消
</el-button>
<el-button type="primary" size="small" @click="addOtherCurrency()">
保存
</el-button>
</div>
</div>
</template>
</ElDialog>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import BigNumber from 'bignumber.js'
import { ElMessage, ElLoading } from 'element-plus'
import ImageView from '@/components/ImageView.vue'
import {
type InterWarehouseDetail,
type InterProductList,
type InterskuList,
type ILocation,
} from '@/types/api/warehouse'
import {
warehouseInfoGetAll,
type warehouseInfo,
getBySkuAndWarehouseIdApi,
getByWareHouseIdAndCodeApi,
addOutRecordApi,
} from '@/api/warehouse'
interface OpenParamsItem {
thirdSkuCode: string
suggestOutQuantity: number
}
interface OpenParams {
warehouseId: number | string
warehouseName?: string
items: OpenParamsItem[]
}
const emit = defineEmits<{
success: []
}>()
const warehouseList = ref<warehouseInfo[]>([])
const locationList = ref<ILocation[]>([])
const newDialogVisible = ref(false)
const editFormRef = ref()
const editForm = ref<InterWarehouseDetail>({
outNo: '',
warehouseId: '',
warehouseName: '',
remark: '',
factoryCode: '',
factoryId: 0,
productList: [],
})
const rules = {
warehouseId: [{ required: true, message: '请选择仓库', trigger: 'change' }],
}
const otherPurchaseData = ref<InterProductList[]>([])
const otherWarehouseSelection = ref<InterProductList[]>([])
const setCostPrice = (item: InterProductList) => {
if (item.costPrice !== 0 && !item.costPrice) {
ElMessage.warning('商品成本价为空,请完善商品成本价')
return
}
if (item) {
const outCount = item.outCount ?? 0
const costPrice = item.costPrice ?? 0
const amount = new BigNumber(outCount).multipliedBy(costPrice).toFixed(2)
item.totalPrice = Number(amount)
}
}
const fetchWarehouseList = async () => {
if (warehouseList.value.length) return
try {
const res = await warehouseInfoGetAll()
warehouseList.value = res.data || []
} catch (e) {
console.error(e)
}
}
const fetchLocationList = async (warehouseId: number | string | undefined) => {
if (!warehouseId) return
try {
const res = await getByWareHouseIdAndCodeApi(warehouseId, '')
const result = res.data || []
locationList.value = result.map((item: ILocation) => {
return {
locationId: item.id,
locationCode: item.locationCode,
}
})
} catch (e) {
console.error(e)
locationList.value = []
}
}
const handleLocationChange = (val: number, row: InterProductList) => {
const found = locationList.value.find(
(item: ILocation) => item.locationId === val,
)
row.locationCode = found ? found.locationCode : ''
}
const productSelectionChange = (v: InterProductList[]) => {
otherWarehouseSelection.value = v
}
const deleteOtherWarehousing = () => {
const arr = otherWarehouseSelection.value
if (!arr.length) return
const idList = arr.map((v: InterProductList) => v.warehouseSku)
otherPurchaseData.value = otherPurchaseData.value.filter(
(item: InterProductList) => !idList.includes(item.warehouseSku),
)
}
const addOtherCurrency = async () => {
try {
await editFormRef.value?.validate()
} catch {
return
}
const arr = otherPurchaseData.value
if (!arr.length) {
ElMessage.error('请至少选择一条数据')
return
}
for (let i = 0; i < arr.length; i++) {
if (!arr[i].outCount) {
ElMessage.error('请输入出库数量')
return
}
const usableInventory = arr[i].usableInventory || 0
if ((arr[i].outCount as number) > usableInventory) {
ElMessage.error('出库数量不能大于可用库存数量')
return
}
if (!arr[i].locationId) {
ElMessage.error('请选择库位')
return
}
}
const params: InterWarehouseDetail = {
...editForm.value,
productList: otherPurchaseData.value,
}
try {
await addOutRecordApi(params)
ElMessage.success('保存成功')
newDialogVisible.value = false
emit('success')
} catch (e) {
console.error(e)
}
}
const open = async (params: OpenParams) => {
editForm.value.outNo = ''
editForm.value.warehouseId = params.warehouseId
editForm.value.warehouseName =
params.warehouseName ||
warehouseList.value.find(
(item: warehouseInfo) => item.id === params.warehouseId,
)?.name ||
''
editForm.value.remark = ''
const userJson = localStorage.getItem('user')
if (userJson) {
try {
const userData = JSON.parse(userJson)
editForm.value.factoryCode = userData.factoryCode || ''
editForm.value.factoryId = userData.factoryId || 0
} catch {
// ignore
}
}
otherPurchaseData.value = []
const skuList = params.items
.map((item) => item.thirdSkuCode)
.filter((sku) => !!sku)
if (!skuList.length) {
newDialogVisible.value = true
return
}
const skuToSuggestQuantity = new Map<string, number>()
params.items.forEach((item) => {
if (item.thirdSkuCode) {
skuToSuggestQuantity.set(item.thirdSkuCode, item.suggestOutQuantity || 0)
}
})
const loading = ElLoading.service({
text: '加载库存中...',
background: 'rgba(0, 0, 0, 0.3)',
})
try {
const res = await getBySkuAndWarehouseIdApi(
params.warehouseId,
'JM240915008_LBU_L',
)
if (res.code !== 200) return
await fetchWarehouseList()
await fetchLocationList(params.warehouseId)
const arr: InterskuList[] = res.data || []
const mergedProductList = arr.map((skuItem) => {
const warehouseSku = skuItem.warehouseSku || ''
const suggestOutQuantity = skuToSuggestQuantity.get(warehouseSku) ?? 0
const outCount = suggestOutQuantity
const costPrice = skuItem.price ?? 0
const totalPrice = new BigNumber(outCount)
.multipliedBy(costPrice || 0)
.toNumber()
return {
skuImage: skuItem.image,
customerId: skuItem.customerId,
userMark: skuItem.userMark,
customerName: skuItem.customerName,
currencyName: skuItem.currencyName ?? undefined,
currencyCode: skuItem.currencyCode ?? undefined,
warehouseSku: skuItem.warehouseSku,
skuName: skuItem.skuName,
productNo: skuItem.productNumber,
locationCode: skuItem.locationCode ?? '',
locationId: skuItem.locationId ?? null,
costPrice,
outCount,
totalPrice,
usableInventory: skuItem.usableInventory,
inventoryId: skuItem.id,
remark: skuItem.remark ?? null,
} as InterProductList
})
otherPurchaseData.value = mergedProductList
} catch (e) {
console.error(e)
otherPurchaseData.value = []
} finally {
loading.close()
}
newDialogVisible.value = true
}
defineExpose({ open })
</script>
<style scoped lang="scss">
.product-dialog-footer {
display: flex;
justify-content: space-between;
margin: 8px 0;
}
</style>
...@@ -19,7 +19,9 @@ ...@@ -19,7 +19,9 @@
<TableView <TableView
:paginated-data="tableData" :paginated-data="tableData"
:columns="columns" :columns="columns"
selectionable
serial-numberable serial-numberable
@selection-change="handleSelectionChange"
> >
<template #skuImage="{ row }"> <template #skuImage="{ row }">
<el-image <el-image
...@@ -53,39 +55,43 @@ ...@@ -53,39 +55,43 @@
<ElButton @click="visible = false">取消</ElButton> <ElButton @click="visible = false">取消</ElButton>
</span> </span>
<span class="item"> <span class="item">
<ElButton <ElButton type="primary" @click="handleCreateOutbound">
type="primary"
:loading="submitLoading"
@click="handleCreateOutbound"
>
快速创建出库单 快速创建出库单
</ElButton> </ElButton>
</span> </span>
</div> </div>
</template> </template>
</ElDialog> </ElDialog>
<CreateOutboundDialog
ref="createOutboundDialogRef"
@success="() => {
visible = false
emit('success')
}"
/>
</template> </template>
<script setup lang="tsx"> <script setup lang="tsx">
import { ref } from 'vue' import { ref } from 'vue'
import { ElMessage } from 'element-plus' import { ElMessage } from 'element-plus'
import { import {
createOutboundOrderApi,
applyForReplenishByIdApi, applyForReplenishByIdApi,
} from '@/api/factoryOrderNew' } from '@/api/factoryOrderNew'
import type { operateOrderListData } from '@/types/api/factoryOrderNew' import type { operateOrderListData } from '@/types/api/factoryOrderNew'
import TableView from '@/components/TableView.vue' import TableView from '@/components/TableView.vue'
import _ from 'lodash' import _ from 'lodash'
import CreateOutboundDialog from './CreateOutboundDialog.vue'
const emit = defineEmits<{ const emit = defineEmits<{
success: [] success: []
}>() }>()
const visible = ref(false) const visible = ref(false)
const submitLoading = ref(false)
const tableData = ref<operateOrderListData[]>([]) const tableData = ref<operateOrderListData[]>([])
const orderIds = ref<(number | string)[]>([]) const orderIds = ref<(number | string)[]>([])
const dialogTitle = ref('拣胚失败') const dialogTitle = ref('拣胚失败')
const selections = ref<operateOrderListData[]>([])
const columns = [ const columns = [
{ {
key: 'warehouseName', key: 'warehouseName',
...@@ -196,6 +202,10 @@ const columns = [ ...@@ -196,6 +202,10 @@ const columns = [
}, },
] ]
const handleSelectionChange = (selection: operateOrderListData[]) => {
selections.value = selection
}
const open = async ( const open = async (
ids: (number | string)[], ids: (number | string)[],
options?: { title?: string; submitType?: string }, options?: { title?: string; submitType?: string },
...@@ -224,18 +234,54 @@ const handleClose = () => { ...@@ -224,18 +234,54 @@ const handleClose = () => {
tableData.value = [] tableData.value = []
} }
const handleCreateOutbound = async () => { const createOutboundDialogRef = ref<InstanceType<
submitLoading.value = true typeof CreateOutboundDialog
try { > | null>(null)
await createOutboundOrderApi(orderIds.value)
ElMessage.success('创建出库单成功') const handleCreateOutbound = () => {
visible.value = false if (selections.value.length === 0) {
emit('success') ElMessage.warning('请至少选择一条数据')
} catch (e: unknown) { return
ElMessage.error((e as Error)?.message || '创建出库单失败') }
} finally {
submitLoading.value = false const warehouseIds = _.uniq(
selections.value
.map((item) => item.warehouseId)
.filter((id) => id !== undefined && id !== null),
)
if (warehouseIds.length !== 1) {
ElMessage.warning('请选择相同仓库的库存SKU!')
return
}
const warehouseId = warehouseIds[0] as number | string
const firstSelection = selections.value[0] as operateOrderListData & {
warehouseName?: string
} }
const warehouseName = firstSelection.warehouseName
const items = selections.value.map((item) => {
const row = item as operateOrderListData & {
thirdSkuCode?: string
inventory?: number
producingQuantity?: number
}
return {
thirdSkuCode: row.thirdSkuCode || '',
suggestOutQuantity: _.subtract(
Number(row.inventory ?? 0),
Number(row.producingQuantity ?? 0),
),
}
})
createOutboundDialogRef.value?.open({
warehouseId,
warehouseName,
items,
})
} }
defineExpose({ open }) defineExpose({ open })
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment