Просмотр исходного кода

✨ ERP:增加结算账户 100%

YunaiV 1 год назад
Родитель
Сommit
d06a22dab3
14 измененных файлов с 693 добавлено и 27 удалено
  1. 9 0
      yudao-module-erp/yudao-module-erp-api/src/main/java/cn/iocoder/yudao/module/erp/enums/ErrorCodeConstants.java
  2. 106 0
      yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/finance/ErpAccountController.java
  3. 24 0
      yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/finance/vo/ErpAccountPageReqVO.java
  4. 50 0
      yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/finance/vo/ErpAccountRespVO.java
  5. 36 0
      yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/finance/vo/ErpAccountSaveReqVO.java
  6. 2 9
      yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/sale/vo/order/ErpSaleOrderSaveReqVO.java
  7. 56 0
      yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/dal/dataobject/finance/ErpAccountDO.java
  8. 36 0
      yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/dal/mysql/finance/ErpAccountMapper.java
  9. 5 0
      yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/dal/redis/no/ErpNoRedisDAO.java
  10. 70 0
      yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/service/finance/ErpAccountService.java
  11. 99 0
      yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/service/finance/ErpAccountServiceImpl.java
  12. 46 18
      yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/service/sale/ErpSaleOrderServiceImpl.java
  13. 12 0
      yudao-module-erp/yudao-module-erp-biz/src/main/resources/mapper/finance/ErpAccountMapper.xml
  14. 142 0
      yudao-module-erp/yudao-module-erp-biz/src/test/java/cn/iocoder/yudao/module/erp/service/finance/ErpAccountServiceImplTest.java

+ 9 - 0
yudao-module-erp/yudao-module-erp-api/src/main/java/cn/iocoder/yudao/module/erp/enums/ErrorCodeConstants.java

