package com.ruoyi.cost.service.impl;
|
|
import com.ruoyi.cost.bean.vo.CostEnergyVo;
|
import com.ruoyi.cost.bean.vo.CostLaborVo;
|
import com.ruoyi.cost.bean.vo.CostMaterialInputVo;
|
import com.ruoyi.cost.bean.vo.CostMaterialPriceVo;
|
import com.ruoyi.cost.bean.vo.CostMissingPriceVo;
|
import com.ruoyi.cost.bean.vo.CostOrderVo;
|
import com.ruoyi.cost.bean.vo.CostSalePriceVo;
|
import com.ruoyi.cost.bean.vo.CostStatVo;
|
import com.ruoyi.cost.bean.vo.CostSummaryVo;
|
import com.ruoyi.cost.mapper.CostAccountingMapper;
|
import com.ruoyi.cost.service.CostAccountingService;
|
import lombok.RequiredArgsConstructor;
|
import org.springframework.stereotype.Service;
|
|
import java.math.BigDecimal;
|
import java.math.RoundingMode;
|
import java.time.LocalDate;
|
import java.time.format.DateTimeFormatter;
|
import java.util.ArrayList;
|
import java.util.HashMap;
|
import java.util.LinkedHashMap;
|
import java.util.List;
|
import java.util.Map;
|
import java.util.stream.Collectors;
|
|
@Service
|
@RequiredArgsConstructor
|
public class CostAccountingServiceImpl implements CostAccountingService {
|
|
private static final BigDecimal HUNDRED = BigDecimal.valueOf(100);
|
private static final DateTimeFormatter MONTH_FORMAT = DateTimeFormatter.ofPattern("yyyy-MM");
|
|
private final CostAccountingMapper costAccountingMapper;
|
|
@Override
|
public List<CostStatVo> stat(LocalDate startDate, LocalDate endDate, String npsNo, String productName) {
|
List<CostOrderVo> orders = costAccountingMapper.listOrders(startDate, endDate, npsNo, productName);
|
if (orders.isEmpty()) {
|
return new ArrayList<>();
|
}
|
Map<Long, List<CostMaterialInputVo>> inputsByOrder = costAccountingMapper
|
.listMaterialInputs(startDate, endDate, npsNo, productName).stream()
|
.filter(i -> i.getProductionOrderId() != null)
|
.collect(Collectors.groupingBy(CostMaterialInputVo::getProductionOrderId));
|
Map<Long, CostMaterialPriceVo> priceByModel = priceByModel();
|
Map<Long, CostLaborVo> laborByOrder = costAccountingMapper
|
.listLaborCosts(startDate, endDate, npsNo, productName).stream()
|
.filter(l -> l.getProductionOrderId() != null)
|
.collect(Collectors.toMap(CostLaborVo::getProductionOrderId, l -> l, (a, b) -> a));
|
Map<Long, BigDecimal> salePriceByOrder = new LinkedHashMap<>();
|
for (CostSalePriceVo sale : costAccountingMapper.listSalePrices(startDate, endDate, npsNo, productName)) {
|
if (sale.getProductionOrderId() != null) {
|
//SQL 已按 slp.id 倒序,保留最新一张销售单的单价
|
salePriceByOrder.putIfAbsent(sale.getProductionOrderId(), sale.getSalePrice());
|
}
|
}
|
|
CostEnergyVo energy = nvlEnergy(costAccountingMapper.sumEnergyCost(startDate, endDate));
|
BigDecimal energyTotal = nvl(energy.getEnergyCost());
|
BigDecimal totalOutput = orders.stream().map(this::outputQty).reduce(BigDecimal.ZERO, BigDecimal::add);
|
|
List<CostStatVo> result = new ArrayList<>();
|
for (CostOrderVo order : orders) {
|
BigDecimal output = outputQty(order);
|
|
//材料成本:Σ(投入量×单价),同时统计已计价消耗量与缺价物料数
|
BigDecimal materialCost = BigDecimal.ZERO;
|
BigDecimal pricedQty = BigDecimal.ZERO;
|
BigDecimal inputQty = BigDecimal.ZERO;
|
int missingKinds = 0;
|
for (CostMaterialInputVo input : inputsByOrder.getOrDefault(order.getProductionOrderId(), new ArrayList<>())) {
|
BigDecimal qty = nvl(input.getInputQty());
|
inputQty = inputQty.add(qty);
|
CostMaterialPriceVo price = input.getProductModelId() == null
|
? null : priceByModel.get(input.getProductModelId());
|
if (price == null || nvl(price.getPrice()).compareTo(BigDecimal.ZERO) <= 0) {
|
if (qty.compareTo(BigDecimal.ZERO) != 0) {
|
missingKinds++;
|
}
|
continue;
|
}
|
pricedQty = pricedQty.add(qty);
|
materialCost = materialCost.add(qty.multiply(nvl(price.getPrice())));
|
}
|
materialCost = scale(materialCost, 2);
|
|
//人工成本
|
CostLaborVo labor = laborByOrder.get(order.getProductionOrderId());
|
BigDecimal laborCost = labor == null ? BigDecimal.ZERO : scale(nvl(labor.getLaborCost()), 2);
|
|
//能耗成本:区间电费按产量占比分摊
|
BigDecimal energyCost = energyTotal.compareTo(BigDecimal.ZERO) > 0 && totalOutput.compareTo(BigDecimal.ZERO) > 0
|
? scale(energyTotal.multiply(output).divide(totalOutput, 6, RoundingMode.HALF_UP), 2)
|
: BigDecimal.ZERO;
|
|
BigDecimal totalCost = materialCost.add(laborCost).add(energyCost);
|
BigDecimal unitCost = output.compareTo(BigDecimal.ZERO) > 0
|
? totalCost.divide(output, 4, RoundingMode.HALF_UP) : null;
|
BigDecimal salePrice = salePriceByOrder.get(order.getProductionOrderId());
|
|
CostStatVo vo = new CostStatVo();
|
vo.setProductionOrderId(order.getProductionOrderId());
|
vo.setNpsNo(order.getNpsNo());
|
vo.setFinishedProduct(order.getFinishedProduct());
|
vo.setUnit(order.getUnit());
|
vo.setOrderDate(order.getCreateTime() == null ? null : order.getCreateTime().toLocalDate());
|
vo.setOutputQty(output);
|
vo.setMaterialCost(materialCost);
|
vo.setLaborCost(laborCost);
|
vo.setEnergyCost(energyCost);
|
vo.setTotalCost(scale(totalCost, 2));
|
vo.setUnitCost(unitCost);
|
vo.setSalePrice(salePrice);
|
if (salePrice != null && unitCost != null) {
|
BigDecimal profit = salePrice.subtract(unitCost).multiply(output);
|
vo.setGrossProfit(scale(profit, 2));
|
BigDecimal revenue = salePrice.multiply(output);
|
if (revenue.compareTo(BigDecimal.ZERO) != 0) {
|
vo.setGrossProfitRate(profit.multiply(HUNDRED).divide(revenue, 2, RoundingMode.HALF_UP));
|
}
|
}
|
if (inputQty.compareTo(BigDecimal.ZERO) > 0) {
|
vo.setPriceCoverageRate(pricedQty.multiply(HUNDRED).divide(inputQty, 2, RoundingMode.HALF_UP));
|
}
|
vo.setMissingPriceKinds(missingKinds);
|
vo.setNoRateRows(labor == null ? 0 : labor.getNoRateRows());
|
result.add(vo);
|
}
|
return result;
|
}
|
|
@Override
|
public List<CostSummaryVo> summary(LocalDate startDate, LocalDate endDate,
|
String npsNo, String productName, String groupBy) {
|
List<CostStatVo> stats = stat(startDate, endDate, npsNo, productName);
|
boolean byProduct = "product".equals(groupBy);
|
Map<String, CostSummaryVo> grouped = new LinkedHashMap<>();
|
for (CostStatVo stat : stats) {
|
String key = byProduct ? nvlText(stat.getFinishedProduct())
|
: (stat.getOrderDate() == null ? "未填日期" : stat.getOrderDate().format(MONTH_FORMAT));
|
CostSummaryVo vo = grouped.computeIfAbsent(key, k -> {
|
CostSummaryVo created = new CostSummaryVo();
|
created.setGroupKey(k);
|
created.setOrderCount(0);
|
created.setOutputQty(BigDecimal.ZERO);
|
created.setMaterialCost(BigDecimal.ZERO);
|
created.setLaborCost(BigDecimal.ZERO);
|
created.setEnergyCost(BigDecimal.ZERO);
|
created.setTotalCost(BigDecimal.ZERO);
|
created.setGrossProfit(BigDecimal.ZERO);
|
return created;
|
});
|
vo.setOrderCount(vo.getOrderCount() + 1);
|
vo.setOutputQty(vo.getOutputQty().add(nvl(stat.getOutputQty())));
|
vo.setMaterialCost(vo.getMaterialCost().add(nvl(stat.getMaterialCost())));
|
vo.setLaborCost(vo.getLaborCost().add(nvl(stat.getLaborCost())));
|
vo.setEnergyCost(vo.getEnergyCost().add(nvl(stat.getEnergyCost())));
|
vo.setTotalCost(vo.getTotalCost().add(nvl(stat.getTotalCost())));
|
vo.setGrossProfit(vo.getGrossProfit().add(nvl(stat.getGrossProfit())));
|
}
|
List<CostSummaryVo> result = new ArrayList<>(grouped.values());
|
for (CostSummaryVo vo : result) {
|
BigDecimal total = vo.getTotalCost();
|
vo.setMaterialCost(scale(vo.getMaterialCost(), 2));
|
vo.setLaborCost(scale(vo.getLaborCost(), 2));
|
vo.setEnergyCost(scale(vo.getEnergyCost(), 2));
|
vo.setTotalCost(scale(total, 2));
|
vo.setGrossProfit(scale(vo.getGrossProfit(), 2));
|
if (vo.getOutputQty().compareTo(BigDecimal.ZERO) > 0) {
|
vo.setUnitCost(total.divide(vo.getOutputQty(), 4, RoundingMode.HALF_UP));
|
}
|
if (total.compareTo(BigDecimal.ZERO) > 0) {
|
vo.setMaterialRate(rate(vo.getMaterialCost(), total));
|
vo.setLaborRate(rate(vo.getLaborCost(), total));
|
vo.setEnergyRate(rate(vo.getEnergyCost(), total));
|
}
|
}
|
return result;
|
}
|
|
@Override
|
public List<CostMissingPriceVo> missingPrice(LocalDate startDate, LocalDate endDate,
|
String npsNo, String productName) {
|
Map<Long, CostMaterialPriceVo> priceByModel = priceByModel();
|
return costAccountingMapper.listConsumedMaterials(startDate, endDate, npsNo, productName).stream()
|
.filter(m -> m.getProductModelId() != null && !priceByModel.containsKey(m.getProductModelId()))
|
.collect(Collectors.toList());
|
}
|
|
/** 单价来源按优先级取第一条(SQL 已按 productModelId、priority 排序) */
|
private Map<Long, CostMaterialPriceVo> priceByModel() {
|
Map<Long, CostMaterialPriceVo> map = new HashMap<>();
|
for (CostMaterialPriceVo price : costAccountingMapper.listMaterialPrices()) {
|
if (price.getProductModelId() != null) {
|
map.putIfAbsent(price.getProductModelId(), price);
|
}
|
}
|
return map;
|
}
|
|
/** 产量口径:完工数量优先,未完工回退计划数量 */
|
private BigDecimal outputQty(CostOrderVo order) {
|
return nvl(order.getCompleteQty()).compareTo(BigDecimal.ZERO) > 0
|
? order.getCompleteQty() : nvl(order.getPlanQty());
|
}
|
|
private BigDecimal rate(BigDecimal part, BigDecimal total) {
|
return part.multiply(HUNDRED).divide(total, 2, RoundingMode.HALF_UP);
|
}
|
|
private CostEnergyVo nvlEnergy(CostEnergyVo energy) {
|
if (energy == null) {
|
energy = new CostEnergyVo();
|
}
|
if (energy.getKwh() == null) {
|
energy.setKwh(BigDecimal.ZERO);
|
}
|
if (energy.getEnergyCost() == null) {
|
energy.setEnergyCost(BigDecimal.ZERO);
|
}
|
return energy;
|
}
|
|
private BigDecimal nvl(BigDecimal value) {
|
return value == null ? BigDecimal.ZERO : value;
|
}
|
|
private String nvlText(String value) {
|
return value == null ? "" : value;
|
}
|
|
private BigDecimal scale(BigDecimal value, int scale) {
|
return nvl(value).setScale(scale, RoundingMode.HALF_UP);
|
}
|
}
|