|
|
@@ -0,0 +1,1803 @@
|
|
|
+package com.dtm.supply.service;
|
|
|
+
|
|
|
+import com.dtm.storage.util.ExcelUtils;
|
|
|
+import com.dtm.supply.mapper.SupplyMonitorMapper;
|
|
|
+import org.apache.poi.ss.usermodel.DataFormatter;
|
|
|
+import org.apache.poi.ss.usermodel.Row;
|
|
|
+import org.apache.poi.ss.usermodel.Sheet;
|
|
|
+import org.apache.poi.ss.usermodel.Workbook;
|
|
|
+import org.apache.poi.ss.usermodel.WorkbookFactory;
|
|
|
+import org.springframework.beans.factory.annotation.Value;
|
|
|
+import org.springframework.stereotype.Service;
|
|
|
+
|
|
|
+import java.io.InputStream;
|
|
|
+import java.nio.file.Files;
|
|
|
+import java.nio.file.Path;
|
|
|
+import java.nio.file.Paths;
|
|
|
+import java.time.LocalDate;
|
|
|
+import java.time.ZoneId;
|
|
|
+import java.time.format.DateTimeFormatter;
|
|
|
+import java.time.format.DateTimeParseException;
|
|
|
+import java.util.ArrayList;
|
|
|
+import java.util.Collections;
|
|
|
+import java.util.Comparator;
|
|
|
+import java.util.Date;
|
|
|
+import java.util.HashMap;
|
|
|
+import java.util.HashSet;
|
|
|
+import java.util.LinkedHashMap;
|
|
|
+import java.util.List;
|
|
|
+import java.util.Locale;
|
|
|
+import java.util.Map;
|
|
|
+import java.util.Optional;
|
|
|
+import java.util.Set;
|
|
|
+import java.util.concurrent.atomic.AtomicReference;
|
|
|
+import java.util.stream.Collectors;
|
|
|
+
|
|
|
+@Service
|
|
|
+public class SupplyMonitorService {
|
|
|
+ private static final long CACHE_EXPIRE_MILLIS = 0L;
|
|
|
+ private static final DateTimeFormatter YMD = DateTimeFormatter.ofPattern("yyyy-MM-dd");
|
|
|
+
|
|
|
+ @Value("${supply.data.path:}")
|
|
|
+ private String supplyDataPath;
|
|
|
+
|
|
|
+ @Value("${supply.qa.path:}")
|
|
|
+ private String supplyQaDataPath;
|
|
|
+
|
|
|
+ private static final Map<String, Double> DEFAULT_SCORE_WEIGHTS;
|
|
|
+ private static final Map<String, Double> DEFAULT_SCORE_RULES;
|
|
|
+ private static volatile Map<String, Double> scoreWeights;
|
|
|
+ private static volatile Map<String, Double> scoreRules;
|
|
|
+
|
|
|
+ static {
|
|
|
+ Map<String, Double> defaults = new LinkedHashMap<>();
|
|
|
+ defaults.put("成本", 0.30D);
|
|
|
+ defaults.put("交付", 0.30D);
|
|
|
+ defaults.put("账期", 0.15D);
|
|
|
+ defaults.put("质量", 0.25D);
|
|
|
+ DEFAULT_SCORE_WEIGHTS = Collections.unmodifiableMap(defaults);
|
|
|
+ scoreWeights = new LinkedHashMap<>(defaults);
|
|
|
+
|
|
|
+ Map<String, Double> ruleDefaults = new LinkedHashMap<>();
|
|
|
+ ruleDefaults.put("costRank1Score", 100D);
|
|
|
+ ruleDefaults.put("costRank2Score", 90D);
|
|
|
+ ruleDefaults.put("costRank3Score", 80D);
|
|
|
+ ruleDefaults.put("costRank4Score", 60D);
|
|
|
+ ruleDefaults.put("deliveryOnTimeWeight", 0.70D);
|
|
|
+ ruleDefaults.put("deliveryFulfillmentWeight", 0.30D);
|
|
|
+ ruleDefaults.put("termExcellentDays", 90D);
|
|
|
+ ruleDefaults.put("termGoodDays", 60D);
|
|
|
+ ruleDefaults.put("termNormalDays", 45D);
|
|
|
+ ruleDefaults.put("termShortDays", 30D);
|
|
|
+ ruleDefaults.put("termExcellentScore", 100D);
|
|
|
+ ruleDefaults.put("termGoodScore", 90D);
|
|
|
+ ruleDefaults.put("termNormalScore", 80D);
|
|
|
+ ruleDefaults.put("termShortScore", 60D);
|
|
|
+ ruleDefaults.put("termDefaultScore", 40D);
|
|
|
+ ruleDefaults.put("qualityWarningRate", 0.95D);
|
|
|
+ ruleDefaults.put("qualityDefaultScore", 80D);
|
|
|
+ DEFAULT_SCORE_RULES = Collections.unmodifiableMap(ruleDefaults);
|
|
|
+ scoreRules = new LinkedHashMap<>(ruleDefaults);
|
|
|
+ }
|
|
|
+
|
|
|
+ private final SupplyMonitorMapper supplyMonitorMapper;
|
|
|
+ private final AtomicReference<CacheEntry> cache = new AtomicReference<>();
|
|
|
+
|
|
|
+ public SupplyMonitorService(SupplyMonitorMapper supplyMonitorMapper) {
|
|
|
+ this.supplyMonitorMapper = supplyMonitorMapper;
|
|
|
+ }
|
|
|
+
|
|
|
+ public List<Map<String, Object>> suppliers(String supplierName, String startDate, String endDate) {
|
|
|
+ List<Map<String, Object>> databaseRows = databaseSuppliers(supplierName, startDate, endDate);
|
|
|
+ if (databaseRows != null) {
|
|
|
+ return databaseRows;
|
|
|
+ }
|
|
|
+
|
|
|
+ SupplyData data = loadData();
|
|
|
+ LocalDate start = parseDate(startDate);
|
|
|
+ LocalDate end = parseDate(endDate);
|
|
|
+ String keyword = normalize(supplierName);
|
|
|
+ Map<String, SupplierStats> stats = buildStats(data, start, end);
|
|
|
+ return stats.values().stream()
|
|
|
+ .filter(item -> keyword.isEmpty() || normalize(item.supplierName).contains(keyword))
|
|
|
+ .map(this::toRow)
|
|
|
+ .sorted(Comparator.comparing((Map<String, Object> row) -> toDouble(row.get("receiptAmount"))).reversed())
|
|
|
+ .collect(Collectors.toList());
|
|
|
+ }
|
|
|
+
|
|
|
+ private List<Map<String, Object>> databaseSuppliers(String supplierName, String startDate, String endDate) {
|
|
|
+ try {
|
|
|
+ long orderCount = safeLong(supplyMonitorMapper.countPurchaseOrders());
|
|
|
+ long receiptCount = safeLong(supplyMonitorMapper.countPurchaseReceipts());
|
|
|
+ long termCount = safeLong(supplyMonitorMapper.countSupplierTerms());
|
|
|
+ long matchCount = safeLong(supplyMonitorMapper.countOrderReceiptMatches());
|
|
|
+ if (orderCount + receiptCount + termCount + matchCount == 0) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ Map<String, SupplierStats> statsMap = new HashMap<>();
|
|
|
+ for (Map<String, Object> row : supplyMonitorMapper.selectSupplierMonitorSummary(supplierName, startDate, endDate)) {
|
|
|
+ String name = stringValue(getMapValue(row, "supplierName"));
|
|
|
+ if (name.isEmpty()) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ SupplierStats stats = new SupplierStats(stringValue(getMapValue(row, "supplierCode")), name);
|
|
|
+ stats.orderLines = toInteger(getMapValue(row, "orderLines"));
|
|
|
+ int orderCountValue = toInteger(getMapValue(row, "orderCount"));
|
|
|
+ for (int i = 0; i < orderCountValue; i++) {
|
|
|
+ stats.orderIds.add(name + "#" + i);
|
|
|
+ }
|
|
|
+ stats.orderQty = toDouble(getMapValue(row, "orderQty"));
|
|
|
+ stats.completedQty = toDouble(getMapValue(row, "completedQty"));
|
|
|
+ stats.uncompletedQty = toDouble(getMapValue(row, "uncompletedQty"));
|
|
|
+ stats.orderAmount = toDouble(getMapValue(row, "orderAmount"));
|
|
|
+ stats.receiptQty = toDouble(getMapValue(row, "receiptQty"));
|
|
|
+ stats.receiptAmount = toDouble(getMapValue(row, "receiptAmount"));
|
|
|
+ stats.receiptLines = toInteger(getMapValue(row, "receiptLines"));
|
|
|
+ stats.matchedLines = toInteger(getMapValue(row, "matchedLines"));
|
|
|
+ stats.onTimeLines = toInteger(getMapValue(row, "onTimeLines"));
|
|
|
+ stats.matchedPlanQty = toDouble(getMapValue(row, "matchedPlanQty"));
|
|
|
+ stats.matchedReceiptQty = toDouble(getMapValue(row, "matchedReceiptQty"));
|
|
|
+ stats.termDays = toInteger(getMapValue(row, "termDays"));
|
|
|
+ statsMap.put(name, stats);
|
|
|
+ }
|
|
|
+ applySupplierCostScoresFromRows(supplyMonitorMapper.selectSupplierSkuCosts(supplierName, startDate, endDate), statsMap);
|
|
|
+ applyQualityStats(statsMap, parseDate(startDate), parseDate(endDate));
|
|
|
+ return statsMap.values().stream()
|
|
|
+ .map(this::toRow)
|
|
|
+ .sorted(Comparator.comparing((Map<String, Object> row) -> toDouble(row.get("receiptAmount"))).reversed())
|
|
|
+ .collect(Collectors.toList());
|
|
|
+ } catch (Exception ignored) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ public List<Map<String, Object>> compare(List<String> supplierNames, String startDate, String endDate) {
|
|
|
+ if (supplierNames == null || supplierNames.isEmpty()) {
|
|
|
+ return Collections.emptyList();
|
|
|
+ }
|
|
|
+ Set<String> selected = supplierNames.stream()
|
|
|
+ .map(this::normalize)
|
|
|
+ .filter(value -> !value.isEmpty())
|
|
|
+ .limit(5)
|
|
|
+ .collect(Collectors.toSet());
|
|
|
+ if (selected.isEmpty()) {
|
|
|
+ return Collections.emptyList();
|
|
|
+ }
|
|
|
+ return suppliers(null, startDate, endDate).stream()
|
|
|
+ .filter(row -> selected.contains(normalize(String.valueOf(row.getOrDefault("supplierName", "")))))
|
|
|
+ .collect(Collectors.toList());
|
|
|
+ }
|
|
|
+
|
|
|
+ public List<Map<String, Object>> paymentPlan(String supplierName, String startDate, String endDate) {
|
|
|
+ List<Map<String, Object>> databaseRows = databasePaymentPlan(supplierName, startDate, endDate);
|
|
|
+ if (databaseRows != null) {
|
|
|
+ return databaseRows;
|
|
|
+ }
|
|
|
+
|
|
|
+ SupplyData data = loadData();
|
|
|
+ LocalDate start = parseDate(startDate);
|
|
|
+ LocalDate end = parseDate(endDate);
|
|
|
+ String keyword = normalize(supplierName);
|
|
|
+ Map<String, PaymentAgg> plan = new HashMap<>();
|
|
|
+ for (ReceiptRecord receipt : data.receipts) {
|
|
|
+ if (receipt.supplierName.isEmpty() || !inRange(receipt.acceptanceDate, start, end)) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ if (!keyword.isEmpty() && !normalize(receipt.supplierName).contains(keyword)) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ int termDays = data.termDays.getOrDefault(receipt.supplierName, 0);
|
|
|
+ LocalDate dueDate = receipt.acceptanceDate == null ? null : receipt.acceptanceDate.plusDays(termDays);
|
|
|
+ String key = receipt.supplierName + "|" + formatDate(dueDate);
|
|
|
+ PaymentAgg agg = plan.computeIfAbsent(key, ignored -> new PaymentAgg(receipt.supplierCode, receipt.supplierName, termDays, dueDate));
|
|
|
+ agg.receiptAmount += receipt.amount;
|
|
|
+ agg.receiptQty += receipt.quantity;
|
|
|
+ agg.receiptLines++;
|
|
|
+ }
|
|
|
+ return plan.values().stream()
|
|
|
+ .sorted(Comparator.comparing((PaymentAgg row) -> row.dueDate == null ? LocalDate.MAX : row.dueDate)
|
|
|
+ .thenComparing(row -> row.supplierName))
|
|
|
+ .map(row -> {
|
|
|
+ Map<String, Object> result = new LinkedHashMap<>();
|
|
|
+ result.put("supplierCode", row.supplierCode);
|
|
|
+ result.put("supplierName", row.supplierName);
|
|
|
+ result.put("termDays", row.termDays);
|
|
|
+ result.put("dueDate", formatDate(row.dueDate));
|
|
|
+ result.put("estimatedPayAmount", round(row.receiptAmount, 2));
|
|
|
+ result.put("receiptQty", round(row.receiptQty, 2));
|
|
|
+ result.put("receiptLines", row.receiptLines);
|
|
|
+ result.put("status", paymentStatus(row.dueDate));
|
|
|
+ return result;
|
|
|
+ })
|
|
|
+ .collect(Collectors.toList());
|
|
|
+ }
|
|
|
+
|
|
|
+ private List<Map<String, Object>> databasePaymentPlan(String supplierName, String startDate, String endDate) {
|
|
|
+ try {
|
|
|
+ if (safeLong(supplyMonitorMapper.countPurchaseReceipts()) == 0) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ return supplyMonitorMapper.selectPaymentPlanRows(supplierName, startDate, endDate).stream()
|
|
|
+ .map(row -> {
|
|
|
+ LocalDate dueDate = parseDateObject(getMapValue(row, "dueDate"));
|
|
|
+ Map<String, Object> result = new LinkedHashMap<>();
|
|
|
+ result.put("supplierCode", stringValue(getMapValue(row, "supplierCode")));
|
|
|
+ result.put("supplierName", stringValue(getMapValue(row, "supplierName")));
|
|
|
+ result.put("termDays", toInteger(getMapValue(row, "termDays")));
|
|
|
+ result.put("dueDate", formatDate(dueDate));
|
|
|
+ result.put("estimatedPayAmount", round(toDouble(getMapValue(row, "estimatedPayAmount")), 2));
|
|
|
+ result.put("receiptQty", round(toDouble(getMapValue(row, "receiptQty")), 2));
|
|
|
+ result.put("receiptLines", toInteger(getMapValue(row, "receiptLines")));
|
|
|
+ result.put("status", paymentStatus(dueDate));
|
|
|
+ return result;
|
|
|
+ })
|
|
|
+ .collect(Collectors.toList());
|
|
|
+ } catch (Exception ignored) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ public List<Map<String, Object>> paymentSummary(String supplierName, String startDate, String endDate) {
|
|
|
+ Map<String, PaymentMonthAgg> summary = new HashMap<>();
|
|
|
+ for (Map<String, Object> row : paymentPlan(supplierName, startDate, endDate)) {
|
|
|
+ String dueDate = stringValue(getMapValue(row, "dueDate"));
|
|
|
+ String month = dueDate.length() >= 7 ? dueDate.substring(0, 7) : "未计算";
|
|
|
+ PaymentMonthAgg agg = summary.computeIfAbsent(month, PaymentMonthAgg::new);
|
|
|
+ double amount = toDouble(getMapValue(row, "estimatedPayAmount"));
|
|
|
+ agg.amount += amount;
|
|
|
+ agg.receiptQty += toDouble(getMapValue(row, "receiptQty"));
|
|
|
+ agg.receiptLines += toInteger(getMapValue(row, "receiptLines"));
|
|
|
+ agg.planCount++;
|
|
|
+ String status = stringValue(getMapValue(row, "status"));
|
|
|
+ if ("已到期".equals(status)) {
|
|
|
+ agg.overdueAmount += amount;
|
|
|
+ agg.overdueCount++;
|
|
|
+ } else if ("7天内到期".equals(status)) {
|
|
|
+ agg.dueSoonAmount += amount;
|
|
|
+ agg.dueSoonCount++;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return summary.values().stream()
|
|
|
+ .sorted(Comparator.comparing(item -> item.month))
|
|
|
+ .map(item -> {
|
|
|
+ Map<String, Object> result = new LinkedHashMap<>();
|
|
|
+ result.put("month", item.month);
|
|
|
+ result.put("planCount", item.planCount);
|
|
|
+ result.put("estimatedPayAmount", round(item.amount, 2));
|
|
|
+ result.put("overdueAmount", round(item.overdueAmount, 2));
|
|
|
+ result.put("dueSoonAmount", round(item.dueSoonAmount, 2));
|
|
|
+ result.put("overdueCount", item.overdueCount);
|
|
|
+ result.put("dueSoonCount", item.dueSoonCount);
|
|
|
+ result.put("receiptQty", round(item.receiptQty, 2));
|
|
|
+ result.put("receiptLines", item.receiptLines);
|
|
|
+ result.put("suggestion", paymentSuggestion(item));
|
|
|
+ return result;
|
|
|
+ })
|
|
|
+ .collect(Collectors.toList());
|
|
|
+ }
|
|
|
+
|
|
|
+ public List<Map<String, Object>> receiptReconciliation(String supplierName, String startDate, String endDate) {
|
|
|
+ List<Map<String, Object>> databaseRows = databaseReceiptReconciliation(supplierName, startDate, endDate);
|
|
|
+ if (databaseRows != null) {
|
|
|
+ return databaseRows;
|
|
|
+ }
|
|
|
+
|
|
|
+ SupplyData data = loadData();
|
|
|
+ LocalDate start = parseDate(startDate);
|
|
|
+ LocalDate end = parseDate(endDate);
|
|
|
+ String keyword = normalize(supplierName);
|
|
|
+ Map<String, ReceiptMonthAgg> summary = new HashMap<>();
|
|
|
+ for (ReceiptRecord receipt : data.receipts) {
|
|
|
+ if (receipt.supplierName.isEmpty() || !inRange(receipt.businessDate, start, end)) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ if (!keyword.isEmpty() && !normalize(receipt.supplierName).contains(keyword)) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ String month = receipt.businessDate == null ? "未计算" : YMD.format(receipt.businessDate).substring(0, 7);
|
|
|
+ String key = month + "|" + receipt.supplierName;
|
|
|
+ ReceiptMonthAgg agg = summary.computeIfAbsent(key, ignored -> new ReceiptMonthAgg(month, receipt.supplierCode, receipt.supplierName));
|
|
|
+ agg.receiptQty += receipt.quantity;
|
|
|
+ agg.receiptAmount += receipt.amount;
|
|
|
+ agg.receiptLines++;
|
|
|
+ if (!receipt.receiptId.isEmpty()) {
|
|
|
+ agg.receiptIds.add(receipt.receiptId);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return summary.values().stream()
|
|
|
+ .sorted(Comparator.comparing((ReceiptMonthAgg item) -> item.month).reversed()
|
|
|
+ .thenComparing(item -> item.supplierName))
|
|
|
+ .map(item -> {
|
|
|
+ Map<String, Object> result = new LinkedHashMap<>();
|
|
|
+ result.put("month", item.month);
|
|
|
+ result.put("supplierCode", item.supplierCode);
|
|
|
+ result.put("supplierName", item.supplierName);
|
|
|
+ result.put("receiptQty", round(item.receiptQty, 2));
|
|
|
+ result.put("receiptAmount", round(item.receiptAmount, 2));
|
|
|
+ result.put("receiptLines", item.receiptLines);
|
|
|
+ result.put("receiptCount", item.receiptIds.size());
|
|
|
+ result.put("avgReceiptPrice", item.receiptQty == 0D ? 0D : round(item.receiptAmount / item.receiptQty, 2));
|
|
|
+ return result;
|
|
|
+ })
|
|
|
+ .collect(Collectors.toList());
|
|
|
+ }
|
|
|
+
|
|
|
+ private List<Map<String, Object>> databaseReceiptReconciliation(String supplierName, String startDate, String endDate) {
|
|
|
+ try {
|
|
|
+ if (safeLong(supplyMonitorMapper.countPurchaseReceipts()) == 0) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ return supplyMonitorMapper.selectReceiptReconciliationRows(supplierName, startDate, endDate).stream()
|
|
|
+ .map(row -> {
|
|
|
+ double receiptQty = toDouble(getMapValue(row, "receiptQty"));
|
|
|
+ double receiptAmount = toDouble(getMapValue(row, "receiptAmount"));
|
|
|
+ Map<String, Object> result = new LinkedHashMap<>();
|
|
|
+ result.put("month", stringValue(getMapValue(row, "month")));
|
|
|
+ result.put("supplierCode", stringValue(getMapValue(row, "supplierCode")));
|
|
|
+ result.put("supplierName", stringValue(getMapValue(row, "supplierName")));
|
|
|
+ result.put("receiptQty", round(receiptQty, 2));
|
|
|
+ result.put("receiptAmount", round(receiptAmount, 2));
|
|
|
+ result.put("receiptLines", toInteger(getMapValue(row, "receiptLines")));
|
|
|
+ result.put("receiptCount", toInteger(getMapValue(row, "receiptCount")));
|
|
|
+ result.put("avgReceiptPrice", receiptQty == 0D ? 0D : round(receiptAmount / receiptQty, 2));
|
|
|
+ return result;
|
|
|
+ })
|
|
|
+ .collect(Collectors.toList());
|
|
|
+ } catch (Exception ignored) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ public Map<String, Object> overviewStats() {
|
|
|
+ Map<String, Object> row = supplyMonitorMapper.selectOverviewStats();
|
|
|
+ Map<String, Object> result = new LinkedHashMap<>();
|
|
|
+ result.put("supplierTotal", safeLongValue(getMapValue(row, "supplierTotal")));
|
|
|
+ result.put("averageScore", round(toDouble(getMapValue(row, "averageScore")), 1));
|
|
|
+ result.put("warningSupplierCount", safeLongValue(getMapValue(row, "warningSupplierCount")));
|
|
|
+ result.put("excellentSupplierCount", safeLongValue(getMapValue(row, "excellentSupplierCount")));
|
|
|
+ return result;
|
|
|
+ }
|
|
|
+
|
|
|
+ public Map<String, Object> getScoreWeights() {
|
|
|
+ Map<String, Object> result = new LinkedHashMap<>();
|
|
|
+ Map<String, Double> weights = new LinkedHashMap<>(scoreWeights);
|
|
|
+ result.putAll(weights);
|
|
|
+ result.put("weights", weights);
|
|
|
+ result.put("rules", new LinkedHashMap<>(scoreRules));
|
|
|
+ return result;
|
|
|
+ }
|
|
|
+
|
|
|
+ public Map<String, Object> updateScoreWeights(Map<String, Object> payload) {
|
|
|
+ Map<String, Double> next = new LinkedHashMap<>();
|
|
|
+ next.put("成本", readWeight(payload, "成本", "成本权重", "cost_weight", "costWeight"));
|
|
|
+ next.put("交付", readWeight(payload, "交付", "交付权重", "delivery_weight", "deliveryWeight"));
|
|
|
+ next.put("账期", readWeight(payload, "账期", "账期权重", "payment_weight", "term_weight", "termWeight"));
|
|
|
+ next.put("质量", readWeight(payload, "质量", "质量权重", "quality_weight", "qualityWeight"));
|
|
|
+ double total = next.values().stream().mapToDouble(Double::doubleValue).sum();
|
|
|
+ if (Math.abs(total - 1D) > 0.001D) {
|
|
|
+ throw new IllegalArgumentException("权重总和必须为1");
|
|
|
+ }
|
|
|
+ scoreWeights = next;
|
|
|
+ scoreRules = readScoreRules(payload);
|
|
|
+ cache.set(null);
|
|
|
+ return getScoreWeights();
|
|
|
+ }
|
|
|
+
|
|
|
+ public List<Map<String, Object>> evaluateProduct(String productCode) {
|
|
|
+ String sku = stringValue(productCode);
|
|
|
+ if (sku.isEmpty()) {
|
|
|
+ return Collections.emptyList();
|
|
|
+ }
|
|
|
+
|
|
|
+ Map<String, ProductSupplierEval> evalMap = new HashMap<>();
|
|
|
+ Map<String, Integer> termDaysBySupplier = new HashMap<>();
|
|
|
+ String productName = "";
|
|
|
+
|
|
|
+ for (Map<String, Object> row : supplyMonitorMapper.selectProductCost(sku)) {
|
|
|
+ String supplierName = stringValue(getMapValue(row, "supplierName"));
|
|
|
+ ProductSupplierEval eval = evalMap.computeIfAbsent(supplierName,
|
|
|
+ key -> new ProductSupplierEval(stringValue(getMapValue(row, "supplierCode")), supplierName));
|
|
|
+ eval.receiptAmount = toDouble(getMapValue(row, "receiptAmount"));
|
|
|
+ eval.receiptQty = toDouble(getMapValue(row, "receiptQty"));
|
|
|
+ }
|
|
|
+
|
|
|
+ for (Map<String, Object> row : supplyMonitorMapper.selectProductDelivery(sku)) {
|
|
|
+ String supplierName = stringValue(getMapValue(row, "supplierName"));
|
|
|
+ ProductSupplierEval eval = evalMap.computeIfAbsent(supplierName,
|
|
|
+ key -> new ProductSupplierEval(stringValue(getMapValue(row, "supplierCode")), supplierName));
|
|
|
+ eval.matchedLines = toInteger(getMapValue(row, "matchedLines"));
|
|
|
+ eval.onTimeLines = toInteger(getMapValue(row, "onTimeLines"));
|
|
|
+ eval.planQty = toDouble(getMapValue(row, "planQty"));
|
|
|
+ eval.receiptQtyForDelivery = toDouble(getMapValue(row, "receiptQtyForDelivery"));
|
|
|
+ termDaysBySupplier.put(supplierName, toInteger(getMapValue(row, "termDays")));
|
|
|
+ if (productName.isEmpty()) {
|
|
|
+ productName = stringValue(getMapValue(row, "productName"));
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ if (evalMap.isEmpty()) {
|
|
|
+ return Collections.emptyList();
|
|
|
+ }
|
|
|
+
|
|
|
+ List<ProductSupplierEval> rows = evalMap.values().stream()
|
|
|
+ .filter(row -> row.receiptQty > 0 || row.matchedLines > 0)
|
|
|
+ .collect(Collectors.toList());
|
|
|
+ rows.sort(Comparator.comparingDouble(row -> row.referencePrice() > 0D ? row.referencePrice() : Double.MAX_VALUE));
|
|
|
+ for (int i = 0; i < rows.size(); i++) {
|
|
|
+ ProductSupplierEval row = rows.get(i);
|
|
|
+ row.costRank = i + 1;
|
|
|
+ row.costScore = costScore(row.costRank);
|
|
|
+ row.deliveryScore = deliveryScore(row);
|
|
|
+ row.termScore = termScore(termDaysBySupplier.getOrDefault(row.supplierName, 0));
|
|
|
+ row.totalScore = row.costScore * 0.4D + row.deliveryScore * 0.4D + row.termScore * 0.2D;
|
|
|
+ }
|
|
|
+
|
|
|
+ rows.sort(Comparator.comparingDouble((ProductSupplierEval row) -> row.totalScore).reversed());
|
|
|
+ String finalProductName = productName;
|
|
|
+ List<Map<String, Object>> result = new ArrayList<>();
|
|
|
+ for (int i = 0; i < Math.min(5, rows.size()); i++) {
|
|
|
+ ProductSupplierEval row = rows.get(i);
|
|
|
+ Map<String, Object> item = new LinkedHashMap<>();
|
|
|
+ item.put("综合排名", i + 1);
|
|
|
+ item.put("产品代码", sku);
|
|
|
+ item.put("产品名称", finalProductName);
|
|
|
+ item.put("供应商代码", row.supplierCode);
|
|
|
+ item.put("供应商名称", row.supplierName);
|
|
|
+ item.put("参考价", round(row.referencePrice(), 2));
|
|
|
+ item.put("成本排名", row.costRank);
|
|
|
+ item.put("成本分数", round(row.costScore, 2));
|
|
|
+ item.put("交付分数", round(row.deliveryScore, 2));
|
|
|
+ item.put("账期分数", round(row.termScore, 2));
|
|
|
+ item.put("综合得分", round(row.totalScore, 2));
|
|
|
+ item.put("rank", i + 1);
|
|
|
+ item.put("productCode", sku);
|
|
|
+ item.put("productName", finalProductName);
|
|
|
+ item.put("supplierCode", row.supplierCode);
|
|
|
+ item.put("supplierName", row.supplierName);
|
|
|
+ item.put("referencePrice", round(row.referencePrice(), 2));
|
|
|
+ item.put("costRank", row.costRank);
|
|
|
+ item.put("costScore", round(row.costScore, 2));
|
|
|
+ item.put("deliveryScore", round(row.deliveryScore, 2));
|
|
|
+ item.put("termScore", round(row.termScore, 2));
|
|
|
+ item.put("totalScore", round(row.totalScore, 2));
|
|
|
+ result.add(item);
|
|
|
+ }
|
|
|
+ return result;
|
|
|
+ }
|
|
|
+
|
|
|
+ public Map<String, Object> productDetails(String productCode) {
|
|
|
+ Map<String, Object> result = new LinkedHashMap<>();
|
|
|
+ result.put("cost_details", evaluateCost(productCode));
|
|
|
+ result.put("delivery_details", evaluateDelivery(productCode));
|
|
|
+ result.put("payment_details", evaluatePayment(productCode));
|
|
|
+ return result;
|
|
|
+ }
|
|
|
+
|
|
|
+ private List<Map<String, Object>> evaluateCost(String productCode) {
|
|
|
+ String sku = stringValue(productCode);
|
|
|
+ if (sku.isEmpty()) {
|
|
|
+ return Collections.emptyList();
|
|
|
+ }
|
|
|
+
|
|
|
+ List<Map<String, Object>> result = new ArrayList<>();
|
|
|
+ List<Map<String, Object>> rows = supplyMonitorMapper.selectProductCost(sku);
|
|
|
+ for (int i = 0; i < rows.size(); i++) {
|
|
|
+ Map<String, Object> row = rows.get(i);
|
|
|
+ int rank = i + 1;
|
|
|
+ Map<String, Object> item = new LinkedHashMap<>();
|
|
|
+ item.put("供应商代码", stringValue(getMapValue(row, "supplierCode")));
|
|
|
+ item.put("供应商名称", stringValue(getMapValue(row, "supplierName")));
|
|
|
+ item.put("参考价", round(toDouble(getMapValue(row, "referencePrice")), 2));
|
|
|
+ item.put("入库数量", round(toDouble(getMapValue(row, "receiptQty")), 2));
|
|
|
+ item.put("入库金额", round(toDouble(getMapValue(row, "receiptAmount")), 2));
|
|
|
+ item.put("入库行数", toInteger(getMapValue(row, "receiptLines")));
|
|
|
+ item.put("首次验收日期", formatDate(parseDateObject(getMapValue(row, "firstAcceptanceDate"))));
|
|
|
+ item.put("最近验收日期", formatDate(parseDateObject(getMapValue(row, "latestAcceptanceDate"))));
|
|
|
+ item.put("成本排名", rank);
|
|
|
+ item.put("成本分数", round(costScore(rank), 2));
|
|
|
+ result.add(item);
|
|
|
+ }
|
|
|
+ return result;
|
|
|
+ }
|
|
|
+
|
|
|
+ private List<Map<String, Object>> evaluateDelivery(String productCode) {
|
|
|
+ String sku = stringValue(productCode);
|
|
|
+ if (sku.isEmpty()) {
|
|
|
+ return Collections.emptyList();
|
|
|
+ }
|
|
|
+
|
|
|
+ return supplyMonitorMapper.selectProductDelivery(sku).stream()
|
|
|
+ .map(row -> {
|
|
|
+ ProductSupplierEval eval = new ProductSupplierEval(
|
|
|
+ stringValue(getMapValue(row, "supplierCode")),
|
|
|
+ stringValue(getMapValue(row, "supplierName")));
|
|
|
+ eval.matchedLines = toInteger(getMapValue(row, "matchedLines"));
|
|
|
+ eval.onTimeLines = toInteger(getMapValue(row, "onTimeLines"));
|
|
|
+ eval.planQty = toDouble(getMapValue(row, "planQty"));
|
|
|
+ eval.receiptQtyForDelivery = toDouble(getMapValue(row, "receiptQtyForDelivery"));
|
|
|
+ int delayedLines = toInteger(getMapValue(row, "delayedLines"));
|
|
|
+ double onTimeRate = eval.matchedLines == 0 ? 0D : round(eval.onTimeLines * 100D / eval.matchedLines, 2);
|
|
|
+ double fulfillmentRate = eval.planQty > 0 ? round(Math.min(1D, eval.receiptQtyForDelivery / eval.planQty) * 100D, 2) : 0D;
|
|
|
+ double avgDeviationDays = round(toDouble(getMapValue(row, "avgDeviationDays")), 2);
|
|
|
+ double maxDelayDays = round(toDouble(getMapValue(row, "maxDelayDays")), 2);
|
|
|
+ double shortageQty = round(Math.max(0D, eval.planQty - eval.receiptQtyForDelivery), 2);
|
|
|
+ String deliveryStatus = deliveryStatus(onTimeRate, fulfillmentRate, maxDelayDays);
|
|
|
+ String riskLevel = deliveryRiskLevel(onTimeRate, fulfillmentRate, maxDelayDays);
|
|
|
+ Map<String, Object> item = new LinkedHashMap<>();
|
|
|
+ item.put("供应商代码", eval.supplierCode);
|
|
|
+ item.put("供应商名称", eval.supplierName);
|
|
|
+ item.put("交付分数", round(deliveryScore(eval), 2));
|
|
|
+ item.put("匹配行数", eval.matchedLines);
|
|
|
+ item.put("准时行数", eval.onTimeLines);
|
|
|
+ item.put("延迟行数", delayedLines);
|
|
|
+ item.put("准时率", onTimeRate);
|
|
|
+ item.put("平均偏差", avgDeviationDays);
|
|
|
+ item.put("最长延迟", maxDelayDays);
|
|
|
+ item.put("满足率", fulfillmentRate);
|
|
|
+ item.put("计划数量", round(eval.planQty, 2));
|
|
|
+ item.put("实际入库数量", round(eval.receiptQtyForDelivery, 2));
|
|
|
+ item.put("未满足数量", shortageQty);
|
|
|
+ item.put("履约状态", deliveryStatus);
|
|
|
+ item.put("风险等级", riskLevel);
|
|
|
+ item.put("D1_准时率", onTimeRate);
|
|
|
+ item.put("D2_平均偏差", avgDeviationDays);
|
|
|
+ item.put("D3_最长延迟", maxDelayDays);
|
|
|
+ item.put("D4_满足率", fulfillmentRate);
|
|
|
+ item.put("supplierCode", eval.supplierCode);
|
|
|
+ item.put("supplierName", eval.supplierName);
|
|
|
+ item.put("deliveryScore", round(deliveryScore(eval), 2));
|
|
|
+ item.put("matchedLines", eval.matchedLines);
|
|
|
+ item.put("onTimeLines", eval.onTimeLines);
|
|
|
+ item.put("delayedLines", delayedLines);
|
|
|
+ item.put("onTimeRate", onTimeRate);
|
|
|
+ item.put("avgDeviationDays", avgDeviationDays);
|
|
|
+ item.put("maxDelayDays", maxDelayDays);
|
|
|
+ item.put("fulfillmentRate", fulfillmentRate);
|
|
|
+ item.put("planQty", round(eval.planQty, 2));
|
|
|
+ item.put("receiptQty", round(eval.receiptQtyForDelivery, 2));
|
|
|
+ item.put("shortageQty", shortageQty);
|
|
|
+ item.put("deliveryStatus", deliveryStatus);
|
|
|
+ item.put("riskLevel", riskLevel);
|
|
|
+ return item;
|
|
|
+ })
|
|
|
+ .collect(Collectors.toList());
|
|
|
+ }
|
|
|
+
|
|
|
+ private List<Map<String, Object>> evaluatePayment(String productCode) {
|
|
|
+ String sku = stringValue(productCode);
|
|
|
+ if (sku.isEmpty()) {
|
|
|
+ return Collections.emptyList();
|
|
|
+ }
|
|
|
+
|
|
|
+ Map<String, Map<String, Object>> supplierMap = new LinkedHashMap<>();
|
|
|
+ for (Map<String, Object> row : supplyMonitorMapper.selectProductCost(sku)) {
|
|
|
+ putProductSupplier(supplierMap, row);
|
|
|
+ }
|
|
|
+ for (Map<String, Object> row : supplyMonitorMapper.selectProductDelivery(sku)) {
|
|
|
+ putProductSupplier(supplierMap, row);
|
|
|
+ }
|
|
|
+ if (supplierMap.isEmpty()) {
|
|
|
+ return Collections.emptyList();
|
|
|
+ }
|
|
|
+
|
|
|
+ Map<String, Map<String, Object>> termMap = new HashMap<>();
|
|
|
+ for (Map<String, Object> row : supplyMonitorMapper.selectSupplierTerms()) {
|
|
|
+ String supplierName = stringValue(getMapValue(row, "supplierName"));
|
|
|
+ if (!supplierName.isEmpty()) {
|
|
|
+ termMap.put(supplierName, row);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return supplierMap.values().stream()
|
|
|
+ .map(row -> {
|
|
|
+ String supplierName = stringValue(getMapValue(row, "supplierName"));
|
|
|
+ Map<String, Object> term = termMap.getOrDefault(supplierName, Collections.emptyMap());
|
|
|
+ int termDays = toInteger(getMapValue(term, "termDays"));
|
|
|
+ double score = termScore(termDays);
|
|
|
+ Map<String, Object> item = new LinkedHashMap<>();
|
|
|
+ item.put("供应商代码", stringValue(getMapValue(row, "supplierCode")));
|
|
|
+ item.put("供应商名称", supplierName);
|
|
|
+ item.put("结算期限", stringValue(getMapValue(term, "paymentTerms")));
|
|
|
+ item.put("账期天数", termDays);
|
|
|
+ item.put("账期分数", round(score, 2));
|
|
|
+ item.put("账期等级", termLevel(termDays));
|
|
|
+ item.put("维护状态", termDays > 0 ? "已维护" : "缺少账期");
|
|
|
+ item.put("supplierCode", stringValue(getMapValue(row, "supplierCode")));
|
|
|
+ item.put("supplierName", supplierName);
|
|
|
+ item.put("paymentTerms", stringValue(getMapValue(term, "paymentTerms")));
|
|
|
+ item.put("termDays", termDays);
|
|
|
+ item.put("termScore", round(score, 2));
|
|
|
+ item.put("termLevel", termLevel(termDays));
|
|
|
+ item.put("termStatus", termDays > 0 ? "已维护" : "缺少账期");
|
|
|
+ return item;
|
|
|
+ })
|
|
|
+ .sorted(Comparator.comparing((Map<String, Object> row) -> toDouble(getMapValue(row, "termScore"))).reversed()
|
|
|
+ .thenComparing(row -> stringValue(getMapValue(row, "supplierName"))))
|
|
|
+ .collect(Collectors.toList());
|
|
|
+ }
|
|
|
+
|
|
|
+ private void putProductSupplier(Map<String, Map<String, Object>> supplierMap, Map<String, Object> row) {
|
|
|
+ String supplierName = stringValue(getMapValue(row, "supplierName"));
|
|
|
+ if (supplierName.isEmpty()) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ supplierMap.computeIfAbsent(supplierName, ignored -> {
|
|
|
+ Map<String, Object> item = new LinkedHashMap<>();
|
|
|
+ item.put("supplierCode", stringValue(getMapValue(row, "supplierCode")));
|
|
|
+ item.put("supplierName", supplierName);
|
|
|
+ return item;
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ private Map<String, SupplierStats> buildStats(SupplyData data, LocalDate start, LocalDate end) {
|
|
|
+ Map<String, SupplierStats> map = new HashMap<>();
|
|
|
+ for (OrderRecord order : data.orders) {
|
|
|
+ if (order.supplierName.isEmpty() || !inRange(order.businessDate, start, end)) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ SupplierStats stats = map.computeIfAbsent(order.supplierName, key -> new SupplierStats(order.supplierCode, order.supplierName));
|
|
|
+ stats.orderIds.add(order.documentId);
|
|
|
+ stats.orderLines++;
|
|
|
+ stats.orderQty += order.orderQty;
|
|
|
+ stats.completedQty += order.completedQty;
|
|
|
+ stats.uncompletedQty += order.uncompletedQty;
|
|
|
+ stats.orderAmount += order.amount;
|
|
|
+ }
|
|
|
+ for (ReceiptRecord receipt : data.receipts) {
|
|
|
+ if (receipt.supplierName.isEmpty() || !inRange(receipt.businessDate, start, end)) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ SupplierStats stats = map.computeIfAbsent(receipt.supplierName, key -> new SupplierStats(receipt.supplierCode, receipt.supplierName));
|
|
|
+ stats.receiptQty += receipt.quantity;
|
|
|
+ stats.receiptAmount += receipt.amount;
|
|
|
+ stats.receiptLines++;
|
|
|
+ }
|
|
|
+ for (MergedRecord merged : data.mergedRecords) {
|
|
|
+ if (merged.supplierName.isEmpty() || !inRange(merged.acceptanceDate, start, end)) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ SupplierStats stats = map.computeIfAbsent(merged.supplierName, key -> new SupplierStats(merged.supplierCode, merged.supplierName));
|
|
|
+ stats.matchedLines++;
|
|
|
+ stats.matchedPlanQty += merged.planQty;
|
|
|
+ stats.matchedReceiptQty += merged.receiptQty;
|
|
|
+ if (merged.acceptanceDate != null && merged.planDeliveryDate != null && !merged.acceptanceDate.isAfter(merged.planDeliveryDate)) {
|
|
|
+ stats.onTimeLines++;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ for (SupplierStats stats : map.values()) {
|
|
|
+ stats.termDays = data.termDays.getOrDefault(stats.supplierName, 0);
|
|
|
+ }
|
|
|
+ applySupplierCostScores(data, map, start, end);
|
|
|
+ applyQualityStats(map, start, end);
|
|
|
+ return map;
|
|
|
+ }
|
|
|
+
|
|
|
+ private Map<String, Object> toRow(SupplierStats stats) {
|
|
|
+ double completionRate = stats.orderQty > 0 ? Math.min(1D, stats.completedQty / stats.orderQty) : 0D;
|
|
|
+ double deliveryRate = stats.matchedLines > 0 ? stats.onTimeLines * 1D / stats.matchedLines : 0D;
|
|
|
+ double qualityPassRate = stats.qaSampleQty > 0D ? Math.max(0D, Math.min(1D, (stats.qaSampleQty - stats.qaDefectQty) / stats.qaSampleQty)) : 0D;
|
|
|
+ double costScore = stats.costScoreCount > 0 ? stats.costScoreTotal / stats.costScoreCount : 0D;
|
|
|
+ Map<String, Double> rules = scoreRules;
|
|
|
+ double deliveryScore = deliveryRate * 100D * rules.getOrDefault("deliveryOnTimeWeight", 0.70D)
|
|
|
+ + completionRate * 100D * rules.getOrDefault("deliveryFulfillmentWeight", 0.30D);
|
|
|
+ double termScoreValue = termScore(stats.termDays);
|
|
|
+ double qualityScore = stats.qaSampleQty > 0D ? qualityPassRate * 100D : rules.getOrDefault("qualityDefaultScore", 80D);
|
|
|
+ Map<String, Double> weights = scoreWeights;
|
|
|
+ double totalScore = costScore * weights.getOrDefault("成本", 0.30D)
|
|
|
+ + deliveryScore * weights.getOrDefault("交付", 0.30D)
|
|
|
+ + termScoreValue * weights.getOrDefault("账期", 0.15D)
|
|
|
+ + qualityScore * weights.getOrDefault("质量", 0.25D);
|
|
|
+ Map<String, Object> row = new LinkedHashMap<>();
|
|
|
+ row.put("supplierCode", stats.supplierCode);
|
|
|
+ row.put("supplierName", stats.supplierName);
|
|
|
+ row.put("termDays", stats.termDays);
|
|
|
+ row.put("costScore", round(costScore, 2));
|
|
|
+ row.put("deliveryScore", round(deliveryScore, 2));
|
|
|
+ row.put("termScore", round(termScoreValue, 2));
|
|
|
+ row.put("qualityScore", round(qualityScore, 2));
|
|
|
+ row.put("totalScore", round(totalScore, 2));
|
|
|
+ row.put("supplierLevel", supplierLevel(totalScore));
|
|
|
+ row.put("qaPassRate", stats.qaSampleQty > 0D ? round(qualityPassRate, 4) : null);
|
|
|
+ row.put("qaSampleQty", round(stats.qaSampleQty, 2));
|
|
|
+ row.put("qaDefectQty", round(stats.qaDefectQty, 2));
|
|
|
+ row.put("qaIncomingQty", round(stats.qaIncomingQty, 2));
|
|
|
+ row.put("qaLines", stats.qaLines);
|
|
|
+ row.put("orderCount", stats.orderIds.size());
|
|
|
+ row.put("orderLines", stats.orderLines);
|
|
|
+ row.put("orderQty", round(stats.orderQty, 2));
|
|
|
+ row.put("completedQty", round(stats.completedQty, 2));
|
|
|
+ row.put("uncompletedQty", round(stats.uncompletedQty, 2));
|
|
|
+ row.put("orderAmount", round(stats.orderAmount, 2));
|
|
|
+ row.put("receiptQty", round(stats.receiptQty, 2));
|
|
|
+ row.put("receiptAmount", round(stats.receiptAmount, 2));
|
|
|
+ row.put("receiptLines", stats.receiptLines);
|
|
|
+ row.put("deliveryRate", round(deliveryRate, 4));
|
|
|
+ row.put("completionRate", round(completionRate, 4));
|
|
|
+ row.put("warningTag", supplierWarningTag(totalScore, deliveryRate, completionRate, qualityPassRate, stats.qaSampleQty));
|
|
|
+ return row;
|
|
|
+ }
|
|
|
+
|
|
|
+ private void applySupplierCostScores(SupplyData data, Map<String, SupplierStats> statsMap, LocalDate start, LocalDate end) {
|
|
|
+ Map<String, Map<String, CostAgg>> skuSupplierCost = new HashMap<>();
|
|
|
+ for (ReceiptRecord receipt : data.receipts) {
|
|
|
+ if (receipt.sku.isEmpty() || receipt.supplierName.isEmpty() || !inRange(receipt.businessDate, start, end)) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ Map<String, CostAgg> supplierCost = skuSupplierCost.computeIfAbsent(receipt.sku, ignored -> new HashMap<>());
|
|
|
+ CostAgg agg = supplierCost.computeIfAbsent(receipt.supplierName, ignored -> new CostAgg(receipt.supplierName));
|
|
|
+ agg.amount += receipt.amount;
|
|
|
+ agg.qty += receipt.quantity;
|
|
|
+ }
|
|
|
+ for (Map<String, CostAgg> supplierCost : skuSupplierCost.values()) {
|
|
|
+ List<CostAgg> rows = supplierCost.values().stream()
|
|
|
+ .filter(item -> item.qty > 0D)
|
|
|
+ .sorted(Comparator.comparingDouble(item -> item.amount / item.qty))
|
|
|
+ .collect(Collectors.toList());
|
|
|
+ for (int i = 0; i < rows.size(); i++) {
|
|
|
+ CostAgg row = rows.get(i);
|
|
|
+ SupplierStats stats = statsMap.get(row.supplierName);
|
|
|
+ if (stats != null) {
|
|
|
+ stats.costScoreTotal += costScore(i + 1);
|
|
|
+ stats.costScoreCount++;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private void applySupplierCostScoresFromRows(List<Map<String, Object>> rows, Map<String, SupplierStats> statsMap) {
|
|
|
+ Map<String, Map<String, CostAgg>> skuSupplierCost = new HashMap<>();
|
|
|
+ for (Map<String, Object> item : rows) {
|
|
|
+ String sku = stringValue(getMapValue(item, "sku"));
|
|
|
+ String supplierName = stringValue(getMapValue(item, "supplierName"));
|
|
|
+ if (sku.isEmpty() || supplierName.isEmpty()) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ Map<String, CostAgg> supplierCost = skuSupplierCost.computeIfAbsent(sku, ignored -> new HashMap<>());
|
|
|
+ CostAgg agg = supplierCost.computeIfAbsent(supplierName, ignored -> new CostAgg(supplierName));
|
|
|
+ agg.amount += toDouble(getMapValue(item, "receiptAmount"));
|
|
|
+ agg.qty += toDouble(getMapValue(item, "receiptQty"));
|
|
|
+ }
|
|
|
+ for (Map<String, CostAgg> supplierCost : skuSupplierCost.values()) {
|
|
|
+ List<CostAgg> costRows = supplierCost.values().stream()
|
|
|
+ .filter(item -> item.qty > 0D)
|
|
|
+ .sorted(Comparator.comparingDouble(item -> item.amount / item.qty))
|
|
|
+ .collect(Collectors.toList());
|
|
|
+ for (int i = 0; i < costRows.size(); i++) {
|
|
|
+ CostAgg row = costRows.get(i);
|
|
|
+ SupplierStats stats = statsMap.get(row.supplierName);
|
|
|
+ if (stats != null) {
|
|
|
+ stats.costScoreTotal += costScore(i + 1);
|
|
|
+ stats.costScoreCount++;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private void applyQualityStats(Map<String, SupplierStats> statsMap, LocalDate start, LocalDate end) {
|
|
|
+ if (statsMap == null || statsMap.isEmpty()) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ for (QaRecord record : loadQaRecords()) {
|
|
|
+ if (record.supplierName.isEmpty() || !inRange(record.inspectDate, start, end)) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ SupplierStats stats = findSupplierStats(statsMap, record.supplierName);
|
|
|
+ if (stats == null) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ stats.qaIncomingQty += record.incomingQty;
|
|
|
+ stats.qaSampleQty += record.sampleQty;
|
|
|
+ stats.qaDefectQty += record.defectQty;
|
|
|
+ stats.qaLines++;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private SupplierStats findSupplierStats(Map<String, SupplierStats> statsMap, String qaSupplierName) {
|
|
|
+ String qaName = normalize(qaSupplierName);
|
|
|
+ SupplierStats exact = statsMap.get(qaSupplierName);
|
|
|
+ if (exact != null) {
|
|
|
+ return exact;
|
|
|
+ }
|
|
|
+ for (SupplierStats stats : statsMap.values()) {
|
|
|
+ String supplierName = normalize(stats.supplierName);
|
|
|
+ if (!qaName.isEmpty() && (supplierName.contains(qaName) || qaName.contains(supplierName))) {
|
|
|
+ return stats;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ private List<QaRecord> loadQaRecords() {
|
|
|
+ List<QaRecord> databaseRecords = loadDatabaseQaRecords();
|
|
|
+ if (!databaseRecords.isEmpty()) {
|
|
|
+ return databaseRecords;
|
|
|
+ }
|
|
|
+ return loadExcelQaRecords();
|
|
|
+ }
|
|
|
+
|
|
|
+ private List<QaRecord> loadDatabaseQaRecords() {
|
|
|
+ try {
|
|
|
+ if (safeLong(supplyMonitorMapper.countQaInspections()) == 0L) {
|
|
|
+ return Collections.emptyList();
|
|
|
+ }
|
|
|
+ List<QaRecord> result = new ArrayList<>();
|
|
|
+ for (Map<String, Object> row : supplyMonitorMapper.selectQaInspections()) {
|
|
|
+ String supplierName = stringValue(getMapValue(row, "supplierName"));
|
|
|
+ double sampleQty = toDouble(getMapValue(row, "sampleQty"));
|
|
|
+ if (supplierName.isEmpty() || sampleQty <= 0D) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ QaRecord record = new QaRecord();
|
|
|
+ record.inspectDate = parseDateObject(getMapValue(row, "inspectDate"));
|
|
|
+ record.supplierName = supplierName;
|
|
|
+ record.incomingQty = toDouble(getMapValue(row, "incomingQty"));
|
|
|
+ record.sampleQty = sampleQty;
|
|
|
+ record.defectQty = Math.max(0D, Math.min(sampleQty, toDouble(getMapValue(row, "defectQty"))));
|
|
|
+ result.add(record);
|
|
|
+ }
|
|
|
+ return result;
|
|
|
+ } catch (Exception ignored) {
|
|
|
+ return Collections.emptyList();
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private List<QaRecord> loadExcelQaRecords() {
|
|
|
+ Path file = resolveQaFile();
|
|
|
+ if (file == null || !Files.exists(file)) {
|
|
|
+ return Collections.emptyList();
|
|
|
+ }
|
|
|
+ List<QaRecord> result = new ArrayList<>();
|
|
|
+ DataFormatter formatter = new DataFormatter();
|
|
|
+ try (InputStream inputStream = Files.newInputStream(file);
|
|
|
+ Workbook workbook = WorkbookFactory.create(inputStream)) {
|
|
|
+ for (int i = 0; i < workbook.getNumberOfSheets(); i++) {
|
|
|
+ Sheet sheet = workbook.getSheetAt(i);
|
|
|
+ if (sheet == null || sheet.getPhysicalNumberOfRows() <= 1) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ int headerRowIndex = findQaHeaderRow(sheet, formatter);
|
|
|
+ Row headerRow = headerRowIndex >= 0 ? sheet.getRow(headerRowIndex) : null;
|
|
|
+ if (headerRow == null) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ List<String> headers = readHeaders(headerRow, formatter);
|
|
|
+ int inspectDateIndex = findHeaderIndex(headers, "验货日期", "检验日期", "来货日期");
|
|
|
+ int supplierIndex = findHeaderIndex(headers, "供应商");
|
|
|
+ int incomingQtyIndex = findHeaderIndex(headers, "来货数量", "来货件数");
|
|
|
+ int sampleQtyIndex = findHeaderIndex(headers, "抽检", "抽检/个数", "抽查数量");
|
|
|
+ int defectQtyIndex = findHeaderIndex(headers, "不合格", "不合格/个", "不合格数");
|
|
|
+ int batchDefectIndex = findHeaderIndex(headers, "大批次品");
|
|
|
+ for (int r = headerRowIndex + 1; r <= sheet.getLastRowNum(); r++) {
|
|
|
+ Row row = sheet.getRow(r);
|
|
|
+ if (row == null) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ String supplierName = cellString(row, supplierIndex, formatter);
|
|
|
+ double sampleQty = cellNumber(row, sampleQtyIndex, formatter);
|
|
|
+ double defectQty = cellNumber(row, defectQtyIndex, formatter) + cellNumber(row, batchDefectIndex, formatter);
|
|
|
+ if (supplierName.isEmpty() || sampleQty <= 0D) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ QaRecord record = new QaRecord();
|
|
|
+ record.inspectDate = parseDateObject(cellRawValue(row, inspectDateIndex, formatter));
|
|
|
+ record.supplierName = supplierName;
|
|
|
+ record.incomingQty = cellNumber(row, incomingQtyIndex, formatter);
|
|
|
+ record.sampleQty = sampleQty;
|
|
|
+ record.defectQty = Math.max(0D, Math.min(sampleQty, defectQty));
|
|
|
+ result.add(record);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ } catch (Exception ignored) {
|
|
|
+ return Collections.emptyList();
|
|
|
+ }
|
|
|
+ return result;
|
|
|
+ }
|
|
|
+
|
|
|
+ private int findQaHeaderRow(Sheet sheet, DataFormatter formatter) {
|
|
|
+ if (sheet == null) {
|
|
|
+ return -1;
|
|
|
+ }
|
|
|
+ int end = Math.min(sheet.getLastRowNum(), sheet.getFirstRowNum() + 10);
|
|
|
+ for (int r = sheet.getFirstRowNum(); r <= end; r++) {
|
|
|
+ Row row = sheet.getRow(r);
|
|
|
+ if (row == null) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ List<String> headers = readHeaders(row, formatter);
|
|
|
+ int supplierIndex = findHeaderIndex(headers, "供应商");
|
|
|
+ int sampleQtyIndex = findHeaderIndex(headers, "抽检", "抽检/个数", "抽查数量");
|
|
|
+ int defectQtyIndex = findHeaderIndex(headers, "不合格", "不合格/个", "不合格数");
|
|
|
+ if (supplierIndex >= 0 && sampleQtyIndex >= 0 && defectQtyIndex >= 0) {
|
|
|
+ return r;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return -1;
|
|
|
+ }
|
|
|
+
|
|
|
+ private List<String> readHeaders(Row headerRow, DataFormatter formatter) {
|
|
|
+ List<String> headers = new ArrayList<>();
|
|
|
+ int maxCol = Math.max(0, headerRow.getLastCellNum());
|
|
|
+ for (int c = 0; c < maxCol; c++) {
|
|
|
+ headers.add(formatter.formatCellValue(headerRow.getCell(c)).trim());
|
|
|
+ }
|
|
|
+ return headers;
|
|
|
+ }
|
|
|
+
|
|
|
+ private synchronized SupplyData loadData() {
|
|
|
+ CacheEntry current = cache.get();
|
|
|
+ long now = System.currentTimeMillis();
|
|
|
+ if (current != null && now - current.loadedAt < CACHE_EXPIRE_MILLIS) {
|
|
|
+ return current.data;
|
|
|
+ }
|
|
|
+ SupplyData loaded = doLoadData();
|
|
|
+ cache.set(new CacheEntry(now, loaded));
|
|
|
+ return loaded;
|
|
|
+ }
|
|
|
+
|
|
|
+ private SupplyData doLoadData() {
|
|
|
+ SupplyData databaseData = loadDatabaseData();
|
|
|
+ if (hasDatabaseData(databaseData)) {
|
|
|
+ return databaseData;
|
|
|
+ }
|
|
|
+ return loadExcelData();
|
|
|
+ }
|
|
|
+
|
|
|
+ private SupplyData loadDatabaseData() {
|
|
|
+ SupplyData data = new SupplyData();
|
|
|
+ try {
|
|
|
+ long orderCount = safeLong(supplyMonitorMapper.countPurchaseOrders());
|
|
|
+ long receiptCount = safeLong(supplyMonitorMapper.countPurchaseReceipts());
|
|
|
+ long termCount = safeLong(supplyMonitorMapper.countSupplierTerms());
|
|
|
+ long matchCount = safeLong(supplyMonitorMapper.countOrderReceiptMatches());
|
|
|
+ if (orderCount + receiptCount + termCount + matchCount == 0) {
|
|
|
+ return data;
|
|
|
+ }
|
|
|
+
|
|
|
+ for (Map<String, Object> row : supplyMonitorMapper.selectSupplierTerms()) {
|
|
|
+ String supplierName = stringValue(getMapValue(row, "supplierName"));
|
|
|
+ if (!supplierName.isEmpty()) {
|
|
|
+ data.termDays.put(supplierName, toInteger(getMapValue(row, "termDays")));
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ for (Map<String, Object> row : supplyMonitorMapper.selectPurchaseOrders()) {
|
|
|
+ OrderRecord record = new OrderRecord();
|
|
|
+ record.businessDate = parseDateObject(getMapValue(row, "businessDate"));
|
|
|
+ record.deliveryDate = parseDateObject(getMapValue(row, "deliveryDate"));
|
|
|
+ record.documentId = stringValue(getMapValue(row, "documentId"));
|
|
|
+ record.supplierCode = stringValue(getMapValue(row, "supplierCode"));
|
|
|
+ record.supplierName = stringValue(getMapValue(row, "supplierName"));
|
|
|
+ record.orderQty = toDouble(getMapValue(row, "orderQty"));
|
|
|
+ record.completedQty = toDouble(getMapValue(row, "completedQty"));
|
|
|
+ record.uncompletedQty = toDouble(getMapValue(row, "uncompletedQty"));
|
|
|
+ record.amount = toDouble(getMapValue(row, "amount"));
|
|
|
+ data.orders.add(record);
|
|
|
+ }
|
|
|
+
|
|
|
+ for (Map<String, Object> row : supplyMonitorMapper.selectPurchaseReceipts()) {
|
|
|
+ ReceiptRecord record = new ReceiptRecord();
|
|
|
+ record.businessDate = parseDateObject(getMapValue(row, "businessDate"));
|
|
|
+ record.acceptanceDate = parseDateObject(getMapValue(row, "acceptanceDate"));
|
|
|
+ record.receiptId = stringValue(getMapValue(row, "receiptId"));
|
|
|
+ record.supplierCode = stringValue(getMapValue(row, "supplierCode"));
|
|
|
+ record.supplierName = stringValue(getMapValue(row, "supplierName"));
|
|
|
+ record.sku = stringValue(getMapValue(row, "sku"));
|
|
|
+ record.quantity = toDouble(getMapValue(row, "quantity"));
|
|
|
+ record.amount = toDouble(getMapValue(row, "amount"));
|
|
|
+ data.receipts.add(record);
|
|
|
+ }
|
|
|
+
|
|
|
+ for (Map<String, Object> row : supplyMonitorMapper.selectOrderReceiptMatches()) {
|
|
|
+ MergedRecord record = new MergedRecord();
|
|
|
+ record.acceptanceDate = parseDateObject(getMapValue(row, "acceptanceDate"));
|
|
|
+ record.planDeliveryDate = parseDateObject(getMapValue(row, "planDeliveryDate"));
|
|
|
+ record.sku = stringValue(getMapValue(row, "sku"));
|
|
|
+ record.productName = stringValue(getMapValue(row, "productName"));
|
|
|
+ record.supplierCode = stringValue(getMapValue(row, "supplierCode"));
|
|
|
+ record.supplierName = stringValue(getMapValue(row, "supplierName"));
|
|
|
+ record.receiptQty = toDouble(getMapValue(row, "receiptQty"));
|
|
|
+ record.planQty = toDouble(getMapValue(row, "planQty"));
|
|
|
+ data.mergedRecords.add(record);
|
|
|
+ }
|
|
|
+ } catch (Exception ignored) {
|
|
|
+ return new SupplyData();
|
|
|
+ }
|
|
|
+ return data;
|
|
|
+ }
|
|
|
+
|
|
|
+ private boolean hasDatabaseData(SupplyData data) {
|
|
|
+ return data != null
|
|
|
+ && (!data.orders.isEmpty()
|
|
|
+ || !data.receipts.isEmpty()
|
|
|
+ || !data.mergedRecords.isEmpty()
|
|
|
+ || !data.termDays.isEmpty());
|
|
|
+ }
|
|
|
+
|
|
|
+ private SupplyData loadExcelData() {
|
|
|
+ SupplyData data = new SupplyData();
|
|
|
+ Path base = resolveDataPath();
|
|
|
+ if (base == null || !Files.exists(base)) {
|
|
|
+ return data;
|
|
|
+ }
|
|
|
+ data.termDays.putAll(loadTerms(base.resolve("供应商账期.xlsx")));
|
|
|
+ data.receipts.addAll(loadReceipts(base.resolve("采购入库统计.xlsx")));
|
|
|
+ data.orders.addAll(loadOrders(base.resolve("采购订单统计01.xlsx")));
|
|
|
+ if (data.orders.isEmpty()) {
|
|
|
+ data.orders.addAll(loadOrders(base.resolve("采购订单统计.xlsx")));
|
|
|
+ }
|
|
|
+ data.mergedRecords.addAll(loadMerged(base.resolve("采购数据_双键合并结果.xlsx")));
|
|
|
+ return data;
|
|
|
+ }
|
|
|
+
|
|
|
+ private long safeLong(Long value) {
|
|
|
+ return value == null ? 0L : value;
|
|
|
+ }
|
|
|
+
|
|
|
+ private Object getMapValue(Map<String, Object> row, String key) {
|
|
|
+ if (row == null || key == null) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ if (row.containsKey(key)) {
|
|
|
+ return row.get(key);
|
|
|
+ }
|
|
|
+ String lowerKey = key.toLowerCase(Locale.ROOT);
|
|
|
+ String upperKey = key.toUpperCase(Locale.ROOT);
|
|
|
+ if (row.containsKey(lowerKey)) {
|
|
|
+ return row.get(lowerKey);
|
|
|
+ }
|
|
|
+ if (row.containsKey(upperKey)) {
|
|
|
+ return row.get(upperKey);
|
|
|
+ }
|
|
|
+ for (Map.Entry<String, Object> entry : row.entrySet()) {
|
|
|
+ if (key.equalsIgnoreCase(entry.getKey())) {
|
|
|
+ return entry.getValue();
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ private String stringValue(Object value) {
|
|
|
+ return value == null ? "" : String.valueOf(value).trim();
|
|
|
+ }
|
|
|
+
|
|
|
+ private int toInteger(Object value) {
|
|
|
+ if (value instanceof Number) {
|
|
|
+ return ((Number) value).intValue();
|
|
|
+ }
|
|
|
+ String text = stringValue(value);
|
|
|
+ if (text.isEmpty()) {
|
|
|
+ return 0;
|
|
|
+ }
|
|
|
+ try {
|
|
|
+ return Integer.parseInt(text);
|
|
|
+ } catch (NumberFormatException ignored) {
|
|
|
+ return 0;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private long safeLongValue(Object value) {
|
|
|
+ if (value instanceof Number) {
|
|
|
+ return ((Number) value).longValue();
|
|
|
+ }
|
|
|
+ String text = stringValue(value);
|
|
|
+ if (text.isEmpty()) {
|
|
|
+ return 0L;
|
|
|
+ }
|
|
|
+ try {
|
|
|
+ return Long.parseLong(text);
|
|
|
+ } catch (NumberFormatException ignored) {
|
|
|
+ return 0L;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private double readWeight(Map<String, Object> payload, String... keys) {
|
|
|
+ for (String key : keys) {
|
|
|
+ Object value = getMapValue(payload, key);
|
|
|
+ if (value != null) {
|
|
|
+ return Math.max(0D, Math.min(1D, toDouble(value)));
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return DEFAULT_SCORE_WEIGHTS.getOrDefault(keys[0], 0D);
|
|
|
+ }
|
|
|
+
|
|
|
+ @SuppressWarnings("unchecked")
|
|
|
+ private Map<String, Double> readScoreRules(Map<String, Object> payload) {
|
|
|
+ Map<String, Object> rulesPayload = null;
|
|
|
+ Object rulesValue = getMapValue(payload, "rules");
|
|
|
+ if (rulesValue instanceof Map<?, ?>) {
|
|
|
+ rulesPayload = (Map<String, Object>) rulesValue;
|
|
|
+ }
|
|
|
+ Map<String, Double> next = new LinkedHashMap<>();
|
|
|
+ for (Map.Entry<String, Double> entry : DEFAULT_SCORE_RULES.entrySet()) {
|
|
|
+ next.put(entry.getKey(), readRuleValue(payload, rulesPayload, entry.getKey(), entry.getValue()));
|
|
|
+ }
|
|
|
+
|
|
|
+ double deliveryTotal = next.get("deliveryOnTimeWeight") + next.get("deliveryFulfillmentWeight");
|
|
|
+ if (Math.abs(deliveryTotal - 1D) > 0.001D) {
|
|
|
+ throw new IllegalArgumentException("交付子项权重总和必须为1");
|
|
|
+ }
|
|
|
+ return next;
|
|
|
+ }
|
|
|
+
|
|
|
+ private double readRuleValue(Map<String, Object> payload, Map<String, Object> rulesPayload, String key, double fallback) {
|
|
|
+ Object value = getMapValue(payload, key);
|
|
|
+ if (value == null && rulesPayload != null) {
|
|
|
+ value = getMapValue(rulesPayload, key);
|
|
|
+ }
|
|
|
+ if (value == null) {
|
|
|
+ return fallback;
|
|
|
+ }
|
|
|
+ return Math.max(0D, toDouble(value));
|
|
|
+ }
|
|
|
+
|
|
|
+ private Map<String, Integer> loadTerms(Path file) {
|
|
|
+ if (!Files.exists(file)) {
|
|
|
+ return Collections.emptyMap();
|
|
|
+ }
|
|
|
+ Map<String, Integer> result = new HashMap<>();
|
|
|
+ ExcelUtils.processSheet(file, 0, -1, new ExcelUtils.SheetHandler() {
|
|
|
+ int supplierNameIndex;
|
|
|
+ int termIndex;
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public void onHeader(List<String> headers) {
|
|
|
+ supplierNameIndex = findHeaderIndex(headers, "供应商名称");
|
|
|
+ termIndex = findHeaderIndex(headers, "结算期限", "账期");
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public void onRow(List<Object> row) {
|
|
|
+ String supplierName = stringAt(row, supplierNameIndex);
|
|
|
+ if (!supplierName.isEmpty()) {
|
|
|
+ result.put(supplierName, parseTermDays(stringAt(row, termIndex)));
|
|
|
+ }
|
|
|
+ }
|
|
|
+ });
|
|
|
+ return result;
|
|
|
+ }
|
|
|
+
|
|
|
+ private List<ReceiptRecord> loadReceipts(Path file) {
|
|
|
+ if (!Files.exists(file)) {
|
|
|
+ return Collections.emptyList();
|
|
|
+ }
|
|
|
+ List<ReceiptRecord> result = new ArrayList<>();
|
|
|
+ ExcelUtils.processSheet(file, 0, -1, new ExcelUtils.SheetHandler() {
|
|
|
+ int businessDateIndex;
|
|
|
+ int acceptanceDateIndex;
|
|
|
+ int supplierCodeIndex;
|
|
|
+ int supplierNameIndex;
|
|
|
+ int amountIndex;
|
|
|
+ int quantityIndex;
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public void onHeader(List<String> headers) {
|
|
|
+ businessDateIndex = findHeaderIndex(headers, "业务日期");
|
|
|
+ acceptanceDateIndex = findHeaderIndex(headers, "实际验收日期");
|
|
|
+ supplierCodeIndex = findHeaderIndex(headers, "供应商代码");
|
|
|
+ supplierNameIndex = findHeaderIndex(headers, "供应商名称");
|
|
|
+ amountIndex = findHeaderIndex(headers, "实际金额", "成本金额", "金额");
|
|
|
+ quantityIndex = findHeaderIndex(headers, "实际入库数量", "入库数量", "数量");
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public void onRow(List<Object> row) {
|
|
|
+ ReceiptRecord record = new ReceiptRecord();
|
|
|
+ record.businessDate = parseDateObject(valueAt(row, businessDateIndex));
|
|
|
+ record.acceptanceDate = parseDateObject(valueAt(row, acceptanceDateIndex));
|
|
|
+ record.supplierCode = stringAt(row, supplierCodeIndex);
|
|
|
+ record.supplierName = stringAt(row, supplierNameIndex);
|
|
|
+ record.amount = numberAt(row, amountIndex);
|
|
|
+ record.quantity = numberAt(row, quantityIndex);
|
|
|
+ result.add(record);
|
|
|
+ }
|
|
|
+ });
|
|
|
+ return result;
|
|
|
+ }
|
|
|
+
|
|
|
+ private List<OrderRecord> loadOrders(Path file) {
|
|
|
+ if (!Files.exists(file)) {
|
|
|
+ return Collections.emptyList();
|
|
|
+ }
|
|
|
+ List<OrderRecord> result = new ArrayList<>();
|
|
|
+ ExcelUtils.processSheet(file, 0, -1, new ExcelUtils.SheetHandler() {
|
|
|
+ int businessDateIndex;
|
|
|
+ int documentIdIndex;
|
|
|
+ int supplierCodeIndex;
|
|
|
+ int supplierNameIndex;
|
|
|
+ int quantityIndex;
|
|
|
+ int completedIndex;
|
|
|
+ int uncompletedIndex;
|
|
|
+ int amountIndex;
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public void onHeader(List<String> headers) {
|
|
|
+ businessDateIndex = findHeaderIndex(headers, "业务日期");
|
|
|
+ documentIdIndex = findHeaderIndex(headers, "单据ID", "联系单据");
|
|
|
+ supplierCodeIndex = findHeaderIndex(headers, "供应商代码");
|
|
|
+ supplierNameIndex = findHeaderIndex(headers, "供应商名称");
|
|
|
+ quantityIndex = findHeaderIndex(headers, "订单计划数量", "数量");
|
|
|
+ completedIndex = findHeaderIndex(headers, "完工数");
|
|
|
+ uncompletedIndex = findHeaderIndex(headers, "未完工数");
|
|
|
+ amountIndex = findHeaderIndex(headers, "实际金额", "选定金额");
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public void onRow(List<Object> row) {
|
|
|
+ OrderRecord record = new OrderRecord();
|
|
|
+ record.businessDate = parseDateObject(valueAt(row, businessDateIndex));
|
|
|
+ record.documentId = stringAt(row, documentIdIndex);
|
|
|
+ record.supplierCode = stringAt(row, supplierCodeIndex);
|
|
|
+ record.supplierName = stringAt(row, supplierNameIndex);
|
|
|
+ record.orderQty = numberAt(row, quantityIndex);
|
|
|
+ record.completedQty = numberAt(row, completedIndex);
|
|
|
+ record.uncompletedQty = numberAt(row, uncompletedIndex);
|
|
|
+ record.amount = numberAt(row, amountIndex);
|
|
|
+ result.add(record);
|
|
|
+ }
|
|
|
+ });
|
|
|
+ return result;
|
|
|
+ }
|
|
|
+
|
|
|
+ private List<MergedRecord> loadMerged(Path file) {
|
|
|
+ if (!Files.exists(file)) {
|
|
|
+ return Collections.emptyList();
|
|
|
+ }
|
|
|
+ List<MergedRecord> result = new ArrayList<>();
|
|
|
+ ExcelUtils.processSheet(file, 0, -1, new ExcelUtils.SheetHandler() {
|
|
|
+ int acceptanceDateIndex;
|
|
|
+ int supplierCodeIndex;
|
|
|
+ int supplierNameIndex;
|
|
|
+ int receiptQtyIndex;
|
|
|
+ int planQtyIndex;
|
|
|
+ int planDeliveryIndex;
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public void onHeader(List<String> headers) {
|
|
|
+ acceptanceDateIndex = findHeaderIndex(headers, "实际验收日期");
|
|
|
+ supplierCodeIndex = findHeaderIndex(headers, "供应商代码");
|
|
|
+ supplierNameIndex = findHeaderIndex(headers, "供应商名称");
|
|
|
+ receiptQtyIndex = findHeaderIndex(headers, "实际入库数量");
|
|
|
+ planQtyIndex = findHeaderIndex(headers, "订单计划数量", "数量");
|
|
|
+ planDeliveryIndex = findHeaderIndex(headers, "计划交货日期", "交货日期");
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public void onRow(List<Object> row) {
|
|
|
+ MergedRecord record = new MergedRecord();
|
|
|
+ record.acceptanceDate = parseDateObject(valueAt(row, acceptanceDateIndex));
|
|
|
+ record.supplierCode = stringAt(row, supplierCodeIndex);
|
|
|
+ record.supplierName = stringAt(row, supplierNameIndex);
|
|
|
+ record.receiptQty = numberAt(row, receiptQtyIndex);
|
|
|
+ record.planQty = numberAt(row, planQtyIndex);
|
|
|
+ record.planDeliveryDate = parseDateObject(valueAt(row, planDeliveryIndex));
|
|
|
+ result.add(record);
|
|
|
+ }
|
|
|
+ });
|
|
|
+ return result;
|
|
|
+ }
|
|
|
+
|
|
|
+ private Path resolveDataPath() {
|
|
|
+ String env = Optional.ofNullable(System.getenv("SUPPLY_DATA_PATH"))
|
|
|
+ .filter(value -> !value.trim().isEmpty())
|
|
|
+ .orElseGet(() -> Optional.ofNullable(System.getenv("DTM_SUPPLY_DATA_PATH")).orElse(""));
|
|
|
+ Path configured = existingPath(env);
|
|
|
+ if (configured != null) {
|
|
|
+ return configured;
|
|
|
+ }
|
|
|
+ configured = existingPath(supplyDataPath);
|
|
|
+ if (configured != null) {
|
|
|
+ return configured;
|
|
|
+ }
|
|
|
+ Path direct = Paths.get(System.getProperty("user.dir"), "data", "supply");
|
|
|
+ if (Files.exists(direct)) {
|
|
|
+ return direct;
|
|
|
+ }
|
|
|
+ return findDesktopSupplyPath();
|
|
|
+ }
|
|
|
+
|
|
|
+ private Path resolveQaFile() {
|
|
|
+ String env = Optional.ofNullable(System.getenv("SUPPLY_QA_DATA_PATH"))
|
|
|
+ .filter(value -> !value.trim().isEmpty())
|
|
|
+ .orElseGet(() -> Optional.ofNullable(System.getenv("DTM_SUPPLY_QA_DATA_PATH")).orElse(""));
|
|
|
+ Path configured = existingPath(env);
|
|
|
+ if (configured != null) {
|
|
|
+ return configured;
|
|
|
+ }
|
|
|
+ configured = existingPath(supplyQaDataPath);
|
|
|
+ if (configured != null) {
|
|
|
+ return configured;
|
|
|
+ }
|
|
|
+ Path base = resolveDataPath();
|
|
|
+ if (base != null) {
|
|
|
+ Path file = base.resolve("2026年每日QA登记表.xlsx");
|
|
|
+ if (Files.exists(file)) {
|
|
|
+ return file;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ Path direct = Paths.get(System.getProperty("user.dir"), "data", "supply", "2026年每日QA登记表.xlsx");
|
|
|
+ return Files.exists(direct) ? direct : null;
|
|
|
+ }
|
|
|
+
|
|
|
+ private Path existingPath(String value) {
|
|
|
+ if (value == null || value.trim().isEmpty()) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ Path path = Paths.get(value.trim());
|
|
|
+ return Files.exists(path) ? path : null;
|
|
|
+ }
|
|
|
+
|
|
|
+ private Path findDesktopSupplyPath() {
|
|
|
+ try {
|
|
|
+ Path desktop = Paths.get(System.getProperty("user.home"), "Desktop");
|
|
|
+ if (!Files.isDirectory(desktop)) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ try (java.util.stream.Stream<Path> stream = Files.list(desktop)) {
|
|
|
+ return stream.map(path -> path.resolve(Paths.get("gongying", "dtm_python", "data")))
|
|
|
+ .filter(Files::exists)
|
|
|
+ .findFirst()
|
|
|
+ .orElse(null);
|
|
|
+ }
|
|
|
+ } catch (Exception ignored) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private int findHeaderIndex(List<String> headers, String... keywords) {
|
|
|
+ if (headers == null || keywords == null) {
|
|
|
+ return -1;
|
|
|
+ }
|
|
|
+ for (int i = 0; i < headers.size(); i++) {
|
|
|
+ String header = normalize(headers.get(i));
|
|
|
+ for (String keyword : keywords) {
|
|
|
+ if (header.contains(normalize(keyword))) {
|
|
|
+ return i;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return -1;
|
|
|
+ }
|
|
|
+
|
|
|
+ private Object valueAt(List<Object> row, int index) {
|
|
|
+ return row != null && index >= 0 && index < row.size() ? row.get(index) : null;
|
|
|
+ }
|
|
|
+
|
|
|
+ private String stringAt(List<Object> row, int index) {
|
|
|
+ Object value = valueAt(row, index);
|
|
|
+ return value == null ? "" : String.valueOf(value).trim();
|
|
|
+ }
|
|
|
+
|
|
|
+ private double numberAt(List<Object> row, int index) {
|
|
|
+ return toDouble(valueAt(row, index));
|
|
|
+ }
|
|
|
+
|
|
|
+ private Object cellRawValue(Row row, int index, DataFormatter formatter) {
|
|
|
+ if (row == null || index < 0) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ org.apache.poi.ss.usermodel.Cell cell = row.getCell(index);
|
|
|
+ if (cell == null) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ if (cell.getCellType() == org.apache.poi.ss.usermodel.CellType.NUMERIC) {
|
|
|
+ if (org.apache.poi.ss.usermodel.DateUtil.isCellDateFormatted(cell)) {
|
|
|
+ Date date = cell.getDateCellValue();
|
|
|
+ return date == null ? null : new Date(date.getTime());
|
|
|
+ }
|
|
|
+ return cell.getNumericCellValue();
|
|
|
+ }
|
|
|
+ return formatter.formatCellValue(cell).trim();
|
|
|
+ }
|
|
|
+
|
|
|
+ private String cellString(Row row, int index, DataFormatter formatter) {
|
|
|
+ Object value = cellRawValue(row, index, formatter);
|
|
|
+ return value == null ? "" : String.valueOf(value).trim();
|
|
|
+ }
|
|
|
+
|
|
|
+ private double cellNumber(Row row, int index, DataFormatter formatter) {
|
|
|
+ return toDouble(cellRawValue(row, index, formatter));
|
|
|
+ }
|
|
|
+
|
|
|
+ private double toDouble(Object value) {
|
|
|
+ if (value == null) {
|
|
|
+ return 0D;
|
|
|
+ }
|
|
|
+ if (value instanceof Number) {
|
|
|
+ return ((Number) value).doubleValue();
|
|
|
+ }
|
|
|
+ String text = String.valueOf(value).replace(",", "").replace("¥", "").trim();
|
|
|
+ if (text.isEmpty()) {
|
|
|
+ return 0D;
|
|
|
+ }
|
|
|
+ try {
|
|
|
+ return Double.parseDouble(text);
|
|
|
+ } catch (NumberFormatException ignored) {
|
|
|
+ return 0D;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private LocalDate parseDate(String value) {
|
|
|
+ if (value == null || value.trim().isEmpty()) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ return parseDateObject(value);
|
|
|
+ }
|
|
|
+
|
|
|
+ private LocalDate parseDateObject(Object value) {
|
|
|
+ if (value == null) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ if (value instanceof LocalDate) {
|
|
|
+ return (LocalDate) value;
|
|
|
+ }
|
|
|
+ if (value instanceof java.sql.Date) {
|
|
|
+ return ((java.sql.Date) value).toLocalDate();
|
|
|
+ }
|
|
|
+ if (value instanceof Date) {
|
|
|
+ return ((Date) value).toInstant().atZone(ZoneId.systemDefault()).toLocalDate();
|
|
|
+ }
|
|
|
+ if (value instanceof Number) {
|
|
|
+ try {
|
|
|
+ return org.apache.poi.ss.usermodel.DateUtil.getJavaDate(((Number) value).doubleValue())
|
|
|
+ .toInstant().atZone(ZoneId.systemDefault()).toLocalDate();
|
|
|
+ } catch (Exception ignored) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ String text = String.valueOf(value).trim();
|
|
|
+ if (text.length() > 10) {
|
|
|
+ text = text.substring(0, 10);
|
|
|
+ }
|
|
|
+ try {
|
|
|
+ return LocalDate.parse(text, YMD);
|
|
|
+ } catch (DateTimeParseException ignored) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private int parseTermDays(String value) {
|
|
|
+ String digits = value == null ? "" : value.replaceAll("[^0-9]", "");
|
|
|
+ if (digits.isEmpty()) {
|
|
|
+ return 0;
|
|
|
+ }
|
|
|
+ try {
|
|
|
+ return Integer.parseInt(digits);
|
|
|
+ } catch (NumberFormatException ignored) {
|
|
|
+ return 0;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private boolean inRange(LocalDate date, LocalDate start, LocalDate end) {
|
|
|
+ if (date == null) {
|
|
|
+ return start == null && end == null;
|
|
|
+ }
|
|
|
+ if (start != null && date.isBefore(start)) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ return end == null || !date.isAfter(end);
|
|
|
+ }
|
|
|
+
|
|
|
+ private String paymentStatus(LocalDate dueDate) {
|
|
|
+ if (dueDate == null) {
|
|
|
+ return "未计算";
|
|
|
+ }
|
|
|
+ LocalDate today = LocalDate.now();
|
|
|
+ if (dueDate.isBefore(today)) {
|
|
|
+ return "已到期";
|
|
|
+ }
|
|
|
+ if (!dueDate.isAfter(today.plusDays(7))) {
|
|
|
+ return "7天内到期";
|
|
|
+ }
|
|
|
+ return "待付款";
|
|
|
+ }
|
|
|
+
|
|
|
+ private String formatDate(LocalDate date) {
|
|
|
+ return date == null ? "" : YMD.format(date);
|
|
|
+ }
|
|
|
+
|
|
|
+ private String normalize(String value) {
|
|
|
+ return value == null ? "" : value.trim().toLowerCase(Locale.ROOT);
|
|
|
+ }
|
|
|
+
|
|
|
+ private double round(double value, int scale) {
|
|
|
+ double factor = Math.pow(10, scale);
|
|
|
+ return Math.round(value * factor) / factor;
|
|
|
+ }
|
|
|
+
|
|
|
+ private double costScore(int rank) {
|
|
|
+ Map<String, Double> rules = scoreRules;
|
|
|
+ if (rank == 1) {
|
|
|
+ return rules.getOrDefault("costRank1Score", 100D);
|
|
|
+ }
|
|
|
+ if (rank == 2) {
|
|
|
+ return rules.getOrDefault("costRank2Score", 90D);
|
|
|
+ }
|
|
|
+ if (rank == 3) {
|
|
|
+ return rules.getOrDefault("costRank3Score", 80D);
|
|
|
+ }
|
|
|
+ if (rank == 4) {
|
|
|
+ return rules.getOrDefault("costRank4Score", 60D);
|
|
|
+ }
|
|
|
+ return 0D;
|
|
|
+ }
|
|
|
+
|
|
|
+ private double deliveryScore(ProductSupplierEval row) {
|
|
|
+ if (row.matchedLines == 0) {
|
|
|
+ return 0D;
|
|
|
+ }
|
|
|
+ double onTimeRate = row.onTimeLines * 1D / row.matchedLines;
|
|
|
+ double quantityRate = row.planQty > 0 ? Math.min(1D, row.receiptQtyForDelivery / row.planQty) : 0D;
|
|
|
+ Map<String, Double> rules = scoreRules;
|
|
|
+ return onTimeRate * 100D * rules.getOrDefault("deliveryOnTimeWeight", 0.70D)
|
|
|
+ + quantityRate * 100D * rules.getOrDefault("deliveryFulfillmentWeight", 0.30D);
|
|
|
+ }
|
|
|
+
|
|
|
+ private double termScore(int termDays) {
|
|
|
+ Map<String, Double> rules = scoreRules;
|
|
|
+ if (termDays >= rules.getOrDefault("termExcellentDays", 90D)) {
|
|
|
+ return rules.getOrDefault("termExcellentScore", 100D);
|
|
|
+ }
|
|
|
+ if (termDays >= rules.getOrDefault("termGoodDays", 60D)) {
|
|
|
+ return rules.getOrDefault("termGoodScore", 90D);
|
|
|
+ }
|
|
|
+ if (termDays >= rules.getOrDefault("termNormalDays", 45D)) {
|
|
|
+ return rules.getOrDefault("termNormalScore", 80D);
|
|
|
+ }
|
|
|
+ if (termDays >= rules.getOrDefault("termShortDays", 30D)) {
|
|
|
+ return rules.getOrDefault("termShortScore", 60D);
|
|
|
+ }
|
|
|
+ return rules.getOrDefault("termDefaultScore", 40D);
|
|
|
+ }
|
|
|
+
|
|
|
+ private String termLevel(int termDays) {
|
|
|
+ if (termDays >= 90) {
|
|
|
+ return "优秀";
|
|
|
+ }
|
|
|
+ if (termDays >= 60) {
|
|
|
+ return "良好";
|
|
|
+ }
|
|
|
+ if (termDays >= 30) {
|
|
|
+ return "一般";
|
|
|
+ }
|
|
|
+ if (termDays > 0) {
|
|
|
+ return "偏短";
|
|
|
+ }
|
|
|
+ return "缺少账期";
|
|
|
+ }
|
|
|
+
|
|
|
+ private String deliveryStatus(double onTimeRate, double fulfillmentRate, double maxDelayDays) {
|
|
|
+ if (fulfillmentRate < 80D || maxDelayDays >= 30D) {
|
|
|
+ return "严重异常";
|
|
|
+ }
|
|
|
+ if (fulfillmentRate < 95D) {
|
|
|
+ return "数量不足";
|
|
|
+ }
|
|
|
+ if (onTimeRate < 80D || maxDelayDays > 0D) {
|
|
|
+ return "存在延迟";
|
|
|
+ }
|
|
|
+ return "正常";
|
|
|
+ }
|
|
|
+
|
|
|
+ private String deliveryRiskLevel(double onTimeRate, double fulfillmentRate, double maxDelayDays) {
|
|
|
+ if (fulfillmentRate < 80D || onTimeRate < 60D || maxDelayDays >= 30D) {
|
|
|
+ return "高";
|
|
|
+ }
|
|
|
+ if (fulfillmentRate < 95D || onTimeRate < 90D || maxDelayDays > 0D) {
|
|
|
+ return "中";
|
|
|
+ }
|
|
|
+ return "低";
|
|
|
+ }
|
|
|
+
|
|
|
+ private String supplierLevel(double totalScore) {
|
|
|
+ if (totalScore >= 85D) {
|
|
|
+ return "A";
|
|
|
+ }
|
|
|
+ if (totalScore >= 70D) {
|
|
|
+ return "B";
|
|
|
+ }
|
|
|
+ if (totalScore >= 60D) {
|
|
|
+ return "C";
|
|
|
+ }
|
|
|
+ return "预警";
|
|
|
+ }
|
|
|
+
|
|
|
+ private String supplierWarningTag(double totalScore, double deliveryRate, double completionRate, double qualityPassRate, double qaSampleQty) {
|
|
|
+ if (totalScore < 60D) {
|
|
|
+ return "综合分预警";
|
|
|
+ }
|
|
|
+ if (qaSampleQty > 0D && qualityPassRate < scoreRules.getOrDefault("qualityWarningRate", 0.95D)) {
|
|
|
+ return "质量预警";
|
|
|
+ }
|
|
|
+ if (deliveryRate < 0.6D) {
|
|
|
+ return "交付预警";
|
|
|
+ }
|
|
|
+ if (completionRate < 0.8D) {
|
|
|
+ return "完成率预警";
|
|
|
+ }
|
|
|
+ return "正常";
|
|
|
+ }
|
|
|
+
|
|
|
+ private String paymentSuggestion(PaymentMonthAgg item) {
|
|
|
+ if (item.overdueAmount > 0D) {
|
|
|
+ return "优先付款";
|
|
|
+ }
|
|
|
+ if (item.dueSoonAmount > 0D) {
|
|
|
+ return "本期安排";
|
|
|
+ }
|
|
|
+ return "暂缓观察";
|
|
|
+ }
|
|
|
+
|
|
|
+ private static class CacheEntry {
|
|
|
+ final long loadedAt;
|
|
|
+ final SupplyData data;
|
|
|
+
|
|
|
+ CacheEntry(long loadedAt, SupplyData data) {
|
|
|
+ this.loadedAt = loadedAt;
|
|
|
+ this.data = data;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private static class SupplyData {
|
|
|
+ final Map<String, Integer> termDays = new HashMap<>();
|
|
|
+ final List<ReceiptRecord> receipts = new ArrayList<>();
|
|
|
+ final List<OrderRecord> orders = new ArrayList<>();
|
|
|
+ final List<MergedRecord> mergedRecords = new ArrayList<>();
|
|
|
+ }
|
|
|
+
|
|
|
+ private static class ReceiptRecord {
|
|
|
+ LocalDate businessDate;
|
|
|
+ LocalDate acceptanceDate;
|
|
|
+ String receiptId = "";
|
|
|
+ String supplierCode = "";
|
|
|
+ String supplierName = "";
|
|
|
+ String sku = "";
|
|
|
+ double amount;
|
|
|
+ double quantity;
|
|
|
+ }
|
|
|
+
|
|
|
+ private static class OrderRecord {
|
|
|
+ LocalDate businessDate;
|
|
|
+ LocalDate deliveryDate;
|
|
|
+ String documentId = "";
|
|
|
+ String supplierCode = "";
|
|
|
+ String supplierName = "";
|
|
|
+ double orderQty;
|
|
|
+ double completedQty;
|
|
|
+ double uncompletedQty;
|
|
|
+ double amount;
|
|
|
+ }
|
|
|
+
|
|
|
+ private static class MergedRecord {
|
|
|
+ LocalDate acceptanceDate;
|
|
|
+ LocalDate planDeliveryDate;
|
|
|
+ String sku = "";
|
|
|
+ String productName = "";
|
|
|
+ String supplierCode = "";
|
|
|
+ String supplierName = "";
|
|
|
+ double receiptQty;
|
|
|
+ double planQty;
|
|
|
+ }
|
|
|
+
|
|
|
+ private static class ProductSupplierEval {
|
|
|
+ String supplierCode;
|
|
|
+ String supplierName;
|
|
|
+ int costRank;
|
|
|
+ int matchedLines;
|
|
|
+ int onTimeLines;
|
|
|
+ double receiptAmount;
|
|
|
+ double receiptQty;
|
|
|
+ double receiptQtyForDelivery;
|
|
|
+ double planQty;
|
|
|
+ double costScore;
|
|
|
+ double deliveryScore;
|
|
|
+ double termScore;
|
|
|
+ double totalScore;
|
|
|
+
|
|
|
+ ProductSupplierEval(String supplierCode, String supplierName) {
|
|
|
+ this.supplierCode = supplierCode;
|
|
|
+ this.supplierName = supplierName;
|
|
|
+ }
|
|
|
+
|
|
|
+ double referencePrice() {
|
|
|
+ return receiptQty == 0D ? 0D : receiptAmount / receiptQty;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private static class SupplierStats {
|
|
|
+ final Set<String> orderIds = new HashSet<>();
|
|
|
+ String supplierCode;
|
|
|
+ String supplierName;
|
|
|
+ int termDays;
|
|
|
+ int orderLines;
|
|
|
+ int receiptLines;
|
|
|
+ int matchedLines;
|
|
|
+ int onTimeLines;
|
|
|
+ double orderQty;
|
|
|
+ double completedQty;
|
|
|
+ double uncompletedQty;
|
|
|
+ double orderAmount;
|
|
|
+ double receiptQty;
|
|
|
+ double receiptAmount;
|
|
|
+ double matchedPlanQty;
|
|
|
+ double matchedReceiptQty;
|
|
|
+ double costScoreTotal;
|
|
|
+ int costScoreCount;
|
|
|
+ double qaIncomingQty;
|
|
|
+ double qaSampleQty;
|
|
|
+ double qaDefectQty;
|
|
|
+ int qaLines;
|
|
|
+
|
|
|
+ SupplierStats(String supplierCode, String supplierName) {
|
|
|
+ this.supplierCode = supplierCode;
|
|
|
+ this.supplierName = supplierName;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private static class QaRecord {
|
|
|
+ LocalDate inspectDate;
|
|
|
+ String supplierName;
|
|
|
+ double incomingQty;
|
|
|
+ double sampleQty;
|
|
|
+ double defectQty;
|
|
|
+ }
|
|
|
+
|
|
|
+ private static class CostAgg {
|
|
|
+ final String supplierName;
|
|
|
+ double amount;
|
|
|
+ double qty;
|
|
|
+
|
|
|
+ CostAgg(String supplierName) {
|
|
|
+ this.supplierName = supplierName;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private static class ReceiptMonthAgg {
|
|
|
+ final String month;
|
|
|
+ final String supplierCode;
|
|
|
+ final String supplierName;
|
|
|
+ final Set<String> receiptIds = new HashSet<>();
|
|
|
+ double receiptQty;
|
|
|
+ double receiptAmount;
|
|
|
+ int receiptLines;
|
|
|
+
|
|
|
+ ReceiptMonthAgg(String month, String supplierCode, String supplierName) {
|
|
|
+ this.month = month;
|
|
|
+ this.supplierCode = supplierCode;
|
|
|
+ this.supplierName = supplierName;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private static class PaymentMonthAgg {
|
|
|
+ final String month;
|
|
|
+ int planCount;
|
|
|
+ int overdueCount;
|
|
|
+ int dueSoonCount;
|
|
|
+ int receiptLines;
|
|
|
+ double amount;
|
|
|
+ double overdueAmount;
|
|
|
+ double dueSoonAmount;
|
|
|
+ double receiptQty;
|
|
|
+
|
|
|
+ PaymentMonthAgg(String month) {
|
|
|
+ this.month = month;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private static class PaymentAgg {
|
|
|
+ String supplierCode;
|
|
|
+ String supplierName;
|
|
|
+ int termDays;
|
|
|
+ LocalDate dueDate;
|
|
|
+ int receiptLines;
|
|
|
+ double receiptAmount;
|
|
|
+ double receiptQty;
|
|
|
+
|
|
|
+ PaymentAgg(String supplierCode, String supplierName, int termDays, LocalDate dueDate) {
|
|
|
+ this.supplierCode = supplierCode;
|
|
|
+ this.supplierName = supplierName;
|
|
|
+ this.termDays = termDays;
|
|
|
+ this.dueDate = dueDate;
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|