@@ -19,6 +19,11 @@ public interface ErrorCodeConstants {
 
     // ========== ERP 销售订单(1-030-201-000) ==========
     ErrorCode SALE_ORDER_NOT_EXISTS = new ErrorCode(1_020_201_000, "销售订单不存在");
+    ErrorCode SALE_ORDER_DELETE_FAIL_APPROVE = new ErrorCode(1_020_201_001, "销售订单({})已审核,无法删除");
+    ErrorCode SALE_ORDER_PROCESS_FAIL = new ErrorCode(1_020_201_002, "反审核失败,只有已审核的销售订单才能反审核");
+    ErrorCode SALE_ORDER_APPROVE_FAIL = new ErrorCode(1_020_201_003, "审核失败,只有未审核的销售订单才能审核");
+    ErrorCode SALE_ORDER_NO_EXISTS = new ErrorCode(1_020_201_004, "生成销售单号失败,请重新提交");
+    ErrorCode SALE_ORDER_UPDATE_FAIL_APPROVE = new ErrorCode(1_020_201_005, "销售订单({})已审核,无法修改");
 
     // ========== ERP 仓库 1-030-400-000 ==========
     ErrorCode WAREHOUSE_NOT_EXISTS = new ErrorCode(1_030_400_000, "仓库不存在");
@@ -78,4 +83,8 @@ public interface ErrorCodeConstants {
     ErrorCode PRODUCT_UNIT_NAME_DUPLICATE = new ErrorCode(1_030_502_001, "已存在该名字的产品单位");
     ErrorCode PRODUCT_UNIT_EXITS_PRODUCT = new ErrorCode(1_030_502_002, "存在产品使用该单位,无法删除");
 
+    // ========== ERP 结算账户 1-030-600-000 ==========
+    ErrorCode ACCOUNT_NOT_EXISTS = new ErrorCode(1_030_600_000, "结算账户不存在");
+    ErrorCode ACCOUNT_NOT_ENABLE = new ErrorCode(1_030_600_001, "结算账户({})未启用");
+
 }

+ 106 - 0
yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/finance/ErpAccountController.java

@@ -0,0 +1,106 @@
+package cn.iocoder.yudao.module.erp.controller.admin.finance;
+
+import cn.iocoder.yudao.framework.common.pojo.CommonResult;
+import cn.iocoder.yudao.framework.common.pojo.PageParam;
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
+import cn.iocoder.yudao.framework.excel.core.util.ExcelUtils;
+import cn.iocoder.yudao.framework.operatelog.core.annotations.OperateLog;
+import cn.iocoder.yudao.module.erp.controller.admin.finance.vo.ErpAccountPageReqVO;
+import cn.iocoder.yudao.module.erp.controller.admin.finance.vo.ErpAccountRespVO;
+import cn.iocoder.yudao.module.erp.controller.admin.finance.vo.ErpAccountSaveReqVO;
+import cn.iocoder.yudao.module.erp.dal.dataobject.finance.ErpAccountDO;
+import cn.iocoder.yudao.module.erp.service.finance.ErpAccountService;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.Parameters;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import jakarta.annotation.Resource;
+import jakarta.servlet.http.HttpServletResponse;
+import jakarta.validation.Valid;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.*;
+
+import java.io.IOException;
+import java.util.List;
+
+import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
+import static cn.iocoder.yudao.framework.operatelog.core.enums.OperateTypeEnum.EXPORT;
+
+@Tag(name = "管理后台 - ERP 结算账户")
+@RestController
+@RequestMapping("/erp/account")
+@Validated
+public class ErpAccountController {
+
+    @Resource
+    private ErpAccountService accountService;
+
+    @PostMapping("/create")
+    @Operation(summary = "创建结算账户")
+    @PreAuthorize("@ss.hasPermission('erp:account:create')")
+    public CommonResult<Long> createAccount(@Valid @RequestBody ErpAccountSaveReqVO createReqVO) {
+        return success(accountService.createAccount(createReqVO));
+    }
+
+    @PutMapping("/update")
+    @Operation(summary = "更新结算账户")
+    @PreAuthorize("@ss.hasPermission('erp:account:update')")
+    public CommonResult<Boolean> updateAccount(@Valid @RequestBody ErpAccountSaveReqVO updateReqVO) {
+        accountService.updateAccount(updateReqVO);
+        return success(true);
+    }
+
+    @PutMapping("/update-default-status")
+    @Operation(summary = "更新结算账户默认状态")
+    @Parameters({
+            @Parameter(name = "id", description = "编号", required = true),
+            @Parameter(name = "status", description = "状态", required = true)
+    })
+    public CommonResult<Boolean> updateAccountDefaultStatus(@RequestParam("id") Long id,
+                                                              @RequestParam("defaultStatus") Boolean defaultStatus) {
+        accountService.updateAccountDefaultStatus(id, defaultStatus);
+        return success(true);
+    }
+
+    @DeleteMapping("/delete")
+    @Operation(summary = "删除结算账户")
+    @Parameter(name = "id", description = "编号", required = true)
+    @PreAuthorize("@ss.hasPermission('erp:account:delete')")
+    public CommonResult<Boolean> deleteAccount(@RequestParam("id") Long id) {
+        accountService.deleteAccount(id);
+        return success(true);
+    }
+
+    @GetMapping("/get")
+    @Operation(summary = "获得结算账户")
+    @Parameter(name = "id", description = "编号", required = true, example = "1024")
+    @PreAuthorize("@ss.hasPermission('erp:account:query')")
+    public CommonResult<ErpAccountRespVO> getAccount(@RequestParam("id") Long id) {
+        ErpAccountDO account = accountService.getAccount(id);
+        return success(BeanUtils.toBean(account, ErpAccountRespVO.class));
+    }
+
+    @GetMapping("/page")
+    @Operation(summary = "获得结算账户分页")
+    @PreAuthorize("@ss.hasPermission('erp:account:query')")
+    public CommonResult<PageResult<ErpAccountRespVO>> getAccountPage(@Valid ErpAccountPageReqVO pageReqVO) {
+        PageResult<ErpAccountDO> pageResult = accountService.getAccountPage(pageReqVO);
+        return success(BeanUtils.toBean(pageResult, ErpAccountRespVO.class));
+    }
+
+    @GetMapping("/export-excel")
+    @Operation(summary = "导出结算账户 Excel")
+    @PreAuthorize("@ss.hasPermission('erp:account:export')")
+    @OperateLog(type = EXPORT)
+    public void exportAccountExcel(@Valid ErpAccountPageReqVO pageReqVO,
+              HttpServletResponse response) throws IOException {
+        pageReqVO.setPageSize(PageParam.PAGE_SIZE_NONE);
+        List<ErpAccountDO> list = accountService.getAccountPage(pageReqVO).getList();
+        // 导出 Excel
+        ExcelUtils.write(response, "结算账户.xls", "数据", ErpAccountRespVO.class,
+                        BeanUtils.toBean(list, ErpAccountRespVO.class));
+    }
+
+}

+ 24 - 0
yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/finance/vo/ErpAccountPageReqVO.java

@@ -0,0 +1,24 @@
+package cn.iocoder.yudao.module.erp.controller.admin.finance.vo;
+
+import cn.iocoder.yudao.framework.common.pojo.PageParam;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.ToString;
+
+@Schema(description = "管理后台 - ERP 结算账户分页 Request VO")
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+public class ErpAccountPageReqVO extends PageParam {
+
+    @Schema(description = "账户编码", example = "A88")
+    private String no;
+
+    @Schema(description = "账户名称", example = "张三")
+    private String name;
+
+    @Schema(description = "备注", example = "随便")
+    private String remark;
+
+}

+ 50 - 0
yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/finance/vo/ErpAccountRespVO.java

@@ -0,0 +1,50 @@
+package cn.iocoder.yudao.module.erp.controller.admin.finance.vo;
+
+import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat;
+import cn.iocoder.yudao.module.system.enums.DictTypeConstants;
+import com.alibaba.excel.annotation.ExcelIgnoreUnannotated;
+import com.alibaba.excel.annotation.ExcelProperty;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import java.time.LocalDateTime;
+
+@Schema(description = "管理后台 - ERP 结算账户 Response VO")
+@Data
+@ExcelIgnoreUnannotated
+public class ErpAccountRespVO {
+
+    @Schema(description = "结算账户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "28684")
+    @ExcelProperty("结算账户编号")
+    private Long id;
+
+    @Schema(description = "账户名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "张三")
+    @ExcelProperty("账户名称")
+    private String name;
+
+    @Schema(description = "账户编码", example = "A88")
+    @ExcelProperty("账户编码")
+    private String no;
+
+    @Schema(description = "备注", example = "随便")
+    @ExcelProperty("备注")
+    private String remark;
+
+    @Schema(description = "开启状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
+    @ExcelProperty("开启状态")
+    @DictFormat(DictTypeConstants.COMMON_STATUS)
+    private Integer status;
+
+    @Schema(description = "排序", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
+    @ExcelProperty("排序")
+    private Integer sort;
+
+    @Schema(description = "是否默认", example = "1")
+    @ExcelProperty("是否默认")
+    private Boolean defaultStatus;
+
+    @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
+    @ExcelProperty("创建时间")
+    private LocalDateTime createTime;
+
+}

+ 36 - 0
yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/finance/vo/ErpAccountSaveReqVO.java

@@ -0,0 +1,36 @@
+package cn.iocoder.yudao.module.erp.controller.admin.finance.vo;
+
+import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
+import cn.iocoder.yudao.framework.common.validation.InEnum;
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.NotEmpty;
+import jakarta.validation.constraints.NotNull;
+import lombok.Data;
+
+@Schema(description = "管理后台 - ERP 结算账户新增/修改 Request VO")
+@Data
+public class ErpAccountSaveReqVO {
+
+    @Schema(description = "结算账户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "28684")
+    private Long id;
+
+    @Schema(description = "账户名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "张三")
+    @NotEmpty(message = "账户名称不能为空")
+    private String name;
+
+    @Schema(description = "账户编码", example = "A88")
+    private String no;
+
+    @Schema(description = "备注", example = "随便")
+    private String remark;
+
+    @Schema(description = "开启状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
+    @NotNull(message = "开启状态不能为空")
+    @InEnum(value = CommonStatusEnum.class)
+    private Integer status;
+
+    @Schema(description = "排序", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
+    @NotNull(message = "排序不能为空")
+    private Integer sort;
+
+}

+ 2 - 9
yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/sale/vo/order/ErpSaleOrderSaveReqVO.java

@@ -1,7 +1,6 @@
 package cn.iocoder.yudao.module.erp.controller.admin.sale.vo.order;
 
 import io.swagger.v3.oas.annotations.media.Schema;
-import jakarta.validation.constraints.NotEmpty;
 import jakarta.validation.constraints.NotNull;
 import lombok.Data;
 
@@ -16,10 +15,6 @@ public class ErpSaleOrderSaveReqVO {
     @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "17386")
     private Long id;
 
-    @Schema(description = "销售单编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "XS001")
-    @NotEmpty(message = "销售单编号不能为空")
-    private String no;
-
     @Schema(description = "客户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1724")
     @NotNull(message = "客户编号不能为空")
     private Long customerId;
@@ -31,15 +26,13 @@ public class ErpSaleOrderSaveReqVO {
     @Schema(description = "销售员编号", example = "1888")
     private Long saleUserId;
 
-    @Schema(description = "结算账户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "31189")
-    @NotNull(message = "结算账户编号不能为空")
+    @Schema(description = "结算账户编号", example = "31189")
     private Long accountId;
 
     @Schema(description = "优惠率,百分比", requiredMode = Schema.RequiredMode.REQUIRED, example = "99.88")
     private BigDecimal discountPercent;
 
-    @Schema(description = "定金金额,单位:元", requiredMode = Schema.RequiredMode.REQUIRED, example = "7127")
-    @NotNull(message = "定金金额,单位:元不能为空")
+    @Schema(description = "定金金额,单位:元", example = "7127")
     private BigDecimal depositPrice;
 
     @Schema(description = "附件地址", example = "https://www.iocoder.cn")

+ 56 - 0
yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/dal/dataobject/finance/ErpAccountDO.java

@@ -0,0 +1,56 @@
+package cn.iocoder.yudao.module.erp.dal.dataobject.finance;
+
+import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
+import com.baomidou.mybatisplus.annotation.KeySequence;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.*;
+
+/**
+ * ERP 结算账户 DO
+ *
+ * @author 芋道源码
+ */
+@TableName("erp_account")
+@KeySequence("erp_account_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class ErpAccountDO extends BaseDO {
+
+    /**
+     * 结算账户编号
+     */
+    @TableId
+    private Long id;
+    /**
+     * 账户名称
+     */
+    private String name;
+    /**
+     * 账户编码
+     */
+    private String no;
+    /**
+     * 备注
+     */
+    private String remark;
+    /**
+     * 开启状态
+     *
+     * 枚举 {@link cn.iocoder.yudao.framework.common.enums.CommonStatusEnum}
+     */
+    private Integer status;
+    /**
+     * 排序
+     */
+    private Integer sort;
+    /**
+     * 是否默认
+     */
+    private Boolean defaultStatus;
+
+}

+ 36 - 0
yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/dal/mysql/finance/ErpAccountMapper.java

@@ -0,0 +1,36 @@
+package cn.iocoder.yudao.module.erp.dal.mysql.finance;
+
+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.module.erp.controller.admin.finance.vo.ErpAccountPageReqVO;
+import cn.iocoder.yudao.module.erp.dal.dataobject.finance.ErpAccountDO;
+import org.apache.ibatis.annotations.Mapper;
+
+import java.util.List;
+
+/**
+ * ERP 结算账户 Mapper
+ *
+ * @author 芋道源码
+ */
+@Mapper
+public interface ErpAccountMapper extends BaseMapperX<ErpAccountDO> {
+
+    default PageResult<ErpAccountDO> selectPage(ErpAccountPageReqVO reqVO) {
+        return selectPage(reqVO, new LambdaQueryWrapperX<ErpAccountDO>()
+                .likeIfPresent(ErpAccountDO::getName, reqVO.getName())
+                .eqIfPresent(ErpAccountDO::getNo, reqVO.getNo())
+                .eqIfPresent(ErpAccountDO::getRemark, reqVO.getRemark())
+                .orderByDesc(ErpAccountDO::getId));
+    }
+
+    default ErpAccountDO selectByDefaultStatus() {
+        return selectOne(ErpAccountDO::getDefaultStatus, true);
+    }
+
+    default List<ErpAccountDO> selectListByStatus(Integer status) {
+        return selectList(ErpAccountDO::getStatus, status);
+    }
+
+}

+ 5 - 0
yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/dal/redis/no/ErpNoRedisDAO.java

@@ -38,6 +38,11 @@ public class ErpNoRedisDAO {
      */
     public static final String STOCK_CHECK_NO_PREFIX = "QCPD";
 
+    /**
+     * 销售订单 {@link cn.iocoder.yudao.module.erp.dal.dataobject.sale.ErpSaleOrderDO}
+     */
+    public static final String SALE_ORDER_NO_PREFIX = "XSDD";
+
     @Resource
     private StringRedisTemplate stringRedisTemplate;
 

+ 70 - 0
yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/service/finance/ErpAccountService.java

@@ -0,0 +1,70 @@
+package cn.iocoder.yudao.module.erp.service.finance;
+
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.module.erp.controller.admin.finance.vo.ErpAccountPageReqVO;
+import cn.iocoder.yudao.module.erp.controller.admin.finance.vo.ErpAccountSaveReqVO;
+import cn.iocoder.yudao.module.erp.dal.dataobject.finance.ErpAccountDO;
+import jakarta.validation.Valid;
+
+/**
+ * ERP 结算账户 Service 接口
+ *
+ * @author 芋道源码
+ */
+public interface ErpAccountService {
+
+    /**
+     * 创建结算账户
+     *
+     * @param createReqVO 创建信息
+     * @return 编号
+     */
+    Long createAccount(@Valid ErpAccountSaveReqVO createReqVO);
+
+    /**
+     * 更新ERP 结算账户
+     *
+     * @param updateReqVO 更新信息
+     */
+    void updateAccount(@Valid ErpAccountSaveReqVO updateReqVO);
+
+    /**
+     * 更新结算账户默认状态
+     *
+     * @param id 编号
+     * @param defaultStatus 默认状态
+     */
+    void updateAccountDefaultStatus(Long id, Boolean defaultStatus);
+
+    /**
+     * 删除结算账户
+     *
+     * @param id 编号
+     */
+    void deleteAccount(Long id);
+
+    /**
+     * 获得结算账户
+     *
+     * @param id 编号
+     * @return 结算账户
+     */
+    ErpAccountDO getAccount(Long id);
+
+    /**
+     * 校验结算账户
+     *
+     * @param id 编号
+     * @return 结算账户
+     */
+    ErpAccountDO validateAccount(Long id);
+
+    /**
+     * 获得结算账户分页
+     *
+     * @param pageReqVO 分页查询
+     * @return 结算账户分页
+     */
+    PageResult<ErpAccountDO> getAccountPage(ErpAccountPageReqVO pageReqVO);
+
+}

+ 99 - 0
yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/service/finance/ErpAccountServiceImpl.java

@@ -0,0 +1,99 @@
+package cn.iocoder.yudao.module.erp.service.finance;
+
+import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
+import cn.iocoder.yudao.module.erp.controller.admin.finance.vo.ErpAccountPageReqVO;
+import cn.iocoder.yudao.module.erp.controller.admin.finance.vo.ErpAccountSaveReqVO;
+import cn.iocoder.yudao.module.erp.dal.dataobject.finance.ErpAccountDO;
+import cn.iocoder.yudao.module.erp.dal.mysql.finance.ErpAccountMapper;
+import jakarta.annotation.Resource;
+import org.springframework.stereotype.Service;
+import org.springframework.validation.annotation.Validated;
+
+import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
+import static cn.iocoder.yudao.module.erp.enums.ErrorCodeConstants.*;
+
+/**
+ * ERP 结算账户 Service 实现类
+ *
+ * @author 芋道源码
+ */
+@Service
+@Validated
+public class ErpAccountServiceImpl implements ErpAccountService {
+
+    @Resource
+    private ErpAccountMapper accountMapper;
+
+    @Override
+    public Long createAccount(ErpAccountSaveReqVO createReqVO) {
+        // 插入
+        ErpAccountDO account = BeanUtils.toBean(createReqVO, ErpAccountDO.class);
+        accountMapper.insert(account);
+        // 返回
+        return account.getId();
+    }
+
+    @Override
+    public void updateAccount(ErpAccountSaveReqVO updateReqVO) {
+        // 校验存在
+        validateAccountExists(updateReqVO.getId());
+        // 更新
+        ErpAccountDO updateObj = BeanUtils.toBean(updateReqVO, ErpAccountDO.class);
+        accountMapper.updateById(updateObj);
+    }
+
+    @Override
+    public void updateAccountDefaultStatus(Long id, Boolean defaultStatus) {
+        // 1. 校验存在
+        validateAccountExists(id);
+
+        // 2.1 如果开启,则需要关闭所有其它的默认
+        if (defaultStatus) {
+            ErpAccountDO account = accountMapper.selectByDefaultStatus();
+            if (account != null) {
+                accountMapper.updateById(new ErpAccountDO().setId(account.getId()).setDefaultStatus(false));
+            }
+        }
+        // 2.2 更新对应的默认状态
+        accountMapper.updateById(new ErpAccountDO().setId(id).setDefaultStatus(defaultStatus));
+    }
+
+    @Override
+    public void deleteAccount(Long id) {
+        // 校验存在
+        validateAccountExists(id);
+        // 删除
+        accountMapper.deleteById(id);
+    }
+
+    private void validateAccountExists(Long id) {
+        if (accountMapper.selectById(id) == null) {
+            throw exception(ACCOUNT_NOT_EXISTS);
+        }
+    }
+
+    @Override
+    public ErpAccountDO getAccount(Long id) {
+        return accountMapper.selectById(id);
+    }
+
+    @Override
+    public ErpAccountDO validateAccount(Long id) {
+        ErpAccountDO account = accountMapper.selectById(id);
+        if (account == null) {
+            throw exception(ACCOUNT_NOT_EXISTS);
+        }
+        if (CommonStatusEnum.isDisable(account.getStatus())) {
+            throw exception(ACCOUNT_NOT_ENABLE, account.getName());
+        }
+        return account;
+    }
+
+    @Override
+    public PageResult<ErpAccountDO> getAccountPage(ErpAccountPageReqVO pageReqVO) {
+        return accountMapper.selectPage(pageReqVO);
+    }
+
+}

+ 46 - 18
yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/service/sale/ErpSaleOrderServiceImpl.java

@@ -13,6 +13,7 @@ import cn.iocoder.yudao.module.erp.dal.mysql.sale.ErpSaleOrderItemMapper;
 import cn.iocoder.yudao.module.erp.dal.mysql.sale.ErpSaleOrderMapper;
 import cn.iocoder.yudao.module.erp.dal.redis.no.ErpNoRedisDAO;
 import cn.iocoder.yudao.module.erp.enums.ErpAuditStatus;
+import cn.iocoder.yudao.module.erp.service.finance.ErpAccountService;
 import cn.iocoder.yudao.module.erp.service.product.ErpProductService;
 import jakarta.annotation.Resource;
 import org.springframework.stereotype.Service;
@@ -52,6 +53,8 @@ public class ErpSaleOrderServiceImpl implements ErpSaleOrderService {
     private ErpProductService productService;
     @Resource
     private ErpCustomerService customerService;
+    @Resource
+    private ErpAccountService accountService;
 
     @Override
     @Transactional(rollbackFor = Exception.class)
@@ -60,17 +63,20 @@ public class ErpSaleOrderServiceImpl implements ErpSaleOrderService {
         List<ErpSaleOrderItemDO> saleOrderItems = validateSaleOrderItems(createReqVO.getItems());
         // 1.2 校验客户
         customerService.validateCustomer(createReqVO.getCustomerId());
-        // 1.3 生成调拨单号,并校验唯一性
-        String no = noRedisDAO.generate(ErpNoRedisDAO.STOCK_MOVE_NO_PREFIX);
+        // 1.3 校验结算账户
+        if (createReqVO.getAccountId() != null) {
+            accountService.validateAccount(createReqVO.getAccountId());
+        }
+        // 1.4 生成调拨单号,并校验唯一性
+        String no = noRedisDAO.generate(ErpNoRedisDAO.SALE_ORDER_NO_PREFIX);
         if (saleOrderMapper.selectByNo(no) != null) {
-            throw exception(STOCK_MOVE_NO_EXISTS);
+            throw exception(SALE_ORDER_NO_EXISTS);
         }
 
         // 2.1 插入订单
         ErpSaleOrderDO saleOrder = BeanUtils.toBean(createReqVO, ErpSaleOrderDO.class, in -> in
-                .setNo(no).setStatus(ErpAuditStatus.PROCESS.getStatus())
-                .setTotalCount(getSumValue(saleOrderItems, ErpSaleOrderItemDO::getCount, BigDecimal::add))
-                .setTotalPrice(getSumValue(saleOrderItems, ErpSaleOrderItemDO::getTotalPrice, BigDecimal::add, BigDecimal.ZERO)));
+                .setNo(no).setStatus(ErpAuditStatus.PROCESS.getStatus()));
+        calculateTotalPrice(saleOrder, saleOrderItems);
         saleOrderMapper.insert(saleOrder);
         // 2.2 插入订单项
         saleOrderItems.forEach(o -> o.setOrderId(saleOrder.getId()));
@@ -84,22 +90,38 @@ public class ErpSaleOrderServiceImpl implements ErpSaleOrderService {
         // 1.1 校验存在
         ErpSaleOrderDO saleOrder = validateSaleOrderExists(updateReqVO.getId());
         if (ErpAuditStatus.APPROVE.getStatus().equals(saleOrder.getStatus())) {
-            throw exception(STOCK_MOVE_UPDATE_FAIL_APPROVE, saleOrder.getNo());
+            throw exception(SALE_ORDER_UPDATE_FAIL_APPROVE, saleOrder.getNo());
         }
         // 1.2 校验客户
         customerService.validateCustomer(updateReqVO.getCustomerId());
-        // 1.3 校验订单项的有效性
+        // 1.3 校验结算账户
+        if (updateReqVO.getAccountId() != null) {
+            accountService.validateAccount(updateReqVO.getAccountId());
+        }
+        // 1.4 校验订单项的有效性
         List<ErpSaleOrderItemDO> saleOrderItems = validateSaleOrderItems(updateReqVO.getItems());
 
         // 2.1 更新订单
-        ErpSaleOrderDO updateObj = BeanUtils.toBean(updateReqVO, ErpSaleOrderDO.class, in -> in
-                .setTotalCount(getSumValue(saleOrderItems, ErpSaleOrderItemDO::getCount, BigDecimal::add))
-                .setTotalPrice(getSumValue(saleOrderItems, ErpSaleOrderItemDO::getTotalPrice, BigDecimal::add)));
+        ErpSaleOrderDO updateObj = BeanUtils.toBean(updateReqVO, ErpSaleOrderDO.class);
+        calculateTotalPrice(updateObj, saleOrderItems);
         saleOrderMapper.updateById(updateObj);
         // 2.2 更新订单项
         updateSaleOrderItemList(updateReqVO.getId(), saleOrderItems);
     }
 
+    private void calculateTotalPrice(ErpSaleOrderDO saleOrder, List<ErpSaleOrderItemDO> saleOrderItems) {
+        saleOrder.setTotalCount(getSumValue(saleOrderItems, ErpSaleOrderItemDO::getCount, BigDecimal::add));
+        saleOrder.setTotalProductPrice(getSumValue(saleOrderItems, ErpSaleOrderItemDO::getTotalPrice, BigDecimal::add, BigDecimal.ZERO));
+        saleOrder.setTotalTaxPrice(getSumValue(saleOrderItems, ErpSaleOrderItemDO::getTaxPrice, BigDecimal::add, BigDecimal.ZERO));
+        saleOrder.setTotalPrice(saleOrder.getTotalProductPrice().add(saleOrder.getTotalTaxPrice()));
+        // 计算优惠价格
+        if (saleOrder.getDiscountPercent() == null) {
+            saleOrder.setDiscountPercent(BigDecimal.ZERO);
+        }
+        saleOrder.setDiscountPrice(MoneyUtils.priceMultiply(saleOrder.getTotalPrice(), saleOrder.getDiscountPercent()));
+        saleOrder.setTotalPrice(saleOrder.getTotalPrice().subtract(saleOrder.getDiscountPrice()));
+    }
+
     @Override
     @Transactional(rollbackFor = Exception.class)
     public void updateSaleOrderStatus(Long id, Integer status) {
@@ -108,7 +130,7 @@ public class ErpSaleOrderServiceImpl implements ErpSaleOrderService {
         ErpSaleOrderDO saleOrder = validateSaleOrderExists(id);
         // 1.2 校验状态
         if (saleOrder.getStatus().equals(status)) {
-            throw exception(approve ? STOCK_MOVE_APPROVE_FAIL : STOCK_MOVE_PROCESS_FAIL);
+            throw exception(approve ? SALE_ORDER_APPROVE_FAIL : SALE_ORDER_PROCESS_FAIL);
         }
         // TODO @芋艿:需要校验是不是有入库、有退货
 
@@ -116,7 +138,7 @@ public class ErpSaleOrderServiceImpl implements ErpSaleOrderService {
         int updateCount = saleOrderMapper.updateByIdAndStatus(id, saleOrder.getStatus(),
                 new ErpSaleOrderDO().setStatus(status));
         if (updateCount == 0) {
-            throw exception(approve ? STOCK_MOVE_APPROVE_FAIL : STOCK_MOVE_PROCESS_FAIL);
+            throw exception(approve ? SALE_ORDER_APPROVE_FAIL : SALE_ORDER_PROCESS_FAIL);
         }
     }
 
@@ -126,9 +148,15 @@ public class ErpSaleOrderServiceImpl implements ErpSaleOrderService {
                 convertSet(list, ErpSaleOrderSaveReqVO.Item::getProductId));
         Map<Long, ErpProductDO> productMap = convertMap(productList, ErpProductDO::getId);
         // 2. 转化为 ErpSaleOrderItemDO 列表
-        return convertList(list, o -> BeanUtils.toBean(o, ErpSaleOrderItemDO.class, item -> item
-                .setProductUnitId(productMap.get(item.getProductId()).getUnitId())
-                .setTotalPrice(MoneyUtils.priceMultiply(item.getProductPrice(), item.getCount()))));
+        return convertList(list, o -> BeanUtils.toBean(o, ErpSaleOrderItemDO.class, item -> {
+            item.setProductUnitId(productMap.get(item.getProductId()).getUnitId());
+            item.setTotalPrice(MoneyUtils.priceMultiply(item.getProductPrice(), item.getCount()));
+            if (item.getTotalPrice() == null) {
+                return;
+            }
+            item.setTaxPrice(MoneyUtils.priceMultiply(item.getTotalPrice(), item.getTaxPercent()));
+            item.setTotalPrice(item.getTotalPrice().add(item.getTaxPrice()));
+        }));
     }
 
     private void updateSaleOrderItemList(Long id, List<ErpSaleOrderItemDO> newList) {
@@ -160,7 +188,7 @@ public class ErpSaleOrderServiceImpl implements ErpSaleOrderService {
         }
         saleOrders.forEach(saleOrder -> {
             if (ErpAuditStatus.APPROVE.getStatus().equals(saleOrder.getStatus())) {
-                throw exception(STOCK_MOVE_DELETE_FAIL_APPROVE, saleOrder.getNo());
+                throw exception(SALE_ORDER_DELETE_FAIL_APPROVE, saleOrder.getNo());
             }
         });
 
@@ -176,7 +204,7 @@ public class ErpSaleOrderServiceImpl implements ErpSaleOrderService {
     private ErpSaleOrderDO validateSaleOrderExists(Long id) {
         ErpSaleOrderDO saleOrder = saleOrderMapper.selectById(id);
         if (saleOrder == null) {
-            throw exception(STOCK_MOVE_NOT_EXISTS);
+            throw exception(SALE_ORDER_NOT_EXISTS);
         }
         return saleOrder;
     }

+ 12 - 0
yudao-module-erp/yudao-module-erp-biz/src/main/resources/mapper/finance/ErpAccountMapper.xml

@@ -0,0 +1,12 @@
+<?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.erp.dal.mysql.finance.ErpAccountMapper">
+
+    <!--
+        一般情况下,尽可能使用 Mapper 进行 CRUD 增删改查即可。
+        无法满足的场景,例如说多表关联查询,才使用 XML 编写 SQL。
+        代码生成器暂时只生成 Mapper XML 文件本身,更多推荐 MybatisX 快速开发插件来生成查询。
+        文档可见:https://www.iocoder.cn/MyBatis/x-plugins/
+     -->
+
+</mapper>

+ 142 - 0
yudao-module-erp/yudao-module-erp-biz/src/test/java/cn/iocoder/yudao/module/erp/service/finance/ErpAccountServiceImplTest.java

@@ -0,0 +1,142 @@
+package cn.iocoder.yudao.module.erp.service.finance;
+
+import org.junit.jupiter.api.Disabled;
+import org.junit.jupiter.api.Test;
+import org.springframework.boot.test.mock.mockito.MockBean;
+
+import jakarta.annotation.Resource;
+
+import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest;
+
+import cn.iocoder.yudao.module.erp.controller.admin.finance.vo.*;
+import cn.iocoder.yudao.module.erp.dal.dataobject.finance.ErpAccountDO;
+import cn.iocoder.yudao.module.erp.dal.mysql.finance.ErpAccountMapper;
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+
+import jakarta.annotation.Resource;
+import org.springframework.context.annotation.Import;
+import java.util.*;
+import java.time.LocalDateTime;
+
+import static cn.hutool.core.util.RandomUtil.*;
+import static cn.iocoder.yudao.module.erp.enums.ErrorCodeConstants.*;
+import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.*;
+import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.*;
+import static cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils.*;
+import static cn.iocoder.yudao.framework.common.util.object.ObjectUtils.*;
+import static cn.iocoder.yudao.framework.common.util.date.DateUtils.*;
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.Mockito.*;
+
+/**
+ * {@link ErpAccountServiceImpl} 的单元测试类
+ *
+ * @author 芋道源码
+ */
+@Import(ErpAccountServiceImpl.class)
+public class ErpAccountServiceImplTest extends BaseDbUnitTest {
+
+    @Resource
+    private ErpAccountServiceImpl accountService;
+
+    @Resource
+    private ErpAccountMapper accountMapper;
+
+    @Test
+    public void testCreateAccount_success() {
+        // 准备参数
+        ErpAccountSaveReqVO createReqVO = randomPojo(ErpAccountSaveReqVO.class).setId(null);
+
+        // 调用
+        Long accountId = accountService.createAccount(createReqVO);
+        // 断言
+        assertNotNull(accountId);
+        // 校验记录的属性是否正确
+        ErpAccountDO account = accountMapper.selectById(accountId);
+        assertPojoEquals(createReqVO, account, "id");
+    }
+
+    @Test
+    public void testUpdateAccount_success() {
+        // mock 数据
+        ErpAccountDO dbAccount = randomPojo(ErpAccountDO.class);
+        accountMapper.insert(dbAccount);// @Sql: 先插入出一条存在的数据
+        // 准备参数
+        ErpAccountSaveReqVO updateReqVO = randomPojo(ErpAccountSaveReqVO.class, o -> {
+            o.setId(dbAccount.getId()); // 设置更新的 ID
+        });
+
+        // 调用
+        accountService.updateAccount(updateReqVO);
+        // 校验是否更新正确
+        ErpAccountDO account = accountMapper.selectById(updateReqVO.getId()); // 获取最新的
+        assertPojoEquals(updateReqVO, account);
+    }
+
+    @Test
+    public void testUpdateAccount_notExists() {
+        // 准备参数
+        ErpAccountSaveReqVO updateReqVO = randomPojo(ErpAccountSaveReqVO.class);
+
+        // 调用, 并断言异常
+        assertServiceException(() -> accountService.updateAccount(updateReqVO), ACCOUNT_NOT_EXISTS);
+    }
+
+    @Test
+    public void testDeleteAccount_success() {
+        // mock 数据
+        ErpAccountDO dbAccount = randomPojo(ErpAccountDO.class);
+        accountMapper.insert(dbAccount);// @Sql: 先插入出一条存在的数据
+        // 准备参数
+        Long id = dbAccount.getId();
+
+        // 调用
+        accountService.deleteAccount(id);
+       // 校验数据不存在了
+       assertNull(accountMapper.selectById(id));
+    }
+
+    @Test
+    public void testDeleteAccount_notExists() {
+        // 准备参数
+        Long id = randomLongId();
+
+        // 调用, 并断言异常
+        assertServiceException(() -> accountService.deleteAccount(id), ACCOUNT_NOT_EXISTS);
+    }
+
+    @Test
+    @Disabled  // TODO 请修改 null 为需要的值,然后删除 @Disabled 注解
+    public void testGetAccountPage() {
+       // mock 数据
+       ErpAccountDO dbAccount = randomPojo(ErpAccountDO.class, o -> { // 等会查询到
+           o.setNo(null);
+           o.setRemark(null);
+           o.setStatus(null);
+           o.setName(null);
+       });
+       accountMapper.insert(dbAccount);
+       // 测试 no 不匹配
+       accountMapper.insert(cloneIgnoreId(dbAccount, o -> o.setNo(null)));
+       // 测试 remark 不匹配
+       accountMapper.insert(cloneIgnoreId(dbAccount, o -> o.setRemark(null)));
+       // 测试 status 不匹配
+       accountMapper.insert(cloneIgnoreId(dbAccount, o -> o.setStatus(null)));
+       // 测试 name 不匹配
+       accountMapper.insert(cloneIgnoreId(dbAccount, o -> o.setName(null)));
+       // 准备参数
+       ErpAccountPageReqVO reqVO = new ErpAccountPageReqVO();
+       reqVO.setNo(null);
+       reqVO.setRemark(null);
+       reqVO.setStatus(null);
+       reqVO.setName(null);
+
+       // 调用
+       PageResult<ErpAccountDO> pageResult = accountService.getAccountPage(reqVO);
+       // 断言
+       assertEquals(1, pageResult.getTotal());
+       assertEquals(1, pageResult.getList().size());
+       assertPojoEquals(dbAccount, pageResult.getList().get(0));
+    }
+
+}