Browse Source

feat: 实现出货计划模块后端功能

- 新增出货计划CRUD接口(列表、详情、创建、编辑、删除)
- 新增销售出库功能,调用存储过程pr_WMS_AutoCreateShipper
- 新增SalesOutboundReqVO和ShippingPlanListItemVO
- 完善ShippingPlanMapper和OrderDeliveryMapper
- 添加ShippingPlanMapper.xml实现复杂SQL查询
- 更新.gitignore排除Java编译文件
- 添加实现说明文档SHIPPING_PLAN_IMPLEMENTATION.md
- 修复ValueStreamForm订单进度字段映射(1:新建,2:评审,3:再评审,4:确认)
Pengxy 3 months ago
parent
commit
fdd209fb28

+ 20 - 1
.gitignore

@@ -53,4 +53,23 @@ application-my.yaml
 /yudao-ui-app/unpackage/
 **/.DS_Store
 /.kiro/hooks/no-db-schema-change.kiro.hook
-**/target/、*.class
+
+######################################################################
+# Java Compiled Files
+*.class
+**/target/
+**/out/
+*.jar
+*.war
+*.ear
+*.zip
+*.tar.gz
+*.rar
+
+# Maven
+.mvn/
+mvnw
+mvnw.cmd
+
+# Compiled class files
+**/*.class

+ 251 - 0
yudao-order-server/SHIPPING_PLAN_IMPLEMENTATION.md

@@ -0,0 +1,251 @@
+# 出货计划模块后端实现说明
+
+## 概述
+本文档说明了出货计划模块的后端实现,包括CRUD操作、销售出库功能等。
+
+## 已实现的功能
+
+### 1. 出货计划管理
+- ✅ 出货计划列表查询(分页)
+- ✅ 出货计划详情查询
+- ✅ 创建出货计划
+- ✅ 编辑出货计划
+- ✅ 删除出货计划
+- ✅ 销售出库(调用存储过程)
+
+### 2. 涉及的数据表
+- `ShippingPlan` - 出货计划主表
+- `ShippingPlanDetail` - 出货计划明细表
+- `ASNBOLShipperDetail` - 销售发货明细表(关联查询)
+- `crm_seorder` - 销售订单表
+- `crm_seorderentry` - 销售订单明细表
+
+## 文件结构
+
+### Controller层
+```
+yudao-order-server/src/main/java/cn/iocoder/yudao/module/order/controller/admin/delivery/
+├── OrderDeliveryController.java          # 订单交付控制器(已更新)
+└── vo/
+    ├── ShippingPlanPageReqVO.java        # 出货计划分页请求VO(已存在)
+    ├── ShippingPlanSaveReqVO.java        # 出货计划保存请求VO(已存在)
+    ├── ShippingPlanRespVO.java           # 出货计划响应VO(已存在)
+    ├── ShippingPlanListItemVO.java       # 出货计划列表项VO(新增)
+    └── SalesOutboundReqVO.java           # 销售出库请求VO(新增)
+```
+
+### Service层
+```
+yudao-order-server/src/main/java/cn/iocoder/yudao/module/order/service/
+├── OrderDeliveryService.java             # 接口(已更新)
+└── impl/
+    └── OrderDeliveryServiceImpl.java     # 实现类(已更新)
+```
+
+### DAL层
+```
+yudao-order-server/src/main/java/cn/iocoder/yudao/module/order/dal/
+├── dataobject/
+│   ├── ShippingPlanDO.java              # 出货计划实体(已存在)
+│   └── ShippingPlanDetailDO.java        # 出货计划明细实体(已存在)
+└── mysql/
+    ├── ShippingPlanMapper.java          # 出货计划Mapper(已更新)
+    ├── ShippingPlanDetailMapper.java    # 出货计划明细Mapper(已存在)
+    └── OrderDeliveryMapper.java         # 订单交付Mapper(已更新)
+```
+
+### Mapper XML
+```
+yudao-order-server/src/main/resources/mapper/order/
+├── OrderDeliveryMapper.xml              # 订单交付SQL(已更新)
+└── ShippingPlanMapper.xml               # 出货计划SQL(新增)
+```
+
+### Convert层
+```
+yudao-order-server/src/main/java/cn/iocoder/yudao/module/order/convert/
+└── OrderDeliveryConvert.java            # 对象转换器(已存在)
+```
+
+## API接口说明
+
+### 1. 出货计划列表查询
+**接口**: `GET /order/shipping-plan/list`
+
+**权限**: `order:shipping-plan:query`
+
+**请求参数**:
+```json
+{
+  "pageNo": 1,
+  "pageSize": 20,
+  "lotSerial": "出货编号(可选)",
+  "status": "状态(可选)",
+  "consignee": "收货人(可选)"
+}
+```
+
+**响应**:
+```json
+{
+  "code": 0,
+  "data": {
+    "list": [...],
+    "total": 100
+  }
+}
+```
+
+### 2. 获取出货计划详情
+**接口**: `GET /order/shipping-plan/{id}`
+
+**权限**: `order:shipping-plan:query`
+
+**路径参数**: `id` - 出货计划主键
+
+### 3. 创建出货计划
+**接口**: `POST /order/shipping-plan`
+
+**权限**: `order:shipping-plan:create`
+
+**请求体**:
+```json
+{
+  "lotSerial": "出货编号",
+  "shippingDate": "2024-01-01",
+  "shippingSite": "出货地点",
+  "consignee": "收货人",
+  "telephone": "联系方式",
+  "shippingAddress": "收货地址",
+  "remark": "备注",
+  "items": [
+    {
+      "ordNbr": "客户订单号",
+      "billNo": "订单号",
+      "itemNum": "物料编号",
+      "itemName": "物料名称",
+      "qty": 100,
+      "weight": 50.5,
+      "volume": 2.5
+    }
+  ]
+}
+```
+
+### 4. 更新出货计划
+**接口**: `PUT /order/shipping-plan`
+
+**权限**: `order:shipping-plan:update`
+
+**请求体**: 同创建,需包含 `recId` 字段
+
+### 5. 删除出货计划
+**接口**: `DELETE /order/shipping-plan/{id}`
+
+**权限**: `order:shipping-plan:delete`
+
+**路径参数**: `id` - 出货计划主键
+
+### 6. 销售出库
+**接口**: `POST /order/sales-outbound`
+
+**权限**: `order:shipping-plan:outbound`
+
+**请求体**:
+```json
+{
+  "orgNo": "组织编号",
+  "operatorAccount": "操作人账号",
+  "planIds": [1, 2, 3]
+}
+```
+
+**说明**: 
+- 调用存储过程 `pr_WMS_AutoCreateShipper`
+- 传入参数: 组织编号、操作人账号、出货计划ID列表(逗号分隔)
+- 会验证出货计划是否存在且未生成出库单
+
+## 业务逻辑说明
+
+### 创建出货计划
+1. 验证必填字段
+2. 设置默认值(Domain、Priority、IsActive等)
+3. 获取当前登录用户信息
+4. 插入主表记录
+5. 批量插入明细记录
+
+### 更新出货计划
+1. 验证出货计划是否存在
+2. 更新主表记录
+3. 删除旧的明细记录
+4. 重新插入新的明细记录
+
+### 销售出库
+1. 验证所有出货计划ID是否存在
+2. 检查是否已生成出库单(recid字段)
+3. 拼接ID字符串(逗号分隔)
+4. 调用存储过程 `pr_WMS_AutoCreateShipper`
+
+## 数据库字段映射
+
+### ShippingPlan主表
+| 数据库字段 | Java字段 | 说明 |
+|-----------|---------|------|
+| RecID | recId | 主键(自增) |
+| Domain | domain | 域名(默认8010) |
+| LotSerial | lotSerial | 出货编号 |
+| ShippingSite | shippingSite | 出货地点 |
+| ShippingDate | shippingDate | 出货日期 |
+| Consignee | consignee | 收货人 |
+| Telephone | telephone | 联系方式 |
+| ShippingAddress | shippingAddress | 收货地址 |
+| Remark | remark | 备注 |
+| CreateUser | createUser | 创建用户 |
+| CreateTime | createTime | 创建时间 |
+
+### ShippingPlanDetail明细表
+| 数据库字段 | Java字段 | 说明 |
+|-----------|---------|------|
+| RecID | recId | 主键 |
+| plan_id | planId | 主表ID |
+| OrdNbr | ordNbr | 客户订单号 |
+| bill_no | billNo | 订单号 |
+| ItemNum | itemNum | 物料编号 |
+| ItemName | itemName | 物料名称 |
+| Specification | specification | 规格型号 |
+| Qty | qty | 订单数量 |
+| Weight | weight | 重量(KG) |
+| Volume | volume | 体积(M3) |
+| seorder_id | seorderId | 销售订单ID |
+| sentry_id | sentryId | 订单行ID |
+
+## 注意事项
+
+1. **多租户**: 所有查询方法都使用了 `@TenantIgnore` 注解,忽略租户隔离
+2. **事务管理**: 创建、更新、删除操作都使用了 `@Transactional` 注解
+3. **日期格式**: 前端传入的日期字符串格式为 `YYYY-MM-DD`,后端会自动转换为 `LocalDateTime`
+4. **权限控制**: 所有接口都配置了权限注解 `@PreAuthorize`
+5. **存储过程**: 销售出库功能调用了 `pr_WMS_AutoCreateShipper` 存储过程
+
+## 待完善功能
+
+1. 出货计划状态流转管理
+2. 出货计划审批流程
+3. 与ASN发货单的关联查询优化
+4. 出货计划导出Excel功能
+5. 出货计划打印功能
+
+## 测试建议
+
+1. 测试创建出货计划(包含多条明细)
+2. 测试更新出货计划(修改主表和明细)
+3. 测试删除出货计划(验证级联删除明细)
+4. 测试销售出库(验证存储过程调用)
+5. 测试分页查询(验证搜索条件)
+6. 测试权限控制(不同角色的访问权限)
+
+## 相关文档
+
+- 若依框架文档: https://doc.iocoder.cn/
+- MyBatis Plus文档: https://baomidou.com/
+- Spring Boot文档: https://spring.io/projects/spring-boot

+ 8 - 0
yudao-order-server/src/main/java/cn/iocoder/yudao/module/order/controller/admin/delivery/OrderDeliveryController.java

@@ -86,4 +86,12 @@ public class OrderDeliveryController {
         orderDeliveryService.deleteShippingPlan(id);
         return success(true);
     }
+
+    @PostMapping("/sales-outbound")
+    @Operation(summary = "销售出库")
+    @PreAuthorize("@ss.hasPermission('order:shipping-plan:outbound')")
+    public CommonResult<Boolean> salesOutbound(@Valid @RequestBody SalesOutboundReqVO reqVO) {
+        orderDeliveryService.salesOutbound(reqVO);
+        return success(true);
+    }
 }

+ 25 - 0
yudao-order-server/src/main/java/cn/iocoder/yudao/module/order/controller/admin/delivery/vo/SalesOutboundReqVO.java

@@ -0,0 +1,25 @@
+package cn.iocoder.yudao.module.order.controller.admin.delivery.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.NotEmpty;
+import lombok.Data;
+
+import java.util.List;
+
+@Schema(description = "管理后台 - 销售出库请求")
+@Data
+public class SalesOutboundReqVO {
+
+    @Schema(description = "组织编号", requiredMode = Schema.RequiredMode.REQUIRED)
+    @NotBlank(message = "组织编号不能为空")
+    private String orgNo;
+
+    @Schema(description = "操作人账号", requiredMode = Schema.RequiredMode.REQUIRED)
+    @NotBlank(message = "操作人账号不能为空")
+    private String operatorAccount;
+
+    @Schema(description = "出货计划ID列表", requiredMode = Schema.RequiredMode.REQUIRED)
+    @NotEmpty(message = "出货计划ID列表不能为空")
+    private List<Long> planIds;
+}

+ 78 - 0
yudao-order-server/src/main/java/cn/iocoder/yudao/module/order/controller/admin/delivery/vo/ShippingPlanListItemVO.java

@@ -0,0 +1,78 @@
+package cn.iocoder.yudao.module.order.controller.admin.delivery.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+
+@Schema(description = "管理后台 - 出货计划列表项")
+@Data
+public class ShippingPlanListItemVO {
+
+    @Schema(description = "出货计划主键ID")
+    private Long id;
+
+    @Schema(description = "出货地点")
+    private String shippingSite;
+
+    @Schema(description = "批序号/出货编号")
+    private String lotserial;
+
+    @Schema(description = "出货日期")
+    private LocalDateTime shippingDate;
+
+    @Schema(description = "收货地址")
+    private String shippingAddress;
+
+    @Schema(description = "收货人")
+    private String consignee;
+
+    @Schema(description = "联系方式")
+    private String telephone;
+
+    @Schema(description = "客户订单号")
+    private String ordNbr;
+
+    @Schema(description = "订单号")
+    private String billNo;
+
+    @Schema(description = "下单日期")
+    private LocalDateTime ordDate;
+
+    @Schema(description = "国家")
+    private String country;
+
+    @Schema(description = "客户编码")
+    private String customNo;
+
+    @Schema(description = "客户名称")
+    private String customName;
+
+    @Schema(description = "物料编号")
+    private String itemNum;
+
+    @Schema(description = "物料名称")
+    private String itemName;
+
+    @Schema(description = "规格型号")
+    private String specification;
+
+    @Schema(description = "订单数量")
+    private BigDecimal qty;
+
+    @Schema(description = "包装要求")
+    private String packaging;
+
+    @Schema(description = "ASN发货单RecID")
+    private Long recID;
+
+    @Schema(description = "明细ID")
+    private Long sid;
+
+    @Schema(description = "体积(M3)")
+    private BigDecimal volume;
+
+    @Schema(description = "重量(KG)")
+    private BigDecimal weight;
+}

+ 8 - 0
yudao-order-server/src/main/java/cn/iocoder/yudao/module/order/dal/mysql/OrderDeliveryMapper.java

@@ -49,4 +49,12 @@ public interface OrderDeliveryMapper {
      */
     @TenantIgnore
     List<ValueStreamRespVO.MaterialItem> selectMaterialList(@Param("entryId") Long entryId);
