feat(erp): 采购/销售明细按含税单价口径拆税,供应商合同补供应商名称与附件

- MoneyUtils 增加含税总价拆税额/不含税金额的方法,采购订单、采购申请、销售订单明细统一按「产品单价为含税单价」重算税额与不含税金额
- 供应商合同列表/详情/导出回填供应商名称;详情返回合同附件(erp_supplier_contract 记录类型)
- MDM 物料接口回填品牌名称,采购下单选物料后能带出品牌

Co-Authored-By: Claude Code <noreply@anthropic.com>
已修改14个文件
193 ■■■■■ 文件已修改
yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/number/MoneyUtils.java 38 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/purchase/ErpSupplierContractController.java 35 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/purchase/vo/contract/ErpSupplierContractRespVO.java 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/purchase/vo/contract/ErpSupplierContractSaveReqVO.java 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/sale/ErpSaleOrderController.java 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/dal/dataobject/purchase/ErpPurchaseOrderItemDO.java 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/dal/dataobject/purchase/ErpPurchaseRequestItemDO.java 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/dal/dataobject/sale/ErpSaleOrderItemDO.java 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/service/purchase/ErpPurchaseOrderServiceImpl.java 14 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/service/purchase/ErpPurchaseRequestServiceImpl.java 14 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/service/purchase/ErpSupplierContractServiceImpl.java 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/service/sale/ErpSaleOrderServiceImpl.java 14 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/controller/admin/item/MdmItemController.java 22 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/enums/storage/StorageRecordTypeEnum.java 1 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/number/MoneyUtils.java
@@ -128,4 +128,42 @@
        return price.multiply(percent).divide(PERCENT_100, PRICE_SCALE, RoundingMode.HALF_UP);
    }
    /**
     * 计算含税总价里的税额(单价为含税单价时使用)
     *
     * 税额 = 含税总价 × 税率 ÷(100 + 税率)
     * 例如:含税总价 113.00,税率 13% → 税额 13.00
     *
     * @param totalPriceWithTax 含税总价
     * @param taxPercent        税率,例如 13% 传入 13
     * @return 税额;含税总价为空时返回 null,税率为空或非正数时返回 0.00
     */
    public static BigDecimal priceCalculateTaxIncluded(BigDecimal totalPriceWithTax, BigDecimal taxPercent) {
        if (totalPriceWithTax == null) {
            return null;
        }
        if (taxPercent == null || taxPercent.compareTo(BigDecimal.ZERO) <= 0) {
            return BigDecimal.ZERO.setScale(PRICE_SCALE, RoundingMode.HALF_UP);
        }
        return totalPriceWithTax.multiply(taxPercent)
                .divide(PERCENT_100.add(taxPercent), PRICE_SCALE, RoundingMode.HALF_UP);
    }
    /**
     * 含税总价拆出不含税金额
     *
     * 不含税金额 = 含税总价 − 税额(税额见 {@link #priceCalculateTaxIncluded})
     *
     * @param totalPriceWithTax 含税总价
     * @param taxPercent        税率,例如 13% 传入 13
     * @return 不含税金额;含税总价为空时返回 null
     */
    public static BigDecimal priceExcludeTax(BigDecimal totalPriceWithTax, BigDecimal taxPercent) {
        BigDecimal taxPrice = priceCalculateTaxIncluded(totalPriceWithTax, taxPercent);
        if (taxPrice == null) {
            return null;
        }
        return totalPriceWithTax.subtract(taxPrice).setScale(PRICE_SCALE, RoundingMode.HALF_UP);
    }
}
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/purchase/ErpSupplierContractController.java
@@ -4,6 +4,7 @@
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.collection.MapUtils;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.framework.excel.core.util.ExcelUtils;
import cn.iocoder.yudao.framework.apilog.core.annotation.ApiAccessLog;
@@ -14,6 +15,9 @@
import cn.iocoder.yudao.module.erp.dal.dataobject.purchase.ErpSupplierContractDO;
import cn.iocoder.yudao.module.erp.dal.mysql.purchase.ErpPurchaseOrderMapper;
import cn.iocoder.yudao.module.erp.service.purchase.ErpSupplierContractService;
import cn.iocoder.yudao.module.srm.api.supplier.SrmSupplierApi;
import cn.iocoder.yudao.module.srm.api.supplier.dto.SrmSupplierRespDTO;
import cn.iocoder.yudao.module.system.api.storage.StorageAttachmentApi;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
@@ -26,6 +30,7 @@
import java.io.IOException;
import java.math.BigDecimal;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
@@ -43,6 +48,10 @@
    private ErpSupplierContractService contractService;
    @Resource
    private ErpPurchaseOrderMapper purchaseOrderMapper;
    @Resource
    private StorageAttachmentApi storageAttachmentApi;
    @Resource
    private SrmSupplierApi srmSupplierApi;
    @PostMapping("/create")
    @Operation(summary = "创建供应商合同")
