buhuazhen
4 天以前 cce61ef716de4bb02c5164eedb8fe43197dee7ee
feat(procurement): 新增采购价格管理模块及自动价格变更处理

- 新增DiscountTypeEnum定义折扣类型枚举
- 新增ProcurementPriceManagement实体类及数据库映射
- 新增ProcurementPriceManagementMapper及分页查询XML配置
- 实现ProcurementPriceManagementService定义自动创建价格记录方法
- 实现ProcurementPriceManagementServiceImpl自动生成价格记录并计算折扣逻辑
- 优化价格记录状态判定(有效、待生效、已过期)
- 在采购台账服务中集成价格管理自动创建逻辑
- 完成采购台账增删改及子表数据处理及文件迁移功能
- 添加采购台账分页列表及明细查询功能及相关Mapper XML配置
已添加2个文件
已修改6个文件
229 ■■■■■ 文件已修改
src/main/java/com/ruoyi/procurementrecord/DiscountTypeEnum.java 23 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/procurementrecord/dto/SimplePP.java 37 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/procurementrecord/pojo/ProcurementPriceManagement.java 22 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/procurementrecord/service/ProcurementPriceManagementService.java 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/procurementrecord/service/impl/ProcurementPriceManagementServiceImpl.java 95 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/purchase/service/impl/PurchaseLedgerServiceImpl.java 29 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/procurementrecord/ProcurementPriceManagementMapper.xml 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/purchase/PurchaseLedgerMapper.xml 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/procurementrecord/DiscountTypeEnum.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,23 @@
package com.ruoyi.procurementrecord;
import lombok.Getter;
/**
 * @author buhuazhen
 * @date 2026/3/28
 * @email 3038525872@qq.com
 */
@Getter
public enum DiscountTypeEnum{
    DISCOUNT_TYPE_NONE("", "无折扣"),
    DISCOUNT_TYPE_PERCENTAGE("percentage", "百分比折扣"),
    DISCOUNT_TYPE_FIXED("fixed", "固定金额");
    private final String code;
    private final String description;
    DiscountTypeEnum(String code, String description) {
        this.code = code;
        this.description = description;
    }
}
src/main/java/com/ruoyi/procurementrecord/dto/SimplePP.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,37 @@
package com.ruoyi.procurementrecord.dto;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.NonNull;
import java.io.Serializable;
import java.math.BigDecimal;
/**
 * ç”¨äºŽè‡ªåŠ¨ç”Ÿæˆé‡‡è´­ä»·æ ¼ç®¡ç†çš„ç®€å•DTO
 * ä¸€ä¸‹
 * @author buhuazhen
 * @date 2026/3/28
 * @email 3038525872@qq.com
 */