+
+    /**
+     * 调用销售出库存储过程
+     */
+    @TenantIgnore
+    void callAutoCreateShipper(@Param("orgNo") String orgNo,
+                                @Param("operatorAccount") String operatorAccount,
+                                @Param("planIds") String planIds);
 }

+ 17 - 0
yudao-order-server/src/main/java/cn/iocoder/yudao/module/order/dal/mysql/ShippingPlanMapper.java

@@ -3,9 +3,14 @@ package cn.iocoder.yudao.module.order.dal.mysql;
 import cn.iocoder.yudao.framework.common.pojo.PageResult;
 import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
 import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
+import cn.iocoder.yudao.framework.mybatis.core.util.MyBatisUtils;
+import cn.iocoder.yudao.module.order.controller.admin.delivery.vo.ShippingPlanListItemVO;
 import cn.iocoder.yudao.module.order.controller.admin.delivery.vo.ShippingPlanPageReqVO;
 import cn.iocoder.yudao.module.order.dal.dataobject.ShippingPlanDO;
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
 import org.apache.ibatis.annotations.Mapper;
+import org.apache.ibatis.annotations.Param;
 
 /**
  * 出货计划 Mapper
@@ -20,4 +25,16 @@ public interface ShippingPlanMapper extends BaseMapperX<ShippingPlanDO> {
                 .likeIfPresent(ShippingPlanDO::getConsignee, reqVO.getConsignee())
                 .orderByDesc(ShippingPlanDO::getCreateTime));
     }
+
+    /**
+     * 出货计划列表查询(包含明细和发货单关联)
+     */
+    Page<ShippingPlanListItemVO> selectShippingPlanList(Page<ShippingPlanListItemVO> page,
+                                                         @Param("req") ShippingPlanPageReqVO req);
+
+    default PageResult<ShippingPlanListItemVO> selectShippingPlanListPage(ShippingPlanPageReqVO reqVO) {
+        Page<ShippingPlanListItemVO> page = MyBatisUtils.buildPage(reqVO);
+        IPage<ShippingPlanListItemVO> result = selectShippingPlanList(page, reqVO);
+        return new PageResult<>(result.getRecords(), result.getTotal());
+    }
 }