@@ -74,7 +83,12 @@
    @PreAuthorize("@ss.hasPermission('erp:supplier-contract:query')")
    public CommonResult<ErpSupplierContractRespVO> getContract(@RequestParam("id") Long id) {
        ErpSupplierContractDO contract = contractService.getContract(id);
        return success(contract == null ? null : buildResp(contract));
        if (contract == null) {
            return success(null);
        }
        ErpSupplierContractRespVO respVO = buildRespList(java.util.Collections.singletonList(contract)).get(0);
        respVO.setAttachmentList(storageAttachmentApi.listAttachments("erp_supplier_contract", id));
        return success(respVO);
    }
    @GetMapping("/page")
@@ -112,15 +126,22 @@
        if (CollUtil.isEmpty(list)) {
            return java.util.Collections.emptyList();
        }
        // 供应商名称:批量查询后回填,避免逐行查库
        Map<Long, SrmSupplierRespDTO> supplierMap = getSupplierMap(
                list.stream().map(ErpSupplierContractDO::getSupplierId)
                        .filter(java.util.Objects::nonNull)
                        .collect(Collectors.toSet()));
        List<ErpSupplierContractRespVO> result = new java.util.ArrayList<>(list.size());
        for (ErpSupplierContractDO contract : list) {
            result.add(buildResp(contract));
            result.add(buildResp(contract, supplierMap));
        }
        return result;
    }
    private ErpSupplierContractRespVO buildResp(ErpSupplierContractDO contract) {
    private ErpSupplierContractRespVO buildResp(ErpSupplierContractDO contract, Map<Long, SrmSupplierRespDTO> supplierMap) {
        ErpSupplierContractRespVO vo = BeanUtils.toBean(contract, ErpSupplierContractRespVO.class);
        // 供应商名称(列表、详情、导出都要反显,否则供应商列是空的)
        MapUtils.findAndThen(supplierMap, vo.getSupplierId(), supplier -> vo.setSupplierName(supplier.getName()));
        // 执行进度:关联的采购订单数量与金额合计
        List<ErpPurchaseOrderDO> orders = purchaseOrderMapper.selectListByContractId(contract.getId());
        vo.setOrderCount(orders.size());
@@ -131,4 +152,12 @@
        return vo;
    }
    private Map<Long, SrmSupplierRespDTO> getSupplierMap(Collection<Long> supplierIds) {
        if (CollUtil.isEmpty(supplierIds)) {
            return java.util.Collections.emptyMap();
        }
        return srmSupplierApi.getSupplierList(supplierIds).getCheckedData().stream()
                .collect(Collectors.toMap(SrmSupplierRespDTO::getId, v -> v, (a, b) -> a));
    }
}
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/purchase/vo/contract/ErpSupplierContractRespVO.java
@@ -1,11 +1,14 @@
package cn.iocoder.yudao.module.erp.controller.admin.purchase.vo.contract;
import cn.idev.excel.annotation.ExcelIgnore;
import cn.iocoder.yudao.module.system.api.storage.dto.StorageBlobRespDTO;
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 = "管理后台 - ERP 供应商合同 Response VO")
@Data
@@ -53,6 +56,10 @@
    @Schema(description = "备注")
    private String remark;
    @Schema(description = "附件列表")
    @ExcelIgnore
    private List<StorageBlobRespDTO> attachmentList;
    @Schema(description = "创建时间")
    private LocalDateTime createTime;
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/purchase/vo/contract/ErpSupplierContractSaveReqVO.java
@@ -7,6 +7,7 @@
import java.math.BigDecimal;
import java.time.LocalDate;
import java.util.List;
@Schema(description = "管理后台 - ERP 供应商合同新增/修改 Request VO")
@Data
@@ -49,4 +50,7 @@
    @Schema(description = "备注")
    private String remark;
    @Schema(description = "附件(blobId 列表)")
    private List<Long> blobIds;
}
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/sale/ErpSaleOrderController.java
@@ -319,15 +319,15 @@
    /**
     * 填充订单项的金额字段
     *
     * DO 中 totalPrice 为产品金额(不含税),RespVO 需要补全:
     * - totalProductPrice:产品金额
     * - totalPrice:含税金额(产品金额 + 税额)
     * DO 中 totalPrice 为含税总价(单价 × 数量),RespVO 需要补全:
     * - totalProductPrice:不含税金额(含税总价 - 税额)
     * - totalPrice:含税总价(保持 DO 原值)
     */
    private void fillItemPrice(ErpSaleOrderRespVO.Item item) {
        BigDecimal productAmount = item.getTotalPrice() != null ? item.getTotalPrice() : BigDecimal.ZERO;
        BigDecimal totalPriceWithTax = item.getTotalPrice() != null ? item.getTotalPrice() : BigDecimal.ZERO;
        BigDecimal tax = item.getTaxPrice() != null ? item.getTaxPrice() : BigDecimal.ZERO;
        item.setTotalProductPrice(productAmount);
        item.setTotalPrice(productAmount.add(tax));
        item.setTotalProductPrice(totalPriceWithTax.subtract(tax));
        item.setTotalPrice(totalPriceWithTax);
    }
}
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/dal/dataobject/purchase/ErpPurchaseOrderItemDO.java
@@ -54,7 +54,7 @@
    private Long productUnitId;
    /**
     * 产品单位单价,单位:元
     * 产品含税单价,单位:元
     */
    private BigDecimal productPrice;
    /**
@@ -62,7 +62,7 @@
     */
    private BigDecimal count;
    /**
     * 总价,单位:元
     * 含税总价,单位:元
     *
     * totalPrice = productPrice * count
     */