@Data
@AllArgsConstructor
@NoArgsConstructor
public class SimplePP implements Serializable {
    private String productName;
    private Long productId;
    private String specification;
    private String supplierName;
    private Long supplierId;
    private String remark;
    private String unit;
    // æœ€ç»ˆä»·æ ¼ è¦æ±‚不含税的单价
    private BigDecimal finalPrice;
}
src/main/java/com/ruoyi/procurementrecord/pojo/ProcurementPriceManagement.java
@@ -8,6 +8,8 @@
import lombok.Data;
import org.springframework.format.annotation.DateTimeFormat;
import java.io.Serializable;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.Date;
@@ -18,7 +20,7 @@
@Data
@TableName("procurement_price_management")
@ApiModel
public class ProcurementPriceManagement {
public class ProcurementPriceManagement implements Serializable {
    private static final long serialVersionUID = 1L;
    /**
@@ -31,8 +33,14 @@
    @Excel(name = "商品名称")
    private String productName;
    @TableField(value = "product_id")
    private Long productId;
    /**
     * æ— æ•ˆå­—段 å¯ä»¥ä½œä¸ºå†—余字段使用
     */
    @ApiModelProperty(value = "商品编码")
    @Excel(name = "商品编码")
//    @Excel(name = "商品编码")
    private String productCode;
    @ApiModelProperty(value = "规格型号")
@@ -43,9 +51,17 @@
    @Excel(name = "供应商名称")
    private String supplierName;
    @TableField(value = "supplier_id")
    private Long supplierId;
    @ApiModelProperty(value = "基础价格")
    @Excel(name = "基础价格")
    private String basePrice;
    @ApiModelProperty(value = "实际价格")
    @TableField(value = "actually_price")
    @Excel
    private BigDecimal actuallyPrice;
    @ApiModelProperty(value = "状态")
    @TableField(exist = false)
@@ -76,6 +92,8 @@
    @Excel(name = "最高价格")
    private String maxPrice;
    @ApiModelProperty(value = "预警阈值(%)")
    private String warningThreshold;
src/main/java/com/ruoyi/procurementrecord/service/ProcurementPriceManagementService.java
@@ -3,9 +3,12 @@
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.IService;
import com.ruoyi.common.exception.ServiceException;
import com.ruoyi.procurementrecord.dto.SimplePP;
import com.ruoyi.procurementrecord.pojo.ProcurementPriceManagement;
import javax.servlet.http.HttpServletResponse;
import javax.validation.constraints.NotNull;
/**
 * @author :yys
@@ -23,4 +26,10 @@
    IPage<ProcurementPriceManagement> listPage(Page page, ProcurementPriceManagement procurementPriceManagement);
    void export(HttpServletResponse response);
    /**
     * è‡ªåŠ¨ç”Ÿæˆä»·æ ¼ç®¡ç†å¹¶ä¸”å¯¹æ¯”ä¸Šä¸€æ¬¡ä»·æ ¼æ•°æ®
     * @param simplePP é‡Œé¢å­—段禁止为空
     */
    void autoCreateRecord(@NotNull SimplePP simplePP) throws ServiceException;
}
src/main/java/com/ruoyi/procurementrecord/service/impl/ProcurementPriceManagementServiceImpl.java
@@ -1,19 +1,27 @@
package com.ruoyi.procurementrecord.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.ruoyi.common.utils.excel.ExcelUtils;
import com.ruoyi.common.exception.ServiceException;
import com.ruoyi.common.utils.poi.ExcelUtil;
import com.ruoyi.procurementrecord.dto.ProcurementPageDto;
import com.ruoyi.procurementrecord.DiscountTypeEnum;
import com.ruoyi.procurementrecord.dto.SimplePP;
import com.ruoyi.procurementrecord.mapper.ProcurementPriceManagementMapper;
import com.ruoyi.procurementrecord.pojo.ProcurementPriceManagement;
import com.ruoyi.procurementrecord.service.ProcurementPriceManagementService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import javax.servlet.http.HttpServletResponse;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.Calendar;
import java.util.Date;
import java.util.List;
/**
@@ -74,5 +82,86 @@
            }
        }
        ExcelUtil<ProcurementPriceManagement> util = new ExcelUtil<ProcurementPriceManagement>(ProcurementPriceManagement.class);
        util.exportExcel(response, procurementPriceManagements, "采购价格管理");}
        util.exportExcel(response, procurementPriceManagements, "采购价格管理");
    }
    @Override
    @Transactional
    public void autoCreateRecord(SimplePP simplePP) throws ServiceException {
        // æ ¹æ®ä¾›åº”商id äº§å“id æŸ¥è¯¢å‡ºæœ€è¿‘修改的一条记录进行比对
        ProcurementPriceManagement insertPriceManagement = new ProcurementPriceManagement();
        BeanUtils.copyProperties(simplePP, insertPriceManagement);
        LambdaQueryWrapper<ProcurementPriceManagement> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(ProcurementPriceManagement::getSupplierId, simplePP.getSupplierId())
                .eq(ProcurementPriceManagement::getProductId, simplePP.getProductId())
                .orderByDesc(ProcurementPriceManagement::getUpdateTime).last("limit 1");
        ProcurementPriceManagement lastRecord = procurementPriceManagementMapper.selectOne(queryWrapper);
        insertPriceManagement.setActuallyPrice(simplePP.getFinalPrice());
        insertPriceManagement.setDiscountType(DiscountTypeEnum.DISCOUNT_TYPE_NONE.getCode());
        insertPriceManagement.setDiscountValue("0");
        // ä½¿ç”¨ Calendar è®¾ç½®è¿œæœŸæ—¥æœŸ (9999-12-31)
        Calendar calendar = Calendar.getInstance();
        calendar.set(2099, Calendar.DECEMBER, 31);
        insertPriceManagement.setDiscountEndTime(calendar.getTime());
        insertPriceManagement.setEffectiveTime(new Date());
        insertPriceManagement.setExpireTime(calendar.getTime());
        insertPriceManagement.setReason("other");
        // è¿›è¡Œåˆ†åˆ«æƒ…况
        BigDecimal currentPrice = simplePP.getFinalPrice();
        if (lastRecord == null) {
            insertPriceManagement.setBasePrice(currentPrice.toString());
            insertPriceManagement.setMinPrice(null);
            insertPriceManagement.setMaxPrice(null);
            procurementPriceManagementMapper.insert(insertPriceManagement);
            return;
        }
        int compareTo = lastRecord.getActuallyPrice().compareTo(currentPrice);
        if (compareTo == 0) {
            log.info("价格未变化,无需创建记录");
            return;
        }
        // ðŸ‘‰ ä»·æ ¼å˜åŒ–逻辑
        insertPriceManagement.setBasePrice(String.valueOf(lastRecord.getActuallyPrice()));
        insertPriceManagement.setMaxPrice(lastRecord.getMaxPrice());
        insertPriceManagement.setMinPrice(lastRecord.getMinPrice());
        // è®¡ç®—折扣
//        if(compareTo > 0){
            BigDecimal basePrice = new BigDecimal(insertPriceManagement.getBasePrice());
            BigDecimal actuallyPrice = insertPriceManagement.getActuallyPrice();
            // é˜²æ­¢é™¤0
            if (basePrice.compareTo(BigDecimal.ZERO) == 0) {
                insertPriceManagement.setDiscountValue("0");
                return;
            }
            // æŠ˜æ‰£ = (原价 - çް价) / åŽŸä»·
            BigDecimal discount = basePrice.subtract(actuallyPrice)
                    .divide(basePrice, 8, RoundingMode.HALF_UP);
            if(discount.compareTo(new BigDecimal("0.01")) < 0 && discount.compareTo(new BigDecimal("0")) > 0){
                // è½¬ä¸ºå›ºå®šé‡‘额
                insertPriceManagement.setDiscountValue(String.valueOf(basePrice.subtract(actuallyPrice)));
                insertPriceManagement.setDiscountType(DiscountTypeEnum.DISCOUNT_TYPE_FIXED.getCode());
            }else {
                insertPriceManagement.setDiscountType(DiscountTypeEnum.DISCOUNT_TYPE_PERCENTAGE.getCode());
                // è½¬ç™¾åˆ†æ¯”(×100)
                BigDecimal percent = discount.multiply(new BigDecimal("100"));
                // ä¿ç•™2位小数
                percent = percent.setScale(2, RoundingMode.HALF_UP);
                // è®¾ç½®å€¼ï¼ˆæ¯”如:20.00%)
                insertPriceManagement.setDiscountValue(String.valueOf(percent));
            }
//        }
        // final insert
        procurementPriceManagementMapper.insert(insertPriceManagement);
    }
}
src/main/java/com/ruoyi/purchase/service/impl/PurchaseLedgerServiceImpl.java
@@ -24,6 +24,8 @@
import com.ruoyi.framework.security.LoginUser;
import com.ruoyi.other.mapper.TempFileMapper;
import com.ruoyi.other.pojo.TempFile;
import com.ruoyi.procurementrecord.dto.SimplePP;
import com.ruoyi.procurementrecord.service.ProcurementPriceManagementService;
import com.ruoyi.project.system.domain.SysUser;
import com.ruoyi.project.system.mapper.SysUserMapper;
import com.ruoyi.purchase.dto.PurchaseLedgerDto;
@@ -50,6 +52,7 @@
import java.io.IOException;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
@@ -97,6 +100,8 @@
    private final InvoiceRegistrationProductMapper invoiceRegistrationProductMapper;
    private final StringRedisTemplate redisTemplate;
    private final ProcurementPriceManagementService procurementPriceManagementService;
    @Value("${file.upload-dir}")
    private String uploadDir;
@@ -220,7 +225,7 @@
        if (products == null || products.isEmpty()) {
            throw new BaseException("产品信息不存在");
        }
        PurchaseLedger ledger = purchaseLedgerMapper.selectById(salesLedgerId);
        // æå‰æ”¶é›†æ‰€æœ‰éœ€è¦æŸ¥è¯¢çš„ID
        Set<Long> productIds = products.stream()
                .map(SalesLedgerProduct::getProductId)
@@ -287,6 +292,28 @@
            }
        }
        updateList.addAll(insertList);
        updateList.forEach(it->{
            SimplePP simplePP = new SimplePP();
            simplePP.setProductId(it.getProductId());
            simplePP.setSupplierName(ledger.getSupplierName());
            simplePP.setSupplierId(ledger.getSupplierId());
            simplePP.setUnit(it.getUnit());
            simplePP.setProductName(it.getProductCategory());
            simplePP.setSpecification(it.getSpecificationModel());
            simplePP.setFinalPrice(it.getTaxInclusiveUnitPrice().divide(
                    BigDecimal.ONE.add(it.getTaxRate().divide(new BigDecimal("100"), 4, RoundingMode.HALF_UP)),
                    2,  // ä¿ç•™4位(可根据业务调整)
                    RoundingMode.HALF_UP
            ));
            simplePP.setRemark(
                    "系统根据采购提交自动生成。采购合同号为:" + purchaseLedger.getPurchaseContractNumber() + "。"
            );
            procurementPriceManagementService.autoCreateRecord(simplePP);
        });
        // è®¡ç®—总含税金额
        BigDecimal totalTaxInclusiveAmount = products.stream()
                .map(SalesLedgerProduct::getTaxInclusiveTotalPrice)
src/main/resources/mapper/procurementrecord/ProcurementPriceManagementMapper.xml
@@ -11,6 +11,13 @@
            <if test="req.supplierName != null and req.supplierName != ''">
                AND supplier_name = #{req.supplierName}
            </if>
            <if test="req.supplierId != null">
                AND supplier_id = #{req.supplierId}
            </if>
            <if test="req.productId != null">
                AND product_id = #{req.productId}
            </if>
        </where>
        order by update_time desc
    </select>
</mapper>
src/main/resources/mapper/purchase/PurchaseLedgerMapper.xml
@@ -39,8 +39,8 @@
            <if test="c.supplierName != null and c.supplierName != ''">
                and pl.supplier_name like concat('%',#{c.supplierName},'%')
            </if>
            <if test="c.ApprovalStatus != null and c.ApprovalStatus != ''">
                and pl.approval_status = #{c.ApprovalStatus}
            <if test="c.approvalStatus != null and c.approvalStatus != ''">
                and pl.approval_status = #{c.approvalStatus}
            </if>
            <if test="c.salesContractNo != null and c.salesContractNo != ''">
                and pl.sales_contract_no like concat('%',#{c.salesContractNo},'%')
@@ -58,7 +58,8 @@
        group by pl.id, pl.purchase_contract_number, pl.sales_contract_no, pl.supplier_name,
        pl.project_name,pl.entry_date,
        pl.recorder_name,
        pl.contract_amount
        pl.contract_amount,
        pl.approval_status
        order by pl.entry_date desc
    </select>