6 天以前 1e123568c2f561013f5636e45dc0db30b17a3517
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
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);
    }
}