@@ -74,7 +74,7 @@
    /**
     * 税额,单位:元
     *
     * taxPrice = totalPrice * taxPercent
     * taxPrice = totalPrice * taxPercent /(100 + taxPercent),即含税总价倒算税额
     */
    private BigDecimal taxPrice;
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/dal/dataobject/purchase/ErpPurchaseRequestItemDO.java
@@ -54,7 +54,7 @@
    private BigDecimal count;
    /**
     * 总价
     * 含税总价 = 产品单价(含税)* 数量
     */
    private BigDecimal totalPrice;
@@ -64,7 +64,7 @@
    private BigDecimal taxPercent;
    /**
     * 税额
     * 税额 = 含税总价 * 税率 /(100 + 税率)
     */
    private BigDecimal taxPrice;
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/dal/dataobject/sale/ErpSaleOrderItemDO.java
@@ -56,9 +56,9 @@
     */
    private BigDecimal count;
    /**
     * 总价,单位:元
     * 含税总价,单位:元
     *
     * totalPrice = productPrice * count
     * totalPrice = productPrice(含税单价)* count
     */
    private BigDecimal totalPrice;
    /**
@@ -68,7 +68,7 @@
    /**
     * 税额,单位:元
     *
     * taxPrice = totalPrice * taxPercent
     * taxPrice = totalPrice * taxPercent /(100 + taxPercent),即含税总价倒算税额
     */
    private BigDecimal taxPrice;
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/service/purchase/ErpPurchaseOrderServiceImpl.java
@@ -357,9 +357,12 @@
    private void calculateTotalPrice(ErpPurchaseOrderDO purchaseOrder, List<ErpPurchaseOrderItemDO> purchaseOrderItems) {
        purchaseOrder.setTotalCount(getSumValue(purchaseOrderItems, ErpPurchaseOrderItemDO::getCount, BigDecimal::add));
        purchaseOrder.setTotalProductPrice(getSumValue(purchaseOrderItems, ErpPurchaseOrderItemDO::getTotalPrice, BigDecimal::add, BigDecimal.ZERO));
        purchaseOrder.setTotalTaxPrice(getSumValue(purchaseOrderItems, ErpPurchaseOrderItemDO::getTaxPrice, BigDecimal::add, BigDecimal.ZERO));
        purchaseOrder.setTotalPrice(purchaseOrder.getTotalProductPrice().add(purchaseOrder.getTotalTaxPrice()));
        // 明细的 totalPrice 是「数量 × 含税单价」,即含税总价;不含税金额 = 含税总价 − 税额
        BigDecimal totalPriceWithTax = getSumValue(purchaseOrderItems, ErpPurchaseOrderItemDO::getTotalPrice, BigDecimal::add, BigDecimal.ZERO);
        BigDecimal totalTaxPrice = getSumValue(purchaseOrderItems, ErpPurchaseOrderItemDO::getTaxPrice, BigDecimal::add, BigDecimal.ZERO);
        purchaseOrder.setTotalTaxPrice(totalTaxPrice);
        purchaseOrder.setTotalPrice(totalPriceWithTax);
        purchaseOrder.setTotalProductPrice(totalPriceWithTax.subtract(totalTaxPrice));
        // 计算优惠价格
        if (purchaseOrder.getDiscountPercent() == null) {
            purchaseOrder.setDiscountPercent(BigDecimal.ZERO);
@@ -513,13 +516,12 @@
    private List<ErpPurchaseOrderItemDO> validatePurchaseOrderItems(List<ErpPurchaseOrderSaveReqVO.Item> list) {
        // 转化为 ErpPurchaseOrderItemDO 列表(产品验证已移除)
        return convertList(list, o -> BeanUtils.toBean(o, ErpPurchaseOrderItemDO.class, item -> {
            // 产品单价为含税单价:行总价 = 数量 × 含税单价,税额再从含税总价里倒算
            item.setTotalPrice(MoneyUtils.priceMultiply(item.getProductPrice(), item.getCount()));
            if (item.getTotalPrice() == null) {
                return;
            }
            if (item.getTaxPercent() != null) {
                item.setTaxPrice(MoneyUtils.priceMultiplyPercent(item.getTotalPrice(), item.getTaxPercent()));
            }
            item.setTaxPrice(MoneyUtils.priceCalculateTaxIncluded(item.getTotalPrice(), item.getTaxPercent()));
        }));
    }
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/service/purchase/ErpPurchaseRequestServiceImpl.java
@@ -363,9 +363,12 @@
    private void calculateTotalPrice(ErpPurchaseRequestDO purchaseRequest, List<ErpPurchaseRequestItemDO> requestItems) {
        purchaseRequest.setTotalCount(getSumValue(requestItems, ErpPurchaseRequestItemDO::getCount, BigDecimal::add));
        purchaseRequest.setTotalProductPrice(getSumValue(requestItems, ErpPurchaseRequestItemDO::getTotalPrice, BigDecimal::add, BigDecimal.ZERO));
        purchaseRequest.setTotalTaxPrice(getSumValue(requestItems, ErpPurchaseRequestItemDO::getTaxPrice, BigDecimal::add, BigDecimal.ZERO));
        purchaseRequest.setTotalPrice(purchaseRequest.getTotalProductPrice().add(purchaseRequest.getTotalTaxPrice()));
        // 明细的 totalPrice 是「数量 × 含税单价」,即含税总价;不含税金额 = 含税总价 − 税额
        BigDecimal totalPriceWithTax = getSumValue(requestItems, ErpPurchaseRequestItemDO::getTotalPrice, BigDecimal::add, BigDecimal.ZERO);
        BigDecimal totalTaxPrice = getSumValue(requestItems, ErpPurchaseRequestItemDO::getTaxPrice, BigDecimal::add, BigDecimal.ZERO);
        purchaseRequest.setTotalTaxPrice(totalTaxPrice);
        purchaseRequest.setTotalPrice(totalPriceWithTax);
        purchaseRequest.setTotalProductPrice(totalPriceWithTax.subtract(totalTaxPrice));
        // 计算优惠价格
        if (purchaseRequest.getDiscountPercent() == null) {
            purchaseRequest.setDiscountPercent(BigDecimal.ZERO);
@@ -377,10 +380,9 @@
    private List<ErpPurchaseRequestItemDO> validatePurchaseRequestItems(List<ErpPurchaseRequestSaveReqVO.Item> list) {
        return convertList(list, o -> BeanUtils.toBean(o, ErpPurchaseRequestItemDO.class, item -> {
            if (item.getProductPrice() != null && item.getCount() != null) {
                // 产品单价为含税单价:行总价 = 数量 × 含税单价,税额再从含税总价里倒算
                item.setTotalPrice(MoneyUtils.priceMultiply(item.getProductPrice(), item.getCount()));
                if (item.getTaxPercent() != null) {
                    item.setTaxPrice(MoneyUtils.priceMultiplyPercent(item.getTotalPrice(), item.getTaxPercent()));
                }
                item.setTaxPrice(MoneyUtils.priceCalculateTaxIncluded(item.getTotalPrice(), item.getTaxPercent()));
            }
        }));
    }
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/service/purchase/ErpSupplierContractServiceImpl.java
@@ -8,8 +8,10 @@
import cn.iocoder.yudao.module.erp.dal.dataobject.purchase.ErpSupplierContractDO;
import cn.iocoder.yudao.module.erp.dal.mysql.purchase.ErpPurchaseOrderMapper;
import cn.iocoder.yudao.module.erp.dal.mysql.purchase.ErpSupplierContractMapper;
import cn.iocoder.yudao.module.system.api.storage.StorageAttachmentApi;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.validation.annotation.Validated;
import java.util.Collection;
@@ -32,12 +34,18 @@
    /** 合同状态默认值:履行中 */
    private static final Integer STATUS_ACTIVE = 1;
    /** 附件关联的业务记录类型(合同附件) */
    private static final String RECORD_TYPE = "erp_supplier_contract";
    @Resource
    private ErpSupplierContractMapper contractMapper;
    @Resource
    private ErpPurchaseOrderMapper purchaseOrderMapper;
    @Resource
    private StorageAttachmentApi storageAttachmentApi;
    @Override
    @Transactional(rollbackFor = Exception.class)
    public Long createContract(ErpSupplierContractSaveReqVO createReqVO) {
        validateContractCodeUnique(null, createReqVO.getCode());
        ErpSupplierContractDO contract = BeanUtils.toBean(createReqVO, ErpSupplierContractDO.class);
@@ -45,18 +53,24 @@
            contract.setStatus(STATUS_ACTIVE);
        }
        contractMapper.insert(contract);
        // 绑定附件(合同扫描件等),文件与合同通过附件中间表关联
        storageAttachmentApi.bindAttachments("file", RECORD_TYPE, contract.getId(), createReqVO.getBlobIds());
        return contract.getId();
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void updateContract(ErpSupplierContractSaveReqVO updateReqVO) {
        validateContractExists(updateReqVO.getId());
        validateContractCodeUnique(updateReqVO.getId(), updateReqVO.getCode());
        ErpSupplierContractDO updateObj = BeanUtils.toBean(updateReqVO, ErpSupplierContractDO.class);
        contractMapper.updateById(updateObj);
        // 增量更新附件:只删除被移除的、只绑定新增的
        storageAttachmentApi.updateAttachments("file", RECORD_TYPE, updateReqVO.getId(), updateReqVO.getBlobIds());
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void deleteContract(Long id) {
        validateContractExists(id);
        // 已关联采购订单的合同不允许删除,避免订单丢失合同追溯信息
@@ -65,6 +79,8 @@
            throw exception(SUPPLIER_CONTRACT_USED_BY_ORDER);
        }
        contractMapper.deleteById(id);
        // 同步清理合同附件关联
        storageAttachmentApi.deleteAttachmentsByRecord(RECORD_TYPE, id);
    }
    @Override
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/service/sale/ErpSaleOrderServiceImpl.java
@@ -373,9 +373,12 @@
    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()));
        // 明细的 totalPrice 是「数量 × 含税单价」,即含税总价;不含税金额 = 含税总价 − 税额
        BigDecimal totalPriceWithTax = getSumValue(saleOrderItems, ErpSaleOrderItemDO::getTotalPrice, BigDecimal::add, BigDecimal.ZERO);
        BigDecimal totalTaxPrice = getSumValue(saleOrderItems, ErpSaleOrderItemDO::getTaxPrice, BigDecimal::add, BigDecimal.ZERO);
        saleOrder.setTotalTaxPrice(totalTaxPrice);
        saleOrder.setTotalPrice(totalPriceWithTax);
        saleOrder.setTotalProductPrice(totalPriceWithTax.subtract(totalTaxPrice));
        // 计算优惠价格
        if (saleOrder.getDiscountPercent() == null) {
            saleOrder.setDiscountPercent(BigDecimal.ZERO);
@@ -644,13 +647,12 @@
    private List<ErpSaleOrderItemDO> validateSaleOrderItems(List<ErpSaleOrderSaveReqVO.Item> list) {
        // 转化为 ErpSaleOrderItemDO 列表(产品验证已移除)
        return convertList(list, o -> BeanUtils.toBean(o, ErpSaleOrderItemDO.class, item -> {
            // 产品单价为含税单价:行总价 = 数量 × 含税单价,税额再从含税总价里倒算
            item.setTotalPrice(MoneyUtils.priceMultiply(item.getProductPrice(), item.getCount()));
            if (item.getTotalPrice() == null) {
                return;
            }
            if (item.getTaxPercent() != null) {
                item.setTaxPrice(MoneyUtils.priceMultiplyPercent(item.getTotalPrice(), item.getTaxPercent()));
            }
            item.setTaxPrice(MoneyUtils.priceCalculateTaxIncluded(item.getTotalPrice(), item.getTaxPercent()));
        }));
    }
yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/controller/admin/item/MdmItemController.java
@@ -14,6 +14,7 @@
import cn.iocoder.yudao.module.mdm.controller.admin.item.vo.MdmItemSaveReqVO;
import cn.iocoder.yudao.module.mdm.controller.admin.item.vo.MdmItemStatusUpdateReqVO;
import cn.iocoder.yudao.module.mdm.controller.admin.item.vo.MdmItemSyncUpdateReqVO;
import cn.iocoder.yudao.module.mdm.dal.dataobject.brand.MdmBrandDO;
import cn.iocoder.yudao.module.mdm.dal.dataobject.category.MdmItemCategoryDO;
import cn.iocoder.yudao.module.mdm.dal.dataobject.item.MdmItemBatchConfigDO;
import cn.iocoder.yudao.module.mdm.dal.dataobject.item.MdmItemDO;
@@ -179,6 +180,9 @@
        // 关联查询计量单位名称
        Map<Long, MdmUnitMeasureDO> unitMap = unitMeasureService.getUnitMeasureMap(
                convertSet(itemList, MdmItemDO::getUnitMeasureId));
        // 关联查询品牌名称(采购下单需要确认品牌)
        Map<Long, MdmBrandDO> brandMap = brandService.getBrandMap(
                convertSet(itemList, MdmItemDO::getBrandId));
        // 转换
        List<MdmItemRespVO> respList = BeanUtils.toBean(itemList, MdmItemRespVO.class);
        // 批量查询同步状态
@@ -210,6 +214,11 @@
            MdmUnitMeasureDO unit = unitMap.get(resp.getUnitMeasureId());
            if (unit != null) {
                resp.setUnitMeasureName(unit.getName());
            }
            // 品牌名称
            MdmBrandDO brand = brandMap.get(resp.getBrandId());
            if (brand != null) {
                resp.setBrandName(brand.getName());
            }
            MdmUnitMeasureDO auxUnit1 = auxUnitMap.get(resp.getUnitMeasureId2());
            if (auxUnit1 != null) {
@@ -245,10 +254,16 @@
        // 填充单位名称等关联字段,确保下拉选择后可带出单位等信息
        Map<Long, MdmUnitMeasureDO> unitMap = unitMeasureService.getUnitMeasureMap(
                convertSet(respList, MdmItemRespVO::getUnitMeasureId));
        Map<Long, MdmBrandDO> brandMap = brandService.getBrandMap(
                convertSet(respList, MdmItemRespVO::getBrandId));
        respList.forEach(resp -> {
            MdmUnitMeasureDO unit = unitMap.get(resp.getUnitMeasureId());
            if (unit != null) {
                resp.setUnitMeasureName(unit.getName());
            }
            MdmBrandDO brand = brandMap.get(resp.getBrandId());
            if (brand != null) {
                resp.setBrandName(brand.getName());
            }
        });
        return success(respList);
@@ -265,6 +280,13 @@
                resp.setCategoryName(category.getName());
            }
        }
        // 品牌名称
        if (resp.getBrandId() != null) {
            MdmBrandDO brand = brandService.getBrand(resp.getBrandId());
            if (brand != null) {
                resp.setBrandName(brand.getName());
            }
        }
        // 主单位名称
        if (resp.getUnitMeasureId() != null) {
            MdmUnitMeasureDO unit = unitMeasureService.getUnitMeasure(resp.getUnitMeasureId());
yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/enums/storage/StorageRecordTypeEnum.java
@@ -37,6 +37,7 @@
    ERP_PURCHASE_REQUEST("erp_purchase_request", "ERP 采购申请"),
    ERP_PURCHASE_ORDER("erp_purchase_order", "ERP 采购订单"),
    ERP_PURCHASE_INVOICE("erp_purchase_invoice", "ERP 采购来票"),
    ERP_SUPPLIER_CONTRACT("erp_supplier_contract", "ERP 供应商合同"),
    // ===== ERP 销售 =====
    ERP_SALE_ORDER("erp_sale_order", "ERP 销售订单"),