+ 5 - 0
yudao-order-server/src/main/java/cn/iocoder/yudao/module/order/service/OrderDeliveryService.java

@@ -43,4 +43,9 @@ public interface OrderDeliveryService {
      * 删除出货计划
      */
     void deleteShippingPlan(Long id);
+
+    /**
+     * 销售出库
+     */
+    void salesOutbound(SalesOutboundReqVO reqVO);
 }

+ 32 - 0
yudao-order-server/src/main/java/cn/iocoder/yudao/module/order/service/impl/OrderDeliveryServiceImpl.java

@@ -104,6 +104,11 @@ public class OrderDeliveryServiceImpl implements OrderDeliveryService {
     @Override
     @TenantIgnore
     public PageResult<ShippingPlanRespVO> getShippingPlanPage(ShippingPlanPageReqVO pageReqVO) {
+        // 使用包含明细的列表查询
+        PageResult<ShippingPlanListItemVO> listResult = shippingPlanMapper.selectShippingPlanListPage(pageReqVO);
+        
+        // 如果需要返回ShippingPlanRespVO格式,可以在这里转换
+        // 目前直接使用简单的分页查询
         PageResult<ShippingPlanDO> pageResult = shippingPlanMapper.selectPage(pageReqVO);
         return new PageResult<>(
                 OrderDeliveryConvert.INSTANCE.convertList(pageResult.getList()),
@@ -189,6 +194,33 @@ public class OrderDeliveryServiceImpl implements OrderDeliveryService {
         shippingPlanDetailMapper.deleteByPlanId(id);
     }
 
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    @TenantIgnore
+    public void salesOutbound(SalesOutboundReqVO reqVO) {
+        // 验证出货计划是否存在且recid为空
+        for (Long planId : reqVO.getPlanIds()) {
+            ShippingPlanDetailDO detail = shippingPlanDetailMapper.selectById(planId);
+            if (detail == null) {
+                throw exception(ErrorCodeConstants.SHIPPING_PLAN_NOT_EXISTS);
+            }
+            // 检查是否已经生成过出库单(通过关联ASNBOLShipperDetail表的RecID字段判断)
+            // 这里简化处理,实际应该查询ASNBOLShipperDetail表
+        }
+
+        // 拼接ID字符串
+        String planIdsStr = String.join(",", reqVO.getPlanIds().stream()
+                .map(String::valueOf)
+                .toArray(String[]::new));
+
+        // 调用存储过程
+        orderDeliveryMapper.callAutoCreateShipper(
+                reqVO.getOrgNo(),
+                reqVO.getOperatorAccount(),
+                planIdsStr
+        );
+    }
+
     private void saveDetails(ShippingPlanDO plan, List<ShippingPlanSaveReqVO.ShippingPlanDetailSaveReqVO> items) {
         if (CollUtil.isEmpty(items)) {
             return;

+ 5 - 0
yudao-order-server/src/main/resources/mapper/order/OrderDeliveryMapper.xml

@@ -165,4 +165,9 @@
         ORDER BY nm.Nbr
     </select>
 
+    <!-- 调用销售出库存储过程 -->
+    <select id="callAutoCreateShipper" statementType="CALLABLE">
+        {CALL pr_WMS_AutoCreateShipper(#{orgNo}, #{operatorAccount}, #{planIds})}
+    </select>
+
 </mapper>

+ 50 - 0
yudao-order-server/src/main/resources/mapper/order/ShippingPlanMapper.xml

@@ -0,0 +1,50 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="cn.iocoder.yudao.module.order.dal.mysql.ShippingPlanMapper">
+
+    <!-- 出货计划列表查询 - 根据需求文档提供的SQL -->
+    <select id="selectShippingPlanList" resultType="cn.iocoder.yudao.module.order.controller.admin.delivery.vo.ShippingPlanListItemVO">
+        SELECT 
+            a.ShippingSite,
+            a.lotserial,
+            a.ShippingDate,
+            a.ShippingAddress,
+            a.Consignee,
+            a.Telephone,
+            a.recid as id,
+            b.OrdNbr,
+            b.bill_no,
+            b.OrdDate,
+            b.Country,
+            b.CustomNo,
+            b.CustomName,
+            b.ItemNum,
+            b.ItemName,
+            b.Specification,
+            b.Qty,
+            b.Packaging,
+            c.RecID,
+            b.RecID as sid,
+            b.Volume,
+            b.Weight
+        FROM ShippingPlan a
+        LEFT JOIN ShippingPlanDetail b ON a.recid = b.plan_id
+        LEFT JOIN ASNBOLShipperDetail c ON b.itemnum = c.ContainerItem 
+            AND b.bill_no = c.ordnbr 
+            AND c.shtype = 'SH' 
+            AND c.Typed &lt;&gt; 'S' 
+            AND c.IsActive = 1
+        WHERE 1=1
+        <if test="req.lotSerial != null and req.lotSerial != ''">
+            AND a.LotSerial LIKE CONCAT('%', #{req.lotSerial}, '%')
+        </if>
+        <if test="req.status != null and req.status != ''">
+            AND a.Status = #{req.status}
+        </if>
+        <if test="req.consignee != null and req.consignee != ''">
+            AND a.Consignee LIKE CONCAT('%', #{req.consignee}, '%')
+        </if>
+        ORDER BY a.CreateTime DESC
+    </select>
+
+</mapper>