package com.ruoyi.production.service.impl;
|
|
import com.ruoyi.common.exception.ServiceException;
|
import com.ruoyi.common.utils.SecurityUtils;
|
import com.ruoyi.common.utils.StringUtils;
|
import com.ruoyi.common.utils.poi.ExcelUtil;
|
import com.ruoyi.energy.pojo.Energy;
|
import com.ruoyi.energy.pojo.EnergyConsumptionDetail;
|
import com.ruoyi.energy.service.EnergyConsumptionDetailService;
|
import com.ruoyi.energy.service.EnergyService;
|
import com.ruoyi.production.dto.ProductionSettlementDetailsDto;
|
import com.ruoyi.production.dto.ProductionSettlementDto;
|
import com.ruoyi.production.dto.ProductionSettlementTotalDto;
|
import com.ruoyi.production.dto.SettlementImportDto;
|
import com.ruoyi.production.enums.ProductionSettlementEnum;
|
import com.ruoyi.production.pojo.*;
|
import com.ruoyi.production.mapper.ProductionSettlementBatchesMapper;
|
import com.ruoyi.production.service.*;
|
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
import com.ruoyi.project.system.domain.SysDictData;
|
import com.ruoyi.project.system.mapper.SysDictDataMapper;
|
import com.ruoyi.production.utils.UnitUtils;
|
import lombok.extern.slf4j.Slf4j;
|
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.stereotype.Service;
|
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.web.multipart.MultipartFile;
|
|
import java.math.BigDecimal;
|
import java.math.RoundingMode;
|
import java.time.LocalDate;
|
import java.time.LocalDateTime;
|
import java.time.YearMonth;
|
import java.time.format.DateTimeFormatter;
|
import java.util.*;
|
import java.util.stream.Collectors;
|
|
/**
|
* <p>
|
* 生产成本核算批次主表 服务实现类
|
* </p>
|
*
|
* @author deslrey
|
* @since 2026-03-30
|
*/
|
@Service
|
@Slf4j
|
public class ProductionSettlementBatchesServiceImpl extends ServiceImpl<ProductionSettlementBatchesMapper, ProductionSettlementBatches> implements IProductionSettlementBatchesService {
|
|
@Autowired
|
private IProductionSettlementDetailsService productionSettlementDetailsService;
|
|
@Autowired
|
private ProductMaterialService productMaterialService;
|
|
@Autowired
|
private ProductMaterialSkuService productMaterialSkuService;
|
|
@Autowired
|
private ProductionProductMainService productionProductMainService;
|
|
@Autowired
|
private ProductionProductInputService productionProductInputService;
|
|
@Autowired
|
private IProductionOrderStructureService productionOrderStructureService;
|
|
@Autowired
|
private ProductOrderService productOrderService;
|
|
@Autowired
|
private IProductionOrderRouteService productionOrderRouteService;
|
|
@Autowired
|
private EnergyService energyService;
|
|
@Autowired
|
private EnergyConsumptionDetailService energyConsumptionDetailService;
|
|
@Autowired
|
private SysDictDataMapper sysDictDataMapper;
|
|
@Override
|
@Transactional(rollbackFor = Exception.class)
|
public void importProductionSettlement(MultipartFile file, ProductionSettlementDto dto) {
|
if (file == null || file.isEmpty()) {
|
throw new ServiceException("导入失败,文件不能为空");
|
}
|
// 核算月份,如果不填则默认为当前月
|
String finalPeriodTime = parsePeriodTime(dto != null ? dto.getPeriodTime() : null);
|
|
List<SettlementImportDto> list;
|
try {
|
list = new ExcelUtil<>(SettlementImportDto.class).importExcel(file.getInputStream());
|
} catch (Exception e) {
|
log.error("解析Excel失败", e);
|
throw new ServiceException("解析Excel文件失败:" + e.getMessage());
|
}
|
|
if (list == null || list.isEmpty()) {
|
throw new ServiceException("导入数据不能为空!");
|
}
|
|
// 如果该月批次已存在,先清除旧批次及其明细,再重新导入
|
ProductionSettlementBatches existBatch = this.lambdaQuery()
|
.eq(ProductionSettlementBatches::getPeriodTime, finalPeriodTime)
|
.one();
|
if (existBatch != null) {
|
productionSettlementDetailsService.lambdaUpdate()
|
.eq(ProductionSettlementDetails::getBatchId, existBatch.getId())
|
.remove();
|
this.removeById(existBatch.getId());
|
log.info("已清除 {} 月份的旧核算数据,批次ID:{}", finalPeriodTime, existBatch.getId());
|
}
|
|
ProductionSettlementBatches batch = new ProductionSettlementBatches();
|
batch.setPeriodTime(finalPeriodTime);
|
batch.setCreateUser(SecurityUtils.getUsername());
|
this.save(batch);
|
|
String lastProductType = "";
|
String lastCategory = "";
|
|
List<ProductionSettlementDetails> detailList = new ArrayList<>();
|
int rowIndex = 2;
|
for (SettlementImportDto importDto : list) {
|
ProductionSettlementDetails detail = new ProductionSettlementDetails();
|
detail.setBatchId(batch.getId());
|
|
if (StringUtils.isNotBlank(importDto.getProductType())) {
|
lastProductType = importDto.getProductType();
|
}
|
if (StringUtils.isNotBlank(importDto.getCategory())) {
|
lastCategory = importDto.getCategory();
|
}
|
|
detail.setProductType(lastProductType);
|
detail.setCategory(lastCategory);
|
detail.setSubjectName(importDto.getSubjectName());
|
detail.setBudgetQty(importDto.getBudgetQty() != null ? importDto.getBudgetQty() : BigDecimal.ZERO);
|
detail.setBudgetPrice(importDto.getBudgetPrice() != null ? importDto.getBudgetPrice() : BigDecimal.ZERO);
|
detail.setBudgetTotal(importDto.getBudgetTotal() != null ? importDto.getBudgetTotal() : BigDecimal.ZERO);
|
|
if (ProductionSettlementEnum.isMaterialCost(detail.getCategory())) {
|
ProductMaterial product = productMaterialService.lambdaQuery()
|
.eq(ProductMaterial::getProductName, importDto.getSubjectName())
|
.one();
|
if (product != null) {
|
detail.setProductId(product.getId());
|
} else {
|
log.warn("第 {} 行,未找到科目产品:{}", rowIndex, importDto.getSubjectName());
|
}
|
}
|
detailList.add(detail);
|
rowIndex++;
|
}
|
|
productionSettlementDetailsService.saveBatch(detailList);
|
}
|
|
@Override
|
public Map<String, List<ProductionSettlementDetailsDto>> getSettlement(ProductionSettlementDto dto) {
|
String finalPeriodTime = parsePeriodTime(dto != null ? dto.getPeriodTime() : null);
|
|
YearMonth yearMonth = YearMonth.parse(finalPeriodTime, DateTimeFormatter.ofPattern("yyyy-MM"));
|
LocalDate date = yearMonth.atDay(1);
|
ProductionSettlementBatches batch = this.lambdaQuery()
|
.eq(ProductionSettlementBatches::getPeriodTime, finalPeriodTime)
|
.one();
|
|
if (batch == null) {
|
return new HashMap<>();
|
}
|
|
String filterProductType = dto != null ? dto.getProductType() : null;
|
String filterSubjectName = dto != null ? dto.getSubjectName() : null;
|
String filterCostType = dto != null ? dto.getCostType() : null;
|
|
List<ProductionSettlementDetails> details = productionSettlementDetailsService.lambdaQuery()
|
.eq(ProductionSettlementDetails::getBatchId, batch.getId())
|
.eq(StringUtils.isNotBlank(filterProductType), ProductionSettlementDetails::getProductType, filterProductType)
|
.eq(StringUtils.isNotBlank(filterSubjectName), ProductionSettlementDetails::getSubjectName, filterSubjectName)
|
.eq(StringUtils.isNotBlank(filterCostType), ProductionSettlementDetails::getCategory, filterCostType)
|
.list();
|
|
List<ProductionSettlementDetailsDto> dtoList = details.stream().map(d -> {
|
ProductionSettlementDetailsDto detailDto = new ProductionSettlementDetailsDto();
|
detailDto.setBatchId(d.getBatchId());
|
detailDto.setProductId(d.getProductId());
|
detailDto.setProductType(d.getProductType());
|
detailDto.setCategory(d.getCategory());
|
detailDto.setSubjectName(d.getSubjectName());
|
detailDto.setBudgetQty(d.getBudgetQty());
|
detailDto.setBudgetPrice(d.getBudgetPrice());
|
detailDto.setBudgetTotal(d.getBudgetTotal());
|
return detailDto;
|
}).collect(Collectors.toList());
|
|
LocalDateTime start = date.atStartOfDay();
|
LocalDateTime end = start.plusMonths(1);
|
|
// 获取产品分类
|
List<SysDictData> sysDictDataList = sysDictDataMapper.selectDictDataByType("product_type");
|
Map<Long, String> dictCodeMap = sysDictDataList.stream()
|
.collect(Collectors.toMap(SysDictData::getDictCode, SysDictData::getDictLabel, (k1, k2) -> k1));
|
|
// 获取物料消耗数据
|
List<ProductionProductMain> mains = productionProductMainService.lambdaQuery()
|
.ge(ProductionProductMain::getCreateTime, start)
|
.lt(ProductionProductMain::getCreateTime, end)
|
.list();
|
|
List<Long> mainIds = mains.stream().map(ProductionProductMain::getId).collect(Collectors.toList());
|
List<Long> orderIds = mains.stream().map(ProductionProductMain::getProductOrderId).distinct().collect(Collectors.toList());
|
|
Map<Long, List<ProductionProductInput>> inputsByMain = new HashMap<>();
|
Map<Long, ProductOrder> orderMap = new HashMap<>();
|
Map<Long, ProductionOrderRoute> orderRouteMap = new HashMap<>();
|
Map<Long, List<ProductionOrderStructure>> structuresByOrder = new HashMap<>();
|
|
if (!mainIds.isEmpty()) {
|
inputsByMain = productionProductInputService.lambdaQuery()
|
.in(ProductionProductInput::getProductMainId, mainIds)
|
.list()
|
.stream()
|
.collect(Collectors.groupingBy(ProductionProductInput::getProductMainId));
|
}
|
|
if (!orderIds.isEmpty()) {
|
orderMap = productOrderService.lambdaQuery()
|
.in(ProductOrder::getId, orderIds)
|
.list()
|
.stream()
|
.collect(Collectors.toMap(ProductOrder::getId, o -> o));
|
|
orderRouteMap = productionOrderRouteService.lambdaQuery()
|
.in(ProductionOrderRoute::getOrderId, orderIds)
|
.list()
|
.stream()
|
.collect(Collectors.toMap(ProductionOrderRoute::getOrderId, r -> r, (k1, k2) -> k1));
|
|
structuresByOrder = productionOrderStructureService.lambdaQuery()
|
.in(ProductionOrderStructure::getOrderId, orderIds)
|
.list()
|
.stream()
|
.collect(Collectors.groupingBy(ProductionOrderStructure::getOrderId));
|
}
|
|
// 获取能耗数据
|
LocalDate startDate = date;
|
LocalDate endDate = date.plusMonths(1);
|
List<EnergyConsumptionDetail> energyDetails = energyConsumptionDetailService.lambdaQuery()
|
.ge(EnergyConsumptionDetail::getMeterReadingDate, startDate)
|
.lt(EnergyConsumptionDetail::getMeterReadingDate, endDate)
|
.list();
|
|
List<Energy> energies = energyService.list();
|
Map<String, Energy> energyMap = new HashMap<>();
|
for (Energy energy : energies) {
|
if (energy.getEnergyName() != null) {
|
energyMap.put(energy.getEnergyName(), energy);
|
}
|
}
|
|
List<ProductMaterialSku> allSkus = productMaterialSkuService.list();
|
Map<Long, Long> skuToMaterialMap = allSkus.stream()
|
.filter(sku -> sku.getProductId() != null)
|
.collect(Collectors.toMap(ProductMaterialSku::getId, ProductMaterialSku::getProductId, (k1, k2) -> k1));
|
|
// 计算实际值
|
for (ProductionSettlementDetailsDto detail : dtoList) {
|
BigDecimal actualQty = BigDecimal.ZERO;
|
BigDecimal actualTotalCost = BigDecimal.ZERO;
|
|
if (ProductionSettlementEnum.isMaterialCost(detail.getCategory())) {
|
if (detail.getProductId() != null) {
|
for (ProductionProductMain main : mains) {
|
ProductOrder order = orderMap.get(main.getProductOrderId());
|
if (order == null) continue;
|
|
String targetType = detail.getProductType() != null ? detail.getProductType().trim() : "";
|
boolean typeMatch = "综合".equals(targetType);
|
|
if (!typeMatch) {
|
ProductionOrderRoute route = orderRouteMap.get(main.getProductOrderId());
|
if (route != null && route.getDictCode() != null) {
|
String dictLabel = dictCodeMap.get(route.getDictCode());
|
if (dictLabel != null && dictLabel.trim().equals(targetType)) {
|
typeMatch = true;
|
}
|
}
|
}
|
|
if (!typeMatch && order.getStrength() != null && order.getStrength().trim().equals(targetType)) {
|
typeMatch = true;
|
}
|
|
if (typeMatch) {
|
List<ProductionProductInput> mainInputs = inputsByMain.get(main.getId());
|
if (mainInputs != null) {
|
for (ProductionProductInput input : mainInputs) {
|
Long inputMaterialId = skuToMaterialMap.get(input.getProductId());
|
if (Objects.equals(inputMaterialId, detail.getProductId())) {
|
BigDecimal rawQty = input.getQuantity() != null ? input.getQuantity() : BigDecimal.ZERO;
|
|
List<ProductionOrderStructure> orderStructures = structuresByOrder.get(order.getId());
|
BigDecimal unitPrice = BigDecimal.ZERO;
|
String unit = "";
|
if (orderStructures != null) {
|
for (ProductionOrderStructure structure : orderStructures) {
|
Long structureMaterialId = skuToMaterialMap.get(structure.getProductModelId());
|
if (Objects.equals(structureMaterialId, detail.getProductId())) {
|
unitPrice = structure.getUnitPrice() != null ? structure.getUnitPrice() : BigDecimal.ZERO;
|
unit = structure.getUnit();
|
break;
|
}
|
}
|
}
|
actualTotalCost = actualTotalCost.add(rawQty.multiply(unitPrice));
|
BigDecimal tonnage = UnitUtils.convertValueToTon(rawQty, unit);
|
actualQty = actualQty.add(tonnage);
|
}
|
}
|
}
|
}
|
}
|
}
|
|
detail.setActualQty(actualQty);
|
detail.setActualTotal(actualTotalCost);
|
if (actualQty.compareTo(BigDecimal.ZERO) > 0) {
|
detail.setActualPrice(actualTotalCost.divide(actualQty, 15, RoundingMode.HALF_UP));
|
} else {
|
detail.setActualPrice(BigDecimal.ZERO);
|
}
|
|
} else if ("能耗成本".equals(detail.getCategory())) {
|
Energy energy = energyMap.get(detail.getSubjectName());
|
if (energy != null) {
|
BigDecimal unitPrice = energy.getUnitPrice() != null ? energy.getUnitPrice() : BigDecimal.ZERO;
|
for (EnergyConsumptionDetail eDetail : energyDetails) {
|
if (Objects.equals(eDetail.getEnergyId(), energy.getId())) {
|
actualQty = actualQty.add(eDetail.getDosage() != null ? eDetail.getDosage() : BigDecimal.ZERO);
|
}
|
}
|
detail.setActualQty(actualQty);
|
detail.setActualPrice(unitPrice);
|
detail.setActualTotal(actualQty.multiply(unitPrice));
|
} else {
|
detail.setActualQty(BigDecimal.ZERO);
|
detail.setActualPrice(BigDecimal.ZERO);
|
detail.setActualTotal(BigDecimal.ZERO);
|
}
|
} else {
|
detail.setActualQty(BigDecimal.ZERO);
|
detail.setActualPrice(BigDecimal.ZERO);
|
detail.setActualTotal(BigDecimal.ZERO);
|
}
|
|
// 差异值百分比计算
|
detail.setDiffQty(calculatePercentage(detail.getActualQty(), detail.getBudgetQty()));
|
detail.setDiffPrice(calculatePercentage(detail.getActualPrice(), detail.getBudgetPrice()));
|
detail.setDiffTotal(calculatePercentage(detail.getActualTotal(), detail.getBudgetTotal()));
|
}
|
|
return dtoList.stream().collect(Collectors.groupingBy(ProductionSettlementDetailsDto::getCategory));
|
}
|
|
@Override
|
public List<String> getProductTypes(ProductionSettlementDto dto) {
|
String finalPeriodTime = parsePeriodTime(dto != null ? dto.getPeriodTime() : null);
|
|
ProductionSettlementBatches batch = this.lambdaQuery()
|
.eq(ProductionSettlementBatches::getPeriodTime, finalPeriodTime)
|
.one();
|
if (batch == null) {
|
return new ArrayList<>();
|
}
|
|
return productionSettlementDetailsService.lambdaQuery()
|
.eq(ProductionSettlementDetails::getBatchId, batch.getId())
|
.list()
|
.stream()
|
.map(ProductionSettlementDetails::getProductType)
|
.filter(StringUtils::isNotBlank)
|
.distinct()
|
.collect(Collectors.toList());
|
}
|
|
|
@Override
|
public List<String> getSubjectNames(ProductionSettlementDto dto) {
|
String finalPeriodTime = parsePeriodTime(dto != null ? dto.getPeriodTime() : null);
|
|
ProductionSettlementBatches batch = this.lambdaQuery()
|
.eq(ProductionSettlementBatches::getPeriodTime, finalPeriodTime)
|
.one();
|
if (batch == null) {
|
return new ArrayList<>();
|
}
|
|
return productionSettlementDetailsService.lambdaQuery()
|
.eq(ProductionSettlementDetails::getBatchId, batch.getId())
|
.list()
|
.stream()
|
.map(ProductionSettlementDetails::getSubjectName)
|
.filter(StringUtils::isNotBlank)
|
.distinct()
|
.collect(Collectors.toList());
|
}
|
|
/**
|
* 核算月份,为空时默认取当前月,格式为 yyyy-MM
|
*/
|
private String parsePeriodTime(String periodTime) {
|
if (StringUtils.isBlank(periodTime)) {
|
return YearMonth.now().format(DateTimeFormatter.ofPattern("yyyy-MM"));
|
}
|
try {
|
return YearMonth.parse(periodTime, DateTimeFormatter.ofPattern("yyyy-MM"))
|
.format(DateTimeFormatter.ofPattern("yyyy-MM"));
|
} catch (Exception e) {
|
throw new ServiceException("日期格式错误,请使用 yyyy-MM 格式");
|
}
|
}
|
|
private String calculatePercentage(BigDecimal actual, BigDecimal budget) {
|
if (budget == null || budget.compareTo(BigDecimal.ZERO) == 0) {
|
return actual.compareTo(BigDecimal.ZERO) > 0 ? "100%" : "0%";
|
}
|
BigDecimal diff = actual.subtract(budget);
|
BigDecimal percentage = diff.divide(budget, 4, RoundingMode.HALF_UP).multiply(new BigDecimal("100"));
|
return percentage.stripTrailingZeros().toPlainString() + "%";
|
}
|
|
@Override
|
public ProductionSettlementTotalDto getTotalCosts(ProductionSettlementDto dto) {
|
// 复用 getSettlement 获取含实际值的明细列表
|
Map<String, List<ProductionSettlementDetailsDto>> settlementMap = getSettlement(dto);
|
|
List<ProductionSettlementDetailsDto> allDetails = settlementMap.values().stream()
|
.flatMap(List::stream)
|
.collect(Collectors.toList());
|
|
// 按 dto 中的其余条件在内存中过滤
|
if (dto != null) {
|
if (StringUtils.isNotBlank(dto.getProductType())) {
|
String targetType = dto.getProductType().trim();
|
allDetails = allDetails.stream()
|
.filter(d -> targetType.equals(d.getProductType() != null ? d.getProductType().trim() : ""))
|
.collect(Collectors.toList());
|
}
|
if (StringUtils.isNotBlank(dto.getSubjectName())) {
|
String targetSubject = dto.getSubjectName().trim();
|
allDetails = allDetails.stream()
|
.filter(d -> targetSubject.equals(d.getSubjectName() != null ? d.getSubjectName().trim() : ""))
|
.collect(Collectors.toList());
|
}
|
if (StringUtils.isNotBlank(dto.getCostType())) {
|
String targetCostType = dto.getCostType().trim();
|
allDetails = allDetails.stream()
|
.filter(d -> targetCostType.equals(d.getCategory() != null ? d.getCategory().trim() : ""))
|
.collect(Collectors.toList());
|
}
|
}
|
|
// 汇总标准成本与实际成本
|
BigDecimal budgetTotal = allDetails.stream()
|
.map(d -> d.getBudgetTotal() != null ? d.getBudgetTotal() : BigDecimal.ZERO)
|
.reduce(BigDecimal.ZERO, BigDecimal::add);
|
|
BigDecimal actualTotal = allDetails.stream()
|
.map(d -> d.getActualTotal() != null ? d.getActualTotal() : BigDecimal.ZERO)
|
.reduce(BigDecimal.ZERO, BigDecimal::add);
|
|
BigDecimal diffTotal = actualTotal.subtract(budgetTotal);
|
|
// 差异率:带正负号
|
String diffRate;
|
if (budgetTotal.compareTo(BigDecimal.ZERO) == 0) {
|
diffRate = actualTotal.compareTo(BigDecimal.ZERO) > 0 ? "+100%" : "0%";
|
} else {
|
BigDecimal rate = diffTotal.divide(budgetTotal, 4, RoundingMode.HALF_UP)
|
.multiply(new BigDecimal("100"));
|
String sign = rate.compareTo(BigDecimal.ZERO) >= 0 ? "+" : "";
|
diffRate = sign + rate.stripTrailingZeros().toPlainString() + "%";
|
}
|
|
ProductionSettlementTotalDto result = new ProductionSettlementTotalDto();
|
result.setBudgetTotal(budgetTotal);
|
result.setActualTotal(actualTotal);
|
result.setDiffTotal(diffTotal);
|
result.setDiffRate(diffRate);
|
return result;
|
}
|
}
|