feat(biz): 新增成品入出库功能并优化销售发货界面

This commit is contained in:
zxy 2026-05-15 17:37:15 +08:00
parent 14b19dc223
commit 70b9038a1e
18 changed files with 1362 additions and 195 deletions

View File

@ -66,9 +66,9 @@ public class SaleDeliveryController {
@Operation(summary = "获得销售出库单主")
@Parameter(name = "id", description = "编号", required = true, example = "1024")
@PreAuthorize("@ss.hasPermission('tso:sale-delivery:query')")
public CommonResult<SaleDeliveryRespVO> getSaleDelivery(@RequestParam("id") Integer id) {
SaleDeliveryDO saleDelivery = saleDeliveryService.getSaleDelivery(id);
return success(BeanUtils.toBean(saleDelivery, SaleDeliveryRespVO.class));
public CommonResult<SaleDeliverySaveReqVO> getSaleDelivery(@RequestParam("id") Integer id) {
SaleDeliverySaveReqVO saleDelivery = saleDeliveryService.getSaleDeliveryWithDetails(id);
return success(BeanUtils.toBean(saleDelivery, SaleDeliverySaveReqVO.class));
}
@GetMapping("/page")

View File

@ -2,12 +2,15 @@ package com.ningxia.yunxi.chemmes.module.biz.controller.admin.saledelivery.vo;
import com.alibaba.excel.annotation.ExcelIgnoreUnannotated;
import com.alibaba.excel.annotation.ExcelProperty;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.ningxia.yunxi.chemmes.module.biz.controller.admin.saledeliverydetail.vo.SaleDeliveryDetailRespVO;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.List;
@Schema(description = "管理后台 - 销售出库单主 Response VO")
@Data
@ -28,6 +31,7 @@ public class SaleDeliveryRespVO {
@Schema(description = "单据日期")
@ExcelProperty("单据日期")
@JsonFormat(pattern = "yyyy-MM-dd")
private LocalDate deliveryDate;
@Schema(description = "客户id", example = "11842")
@ -118,4 +122,7 @@ public class SaleDeliveryRespVO {
@ExcelProperty("发货数量")
private BigDecimal deliveriedQty;
@Schema(description = "出库明细列表")
private List<SaleDeliveryDetailRespVO> detailList;
}

View File

@ -1,10 +1,12 @@
package com.ningxia.yunxi.chemmes.module.biz.controller.admin.saledelivery.vo;
import com.ningxia.yunxi.chemmes.module.biz.controller.admin.saledeliverydetail.vo.SaleDeliveryDetailSaveReqVO;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.util.List;
@Schema(description = "管理后台 - 销售出库单主新增/修改 Request VO")
@Data
@ -85,4 +87,6 @@ public class SaleDeliverySaveReqVO {
@Schema(description = "发货数量")
private BigDecimal deliveriedQty;
private List<SaleDeliveryDetailSaveReqVO> detailList;
}

View File

@ -45,4 +45,11 @@ public interface SaleDeliveryMapper extends BaseMapperX<SaleDeliveryDO> {
.orderByDesc(SaleDeliveryDO::getId));
}
default String selectMaxSaleDeliveryNo() {
SaleDeliveryDO saleDelivery = selectOne(new LambdaQueryWrapperX<SaleDeliveryDO>()
.orderByDesc(SaleDeliveryDO::getSaleDeliveryNo)
.last("LIMIT 1"));
return saleDelivery != null ? saleDelivery.getSaleDeliveryNo() : null;
}
}

View File

@ -1,11 +1,16 @@
package com.ningxia.yunxi.chemmes.module.biz.dal.mysql.saledeliverydetail;
import com.baomidou.mybatisplus.annotation.InterceptorIgnore;
import com.ningxia.yunxi.chemmes.framework.common.pojo.PageResult;
import com.ningxia.yunxi.chemmes.framework.mybatis.core.mapper.BaseMapperX;
import com.ningxia.yunxi.chemmes.framework.mybatis.core.query.LambdaQueryWrapperX;
import com.ningxia.yunxi.chemmes.module.biz.controller.admin.saledeliverydetail.vo.SaleDeliveryDetailPageReqVO;
import com.ningxia.yunxi.chemmes.module.biz.dal.dataobject.saledeliverydetail.SaleDeliveryDetailDO;
import org.apache.ibatis.annotations.Delete;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.util.List;
/**
* 销售出库单子 Mapper
@ -15,6 +20,15 @@ import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface SaleDeliveryDetailMapper extends BaseMapperX<SaleDeliveryDetailDO> {
default List<SaleDeliveryDetailDO> selectBySaleDeliveryId(Integer saleDeliveryId) {
return selectList(SaleDeliveryDetailDO::getSaleDeliveryId, saleDeliveryId);
}
@Delete("DELETE FROM tso_sale_delivery_detail WHERE sale_delivery_id = #{saleDeliveryId}")
@InterceptorIgnore(tenantLine = "true")
int physicalDeleteBySaleDeliveryId(@Param("saleDeliveryId") Integer saleDeliveryId);
default PageResult<SaleDeliveryDetailDO> selectPage(SaleDeliveryDetailPageReqVO reqVO) {
return selectPage(reqVO, new LambdaQueryWrapperX<SaleDeliveryDetailDO>()
.betweenIfPresent(SaleDeliveryDetailDO::getCreateTime, reqVO.getCreateTime())

View File

@ -64,6 +64,9 @@ public class OrderServiceImpl implements OrderService {
if ("9".equals(order.getOrdStatus())) {
updateReqVO.setOrdStatus("1");
}
if ("2".equals(order.getOrdStatus())) {
updateReqVO.setOrdStatus("3");
}
// 更新主表
OrderDO updateObj = BeanUtils.toBean(updateReqVO, OrderDO.class);

View File

@ -44,6 +44,14 @@ public interface SaleDeliveryService {
*/
SaleDeliveryDO getSaleDelivery(Integer id);
/**
* 获得销售出库单详情包含明细
*
* @param id 编号
* @return 销售出库单详情
*/
SaleDeliverySaveReqVO getSaleDeliveryWithDetails(Integer id);
/**
* 获得销售出库单主分页
*

View File

@ -4,12 +4,19 @@ import com.ningxia.yunxi.chemmes.framework.common.pojo.PageResult;
import com.ningxia.yunxi.chemmes.framework.common.util.object.BeanUtils;
import com.ningxia.yunxi.chemmes.module.biz.controller.admin.saledelivery.vo.SaleDeliveryPageReqVO;
import com.ningxia.yunxi.chemmes.module.biz.controller.admin.saledelivery.vo.SaleDeliverySaveReqVO;
import com.ningxia.yunxi.chemmes.module.biz.controller.admin.saledeliverydetail.vo.SaleDeliveryDetailSaveReqVO;
import com.ningxia.yunxi.chemmes.module.biz.dal.dataobject.saledelivery.SaleDeliveryDO;
import com.ningxia.yunxi.chemmes.module.biz.dal.dataobject.saledeliverydetail.SaleDeliveryDetailDO;
import com.ningxia.yunxi.chemmes.module.biz.dal.mysql.saledelivery.SaleDeliveryMapper;
import com.ningxia.yunxi.chemmes.module.biz.dal.mysql.saledeliverydetail.SaleDeliveryDetailMapper;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.validation.annotation.Validated;
import javax.annotation.Resource;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.List;
import static com.ningxia.yunxi.chemmes.framework.common.exception.util.ServiceExceptionUtil.exception;
@ -25,30 +32,78 @@ public class SaleDeliveryServiceImpl implements SaleDeliveryService {
@Resource
private SaleDeliveryMapper saleDeliveryMapper;
@Resource
private SaleDeliveryDetailMapper saleDeliveryDetailMapper;
@Override
@Transactional(rollbackFor = Exception.class)
public Integer createSaleDelivery(SaleDeliverySaveReqVO createReqVO) {
String saleDeliveryNo = generateSaleDeliveryNo();
createReqVO.setSaleDeliveryNo(saleDeliveryNo);
// 插入
SaleDeliveryDO saleDelivery = BeanUtils.toBean(createReqVO, SaleDeliveryDO.class);
saleDeliveryMapper.insert(saleDelivery);
createSaleDeliveryDetailList(saleDelivery.getId(), createReqVO.getDetailList());
// 返回
return saleDelivery.getId();
}
@Override
@Transactional(rollbackFor = Exception.class)
public void updateSaleDelivery(SaleDeliverySaveReqVO updateReqVO) {
// 校验存在
validateSaleDeliveryExists(updateReqVO.getId());
// 更新
SaleDeliveryDO updateObj = BeanUtils.toBean(updateReqVO, SaleDeliveryDO.class);
saleDeliveryMapper.updateById(updateObj);
updateSaleDeliveryDetailList(updateReqVO.getId(), updateReqVO.getDetailList());
}
/**
* 创建销售出库单子表列表
*/
private void createSaleDeliveryDetailList(Integer saleDeliveryId, List<SaleDeliveryDetailSaveReqVO> list) {
if (list == null || list.isEmpty()) {
return;
}
List<SaleDeliveryDetailDO> saleDeliveryDetails = BeanUtils.toBean(list, SaleDeliveryDetailDO.class);
saleDeliveryDetails.forEach(detail ->
detail.setSaleDeliveryId(saleDeliveryId)
.setId(null)
);
saleDeliveryDetailMapper.insertBatch(saleDeliveryDetails);
}
/**
* 更新销售出库单子表列表
*/
private void updateSaleDeliveryDetailList(Integer saleDeliveryId, List<SaleDeliveryDetailSaveReqVO> list) {
// 先删除旧的子表记录
deleteSaleDeliveryDetailBySaleDeliveryId(saleDeliveryId);
// 再插入新的子表记录
createSaleDeliveryDetailList(saleDeliveryId, list);
}
/**
* 根据销售出库单ID删除子表记录
*/
private void deleteSaleDeliveryDetailBySaleDeliveryId(Integer saleDeliveryId) {
saleDeliveryDetailMapper.physicalDeleteBySaleDeliveryId(saleDeliveryId);
}
@Override
@Transactional(rollbackFor = Exception.class)
public void deleteSaleDelivery(Integer id) {
// 校验存在
validateSaleDeliveryExists(id);
// 删除
saleDeliveryMapper.deleteById(id);
// 删除子表 物理删除
saleDeliveryDetailMapper.physicalDeleteBySaleDeliveryId(id);
}
private void validateSaleDeliveryExists(Integer id) {
@ -62,9 +117,49 @@ public class SaleDeliveryServiceImpl implements SaleDeliveryService {
return saleDeliveryMapper.selectById(id);
}
@Override
public SaleDeliverySaveReqVO getSaleDeliveryWithDetails(Integer id) {
// 查询主表
SaleDeliveryDO saleDelivery = saleDeliveryMapper.selectById(id);
if (saleDelivery == null) {
throw exception("销售出库单主不存在");
}
// 转换为主表VO
SaleDeliverySaveReqVO saleDeliveryVO = BeanUtils.toBean(saleDelivery, SaleDeliverySaveReqVO.class);
// 查询子表
List<SaleDeliveryDetailDO> detailList = saleDeliveryDetailMapper.selectBySaleDeliveryId(id);
// 转换为子表VO列表
saleDeliveryVO.setDetailList(BeanUtils.toBean(detailList, SaleDeliveryDetailSaveReqVO.class));
return saleDeliveryVO;
}
@Override
public PageResult<SaleDeliveryDO> getSaleDeliveryPage(SaleDeliveryPageReqVO pageReqVO) {
return saleDeliveryMapper.selectPage(pageReqVO);
}
/**
* 生成销售出库单号
* 规则SC + 年份(4位) + 月份(2位) + 流水号(3位)
*/
private String generateSaleDeliveryNo() {
String ym = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyyMM"));
String maxSaleDeliveryNo = saleDeliveryMapper.selectMaxSaleDeliveryNo();
if (maxSaleDeliveryNo == null || maxSaleDeliveryNo.length() < 9
|| !maxSaleDeliveryNo.substring(2, 8).equals(ym)) {
return "SC" + ym + "001";
} else {
String prefix = maxSaleDeliveryNo.substring(0, 8);
int sequence = Integer.parseInt(maxSaleDeliveryNo.substring(8));
sequence++;
return prefix + String.format("%03d", sequence);
}
}
}

View File

@ -1,12 +1,61 @@
node_modules
# 测试文件夹
tests/
test-results/
test-reports/
html-report/
# 截图文件
*.png
*.jpg
*.jpeg
# 测试日志
*.log
logs/
# pytest 缓存
.pytest_cache/
.coverage
htmlcov/
# Node.js 依赖
node_modules/
# 构建输出
dist/
build/
*.zip
# 编辑器配置
.vscode/
.idea/
*.swp
*.swo
*~
# 操作系统文件
.DS_Store
dist
dist-ssr
*.local
/dist*
*-lock.*
pnpm-debug
auto-*.d.ts
.idea
.history
/.trae/
Thumbs.db
# 环境变量
.env
.env.local
.env.*.local
# 临时文件
*.tmp
*.temp
# Python 字节码
__pycache__/
*.pyc
*.pyo
*.pyd
.Python
# 虚拟环境
venv/
env/
.venv/
/pytest.ini
/pnpm-lock.yaml

View File

@ -0,0 +1,48 @@
import request from '@/config/axios'
export interface ProStorageVO {
id: number
billNo: string
operatorType: boolean
businessType: number
remark: string
status: boolean
billDate: localdate
operatorId: number
operatorName: string
relarionNo: string
relarionId: number
billType: string
sourceNo: string
sourceId: number
}
// 查询成品入/出库分页
export const getProStoragePage = async (params) => {
return await request.get({ url: `/twm/pro-storage/page`, params })
}
// 查询成品入/出库详情
export const getProStorage = async (id: number) => {
return await request.get({ url: `/twm/pro-storage/get?id=` + id })
}
// 新增成品入/出库
export const createProStorage = async (data: ProStorageVO) => {
return await request.post({ url: `/twm/pro-storage/create`, data })
}
// 修改成品入/出库
export const updateProStorage = async (data: ProStorageVO) => {
return await request.put({ url: `/twm/pro-storage/update`, data })
}
// 删除成品入/出库
export const deleteProStorage = async (id: number) => {
return await request.delete({ url: `/twm/pro-storage/delete?id=` + id })
}
// 导出成品入/出库 Excel
export const exportProStorage = async (params) => {
return await request.download({ url: `/twm/pro-storage/export-excel`, params })
}

View File

@ -0,0 +1,56 @@
import request from '@/config/axios'
export interface ProStorageLogVO {
id: number
stockId: number
description: string
status: boolean
storeHouseId: number
storeAreaId: number
storeHouseCd: string
storeHouseName: string
storeAreCd: string
storeAreaName: string
materialId: number
matName: string
matCode: string
spec: string
unit: string
lotNo: string
operatorQty: number
inOutType: boolean
billType: number
storageAft: number
storageBef: number
stockItemId: number
}
// 查询成品入/出库日志分页
export const getProStorageLogPage = async (params) => {
return await request.get({ url: `/twm/pro-storage-log/page`, params })
}
// 查询成品入/出库日志详情
export const getProStorageLog = async (id: number) => {
return await request.get({ url: `/twm/pro-storage-log/get?id=` + id })
}
// 新增成品入/出库日志
export const createProStorageLog = async (data: ProStorageLogVO) => {
return await request.post({ url: `/twm/pro-storage-log/create`, data })
}
// 修改成品入/出库日志
export const updateProStorageLog = async (data: ProStorageLogVO) => {
return await request.put({ url: `/twm/pro-storage-log/update`, data })
}
// 删除成品入/出库日志
export const deleteProStorageLog = async (id: number) => {
return await request.delete({ url: `/twm/pro-storage-log/delete?id=` + id })
}
// 导出成品入/出库日志 Excel
export const exportProStorageLog = async (params) => {
return await request.download({ url: `/twm/pro-storage-log/export-excel`, params })
}

View File

@ -0,0 +1,56 @@
import request from '@/config/axios'
export interface ProStorageMatVO {
id: number
stockId: number
description: string
storeHouseId: number
storeAreaId: number
storeHouseCd: string
storeHouseName: string
storeAreCd: string
storeAreaName: string
materialId: number
matName: string
matCode: string
spec: string
unit: string
lotNo: string
operatorQty: number
sourceId: number
relarionId: number
bagSpec: number
bagQty: number
planId: number
proNo: string
}
// 查询成品入/出库子分页
export const getProStorageMatPage = async (params) => {
return await request.get({ url: `/twm/pro-storage-mat/page`, params })
}
// 查询成品入/出库子详情
export const getProStorageMat = async (id: number) => {
return await request.get({ url: `/twm/pro-storage-mat/get?id=` + id })
}
// 新增成品入/出库子
export const createProStorageMat = async (data: ProStorageMatVO) => {
return await request.post({ url: `/twm/pro-storage-mat/create`, data })
}
// 修改成品入/出库子
export const updateProStorageMat = async (data: ProStorageMatVO) => {
return await request.put({ url: `/twm/pro-storage-mat/update`, data })
}
// 删除成品入/出库子
export const deleteProStorageMat = async (id: number) => {
return await request.delete({ url: `/twm/pro-storage-mat/delete?id=` + id })
}
// 导出成品入/出库子 Excel
export const exportProStorageMat = async (params) => {
return await request.download({ url: `/twm/pro-storage-mat/export-excel`, params })
}

View File

@ -0,0 +1,331 @@
<template>
<!--
通用金额输入组件
支持小数位数限制负数控制前后缀显示等功能
-->
<el-input
v-model="displayValue"
:placeholder="placeholder"
:disabled="disabled"
:readonly="readonly"
:clearable="clearable"
@input="handleInput"
@blur="handleBlur"
@focus="handleFocus"
>
<!-- 前缀模板根据 showPrefix 控制显示 -->
<template v-if="showPrefix" #prefix>{{ prefix }}</template>
<!-- 后缀模板根据 showSuffix 控制显示 -->
<template v-if="showSuffix" #suffix>{{ suffix }}</template>
</el-input>
</template>
<script setup lang="ts">
/**
* 通用金额输入组件 MoneyInput
*
* 功能特性
* - 自动限制小数位数
* - 可配置是否允许负数输入
* - 支持自定义前后缀符号 ¥$%
* - 自动格式化显示失焦时自动补全小数位
* - 输入时自动过滤非法字符
*
* 使用示例
* ```vue
* <!-- 基本用法人民币金额2位小数 -->
* <MoneyInput v-model="amount" />
*
* <!-- 订单数量整数不允许负数无符号 -->
* <MoneyInput
* v-model="quantity"
* :decimal-places="0"
* :allow-negative="false"
* :show-prefix="false"
* />
*
* <!-- 美元金额 -->
* <MoneyInput
* v-model="usdAmount"
* prefix="$"
* :decimal-places="2"
* />
*
* <!-- 百分比显示2位小数显示%后缀 -->
* <MoneyInput
* v-model="rate"
* :decimal-places="2"
* suffix="%"
* :show-suffix="true"
* :show-prefix="false"
* />
* ```
*/
import { ref, watch } from 'vue'
/**
* Props 定义
*/
interface Props {
/**
* 绑定值双向绑定
*/
modelValue?: number | string
/**
* 输入框占位文本
* @default '请输入'
*/
placeholder?: string
/**
* 是否禁用输入框
* @default false
*/
disabled?: boolean
/**
* 是否只读
* @default false
*/
readonly?: boolean
/**
* 是否显示清除按钮
* @default true
*/
clearable?: boolean
/**
* 小数位数
* @default 2
*/
decimalPlaces?: number
/**
* 是否允许输入负数
* @default true
*/
allowNegative?: boolean
/**
* 前缀符号 ¥$
* @default '¥'
*/
prefix?: string
/**
* 后缀符号 %
* @default ''
*/
suffix?: string
/**
* 是否显示前缀
* @default true
*/
showPrefix?: boolean
/**
* 是否显示后缀
* @default false
*/
showSuffix?: boolean
}
/**
* Props 默认值配置
*/
const props = withDefaults(defineProps<Props>(), {
modelValue: undefined,
placeholder: '请输入',
disabled: false,
readonly: false,
clearable: true,
decimalPlaces: 2,
allowNegative: true,
prefix: '¥',
suffix: '',
showPrefix: true,
showSuffix: false
})
/**
* 事件定义
*/
const emit = defineEmits<{
/**
* 值变化事件双向绑定
* @param value - 新的值number | string | undefined
*/
'update:modelValue': [value: number | string | undefined]
/**
* 值改变事件失焦时触发
* @param value - 新的值number | string | undefined
*/
'change': [value: number | string | undefined]
}>()
/**
* 显示值格式化后的字符串
*/
const displayValue = ref<string>('')
/**
* 初始化显示值
* modelValue 转换为格式化后的字符串
*/
const initDisplayValue = () => {
if (props.modelValue !== undefined && props.modelValue !== null && props.modelValue !== '') {
const num = Number(props.modelValue)
if (!isNaN(num)) {
displayValue.value = num.toFixed(props.decimalPlaces)
} else {
displayValue.value = ''
}
} else {
displayValue.value = ''
}
}
//
initDisplayValue()
/**
* 监听 modelValue 变化同步更新显示值
*/
watch(() => props.modelValue, () => {
initDisplayValue()
}, { immediate: true })
/**
* 处理输入事件
* 过滤非法字符限制小数位数
*/
const handleInput = (value: string) => {
let inputValue = value
//
if (props.allowNegative) {
inputValue = inputValue.replace(/[^\d.-]/g, '')
const negativeCount = (inputValue.match(/-/g) || []).length
if (negativeCount > 1) {
inputValue = '-' + inputValue.replace(/-/g, '').slice(1)
}
if (inputValue.indexOf('-') > 0) {
inputValue = inputValue.replace(/-/g, '')
}
} else {
//
inputValue = inputValue.replace(/[^\d.]/g, '')
}
// decimalPlaces 0
if (props.decimalPlaces === 0) {
inputValue = inputValue.replace(/\./g, '')
displayValue.value = inputValue
return
}
// 0
if (inputValue.startsWith('.')) {
inputValue = '0' + inputValue
}
// 0
if (inputValue.startsWith('-') && inputValue.length > 1 && inputValue[1] === '.') {
inputValue = '-0' + inputValue.slice(1)
}
//
const dotIndex = inputValue.indexOf('.')
if (dotIndex !== -1) {
const integerPart = inputValue.slice(0, dotIndex)
const decimalPart = inputValue.slice(dotIndex + 1)
const limitedDecimal = decimalPart.slice(0, props.decimalPlaces)
inputValue = integerPart + '.' + limitedDecimal
}
//
const dotCount = (inputValue.match(/\./g) || []).length
if (dotCount > 1) {
const firstDotIndex = inputValue.indexOf('.')
inputValue = inputValue.slice(0, firstDotIndex) + inputValue.slice(firstDotIndex).replace(/\./g, '')
}
//
if (inputValue === '-' || inputValue === '.') {
displayValue.value = inputValue
return
}
//
if (inputValue !== '' && inputValue !== '-') {
const num = Number(inputValue)
if (!isNaN(num)) {
if (!props.allowNegative && num < 0) {
inputValue = Math.abs(num).toFixed(props.decimalPlaces)
}
}
}
displayValue.value = inputValue
}
/**
* 处理失焦事件
* 格式化最终值并触发事件
*/
const handleBlur = () => {
let value = displayValue.value
//
if (value === '' || value === '-' || value === '.') {
displayValue.value = ''
emit('update:modelValue', undefined)
emit('change', undefined)
return
}
//
const num = Number(value)
if (isNaN(num)) {
displayValue.value = ''
emit('update:modelValue', undefined)
emit('change', undefined)
return
}
//
if (!props.allowNegative && num < 0) {
const absNum = Math.abs(num)
displayValue.value = absNum.toFixed(props.decimalPlaces)
emit('update:modelValue', absNum)
emit('change', absNum)
return
}
//
displayValue.value = num.toFixed(props.decimalPlaces)
emit('update:modelValue', num)
emit('change', num)
}
/**
* 处理聚焦事件
* 聚焦时移除格式化显示原始数值
*/
const handleFocus = () => {
if (displayValue.value !== '') {
const num = Number(displayValue.value)
if (!isNaN(num)) {
displayValue.value = num.toString()
}
}
}
</script>
<style scoped>
/* 组件样式 */
</style>

View File

@ -0,0 +1,293 @@
<template>
<Dialog :title="'选择库存'" v-model="dialogVisible" width="1500px">
<!-- 搜索区域 -->
<el-form :model="queryParams" inline class="mb-4">
<el-form-item label="仓库">
<el-select
v-model="queryParams.storeHouseId"
placeholder="请选择仓库"
clearable
class="!w-240px"
@change="handleStoreHouseChange"
>
<el-option
v-for="item in storeHouseList"
:key="item.id"
:label="item.storeHouseName"
:value="item.id"
/>
</el-select>
</el-form-item>
<el-form-item label="库区">
<el-select
v-model="queryParams.storeAreaId"
placeholder="请选择库区"
clearable
class="!w-240px"
>
<el-option
v-for="item in storeAreaList"
:key="item.id"
:label="item.storeAreaName"
:value="item.id"
/>
</el-select>
</el-form-item>
<el-form-item>
<el-button @click="handleQuery" type="primary">搜索</el-button>
<el-button @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>
<!-- 库存列表 -->
<el-table
ref="inventoryTableRef"
v-loading="loading"
:data="inventoryList"
:show-overflow-tooltip="true"
:height="400"
border
:row-key="'id'"
:reserve-selection="true"
>
<el-table-column type="selection" width="50px" align="center" />
<el-table-column label="序号" type="index" width="60px" align="center" />
<el-table-column label="仓库" align="center" prop="storeHouseName" width="120px" />
<el-table-column label="库区" align="center" prop="storeAreaName" width="120px" />
<el-table-column label="批次号" align="center" prop="lotNo" width="120px" />
<el-table-column label="库存袋数" align="center" prop="packQty" width="100px" />
<el-table-column label="单袋规格" align="center" prop="bagSpec" width="100px" />
<el-table-column label="库存数量" align="center" prop="yardQty" width="100px" />
<el-table-column label="最早入库日期" align="center" prop="earStoreDate" width="130px" />
<el-table-column label="产品编码" align="center" prop="matCode" width="120px" />
<el-table-column label="产品名称" align="center" prop="matName" width="180px" />
<el-table-column label="规格型号" align="center" prop="spec" width="120px" />
<el-table-column label="单位" align="center" prop="unit" width="80px" />
</el-table>
<!-- 分页 -->
<div class="pagination-container">
<Pagination
:total="total"
v-model:page="queryParams.pageNo"
v-model:limit="queryParams.pageSize"
@pagination="getList"
class="mb-4 mt-4"
/>
</div>
<!-- 底部按钮 -->
<template #footer>
<el-button @click="handleSave" type="primary">保存</el-button>
<el-button @click="dialogVisible = false">取消</el-button>
</template>
</Dialog>
</template>
<script setup lang="ts">
import { ref, reactive, watch } from 'vue'
import * as InventoryApi from '@/api/biz/prostorageinventory'
import * as StoreHouseApi from '@/api/biz/storehouse'
import * as StoreAreaApi from '@/api/biz/storearea'
import { useMessage } from '@/hooks/web/useMessage'
const message = useMessage()
const dialogVisible = ref(false)
const loading = ref(false)
const inventoryList = ref([])
const total = ref(0)
const inventoryTableRef = ref(null)
const emit = defineEmits(['select', 'close'])
// ID
const selectedInventoryIds = ref<number[]>([])
// ID
const setSelectedIds = (ids: number[]) => {
selectedInventoryIds.value = ids
}
const queryParams = reactive({
pageNo: 1,
pageSize: 10,
storeHouseId: undefined,
storeAreaId: undefined,
matName: undefined,
spec: undefined,
})
// store_type = '3' and enabled_status = 1
const storeHouseList = ref([])
// store_house_id = ID and enabled_status = 1
const storeAreaList = ref([])
/** 获取仓储列表 */
const getStoreHouseList = async () => {
try {
// 使store_type = '3' and enabled_status = 1
const data = await StoreHouseApi.getStoreHousePage({
storeType: '3',
enabledStatus: 1,
pageNo: 1,
pageSize: 100,
})
storeHouseList.value = data.list || []
} catch (error) {
console.error('获取仓储列表失败:', error)
storeHouseList.value = []
}
}
/** 获取库区列表 */
const getStoreAreaList = async (storeHouseId?: number) => {
storeAreaList.value = []
if (!storeHouseId) return
try {
// 使store_house_id = ID and enabled_status = 1
const data = await StoreAreaApi.getStoreAreaPage({
storeHouseId,
enabledStatus: 1,
pageNo: 1,
pageSize: 100,
})
storeAreaList.value = data.list || []
} catch (error) {
console.error('获取库区列表失败:', error)
storeAreaList.value = []
}
}
/** 仓储变更时刷新库区列表 */
const handleStoreHouseChange = (val) => {
queryParams.storeAreaId = undefined
getStoreAreaList(val)
}
/** 获取库存列表 */
const getList = async () => {
loading.value = true
try {
const params: any = {
...queryParams
}
//
if (queryParams.storeHouseId) {
params.storeHouseName = { like: '%仓库%' }
}
if (queryParams.storeAreaId) {
params.storeAreaName = { like: '%库区%' }
}
const data = await InventoryApi.getProStorageInventoryPage(params)
//
inventoryList.value = (data.list || []).sort((a, b) => {
const lotA = a.lotNo || ''
const lotB = b.lotNo || ''
return lotA.localeCompare(lotB)
})
total.value = data.total || 0
//
setTimeout(() => {
setDefaultSelection()
}, 100)
} catch (error) {
console.error('获取库存列表失败:', error)
inventoryList.value = []
total.value = 0
} finally {
loading.value = false
}
}
/** 设置默认选中 */
const setDefaultSelection = () => {
if (!inventoryTableRef.value || !selectedInventoryIds.value.length) return
inventoryList.value.forEach(row => {
if (selectedInventoryIds.value.includes(row.id)) {
inventoryTableRef.value.toggleRowSelection(row, true)
}
})
}
/** 搜索 */
const handleQuery = () => {
queryParams.pageNo = 1
getList()
}
/** 重置 */
const resetQuery = () => {
queryParams.storeHouseId = undefined
queryParams.storeAreaId = undefined
queryParams.matName = undefined
queryParams.spec = undefined
queryParams.pageNo = 1
storeAreaList.value = []
getList()
}
/** 保存按钮 */
const handleSave = () => {
if (!inventoryTableRef.value) return
// - 使 getSelectionRows()
const selectedRows = inventoryTableRef.value.getSelectionRows ? inventoryTableRef.value.getSelectionRows() : []
if (selectedRows.length === 0) {
message.warning('请至少选择一条库存记录')
return
}
//
const selectData = selectedRows.map(row => ({
id: row.id,
storeHouseId: row.storeHouseId,
storeHouseName: row.storeHouseName,
storeAreaId: row.storeAreaId,
storeAreaName: row.storeAreaName,
lotNo: row.lotNo,
packQty: row.packQty,
bagSpec: row.bagSpec,
yardQty: row.yardQty,
earStoreDate: row.earStoreDate,
matCode: row.matCode,
matName: row.matName,
spec: row.spec,
unit: row.unit,
materialId: row.materialId,
}))
emit('select', selectData)
dialogVisible.value = false
}
/** 打开弹窗 */
const open = () => {
dialogVisible.value = true
//
getStoreHouseList()
//
// handleQuery()
}
defineExpose({ open, setSelectedIds })
watch(dialogVisible, (val) => {
if (!val) {
emit('close')
}
})
</script>
<style scoped>
.pagination-container {
margin-bottom: 16px;
}
</style>

View File

@ -9,6 +9,7 @@
>
<!-- 基础信息 -->
<el-card title="基础信息" class="mb-4">
<span>基础信息</span>
<el-row :gutter="20">
<el-col :span="6">
<el-form-item label="销售订单" prop="saleOrdId">
@ -84,13 +85,12 @@
</el-col>
<el-col :span="6">
<el-form-item label="发货数量" prop="deliveriedQty">
<el-input
v-model="formData.deliveriedQty"
placeholder="请输入发货数量"
type="number"
:min="0"
:max="formData.remaimQty || 0"
/>
<MoneyInput
v-model="formData.deliveriedQty"
:decimal-places="0"
:allow-negative="false"
:show-prefix="false"
/>
</el-form-item>
</el-col>
</el-row>
@ -110,6 +110,7 @@
<!-- 收货信息 -->
<el-card title="收货信息" class="mb-4">
<span>收货信息</span>
<el-row :gutter="20">
<el-col :span="8">
<el-form-item label="联系人" prop="contact">
@ -130,59 +131,61 @@
</el-card>
<!-- 产品信息 -->
<el-card title="产品信息">
<div class="mb-3 flex items-center justify-between">
<span></span>
<el-button type="primary" plain @click="addProductItem">新增</el-button>
</div>
<el-table :data="productList" show-summary border :summary-method="getSummary">
<el-table-column label="序号" type="index" width="60px" />
<el-table-column label="仓库(*)" prop="warehouse" width="100px">
<el-card>
<template #header>
<div class="flex items-center">
<span>产品信息</span>
<el-button type="primary" plain @click="addProductItem" class="ml-4">新增</el-button>
</div>
</template>
<el-table :data="productList" show-summary border :summary-method="getSummary">
<el-table-column label="序号" type="index" width="60px" align="center" />
<el-table-column label="仓库(*)" prop="warehouse" width="150px" align="center">
<template #default="scope">
<el-input v-model="scope.row.warehouse" placeholder="请输入仓库" />
<el-input v-model="scope.row.warehouse" placeholder="从库存选择" readonly />
</template>
</el-table-column>
<el-table-column label="库区(*)" prop="warehouseArea" width="100px">
<el-table-column label="库区(*)" prop="warehouseArea" width="150px" align="center">
<template #default="scope">
<el-input v-model="scope.row.warehouseArea" placeholder="请输入库区" />
<el-input v-model="scope.row.warehouseArea" placeholder="从库存选择" readonly />
</template>
</el-table-column>
<el-table-column label="批次号(*)" prop="batchNo" width="120px">
<el-table-column label="批次号(*)" prop="batchNo" width="150px" align="center">
<template #default="scope">
<el-input v-model="scope.row.batchNo" placeholder="请输入批次号" />
<el-input v-model="scope.row.batchNo" placeholder="从库存选择" readonly />
</template>
</el-table-column>
<el-table-column label="库存数量" prop="stockQty" width="100px">
<el-table-column label="库存数量" prop="stockQty" width="150px" align="center">
<template #default="scope">
<el-input v-model="scope.row.stockQty" placeholder="库存数量" readonly />
</template>
</el-table-column>
<el-table-column label="库存袋数" prop="stockBag" width="100px">
<el-table-column label="库存袋数" prop="stockBag" width="150px" align="center">
<template #default="scope">
<el-input v-model="scope.row.stockBag" placeholder="库存袋数" readonly />
</template>
</el-table-column>
<el-table-column label="单袋规格" prop="bagSpec" width="100px">
<el-table-column label="单袋规格" prop="bagSpec" width="150px" align="center">
<template #default="scope">
<el-input v-model="scope.row.bagSpec" placeholder="手动录入" />
</template>
</el-table-column>
<el-table-column label="发货袋数(*)" prop="deliveriedBag" width="120px">
<el-table-column label="发货袋数(*)" prop="deliveriedBag" width="120px" align="center">
<template #default="scope">
<el-input v-model="scope.row.deliveriedBag" placeholder="请输入" />
<el-input v-model="scope.row.deliveriedBag" placeholder="请输入" @input="refreshTable" />
</template>
</el-table-column>
<el-table-column label="发货数量(*)" prop="deliveriedQty" width="120px">
<el-table-column label="发货数量(*)" prop="deliveriedQty" width="120px" align="center">
<template #default="scope">
<el-input v-model="scope.row.deliveriedQty" placeholder="请输入" />
<el-input v-model="scope.row.deliveriedQty" placeholder="请输入" @input="refreshTable" />
</template>
</el-table-column>
<el-table-column label="备注" prop="remark" width="150px">
<el-table-column label="备注" prop="remark" width="120px" align="center">
<template #default="scope">
<el-input v-model="scope.row.remark" placeholder="文本-手动录入" />
<el-input v-model="scope.row.remark" />
</template>
</el-table-column>
<el-table-column label="操作" width="80px">
<el-table-column label="操作" width="80px" align="center">
<template #default="scope">
<el-button
link
@ -205,6 +208,9 @@
<!-- 订单选择弹窗 -->
<OrderSelectDialog ref="orderSelectRef" @select="handleOrderSelect" />
<!-- 库存选择弹窗 -->
<ProStorageInventorySelectDialog ref="inventorySelectRef" @select="handleInventorySelect" />
</template>
<script setup lang="ts">
@ -214,6 +220,7 @@ import * as OrderApi from '@/api/biz/tsoorder'
import * as CustomerApi from '@/api/biz/customer'
import { watch } from 'vue'
import OrderSelectDialog from './OrderSelectDialog.vue'
import ProStorageInventorySelectDialog from '../prostorageinventory/ProStorageInventorySelectDialog.vue'
const { t } = useI18n()
const message = useMessage()
@ -229,6 +236,8 @@ const saleOrderOptions = ref([])
//
const orderSelectRef = ref()
//
const inventorySelectRef = ref()
//
const getToday = () => {
@ -267,19 +276,7 @@ const formData = reactive({
})
//
const productList = ref([
{
warehouse: '',
warehouseArea: '',
batchNo: '',
stockQty: '',
stockBag: '',
bagSpec: '',
deliveriedBag: '',
deliveriedQty: '',
remark: '',
},
])
const productList = ref([])
//
const totalBag = computed(() => {
@ -294,15 +291,32 @@ const totalQty = computed(() => {
const getSummary = (param: any) => {
const { columns, data } = param
const sums: any[] = []
//
if (!Array.isArray(data) || data.length === 0) {
columns.forEach((column: any, index: number) => {
sums[index] = index === 0 ? '合计' : ''
})
return sums
}
//
const totalDeliveriedBag = data.reduce((sum: number, item: any) => sum + (parseInt(item.deliveriedBag) || 0), 0)
//
const totalDeliveriedQty = data.reduce((sum: number, item: any) => sum + (parseInt(item.deliveriedQty) || 0), 0)
// 使
// 0-, 1-, 2-, 3-, 4-, 5-, 6-, 7-, 8-, 9-, 10-
columns.forEach((column: any, index: number) => {
if (index === 0) {
sums[index] = '合计'
} else if (column.prop === 'deliveriedBag') {
const total = data.reduce((sum: number, item: any) => sum + (parseInt(item.deliveriedBag) || 0), 0)
sums[index] = total
} else if (column.prop === 'deliveriedQty') {
const total = data.reduce((sum: number, item: any) => sum + (parseInt(item.deliveriedQty) || 0), 0)
sums[index] = total
} else if (index === 7) {
//
sums[index] = totalDeliveriedBag
} else if (index === 8) {
//
sums[index] = totalDeliveriedQty
} else {
sums[index] = ''
}
@ -310,6 +324,14 @@ const getSummary = (param: any) => {
return sums
}
/** 强制刷新表格(用于实时更新合计) */
const refreshTable = () => {
// key
if (productList.value.length > 0) {
productList.value = [...productList.value]
}
}
//
watch(dialogVisible, (val) => {
if (!val) {
@ -319,7 +341,7 @@ watch(dialogVisible, (val) => {
const formRules = reactive({
saleOrdId: [{ required: true, message: '销售订单不能为空', trigger: 'change' }],
deliveryDate: [{ required: true, message: '单据日期不能为空', trigger: 'change' }],
ordDate: [{ required: true, message: '单据日期不能为空', trigger: 'change' }],
deliveryStatus: [{ required: true, message: '单据状态不能为空', trigger: 'change' }],
deliveriedQty: [
{ required: true, message: '发货数量不能为空', trigger: 'change' },
@ -334,6 +356,13 @@ const formRules = reactive({
trigger: ['change', 'blur']
}
],
custName: [{ required: true, message: '客户名称不能为空', trigger: 'change' }],
materialName: [{ required: true, message: '产品名称不能为空', trigger: 'change' }],
spec: [{ required: true, message: '规格型号不能为空', trigger: 'change' }],
saleDeliveryNo: [{ required: true, message: '出库单号不能为空', trigger: 'change' }],
ordQty: [{ required: true, message: '订单数量不能为空', trigger: 'change' }],
remaimQty: [{ required: true, message: '剩余数量不能为空', trigger: 'change' }],
deliveriedQty: [{ required: true, message: '发货数量不能为空', trigger: 'change' }],
})
const formRef = ref()
@ -487,16 +516,45 @@ const loadCustomerContact = async (custId: number) => {
/** 添加产品项 */
const addProductItem = () => {
productList.value.push({
warehouse: '',
warehouseArea: '',
batchNo: '',
stockQty: '',
stockBag: '',
bagSpec: '',
deliveriedBag: '',
deliveriedQty: '',
remark: '',
// ID
const selectedIds = productList.value
.filter(item => item.inventoryId)
.map(item => item.inventoryId)
// ID便
inventorySelectRef.value.setSelectedIds(selectedIds)
//
inventorySelectRef.value.open()
}
/** 处理库存选择 */
const handleInventorySelect = (data: any[]) => {
// data
if (!data || data.length === 0) return
// id
data.forEach(inventory => {
const exists = productList.value.find(item => item.inventoryId === inventory.id)
if (!exists) {
//
productList.value.push({
inventoryId: inventory.id, // ID
storeHouseId: inventory.storeHouseId, // ID
storeAreaId: inventory.storeAreaId, // ID
warehouse: inventory.storeHouseName, //
warehouseArea: inventory.storeAreaName, //
batchNo: inventory.lotNo, //
stockQty: inventory.yardQty, //
stockBag: inventory.packQty, //
bagSpec: inventory.bagSpec, //
deliveriedBag: '', //
deliveriedQty: '', //
remark: '', //
})
}
//
})
}
@ -510,16 +568,50 @@ const removeProductItem = (index: number) => {
/** 提交表单 */
const emit = defineEmits(['success', 'close'])
/** 验证发货数量 */
const validateDeliveryQty = () => {
const remaimQty = parseInt(formData.remaimQty) || 0
const totalDeliveriedQty = totalQty.value
if (totalDeliveriedQty > remaimQty) {
message.warning(`发货总数量(${totalDeliveriedQty})不能超过剩余数量(${remaimQty})`)
return false
}
return true
}
const submitForm = async () => {
//
if (!validateDeliveryQty()) {
return
}
//
await formRef.value.validate()
//
formLoading.value = true
try {
// detailList
const detailList = productList.value.map(item => ({
id: item.id,
inventoryId: item.inventoryId,
storeHouseId: item.storeHouseId,
storeAreaId: item.storeAreaId,
warehouse: item.warehouse,
warehouseArea: item.warehouseArea,
batchNo: item.batchNo,
stockQty: item.stockQty,
stockBag: item.stockBag,
bagSpec: item.bagSpec,
deliveriedBag: item.deliveriedBag,
deliveriedQty: item.deliveriedQty,
remark: item.remark,
}))
const data = {
...formData,
items: productList.value,
detailList,
}
if (formType.value === 'create') {
@ -538,16 +630,38 @@ const submitForm = async () => {
/** 提交确认 */
const submitAudit = async () => {
//
if (!validateDeliveryQty()) {
return
}
//
await formRef.value.validate()
//
formLoading.value = true
try {
// detailList
const detailList = productList.value.map(item => ({
id: item.id,
inventoryId: item.inventoryId,
storeHouseId: item.storeHouseId,
storeAreaId: item.storeAreaId,
warehouse: item.warehouse,
warehouseArea: item.warehouseArea,
batchNo: item.batchNo,
stockQty: item.stockQty,
stockBag: item.stockBag,
bagSpec: item.bagSpec,
deliveriedBag: item.deliveriedBag,
deliveriedQty: item.deliveriedQty,
remark: item.remark,
}))
const data = {
...formData,
deliveryStatus: '2', //
items: productList.value,
detailList,
}
if (formType.value === 'create') {
@ -593,17 +707,7 @@ const resetForm = () => {
ordDate: null,
})
productList.value = [{
warehouse: '',
warehouseArea: '',
batchNo: '',
stockQty: '',
stockBag: '',
bagSpec: '',
deliveriedBag: '',
deliveriedQty: '',
remark: '',
}]
productList.value = []
formRef.value?.resetFields()
}

View File

@ -71,90 +71,92 @@
</el-form>
</ContentWrap>
<!-- 主列表 -->
<ContentWrap>
<el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true">
<el-table-column type="selection" width="55px" />
<el-table-column label="序号" align="center" type="index" width="60px"/>
<el-table-column label="出库单号" align="center" prop="saleDeliveryNo" width="140px" />
<el-table-column label="单据日期" align="center" prop="ordDate" width="120px" />
<el-table-column label="客户名称" align="center" prop="custName" width="120px" />
<el-table-column label="产品名称" align="center" prop="materialName" width="120px" />
<el-table-column label="产品规格" align="center" prop="spec" width="120px" />
<el-table-column label="销售订单号" align="center" prop="saleOrdNo" width="140px" />
<el-table-column label="订单数量" align="center" prop="ordQty" width="100px" />
<el-table-column label="剩余数量" align="center" prop="remaimQty" width="100px" />
<el-table-column label="发货数量" align="center" prop="deliveriedQty" width="100px" />
<el-table-column label="单位" align="center" prop="unit" width="80px" />
<el-table-column label="出库人" align="center" prop="deliveryEmpName" width="100px" />
<el-table-column label="交货方式" align="center" prop="deliveryType" width="100px">
<template #default="scope">
<dict-tag :type="DICT_TYPE.DELIVERY_METHOD" :value="scope.row.deliveryType" />
</template>
</el-table-column>
<el-table-column label="联系人" align="center" prop="contact" width="100px" />
<el-table-column label="联系电话" align="center" prop="conPhone" width="120px" />
<el-table-column label="联系地址" align="center" prop="conAddress" />
<el-table-column label="单据状态" align="center" prop="deliveryStatus" width="100px">
<template #default="scope">
<dict-tag :type="DICT_TYPE.BILL_STATUS" :value="scope.row.deliveryStatus" />
</template>
</el-table-column>
<el-table-column label="操作" align="center" width="180px">
<template #default="scope">
<el-button
link
type="primary"
@click="openForm('update', scope.row.id)"
v-hasPermi="['tso:sale-delivery:update']"
>
编辑
</el-button>
<el-button
link
type="danger"
@click="handleDelete(scope.row.id)"
v-hasPermi="['tso:sale-delivery:delete']"
>
删除
</el-button>
<el-button
link
type="primary"
@click="viewDetail(scope.row)"
v-hasPermi="['tso:sale-delivery:query']"
>
详情
</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<Pagination
:total="total"
v-model:page="queryParams.pageNo"
v-model:limit="queryParams.pageSize"
@pagination="getList"
/>
</ContentWrap>
<!-- 表格区域主列表 + 明细表格上下各占一半 -->
<ContentWrap class="!p-15px">
<div style="display: flex; flex-direction: column; height: calc(100vh - 280px);">
<!-- 主列表 -->
<div style="flex: 1; min-height: 0;">
<div style="font-weight: bold; margin-bottom: 8px;">出库单列表</div>
<el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true" border @row-click="handleRowClick" style="height: calc(50vh - 220px);">
<el-table-column type="selection" width="55px" align="right" fixed="left" />
<el-table-column label="序号" align="center" type="index" width="60px" fixed="left" />
<el-table-column label="出库单号" align="center" prop="saleDeliveryNo" width="130px" fixed="left" />
<el-table-column label="单据日期" align="center" prop="deliveryDate" width="110px" fixed="left" />
<el-table-column label="客户名称" align="center" prop="custName" width="250px" />
<el-table-column label="产品名称" align="center" prop="materialName" width="160px" />
<el-table-column label="产品规格" align="center" prop="spec" width="120px" />
<el-table-column label="销售订单号" align="center" prop="saleOrdNo" width="140px" />
<el-table-column label="订单数量" align="center" prop="ordQty" width="100px" />
<el-table-column label="剩余数量" align="center" prop="remaimQty" width="100px" />
<el-table-column label="发货数量" align="center" prop="deliveriedQty" width="100px" />
<el-table-column label="单位" align="center" prop="unit" width="80px" />
<el-table-column label="出库人" align="center" prop="deliveryEmpName" width="100px" />
<el-table-column label="交货方式" align="center" prop="deliveryType" width="100px">
<template #default="scope">
<dict-tag :type="DICT_TYPE.DELIVERY_METHOD" :value="scope.row.deliveryType" />
</template>
</el-table-column>
<el-table-column label="联系人" align="center" prop="contact" width="100px" />
<el-table-column label="联系电话" align="center" prop="conPhone" width="120px" />
<el-table-column label="联系地址" align="center" prop="conAddress" width="180px" />
<el-table-column label="单据状态" align="center" prop="deliveryStatus" width="100px" fixed="right">
<template #default="scope">
<dict-tag :type="DICT_TYPE.BILL_STATUS" :value="scope.row.deliveryStatus" />
</template>
</el-table-column>
<el-table-column label="操作" align="center" width="170px" fixed="right">
<template #default="scope">
<el-button
link
type="primary"
@click="openForm('update', scope.row.id)"
v-hasPermi="['tso:sale-delivery:update']"
>
编辑
</el-button>
<el-button
link
type="danger"
@click="handleDelete(scope.row.id)"
v-hasPermi="['tso:sale-delivery:delete']"
>
删除
</el-button>
<el-button
link
type="primary"
@click="viewDetail(scope.row)"
v-hasPermi="['tso:sale-delivery:query']"
>
详情
</el-button>
</template>
</el-table-column>
</el-table>
<Pagination
:total="total"
v-model:page="queryParams.pageNo"
v-model:limit="queryParams.pageSize"
@pagination="getList"
style="margin-top: 8px;"
/>
</div>
<!-- 明细表格区域 -->
<ContentWrap v-if="selectedRow">
<el-table :data="selectedRow.items" :stripe="true" :show-overflow-tooltip="true">
<el-table-column label="序号" align="center" type="index" width="60px"/>
<el-table-column label="库区" align="center" prop="warehouseArea" width="100px" />
<el-table-column label="库位" align="center" prop="warehouseLoc" width="100px" />
<el-table-column label="批次号" align="center" prop="batchNo" width="120px" />
<el-table-column label="发货数量" align="center" prop="deliveriedQty" width="100px" />
<el-table-column label="发货袋数" align="center" prop="deliveriedBag" width="100px" />
<el-table-column label="单据规格" align="center" prop="spec" width="120px" />
<el-table-column label="单位" align="center" prop="unit" width="80px" />
<el-table-column label="备注" align="center" prop="remark" />
</el-table>
<div class="mt-2 flex justify-end">
<span class="font-bold">合计: </span>
<span>发货数量: {{ totalDeliveriedQty }}</span>
<span class="ml-4">发货袋数: {{ totalDeliveriedBag }}</span>
<!-- 明细表格 -->
<div style="flex: 1; min-height: 0;">
<div style="font-weight: bold; margin-bottom: 8px;">出库单明细</div>
<el-table v-loading="detailLoading" :data="detailList" :stripe="true" :show-overflow-tooltip="true" border :summary-method="getDetailSummary" show-summary>
<el-table-column label="序号" align="center" type="index" width="60px"/>
<el-table-column label="库区" align="center" prop="warehouseArea" width="100px" />
<el-table-column label="库位" align="center" prop="warehouseLoc" width="100px" />
<el-table-column label="批次号" align="center" prop="batchNo" width="120px" />
<el-table-column label="发货数量" align="center" prop="deliveriedQty" width="100px" />
<el-table-column label="发货袋数" align="center" prop="deliveriedBag" width="100px" />
<el-table-column label="单据规格" align="center" prop="spec" width="120px" />
<el-table-column label="单位" align="center" prop="unit" width="80px" />
<el-table-column label="备注" align="center" prop="remark" />
</el-table>
</div>
</div>
</ContentWrap>
@ -163,7 +165,7 @@
</template>
<script setup lang="ts">
import { ref, reactive, computed } from 'vue'
import { ref, reactive } from 'vue'
import { dateFormatter } from '@/utils/formatTime'
import download from '@/utils/download'
import * as SaleDeliveryApi from '@/api/biz/saledelivery'
@ -179,6 +181,8 @@ const loading = ref(false)
const list = ref([])
const total = ref(0)
const selectedRow = ref(null)
const detailLoading = ref(false)
const detailList = ref([])
const queryParams = reactive({
pageNo: 1,
@ -192,16 +196,31 @@ const queryParams = reactive({
const queryFormRef = ref()
const exportLoading = ref(false)
//
const totalDeliveriedQty = computed(() => {
if (!selectedRow.value?.items) return 0
return selectedRow.value.items.reduce((sum, item) => sum + (item.deliveriedQty || 0), 0)
})
const totalDeliveriedBag = computed(() => {
if (!selectedRow.value?.items) return 0
return selectedRow.value.items.reduce((sum, item) => sum + (item.deliveriedBag || 0), 0)
})
/** 明细表格合计 */
const getDetailSummary = (param: any) => {
const { columns, data } = param
const sums: any[] = []
if (!Array.isArray(data) || data.length === 0) {
columns.forEach((column: any, index: number) => {
sums[index] = index === 0 ? '合计' : ''
})
return sums
}
const totalDeliveriedBag = data.reduce((sum: number, item: any) => sum + (parseInt(item.deliveriedBag) || 0), 0)
const totalDeliveriedQty = data.reduce((sum: number, item: any) => sum + (parseInt(item.deliveriedQty) || 0), 0)
columns.forEach((column: any, index: number) => {
if (index === 0) {
sums[index] = '合计'
} else if (index === 4) {
sums[index] = totalDeliveriedQty
} else if (index === 5) {
sums[index] = totalDeliveriedBag
} else {
sums[index] = ''
}
})
return sums
}
/** 查询列表 */
const getList = async () => {
@ -210,6 +229,15 @@ const getList = async () => {
const data = await SaleDeliveryApi.getSaleDeliveryPage(queryParams)
list.value = data.list
total.value = data.total
//
if (list.value.length > 0) {
await viewDetail(list.value[0])
} else {
//
detailList.value = []
selectedRow.value = null
}
} finally {
loading.value = false
}
@ -244,13 +272,24 @@ const handleDelete = async (id: number) => {
} catch {}
}
/** 行点击事件 */
const handleRowClick = async (row: any) => {
await viewDetail(row)
}
/** 查看详情 */
const viewDetail = async (row: any) => {
detailLoading.value = true
try {
const data = await SaleDeliveryApi.getSaleDelivery(row.id)
selectedRow.value = data
// 使 detailList
detailList.value = data.items || data.detailList || []
} catch (e) {
console.error('获取详情失败', e)
detailList.value = []
} finally {
detailLoading.value = false
}
}

View File

@ -62,16 +62,16 @@
<el-row :gutter="20">
<el-col :span="6">
<el-form-item label="业务部门" prop="saleDeptId" >
<el-input
v-model="formData.saleDeptName"
<el-tree-select
v-model="formData.saleDeptId"
:data="deptList"
:props="{ label: 'name', value: 'id', children: 'children' }"
placeholder="请选择业务部门"
readonly
@click="openDeptSelect"
>
<template #suffix>
<Icon icon="ep:caret-bottom" />
</template>
</el-input>
filterable
check-strictly
class="w-full"
@change="handleDeptChange"
/>
</el-form-item>
</el-col>
<el-col :span="6">
@ -181,12 +181,18 @@
<el-row :gutter="20">
<el-col :span="6">
<el-form-item label="税率" prop="taxRate" >
<el-input v-model="formData.taxRate" placeholder="请输入税率" class="text-right" />
<MoneyInput v-model="formData.taxRate" :decimal-places="2" suffix="%" :show-suffix="true" :show-prefix="false" />
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="含税总金额" prop="totalAmount" >
<el-input v-model="formData.totalAmount" placeholder="请输入含税总金额" class="text-right" />
<MoneyInput
v-model="formData.totalAmount"
:decimal-places="2"
:allow-negative="false"
:show-prefix="false"
placeholder="请输入"
/>
</el-form-item>
</el-col>
<el-col :span="6">
@ -245,12 +251,23 @@
<el-table-column label="订单数量(*)" align="center" prop="ordQty">
<template #default="scope">
<el-input v-model="scope.row.ordQty" placeholder="请输入" />
<MoneyInput
v-model="scope.row.ordQty"
:decimal-places="2"
:allow-negative="false"
:show-prefix="false"
placeholder="请输入"
/>
</template>
</el-table-column>
<el-table-column label="含税单价(*)" align="center" prop="priceTax">
<template #default="scope">
<el-input v-model="scope.row.priceTax" placeholder="请输入" />
<MoneyInput
v-model="scope.row.priceTax"
:decimal-places="2"
:allow-negative="false"
placeholder="请输入"
/>
</template>
</el-table-column>
<el-table-column label="用途(*)" align="center" width="120px">
@ -358,11 +375,14 @@ import { ref, reactive, nextTick } from 'vue'
import * as OrderApi from '@/api/biz/tsoorder/'
import * as CustomerApi from '@/api/biz/customer'
import { getIntDictOptions, getStrDictOptions, DICT_TYPE } from '@/utils/dict'
import { getDeptSimpleName } from '@/api/system/dept'
import { getDeptSimpleName, getSimpleDeptList } from '@/api/system/dept'
import * as UserApi from '@/api/system/user'
import * as DeptApi from '@/api/system/dept'
import { handleTree } from '@/utils/tree'
import { Icon } from '@/components/Icon'
import MaterialSelect from '@/views/biz/material/MaterialSelect.vue'
import DeptTree from '@/views/system/user/DeptTree.vue'
import MoneyInput from '@/views/biz/components/MoneyInput.vue'
//
const getToday = () => {
@ -388,6 +408,7 @@ const deptSelectVisible = ref(false)
const deptTreeRef = ref()
const userList = ref<UserApi.UserVO[]>([])
const userSelectLoading = ref(false)
const deptList = ref<DeptApi.DeptVO[]>([])
//
const uploadRef = ref()
@ -486,10 +507,21 @@ const unwrapOrderDetail = (raw: Record<string, any>) => {
return raw
}
/** 获取业务部门列表 */
const loadDeptList = async () => {
try {
const list = await getSimpleDeptList()
deptList.value = handleTree(list) as DeptApi.DeptVO[]
} catch (e) {
console.error('获取业务部门列表失败', e)
}
}
const open = async (type: string, id?: number) => {
formType.value = type
dialogTitle.value = t('action.' + type)
resetForm()
await loadDeptList()
if (id) {
formLoading.value = true
try {
@ -752,6 +784,18 @@ const openMaterialSelect = async () => {
materialSelectRef.value?.open(selectedIds)
}
//
const handleDeptChange = (deptId: number) => {
const dept = deptList.value.find(d => d.id === deptId)
if (dept) {
formData.saleDeptName = dept.name
}
//
formData.saleMan = undefined
formData.saleManName = undefined
searchUsers('')
}
//
const openDeptSelect = () => {
deptSelectVisible.value = true

View File

@ -156,6 +156,15 @@
v-hasPermi="['biz:order:update']"
>
编辑
</el-button>
<el-button
link
type="primary"
v-if="scope.row.ordStatus === '2'"
@click="openForm('update', scope.row.id)"
v-hasPermi="['biz:order:update']"
>
订单变更
</el-button>
<el-button
link