@chenchaolong/plugin-trade-compliance-workbench 1.0.50 → 1.0.52
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/lib/analytics-export.d.ts +7 -0
- package/dist/lib/analytics-export.d.ts.map +1 -0
- package/dist/lib/analytics-export.js +445 -0
- package/dist/lib/analytics-export.js.map +1 -0
- package/dist/lib/analytics-query.d.ts +43 -0
- package/dist/lib/analytics-query.d.ts.map +1 -0
- package/dist/lib/analytics-query.js +710 -0
- package/dist/lib/analytics-query.js.map +1 -0
- package/dist/lib/constants.d.ts +1 -0
- package/dist/lib/constants.d.ts.map +1 -1
- package/dist/lib/constants.js +1 -0
- package/dist/lib/constants.js.map +1 -1
- package/dist/lib/import-jobs/import-command.builder.js +2 -2
- package/dist/lib/import-jobs/import-command.builder.js.map +1 -1
- package/dist/lib/import-jobs/import-job-callback.processor.d.ts.map +1 -1
- package/dist/lib/import-jobs/import-job-callback.processor.js +2 -0
- package/dist/lib/import-jobs/import-job-callback.processor.js.map +1 -1
- package/dist/lib/remote-components/trade-compliance-workbench/app.css +1 -1
- package/dist/lib/remote-components/trade-compliance-workbench/app.js +5 -5
- package/dist/lib/remote-components/trade-compliance-workbench/editor-source.mjs +17 -1
- package/dist/lib/remote-components/trade-compliance-workbench/editor.js +100 -100
- package/dist/lib/remote-ui/app-source.js +11 -3
- package/dist/lib/remote-ui/app-source.js.map +1 -1
- package/dist/lib/remote-ui/contract-import.d.ts.map +1 -1
- package/dist/lib/remote-ui/contract-import.js +3 -1
- package/dist/lib/remote-ui/contract-import.js.map +1 -1
- package/dist/lib/remote-ui/contracts.d.ts +10 -0
- package/dist/lib/remote-ui/contracts.d.ts.map +1 -1
- package/dist/lib/remote-ui/pages/analytics-dashboard.js +5 -5
- package/dist/lib/remote-ui/pages/analytics-dashboard.js.map +1 -1
- package/dist/lib/remote-ui/pages/analytics-detail-list.js +1 -1
- package/dist/lib/remote-ui/pages/analytics-detail-list.js.map +1 -1
- package/dist/lib/remote-ui/pages/analytics-format.js +4 -4
- package/dist/lib/remote-ui/pages/analytics-format.js.map +1 -1
- package/dist/lib/remote-ui/pages/catalog-import-history.d.ts +18 -0
- package/dist/lib/remote-ui/pages/catalog-import-history.d.ts.map +1 -0
- package/dist/lib/remote-ui/pages/catalog-import-history.js +38 -0
- package/dist/lib/remote-ui/pages/catalog-import-history.js.map +1 -0
- package/dist/lib/remote-ui/pages/catalogs.css +110 -0
- package/dist/lib/remote-ui/pages/catalogs.d.ts +7 -4
- package/dist/lib/remote-ui/pages/catalogs.d.ts.map +1 -1
- package/dist/lib/remote-ui/pages/catalogs.js +47 -12
- package/dist/lib/remote-ui/pages/catalogs.js.map +1 -1
- package/dist/lib/remote-ui/pages/sales.d.ts +4 -1
- package/dist/lib/remote-ui/pages/sales.d.ts.map +1 -1
- package/dist/lib/remote-ui/pages/sales.js +9 -4
- package/dist/lib/remote-ui/pages/sales.js.map +1 -1
- package/dist/lib/remote-ui/styles.css +1 -1
- package/dist/lib/sales-file.service.d.ts.map +1 -1
- package/dist/lib/sales-file.service.js +32 -0
- package/dist/lib/sales-file.service.js.map +1 -1
- package/dist/lib/types.d.ts +67 -0
- package/dist/lib/types.d.ts.map +1 -1
- package/dist/lib/types.js.map +1 -1
- package/dist/lib/workbench-view.provider.d.ts +7 -0
- package/dist/lib/workbench-view.provider.d.ts.map +1 -1
- package/dist/lib/workbench-view.provider.js +5 -1
- package/dist/lib/workbench-view.provider.js.map +1 -1
- package/dist/lib/workbench.middleware.js +3 -3
- package/dist/lib/workbench.middleware.js.map +1 -1
- package/dist/lib/workbench.service.d.ts +29 -7
- package/dist/lib/workbench.service.d.ts.map +1 -1
- package/dist/lib/workbench.service.js +97 -466
- package/dist/lib/workbench.service.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,710 @@
|
|
|
1
|
+
import { Decimal } from 'decimal.js';
|
|
2
|
+
import { AnalyticsComplianceCategoryKeys, DomainError, DomainErrorCodes } from './types.js';
|
|
3
|
+
function requiredId(value, label) { const id = String(value ?? '').trim(); if (!id)
|
|
4
|
+
throw new DomainError(DomainErrorCodes.INVALID_INPUT, `缺少${label}`); return id; }
|
|
5
|
+
export function resolveAnalyticsPeriod(query, currentYear) {
|
|
6
|
+
const requested = query.filters.orderDate;
|
|
7
|
+
const yearFrom = `${currentYear}-01-01`;
|
|
8
|
+
const yearTo = `${currentYear}-12-31`;
|
|
9
|
+
return { from: requested?.from ?? yearFrom, to: requested?.to ?? yearTo };
|
|
10
|
+
}
|
|
11
|
+
export function analyticsDashboardStatement(scope, query, currentYear) {
|
|
12
|
+
const parameters = [];
|
|
13
|
+
const parameter = (value) => { parameters.push(value); return `$${parameters.length}`; };
|
|
14
|
+
const tenant = parameter(scope.tenantId);
|
|
15
|
+
const organization = parameter(scope.organizationId);
|
|
16
|
+
const range = resolveAnalyticsPeriod(query, currentYear);
|
|
17
|
+
const from = parameter(range.from);
|
|
18
|
+
const to = parameter(range.to);
|
|
19
|
+
return { sql: `SELECT ${analyticsDashboardJsonSql(tenant, organization, from, to)} AS "dashboardJson"`, parameters };
|
|
20
|
+
}
|
|
21
|
+
function analyticsPurchaseScopeSql(tenant, organization, from, to) {
|
|
22
|
+
return `SELECT purchase.id, purchase."supplierId", supplier.name AS "supplierName", purchase."contractNo", purchase."orderDate",
|
|
23
|
+
purchase."contractAmountCny" AS "purchaseTotalCny", purchase."paidAmountCny" AS "paidCny",
|
|
24
|
+
GREATEST(purchase."contractAmountCny" - purchase."paidAmountCny", 0) AS "unpaidCny",
|
|
25
|
+
purchase."refundedAmountCny" AS "refundedCny",
|
|
26
|
+
GREATEST((SELECT COALESCE(SUM(refund_line."lineAmountTaxIncluded" / (1 + COALESCE(refund_line."vatRate", 0)) * COALESCE(refund_line."refundRate", 0)), 0) FROM plugin_trade_compliance_purchase_order_line refund_line WHERE refund_line."purchaseOrderId" = purchase.id AND refund_line."tenantId" = purchase."tenantId" AND refund_line."organizationId" = purchase."organizationId" AND refund_line."deletedAt" IS NULL) - purchase."refundedAmountCny", 0) AS "unrefundedCny"
|
|
27
|
+
FROM plugin_trade_compliance_purchase_order purchase
|
|
28
|
+
INNER JOIN plugin_trade_compliance_supplier supplier ON supplier.id = purchase."supplierId" AND supplier."tenantId" = purchase."tenantId" AND supplier."organizationId" = purchase."organizationId" AND supplier."deletedAt" IS NULL
|
|
29
|
+
WHERE purchase."tenantId" = ${tenant} AND purchase."organizationId" = ${organization} AND purchase."deletedAt" IS NULL AND purchase."orderDate" BETWEEN ${from} AND ${to}`;
|
|
30
|
+
}
|
|
31
|
+
function analyticsSalesScopeCtesSql(tenant, organization, from, to) {
|
|
32
|
+
return `sales_pairs AS (
|
|
33
|
+
SELECT contract.id AS "customerContractId", purchase.id AS "purchaseOrderId", customer.id AS "customerId", customer.name AS "customerName",
|
|
34
|
+
contract."contractNo", purchase."orderDate", purchase."contractAmountCny" AS "purchaseTotalCny",
|
|
35
|
+
COALESCE(manual."receivedAmountCny", 0) AS "receivedCny",
|
|
36
|
+
COALESCE(manual."pricingProfitRateOverride", customer."defaultPricingProfitRate") AS "effectivePricingProfitRate"
|
|
37
|
+
FROM plugin_trade_compliance_customer_contract contract
|
|
38
|
+
INNER JOIN plugin_trade_compliance_customer customer ON customer.id = contract."customerId" AND customer."tenantId" = contract."tenantId" AND customer."organizationId" = contract."organizationId" AND customer."deletedAt" IS NULL
|
|
39
|
+
INNER JOIN plugin_trade_compliance_purchase_order purchase ON purchase."tenantId" = contract."tenantId" AND purchase."organizationId" = contract."organizationId" AND purchase."deletedAt" IS NULL AND contract."normalizedContractNo" = regexp_replace(purchase."normalizedContractNo", '(-[0-9]+)?C$', '') || 'X'
|
|
40
|
+
LEFT JOIN plugin_trade_compliance_derived_sales_manual manual ON manual."customerContractId" = contract.id AND manual."purchaseOrderId" = purchase.id AND manual."tenantId" = contract."tenantId" AND manual."organizationId" = contract."organizationId" AND manual."deletedAt" IS NULL
|
|
41
|
+
WHERE contract."tenantId" = ${tenant} AND contract."organizationId" = ${organization} AND contract."deletedAt" IS NULL AND purchase."orderDate" BETWEEN ${from} AND ${to}
|
|
42
|
+
AND (manual."pricingProfitRateOverride" IS NOT NULL OR (customer."pricingProfitRateStatus" = 'CONFIGURED' AND customer."defaultPricingProfitRate" IS NOT NULL))
|
|
43
|
+
), sales_line_totals AS (
|
|
44
|
+
SELECT sales_pairs."customerContractId", sales_pairs."purchaseOrderId",
|
|
45
|
+
COALESCE(SUM(ROUND(ROUND(line."unitPriceTaxIncluded" * (1 + sales_pairs."effectivePricingProfitRate"), 0) * line.quantity, 0)), 0) AS "salesTotalCny"
|
|
46
|
+
FROM sales_pairs
|
|
47
|
+
LEFT JOIN plugin_trade_compliance_purchase_order_line line ON line."purchaseOrderId" = sales_pairs."purchaseOrderId" AND line."tenantId" = ${tenant} AND line."organizationId" = ${organization} AND line."deletedAt" IS NULL
|
|
48
|
+
GROUP BY sales_pairs."customerContractId", sales_pairs."purchaseOrderId"
|
|
49
|
+
), latest_sales_files AS (
|
|
50
|
+
SELECT DISTINCT ON (file."customerContractId", file."purchaseOrderId") file."customerContractId", file."purchaseOrderId", file."salesOrderNoSnapshot"
|
|
51
|
+
FROM plugin_trade_compliance_generated_sales_file file
|
|
52
|
+
INNER JOIN sales_pairs ON sales_pairs."customerContractId" = file."customerContractId" AND sales_pairs."purchaseOrderId" = file."purchaseOrderId"
|
|
53
|
+
WHERE file."tenantId" = ${tenant} AND file."organizationId" = ${organization} AND file."deletedAt" IS NULL
|
|
54
|
+
ORDER BY file."customerContractId", file."purchaseOrderId", file."generatedAt" DESC, file.id ASC
|
|
55
|
+
), sales_scope AS (
|
|
56
|
+
SELECT sales_pairs."customerContractId", sales_pairs."purchaseOrderId", sales_pairs."customerId", sales_pairs."customerName",
|
|
57
|
+
COALESCE(latest_sales_files."salesOrderNoSnapshot", sales_pairs."contractNo") AS "salesOrderNo", sales_pairs."orderDate",
|
|
58
|
+
sales_pairs."purchaseTotalCny", sales_line_totals."salesTotalCny", sales_pairs."receivedCny",
|
|
59
|
+
GREATEST(sales_line_totals."salesTotalCny" - sales_pairs."receivedCny", 0) AS "unreceivedCny",
|
|
60
|
+
GREATEST(sales_line_totals."salesTotalCny" - sales_pairs."purchaseTotalCny", 0) AS "estimatedGrossProfitCny"
|
|
61
|
+
FROM sales_pairs
|
|
62
|
+
INNER JOIN sales_line_totals ON sales_line_totals."customerContractId" = sales_pairs."customerContractId" AND sales_line_totals."purchaseOrderId" = sales_pairs."purchaseOrderId"
|
|
63
|
+
LEFT JOIN latest_sales_files ON latest_sales_files."customerContractId" = sales_pairs."customerContractId" AND latest_sales_files."purchaseOrderId" = sales_pairs."purchaseOrderId"
|
|
64
|
+
)`;
|
|
65
|
+
}
|
|
66
|
+
function analyticsSupplierRiskCountCtesSql(tenant, organization, from, to) {
|
|
67
|
+
return `supplier_sanction_counts AS (
|
|
68
|
+
SELECT sanction_match."supplierId", COUNT(DISTINCT sanction_match.id)::bigint AS "sanctionHitCount"
|
|
69
|
+
FROM plugin_trade_compliance_company_sanction_match sanction_match
|
|
70
|
+
INNER JOIN (SELECT DISTINCT "supplierId" FROM purchase_scope) scoped_supplier ON scoped_supplier."supplierId" = sanction_match."supplierId"
|
|
71
|
+
INNER JOIN plugin_trade_compliance_sanctioned_company company ON company.id = sanction_match."sanctionedCompanyId" AND company."tenantId" = sanction_match."tenantId" AND company."organizationId" = sanction_match."organizationId" AND company."deletedAt" IS NULL
|
|
72
|
+
WHERE sanction_match."tenantId" = ${tenant} AND sanction_match."organizationId" = ${organization} AND sanction_match."deletedAt" IS NULL AND sanction_match."subjectType" = 'SUPPLIER'
|
|
73
|
+
GROUP BY sanction_match."supplierId"
|
|
74
|
+
), supplier_product_counts AS (
|
|
75
|
+
SELECT risk_purchase."supplierId", COUNT(DISTINCT product_match.id)::bigint AS "productRiskHitCount"
|
|
76
|
+
FROM plugin_trade_compliance_purchase_order risk_purchase
|
|
77
|
+
INNER JOIN plugin_trade_compliance_purchase_order_line risk_line ON risk_line."purchaseOrderId" = risk_purchase.id AND risk_line."tenantId" = risk_purchase."tenantId" AND risk_line."organizationId" = risk_purchase."organizationId" AND risk_line."deletedAt" IS NULL
|
|
78
|
+
INNER JOIN plugin_trade_compliance_product_match product_match ON product_match."purchaseOrderLineId" = risk_line.id AND product_match."tenantId" = risk_line."tenantId" AND product_match."organizationId" = risk_line."organizationId" AND product_match."deletedAt" IS NULL
|
|
79
|
+
INNER JOIN plugin_trade_compliance_controlled_goods goods ON goods.id = product_match."controlledGoodsId" AND goods."tenantId" = product_match."tenantId" AND goods."organizationId" = product_match."organizationId" AND goods."deletedAt" IS NULL
|
|
80
|
+
WHERE risk_purchase."tenantId" = ${tenant} AND risk_purchase."organizationId" = ${organization} AND risk_purchase."deletedAt" IS NULL AND risk_purchase."orderDate" BETWEEN ${from} AND ${to}
|
|
81
|
+
GROUP BY risk_purchase."supplierId"
|
|
82
|
+
)`;
|
|
83
|
+
}
|
|
84
|
+
function analyticsDashboardJsonSql(tenant, organization, from, to) {
|
|
85
|
+
const complianceDetails = analyticsComplianceDetailSql(tenant, organization, from, to);
|
|
86
|
+
const categoryCount = (key) => `(SELECT COUNT(*) FROM compliance_details WHERE "categoryKey" = '${key}')`;
|
|
87
|
+
const categoryAmount = (key) => `(SELECT COALESCE(SUM("amountCny"), 0) FROM compliance_details WHERE "categoryKey" = '${key}')`;
|
|
88
|
+
const customerContractReviewCount = categoryCount('customer-contract-review');
|
|
89
|
+
const purchaseContractReviewCount = categoryCount('purchase-contract-review');
|
|
90
|
+
const productRiskCount = categoryCount('product-risk-review');
|
|
91
|
+
const sanctionHitCount = categoryCount('sanction-review');
|
|
92
|
+
const salesFileReviewCount = categoryCount('sales-file-review');
|
|
93
|
+
const complianceTodoCount = `(${customerContractReviewCount} + ${purchaseContractReviewCount} + ${productRiskCount} + ${sanctionHitCount} + ${salesFileReviewCount})`;
|
|
94
|
+
const monthlyTrend = `(SELECT COALESCE(jsonb_agg(jsonb_build_object(
|
|
95
|
+
'month', monthly.month_no,
|
|
96
|
+
'purchaseAmountCny', monthly."purchaseAmountCny",
|
|
97
|
+
'salesAmountCny', monthly."salesAmountCny",
|
|
98
|
+
'estimatedGrossProfitCny', GREATEST(monthly."salesAmountCny" - monthly."purchaseAmountCny", 0),
|
|
99
|
+
'receivedCny', monthly."receivedCny"
|
|
100
|
+
) ORDER BY monthly.month_no), '[]'::jsonb)
|
|
101
|
+
FROM (
|
|
102
|
+
SELECT month_no,
|
|
103
|
+
COALESCE(purchase_month."purchaseAmountCny", 0) AS "purchaseAmountCny",
|
|
104
|
+
COALESCE(sales_month."salesAmountCny", 0) AS "salesAmountCny",
|
|
105
|
+
COALESCE(sales_month."receivedCny", 0) AS "receivedCny"
|
|
106
|
+
FROM generate_series(1, 12) AS month_no
|
|
107
|
+
LEFT JOIN (
|
|
108
|
+
SELECT EXTRACT(MONTH FROM purchase."orderDate")::int AS month_no, SUM(purchase."contractAmountCny") AS "purchaseAmountCny"
|
|
109
|
+
FROM plugin_trade_compliance_purchase_order purchase
|
|
110
|
+
WHERE purchase."tenantId" = ${tenant} AND purchase."organizationId" = ${organization} AND purchase."deletedAt" IS NULL AND purchase."orderDate" BETWEEN ${from} AND ${to}
|
|
111
|
+
GROUP BY EXTRACT(MONTH FROM purchase."orderDate")::int
|
|
112
|
+
) purchase_month USING (month_no)
|
|
113
|
+
LEFT JOIN (
|
|
114
|
+
SELECT EXTRACT(MONTH FROM purchase."orderDate")::int AS month_no,
|
|
115
|
+
SUM(ROUND(ROUND(line."unitPriceTaxIncluded" * (1 + COALESCE(manual."pricingProfitRateOverride", customer."defaultPricingProfitRate")), 0) * line.quantity, 0)) AS "salesAmountCny",
|
|
116
|
+
SUM(COALESCE(manual."receivedAmountCny", 0)) AS "receivedCny"
|
|
117
|
+
FROM plugin_trade_compliance_customer_contract contract
|
|
118
|
+
INNER JOIN plugin_trade_compliance_customer customer ON customer.id = contract."customerId" AND customer."tenantId" = contract."tenantId" AND customer."organizationId" = contract."organizationId" AND customer."deletedAt" IS NULL
|
|
119
|
+
INNER JOIN plugin_trade_compliance_purchase_order purchase ON purchase."tenantId" = contract."tenantId" AND purchase."organizationId" = contract."organizationId" AND purchase."deletedAt" IS NULL AND contract."normalizedContractNo" = regexp_replace(purchase."normalizedContractNo", '(-[0-9]+)?C$', '') || 'X'
|
|
120
|
+
LEFT JOIN plugin_trade_compliance_derived_sales_manual manual ON manual."customerContractId" = contract.id AND manual."purchaseOrderId" = purchase.id AND manual."tenantId" = contract."tenantId" AND manual."organizationId" = contract."organizationId" AND manual."deletedAt" IS NULL
|
|
121
|
+
INNER JOIN plugin_trade_compliance_purchase_order_line line ON line."purchaseOrderId" = purchase.id AND line."tenantId" = purchase."tenantId" AND line."organizationId" = purchase."organizationId" AND line."deletedAt" IS NULL
|
|
122
|
+
WHERE contract."tenantId" = ${tenant} AND contract."organizationId" = ${organization} AND contract."deletedAt" IS NULL AND purchase."orderDate" BETWEEN ${from} AND ${to}
|
|
123
|
+
AND (manual."pricingProfitRateOverride" IS NOT NULL OR (customer."pricingProfitRateStatus" = 'CONFIGURED' AND customer."defaultPricingProfitRate" IS NOT NULL))
|
|
124
|
+
GROUP BY EXTRACT(MONTH FROM purchase."orderDate")::int
|
|
125
|
+
) sales_month USING (month_no)
|
|
126
|
+
) monthly)`;
|
|
127
|
+
return `(WITH compliance_details AS (${complianceDetails}), dashboard_sales_file_warning_source AS (${analyticsSalesFileWarningSourceSql(tenant, organization, from, to)}), purchase_scope AS (${analyticsPurchaseScopeSql(tenant, organization, from, to)}), ${analyticsSalesScopeCtesSql(tenant, organization, from, to)}, global_purchase AS (
|
|
128
|
+
SELECT COALESCE(SUM("purchaseTotalCny"), 0) AS "purchaseTotalCny", COALESCE(SUM("paidCny"), 0) AS "paidCny", COALESCE(SUM("unpaidCny"), 0) AS "unpaidCny", COALESCE(SUM("refundedCny"), 0) AS "refundedCny", COALESCE(SUM("unrefundedCny"), 0) AS "unrefundedCny"
|
|
129
|
+
FROM purchase_scope
|
|
130
|
+
), global_sales AS (
|
|
131
|
+
SELECT COALESCE(SUM("salesTotalCny"), 0) AS "salesTotalCny", COALESCE(SUM("receivedCny"), 0) AS "receivedCny", COALESCE(SUM("unreceivedCny"), 0) AS "unreceivedCny", COALESCE(SUM("estimatedGrossProfitCny"), 0) AS "estimatedGrossProfitCny"
|
|
132
|
+
FROM sales_scope
|
|
133
|
+
), customer_rollup AS (
|
|
134
|
+
SELECT "customerId", MIN("customerName") AS "customerName", SUM("salesTotalCny") AS "salesAmountCny", SUM("estimatedGrossProfitCny") AS "estimatedGrossProfitCny", SUM("unreceivedCny") AS "unreceivedCny", COUNT(*)::bigint AS "orderCount", MAX("orderDate") AS "latestOrderDate"
|
|
135
|
+
FROM sales_scope GROUP BY "customerId"
|
|
136
|
+
), customer_value_top AS (
|
|
137
|
+
SELECT * FROM customer_rollup ORDER BY "salesAmountCny" DESC LIMIT 5
|
|
138
|
+
), receivable_top AS (
|
|
139
|
+
SELECT * FROM customer_rollup WHERE "unreceivedCny" > 0 ORDER BY "unreceivedCny" DESC LIMIT 5
|
|
140
|
+
), ${analyticsSupplierRiskCountCtesSql(tenant, organization, from, to)}, supplier_top AS (
|
|
141
|
+
SELECT purchase_scope."supplierId", MIN(purchase_scope."supplierName") AS "supplierName", SUM(purchase_scope."unpaidCny") AS "unpaidCny", COUNT(DISTINCT purchase_scope.id)::bigint AS "purchaseOrderCount",
|
|
142
|
+
COALESCE(MIN(supplier_product_counts."productRiskHitCount"), 0)::bigint AS "productRiskHitCount",
|
|
143
|
+
COALESCE(MIN(supplier_sanction_counts."sanctionHitCount"), 0)::bigint AS "sanctionHitCount"
|
|
144
|
+
FROM purchase_scope
|
|
145
|
+
LEFT JOIN supplier_product_counts ON supplier_product_counts."supplierId" = purchase_scope."supplierId"
|
|
146
|
+
LEFT JOIN supplier_sanction_counts ON supplier_sanction_counts."supplierId" = purchase_scope."supplierId"
|
|
147
|
+
GROUP BY purchase_scope."supplierId" ORDER BY SUM(purchase_scope."unpaidCny") DESC LIMIT 5
|
|
148
|
+
)
|
|
149
|
+
SELECT jsonb_build_object(
|
|
150
|
+
'kpis', jsonb_build_object(
|
|
151
|
+
'purchaseTotalCny', (SELECT "purchaseTotalCny" FROM global_purchase), 'salesTotalCny', (SELECT "salesTotalCny" FROM global_sales),
|
|
152
|
+
'receivedCny', (SELECT "receivedCny" FROM global_sales), 'unreceivedCny', (SELECT "unreceivedCny" FROM global_sales), 'paidCny', (SELECT "paidCny" FROM global_purchase), 'unpaidCny', (SELECT "unpaidCny" FROM global_purchase),
|
|
153
|
+
'refundedCny', (SELECT "refundedCny" FROM global_purchase), 'unrefundedCny', (SELECT "unrefundedCny" FROM global_purchase),
|
|
154
|
+
'estimatedGrossProfitCny', (SELECT "estimatedGrossProfitCny" FROM global_sales),
|
|
155
|
+
'estimatedGrossProfitRate', CASE WHEN (SELECT "salesTotalCny" FROM global_sales) > 0 THEN (SELECT "estimatedGrossProfitCny" FROM global_sales) / (SELECT "salesTotalCny" FROM global_sales) ELSE 0 END,
|
|
156
|
+
'complianceTodoCount', ${complianceTodoCount},
|
|
157
|
+
'riskExposureCny', (SELECT COALESCE(SUM("amountCny"), 0) FROM compliance_details)
|
|
158
|
+
),
|
|
159
|
+
'monthlyTrend', ${monthlyTrend},
|
|
160
|
+
'todos', jsonb_build_array(
|
|
161
|
+
jsonb_build_object('key', 'customer-contract-review', 'label', '客户合同待审核', 'count', ${customerContractReviewCount}, 'amountCny', ${categoryAmount('customer-contract-review')}, 'priority', 'high', 'description', 'AI 解析完成但未人工确认的客户合同'),
|
|
162
|
+
jsonb_build_object('key', 'purchase-contract-review', 'label', '采购合同待审核', 'count', ${purchaseContractReviewCount}, 'amountCny', ${categoryAmount('purchase-contract-review')}, 'priority', 'high', 'description', '上传完成但未审核入库的采购合同'),
|
|
163
|
+
jsonb_build_object('key', 'product-risk-review', 'label', '商品风险待确认', 'count', ${productRiskCount}, 'amountCny', ${categoryAmount('product-risk-review')}, 'priority', 'medium', 'description', '命中管控商品目录的商品明细'),
|
|
164
|
+
jsonb_build_object('key', 'sanction-review', 'label', '制裁公司待复核', 'count', ${sanctionHitCount}, 'amountCny', ${categoryAmount('sanction-review')}, 'priority', 'high', 'description', '客户或供应商命中公司制裁目录'),
|
|
165
|
+
jsonb_build_object('key', 'sales-file-review', 'label', '销售文件待复核', 'count', ${salesFileReviewCount}, 'amountCny', ${categoryAmount('sales-file-review')}, 'priority', 'medium', 'description', '生成时记录了真实业务异常的销售文件')
|
|
166
|
+
),
|
|
167
|
+
'riskDistribution', jsonb_build_array(
|
|
168
|
+
jsonb_build_object('key', 'customer-contract-review', 'label', '客户合同待审核', 'count', ${customerContractReviewCount}, 'amountCny', ${categoryAmount('customer-contract-review')}),
|
|
169
|
+
jsonb_build_object('key', 'purchase-contract-review', 'label', '采购合同待审核', 'count', ${purchaseContractReviewCount}, 'amountCny', ${categoryAmount('purchase-contract-review')}),
|
|
170
|
+
jsonb_build_object('key', 'product-risk-review', 'label', '商品风险待确认', 'count', ${productRiskCount}, 'amountCny', ${categoryAmount('product-risk-review')}),
|
|
171
|
+
jsonb_build_object('key', 'sanction-review', 'label', '制裁公司待复核', 'count', ${sanctionHitCount}, 'amountCny', ${categoryAmount('sanction-review')}),
|
|
172
|
+
jsonb_build_object('key', 'sales-file-review', 'label', '销售文件待复核', 'count', ${salesFileReviewCount}, 'amountCny', ${categoryAmount('sales-file-review')})
|
|
173
|
+
),
|
|
174
|
+
'customerValueTop5', (SELECT COALESCE(jsonb_agg(jsonb_build_object('customerId', customer_value_top."customerId", 'customerName', customer_value_top."customerName", 'salesAmountCny', customer_value_top."salesAmountCny", 'estimatedGrossProfitCny', customer_value_top."estimatedGrossProfitCny", 'estimatedGrossProfitRate', CASE WHEN customer_value_top."salesAmountCny" > 0 THEN customer_value_top."estimatedGrossProfitCny" / customer_value_top."salesAmountCny" ELSE 0 END, 'unreceivedCny', customer_value_top."unreceivedCny", 'riskStatus', CASE WHEN customer_value_top."unreceivedCny" >= 100000 THEN 'HIGH' WHEN customer_value_top."unreceivedCny" > 0 THEN 'MEDIUM' ELSE 'LOW' END) ORDER BY customer_value_top."salesAmountCny" DESC), '[]'::jsonb) FROM customer_value_top),
|
|
175
|
+
'receivableRiskTop5', (SELECT COALESCE(jsonb_agg(jsonb_build_object('customerId', receivable_top."customerId", 'customerName', receivable_top."customerName", 'unreceivedCny', receivable_top."unreceivedCny", 'orderCount', receivable_top."orderCount", 'latestOrderDate', receivable_top."latestOrderDate") ORDER BY receivable_top."unreceivedCny" DESC), '[]'::jsonb) FROM receivable_top),
|
|
176
|
+
'supplierRiskTop5', (SELECT COALESCE(jsonb_agg(jsonb_build_object('supplierId', supplier_top."supplierId", 'supplierName', supplier_top."supplierName", 'unpaidCny', supplier_top."unpaidCny", 'sanctionHitCount', supplier_top."sanctionHitCount", 'productRiskHitCount', supplier_top."productRiskHitCount", 'purchaseOrderCount', supplier_top."purchaseOrderCount") ORDER BY supplier_top."unpaidCny" DESC), '[]'::jsonb) FROM supplier_top WHERE supplier_top."unpaidCny" > 0 OR supplier_top."sanctionHitCount" > 0 OR supplier_top."productRiskHitCount" > 0),
|
|
177
|
+
'salesFileReviews', (SELECT COALESCE(jsonb_agg(jsonb_build_object('fileId', file.id, 'fileName', file."fileName", 'fileType', file."fileType", 'salesOrderNo', file."salesOrderNoSnapshot", 'reason', file.reason, 'generatedAt', file."generatedAt") ORDER BY file."generatedAt" DESC, file.id ASC), '[]'::jsonb) FROM (SELECT source.* FROM dashboard_sales_file_warning_source source ORDER BY source."generatedAt" DESC, source.id ASC LIMIT 5) file),
|
|
178
|
+
'riskOrders', (SELECT COALESCE(jsonb_agg(risk_item.item), '[]'::jsonb) FROM (SELECT jsonb_build_object('orderKey', 'sales:' || sales_scope."purchaseOrderId"::text, 'orderNo', sales_scope."salesOrderNo", 'partyName', sales_scope."customerName", 'riskType', '未收款风险', 'riskAmountCny', sales_scope."unreceivedCny", 'status', '待跟进') AS item, sales_scope."unreceivedCny" AS amount FROM sales_scope WHERE sales_scope."unreceivedCny" > 0 UNION ALL SELECT jsonb_build_object('orderKey', 'purchase:' || purchase_scope.id::text, 'orderNo', purchase_scope."contractNo", 'partyName', purchase_scope."supplierName", 'riskType', '未付款风险', 'riskAmountCny', purchase_scope."unpaidCny", 'status', '待跟进') AS item, purchase_scope."unpaidCny" AS amount FROM purchase_scope WHERE purchase_scope."unpaidCny" > 0 ORDER BY amount DESC LIMIT 5) risk_item),
|
|
179
|
+
'dataSources', jsonb_build_array(
|
|
180
|
+
jsonb_build_object('label', '采购订单', 'description', '采购金额、付款记录、退税记录'),
|
|
181
|
+
jsonb_build_object('label', '采购商品明细', 'description', '采购商品金额、商品风险命中'),
|
|
182
|
+
jsonb_build_object('label', '客户合同审核结果', 'description', '客户合同风险、待审核合同'),
|
|
183
|
+
jsonb_build_object('label', '公司制裁目录匹配结果', 'description', '客户/供应商制裁命中'),
|
|
184
|
+
jsonb_build_object('label', '付款记录', 'description', '已付款、未付款金额'),
|
|
185
|
+
jsonb_build_object('label', '销售订单', 'description', '销售金额、收款记录、客户合同关联'),
|
|
186
|
+
jsonb_build_object('label', '销售商品明细', 'description', '销售商品金额、管控商品命中'),
|
|
187
|
+
jsonb_build_object('label', '管控商品目录匹配结果', 'description', '商品风险命中与处理状态'),
|
|
188
|
+
jsonb_build_object('label', '收款记录', 'description', '已收款、未收款金额'),
|
|
189
|
+
jsonb_build_object('label', '退税记录', 'description', '已退税、未退税金额'),
|
|
190
|
+
jsonb_build_object('label', '销售文件生成与审核记录', 'description', '销售单据生成、复核状态与异常原因'),
|
|
191
|
+
jsonb_build_object('label', '系统设置中的币种与汇率规则', 'description', '币种、月度汇率与金额折算规则')
|
|
192
|
+
)
|
|
193
|
+
))`;
|
|
194
|
+
}
|
|
195
|
+
export function analyticsExportStatements(scope, query, currentYear, maxRows) {
|
|
196
|
+
const range = resolveAnalyticsPeriod(query, currentYear);
|
|
197
|
+
const parameters = [scope.tenantId, scope.organizationId, range.from, range.to, maxRows + 1];
|
|
198
|
+
const [tenant, organization, from, to] = ['$1', '$2', '$3', '$4'];
|
|
199
|
+
const purchaseScope = analyticsPurchaseScopeSql(tenant, organization, from, to);
|
|
200
|
+
const salesScopeCtes = analyticsSalesScopeCtesSql(tenant, organization, from, to);
|
|
201
|
+
const statement = (sql) => ({ sql: `${sql}\n LIMIT $5`, parameters: [...parameters] });
|
|
202
|
+
const fundRisks = statement(`WITH purchase_scope AS (${purchaseScope}), ${salesScopeCtes}
|
|
203
|
+
SELECT risks."riskType", risks."orderNo", risks."partyName", risks."orderDate", risks."orderAmountCny", risks."settledAmountCny", risks."riskGapCny", risks.reason, risks.status,
|
|
204
|
+
COUNT(*) OVER() AS "__totalCount"
|
|
205
|
+
FROM (
|
|
206
|
+
SELECT '未收款风险' AS "riskType", sales_scope."salesOrderNo" AS "orderNo", sales_scope."customerName" AS "partyName", sales_scope."orderDate"::text AS "orderDate",
|
|
207
|
+
sales_scope."salesTotalCny" AS "orderAmountCny", sales_scope."receivedCny" AS "settledAmountCny", sales_scope."unreceivedCny" AS "riskGapCny", '未收款' AS reason, '待跟进' AS status,
|
|
208
|
+
('sales:' || sales_scope."customerContractId"::text || ':' || sales_scope."purchaseOrderId"::text) AS "stableId"
|
|
209
|
+
FROM sales_scope
|
|
210
|
+
UNION ALL
|
|
211
|
+
SELECT '未付款风险', purchase_scope."contractNo", purchase_scope."supplierName", purchase_scope."orderDate"::text,
|
|
212
|
+
purchase_scope."purchaseTotalCny", purchase_scope."paidCny", purchase_scope."unpaidCny", '未付款', '待跟进',
|
|
213
|
+
('purchase:' || purchase_scope.id::text)
|
|
214
|
+
FROM purchase_scope
|
|
215
|
+
) risks
|
|
216
|
+
WHERE risks."riskGapCny" > 0
|
|
217
|
+
ORDER BY "riskGapCny" DESC, "stableId" ASC`);
|
|
218
|
+
const compliance = statement(`WITH compliance_details AS (${analyticsComplianceDetailSql(tenant, organization, from, to)})
|
|
219
|
+
SELECT "categoryKey", "categoryLabel", "referenceNo", "subjectName", "businessDate", reason, "amountCny",
|
|
220
|
+
CASE WHEN "categoryKey" IN ('customer-contract-review', 'purchase-contract-review', 'sanction-review') THEN 'high' ELSE 'medium' END AS priority,
|
|
221
|
+
status, COUNT(*) OVER() AS "__totalCount"
|
|
222
|
+
FROM compliance_details
|
|
223
|
+
ORDER BY "businessDate" DESC NULLS LAST, id DESC`);
|
|
224
|
+
const customers = statement(`WITH ${salesScopeCtes}, customer_rollup AS (
|
|
225
|
+
SELECT "customerId", MIN("customerName") AS "customerName", SUM("salesTotalCny") AS "salesAmountCny",
|
|
226
|
+
SUM("purchaseTotalCny") AS "purchaseCostCny", SUM("estimatedGrossProfitCny") AS "estimatedGrossProfitCny",
|
|
227
|
+
SUM("receivedCny") AS "receivedCny", SUM("unreceivedCny") AS "unreceivedCny", COUNT(*)::bigint AS "orderCount", MAX("orderDate")::text AS "latestOrderDate"
|
|
228
|
+
FROM sales_scope
|
|
229
|
+
GROUP BY "customerId"
|
|
230
|
+
)
|
|
231
|
+
SELECT "customerName", "salesAmountCny", "purchaseCostCny", "estimatedGrossProfitCny",
|
|
232
|
+
CASE WHEN "salesAmountCny" > 0 THEN "estimatedGrossProfitCny" / "salesAmountCny" ELSE 0 END AS "estimatedGrossProfitRate",
|
|
233
|
+
"receivedCny", "unreceivedCny", "orderCount", "latestOrderDate",
|
|
234
|
+
CASE WHEN "unreceivedCny" >= 100000 THEN 'HIGH' WHEN "unreceivedCny" > 0 THEN 'MEDIUM' ELSE 'LOW' END AS "riskStatus",
|
|
235
|
+
COUNT(*) OVER() AS "__totalCount"
|
|
236
|
+
FROM customer_rollup
|
|
237
|
+
ORDER BY "salesAmountCny" DESC, "customerId" ASC`);
|
|
238
|
+
const suppliers = statement(`WITH purchase_scope AS (${purchaseScope}), ${analyticsSupplierRiskCountCtesSql(tenant, organization, from, to)}, supplier_rollup AS (
|
|
239
|
+
SELECT purchase_scope."supplierId", MIN(purchase_scope."supplierName") AS "supplierName",
|
|
240
|
+
SUM(purchase_scope."purchaseTotalCny") AS "purchaseAmountCny", SUM(purchase_scope."paidCny") AS "paidCny", SUM(purchase_scope."unpaidCny") AS "unpaidCny",
|
|
241
|
+
COUNT(DISTINCT purchase_scope.id)::bigint AS "purchaseOrderCount", MAX(purchase_scope."orderDate")::text AS "latestOrderDate",
|
|
242
|
+
COALESCE(MIN(supplier_sanction_counts."sanctionHitCount"), 0)::bigint AS "sanctionHitCount",
|
|
243
|
+
COALESCE(MIN(supplier_product_counts."productRiskHitCount"), 0)::bigint AS "productRiskHitCount"
|
|
244
|
+
FROM purchase_scope
|
|
245
|
+
LEFT JOIN supplier_sanction_counts ON supplier_sanction_counts."supplierId" = purchase_scope."supplierId"
|
|
246
|
+
LEFT JOIN supplier_product_counts ON supplier_product_counts."supplierId" = purchase_scope."supplierId"
|
|
247
|
+
GROUP BY purchase_scope."supplierId"
|
|
248
|
+
)
|
|
249
|
+
SELECT "supplierName", "purchaseAmountCny", "paidCny", "unpaidCny", "purchaseOrderCount", "latestOrderDate", "sanctionHitCount", "productRiskHitCount",
|
|
250
|
+
COUNT(*) OVER() AS "__totalCount"
|
|
251
|
+
FROM supplier_rollup
|
|
252
|
+
ORDER BY "sanctionHitCount" DESC, "productRiskHitCount" DESC, "unpaidCny" DESC, "supplierId" ASC`);
|
|
253
|
+
const salesFileReviews = statement(`WITH sales_file_warning_source AS (${analyticsSalesFileWarningSourceSql(tenant, organization, from, to)})
|
|
254
|
+
SELECT file."fileName", file."fileType", file."salesOrderNoSnapshot" AS "salesOrderNo", customer.name AS "customerName",
|
|
255
|
+
file."generatedAt"::text AS "generatedAt", file.reason, '待复核' AS status, COUNT(*) OVER() AS "__totalCount"
|
|
256
|
+
FROM sales_file_warning_source file
|
|
257
|
+
INNER JOIN plugin_trade_compliance_customer_contract contract ON contract.id = file."customerContractId" AND contract."tenantId" = file."tenantId" AND contract."organizationId" = file."organizationId" AND contract."deletedAt" IS NULL
|
|
258
|
+
INNER JOIN plugin_trade_compliance_customer customer ON customer.id = contract."customerId" AND customer."tenantId" = contract."tenantId" AND customer."organizationId" = contract."organizationId" AND customer."deletedAt" IS NULL
|
|
259
|
+
ORDER BY file."generatedAt" DESC, file.id ASC`);
|
|
260
|
+
return { fundRisks, compliance, customers, suppliers, salesFileReviews };
|
|
261
|
+
}
|
|
262
|
+
export function analyticsDetailStatement(scope, query, currentYear) {
|
|
263
|
+
const parameters = [];
|
|
264
|
+
const parameter = (value) => { parameters.push(value); return `$${parameters.length}`; };
|
|
265
|
+
const tenant = parameter(scope.tenantId);
|
|
266
|
+
const organization = parameter(scope.organizationId);
|
|
267
|
+
const range = resolveAnalyticsPeriod(query, currentYear);
|
|
268
|
+
const from = parameter(range.from);
|
|
269
|
+
const to = parameter(range.to);
|
|
270
|
+
const source = query.kind === 'COMPLIANCE_TODO' || query.kind === 'COMPLIANCE_RISK'
|
|
271
|
+
? analyticsComplianceDetailSql(tenant, organization, from, to)
|
|
272
|
+
: query.kind === 'SALES_FILE_REVIEW'
|
|
273
|
+
? analyticsSalesFileReviewDetailSql(tenant, organization, from, to)
|
|
274
|
+
: analyticsRiskOrderDetailSql(tenant, organization, from, to);
|
|
275
|
+
const category = query.categoryKey ? `WHERE details."categoryKey" = ${parameter(query.categoryKey)}` : '';
|
|
276
|
+
const offset = parameter((query.page - 1) * query.pageSize);
|
|
277
|
+
const limit = parameter(query.pageSize);
|
|
278
|
+
return {
|
|
279
|
+
sql: `WITH details AS (${source}), filtered AS (SELECT * FROM details ${category}), counted AS (SELECT COUNT(*) AS total FROM filtered), paged AS (SELECT * FROM filtered ORDER BY "businessDate" DESC NULLS LAST, id DESC OFFSET ${offset} LIMIT ${limit}) SELECT paged.*, counted.total FROM counted LEFT JOIN paged ON TRUE ORDER BY paged."businessDate" DESC NULLS LAST, paged.id DESC`,
|
|
280
|
+
parameters
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
function analyticsSalesFileWarningSourceSql(tenant, organization, from, to) {
|
|
284
|
+
const warningText = `COALESCE(NULLIF(BTRIM(warning->>'message'), ''), NULLIF(BTRIM(warning->>'reason'), ''), NULLIF(BTRIM(warning->>'code'), ''))`;
|
|
285
|
+
return `SELECT file.*, actual_warning.reason
|
|
286
|
+
FROM plugin_trade_compliance_generated_sales_file file
|
|
287
|
+
CROSS JOIN LATERAL (
|
|
288
|
+
SELECT ${warningText} AS reason
|
|
289
|
+
FROM jsonb_array_elements(CASE WHEN jsonb_typeof(file."generationWarnings") = 'array' THEN file."generationWarnings" ELSE '[]'::jsonb END) WITH ORDINALITY warnings(warning, ordinal)
|
|
290
|
+
WHERE ${warningText} IS NOT NULL
|
|
291
|
+
ORDER BY ordinal
|
|
292
|
+
LIMIT 1
|
|
293
|
+
) actual_warning
|
|
294
|
+
WHERE file."tenantId" = ${tenant} AND file."organizationId" = ${organization} AND file."deletedAt" IS NULL AND file."reviewedAt" IS NULL
|
|
295
|
+
AND (file."generatedAt" AT TIME ZONE 'Asia/Shanghai')::date BETWEEN ${from} AND ${to}`;
|
|
296
|
+
}
|
|
297
|
+
function analyticsSalesFileReviewRowsSql() {
|
|
298
|
+
return `SELECT file.id::text AS id, 'sales-file-review' AS "categoryKey", '销售文件待复核' AS "categoryLabel", file."salesOrderNoSnapshot" AS "referenceNo",
|
|
299
|
+
file."fileName" AS "subjectName", file."generatedAt"::text AS "businessDate", file.reason,
|
|
300
|
+
NULL::numeric AS "amountCny", '待复核' AS status,
|
|
301
|
+
jsonb_build_object('kind', 'SALES_FILE_REVIEW', 'label', '复核', 'id', file.id, 'customerContractId', file."customerContractId", 'purchaseOrderId', file."purchaseOrderId", 'salesOrderNoSnapshot', file."salesOrderNoSnapshot", 'fileType', file."fileType", 'fileName', file."fileName", 'storageFileId', file."storageFileId", 'editVersion', file."editVersion", 'generatedAt', file."generatedAt") AS action
|
|
302
|
+
FROM sales_file_warning_source file`;
|
|
303
|
+
}
|
|
304
|
+
function analyticsComplianceDetailSql(tenant, organization, from, to) {
|
|
305
|
+
const salesFileRows = analyticsSalesFileReviewRowsSql();
|
|
306
|
+
return `WITH sales_file_warning_source AS (${analyticsSalesFileWarningSourceSql(tenant, organization, from, to)})
|
|
307
|
+
SELECT DISTINCT ON ("categoryKey", id) * FROM (
|
|
308
|
+
SELECT task.id::text AS id, 'customer-contract-review' AS "categoryKey", '客户合同待审核' AS "categoryLabel",
|
|
309
|
+
task."sourceFileName" AS "referenceNo", task."sourceFileName" AS "subjectName", task."createdAt"::text AS "businessDate",
|
|
310
|
+
NULL::text AS reason, NULL::numeric AS "amountCny", '待审核' AS status,
|
|
311
|
+
jsonb_build_object('kind', 'CUSTOMER_CONTRACT_REVIEW', 'label', '审核', 'id', task.id, 'importTaskId', task.id) AS action
|
|
312
|
+
FROM plugin_trade_compliance_import_task task
|
|
313
|
+
WHERE task."tenantId" = ${tenant} AND task."organizationId" = ${organization} AND task."deletedAt" IS NULL AND task."documentType" = 'CUSTOMER_CONTRACT' AND task.status = 'AWAITING_REVIEW' AND (task."createdAt" AT TIME ZONE 'Asia/Shanghai')::date BETWEEN ${from} AND ${to}
|
|
314
|
+
UNION ALL
|
|
315
|
+
SELECT task.id::text, 'purchase-contract-review', '采购合同待审核', task."sourceFileName", task."sourceFileName", task."createdAt"::text,
|
|
316
|
+
NULL::text, NULL::numeric, '待审核', jsonb_build_object('kind', 'PURCHASE_CONTRACT_REVIEW', 'label', '审核', 'id', task.id, 'importTaskId', task.id)
|
|
317
|
+
FROM plugin_trade_compliance_import_task task
|
|
318
|
+
WHERE task."tenantId" = ${tenant} AND task."organizationId" = ${organization} AND task."deletedAt" IS NULL AND task."documentType" = 'PURCHASE_CONTRACT' AND task.status = 'AWAITING_REVIEW' AND (task."createdAt" AT TIME ZONE 'Asia/Shanghai')::date BETWEEN ${from} AND ${to}
|
|
319
|
+
UNION ALL
|
|
320
|
+
SELECT match.id::text, 'product-risk-review', '商品风险待确认', purchase."contractNo", line."productName", purchase."orderDate"::text,
|
|
321
|
+
COALESCE(goods."controlDescription", goods."productName"), line."lineAmountTaxIncluded", '待确认',
|
|
322
|
+
jsonb_build_object('kind', 'PURCHASE_ORDER_VIEW', 'label', '查看', 'id', match.id, 'purchaseOrderId', purchase.id, 'supplierId', purchase."supplierId")
|
|
323
|
+
FROM plugin_trade_compliance_product_match match
|
|
324
|
+
INNER JOIN plugin_trade_compliance_purchase_order_line line ON line.id = match."purchaseOrderLineId" AND line."tenantId" = match."tenantId" AND line."organizationId" = match."organizationId" AND line."deletedAt" IS NULL
|
|
325
|
+
INNER JOIN plugin_trade_compliance_purchase_order purchase ON purchase.id = line."purchaseOrderId" AND purchase."tenantId" = match."tenantId" AND purchase."organizationId" = match."organizationId" AND purchase."deletedAt" IS NULL
|
|
326
|
+
INNER JOIN plugin_trade_compliance_controlled_goods goods ON goods.id = match."controlledGoodsId" AND goods."tenantId" = match."tenantId" AND goods."organizationId" = match."organizationId" AND goods."deletedAt" IS NULL
|
|
327
|
+
WHERE match."tenantId" = ${tenant} AND match."organizationId" = ${organization} AND match."deletedAt" IS NULL AND purchase."orderDate" BETWEEN ${from} AND ${to}
|
|
328
|
+
UNION ALL
|
|
329
|
+
SELECT ('supplier:' || match.id::text || ':' || purchase.id::text), 'sanction-review', '制裁公司待复核', purchase."contractNo", supplier.name, purchase."orderDate"::text,
|
|
330
|
+
company."sanctionReason", purchase."contractAmountCny", '待复核', jsonb_build_object('kind', 'PURCHASE_ORDER_VIEW', 'label', '查看', 'id', match.id, 'purchaseOrderId', purchase.id, 'supplierId', supplier.id)
|
|
331
|
+
FROM plugin_trade_compliance_company_sanction_match match
|
|
332
|
+
INNER JOIN plugin_trade_compliance_supplier supplier ON supplier.id = match."supplierId" AND supplier."tenantId" = match."tenantId" AND supplier."organizationId" = match."organizationId" AND supplier."deletedAt" IS NULL
|
|
333
|
+
INNER JOIN plugin_trade_compliance_purchase_order purchase ON purchase."supplierId" = supplier.id AND purchase."tenantId" = match."tenantId" AND purchase."organizationId" = match."organizationId" AND purchase."deletedAt" IS NULL AND purchase."orderDate" BETWEEN ${from} AND ${to}
|
|
334
|
+
INNER JOIN plugin_trade_compliance_sanctioned_company company ON company.id = match."sanctionedCompanyId" AND company."tenantId" = match."tenantId" AND company."organizationId" = match."organizationId" AND company."deletedAt" IS NULL
|
|
335
|
+
WHERE match."tenantId" = ${tenant} AND match."organizationId" = ${organization} AND match."deletedAt" IS NULL AND match."subjectType" = 'SUPPLIER'
|
|
336
|
+
UNION ALL
|
|
337
|
+
SELECT ('customer:' || match.id::text || ':' || purchase.id::text), 'sanction-review', '制裁公司待复核', COALESCE(file."salesOrderNoSnapshot", contract."contractNo"), customer.name, purchase."orderDate"::text,
|
|
338
|
+
company."sanctionReason", contract."contractAmountReviewCny", '待复核', jsonb_build_object('kind', 'SALES_ORDER_VIEW', 'label', '查看', 'id', match.id, 'customerId', customer.id, 'customerContractId', contract.id, 'purchaseOrderId', purchase.id)
|
|
339
|
+
FROM plugin_trade_compliance_company_sanction_match match
|
|
340
|
+
INNER JOIN plugin_trade_compliance_customer customer ON customer.id = match."customerId" AND customer."tenantId" = match."tenantId" AND customer."organizationId" = match."organizationId" AND customer."deletedAt" IS NULL
|
|
341
|
+
INNER JOIN plugin_trade_compliance_customer_contract contract ON contract."customerId" = customer.id AND contract."tenantId" = match."tenantId" AND contract."organizationId" = match."organizationId" AND contract."deletedAt" IS NULL
|
|
342
|
+
INNER JOIN plugin_trade_compliance_purchase_order purchase ON purchase."tenantId" = contract."tenantId" AND purchase."organizationId" = contract."organizationId" AND purchase."deletedAt" IS NULL AND contract."normalizedContractNo" = regexp_replace(purchase."normalizedContractNo", '(-[0-9]+)?C$', '') || 'X' AND purchase."orderDate" BETWEEN ${from} AND ${to}
|
|
343
|
+
INNER JOIN plugin_trade_compliance_sanctioned_company company ON company.id = match."sanctionedCompanyId" AND company."tenantId" = match."tenantId" AND company."organizationId" = match."organizationId" AND company."deletedAt" IS NULL
|
|
344
|
+
LEFT JOIN LATERAL (SELECT generated."salesOrderNoSnapshot" FROM plugin_trade_compliance_generated_sales_file generated WHERE generated."customerContractId" = contract.id AND generated."purchaseOrderId" = purchase.id AND generated."tenantId" = contract."tenantId" AND generated."organizationId" = contract."organizationId" AND generated."deletedAt" IS NULL ORDER BY generated."generatedAt" DESC, generated.id ASC LIMIT 1) file ON TRUE
|
|
345
|
+
WHERE match."tenantId" = ${tenant} AND match."organizationId" = ${organization} AND match."deletedAt" IS NULL AND match."subjectType" = 'CUSTOMER'
|
|
346
|
+
UNION ALL ${salesFileRows}
|
|
347
|
+
) compliance_source
|
|
348
|
+
ORDER BY "categoryKey", id, "businessDate" DESC NULLS LAST`;
|
|
349
|
+
}
|
|
350
|
+
function analyticsSalesFileReviewDetailSql(tenant, organization, from, to) {
|
|
351
|
+
return `WITH sales_file_warning_source AS (${analyticsSalesFileWarningSourceSql(tenant, organization, from, to)})
|
|
352
|
+
${analyticsSalesFileReviewRowsSql()}`;
|
|
353
|
+
}
|
|
354
|
+
function analyticsRiskOrderDetailSql(tenant, organization, from, to) {
|
|
355
|
+
return `SELECT ('purchase:' || purchase.id::text) AS id, 'risk-order' AS "categoryKey", '未付款订单' AS "categoryLabel", purchase."contractNo" AS "referenceNo", supplier.name AS "subjectName", purchase."orderDate"::text AS "businessDate", '未付款' AS reason,
|
|
356
|
+
GREATEST(purchase."contractAmountCny" - purchase."paidAmountCny", 0) AS "amountCny", '待跟进' AS status,
|
|
357
|
+
jsonb_build_object('kind', 'PURCHASE_ORDER_VIEW', 'label', '跟进', 'id', purchase.id, 'purchaseOrderId', purchase.id, 'supplierId', supplier.id) AS action
|
|
358
|
+
FROM plugin_trade_compliance_purchase_order purchase
|
|
359
|
+
INNER JOIN plugin_trade_compliance_supplier supplier ON supplier.id = purchase."supplierId" AND supplier."tenantId" = purchase."tenantId" AND supplier."organizationId" = purchase."organizationId" AND supplier."deletedAt" IS NULL
|
|
360
|
+
WHERE purchase."tenantId" = ${tenant} AND purchase."organizationId" = ${organization} AND purchase."deletedAt" IS NULL AND purchase."orderDate" BETWEEN ${from} AND ${to} AND GREATEST(purchase."contractAmountCny" - purchase."paidAmountCny", 0) > 0
|
|
361
|
+
UNION ALL
|
|
362
|
+
SELECT ('sales:' || contract.id::text || ':' || purchase.id::text), 'risk-order', '未收款订单', COALESCE(file."salesOrderNoSnapshot", contract."contractNo"), customer.name, purchase."orderDate"::text, '未收款',
|
|
363
|
+
GREATEST(sales_amount."salesTotalCny" - COALESCE(manual."receivedAmountCny", 0), 0) AS "amountCny", '待跟进',
|
|
364
|
+
jsonb_build_object('kind', 'SALES_ORDER_VIEW', 'label', '跟进', 'id', contract.id::text || ':' || purchase.id::text, 'customerId', customer.id, 'customerContractId', contract.id, 'purchaseOrderId', purchase.id)
|
|
365
|
+
FROM plugin_trade_compliance_customer_contract contract
|
|
366
|
+
INNER JOIN plugin_trade_compliance_customer customer ON customer.id = contract."customerId" AND customer."tenantId" = contract."tenantId" AND customer."organizationId" = contract."organizationId" AND customer."deletedAt" IS NULL
|
|
367
|
+
INNER JOIN plugin_trade_compliance_purchase_order purchase ON purchase."tenantId" = contract."tenantId" AND purchase."organizationId" = contract."organizationId" AND purchase."deletedAt" IS NULL AND contract."normalizedContractNo" = regexp_replace(purchase."normalizedContractNo", '(-[0-9]+)?C$', '') || 'X'
|
|
368
|
+
LEFT JOIN plugin_trade_compliance_derived_sales_manual manual ON manual."customerContractId" = contract.id AND manual."purchaseOrderId" = purchase.id AND manual."tenantId" = contract."tenantId" AND manual."organizationId" = contract."organizationId" AND manual."deletedAt" IS NULL
|
|
369
|
+
CROSS JOIN LATERAL (SELECT COALESCE(SUM(ROUND(ROUND(line."unitPriceTaxIncluded" * (1 + COALESCE(manual."pricingProfitRateOverride", customer."defaultPricingProfitRate")), 0) * line.quantity, 0)), 0) AS "salesTotalCny" FROM plugin_trade_compliance_purchase_order_line line WHERE line."purchaseOrderId" = purchase.id AND line."tenantId" = purchase."tenantId" AND line."organizationId" = purchase."organizationId" AND line."deletedAt" IS NULL) sales_amount
|
|
370
|
+
LEFT JOIN LATERAL (SELECT generated."salesOrderNoSnapshot" FROM plugin_trade_compliance_generated_sales_file generated WHERE generated."customerContractId" = contract.id AND generated."purchaseOrderId" = purchase.id AND generated."tenantId" = contract."tenantId" AND generated."organizationId" = contract."organizationId" AND generated."deletedAt" IS NULL ORDER BY generated."generatedAt" DESC LIMIT 1) file ON TRUE
|
|
371
|
+
WHERE contract."tenantId" = ${tenant} AND contract."organizationId" = ${organization} AND contract."deletedAt" IS NULL AND purchase."orderDate" BETWEEN ${from} AND ${to}
|
|
372
|
+
AND (manual."pricingProfitRateOverride" IS NOT NULL OR (customer."pricingProfitRateStatus" = 'CONFIGURED' AND customer."defaultPricingProfitRate" IS NOT NULL))
|
|
373
|
+
AND GREATEST(sales_amount."salesTotalCny" - COALESCE(manual."receivedAmountCny", 0), 0) > 0`;
|
|
374
|
+
}
|
|
375
|
+
export function analyticsBusinessDetailFromRaw(row) {
|
|
376
|
+
const action = typeof row.action === 'string' ? safeJsonParse(row.action) : row.action;
|
|
377
|
+
return {
|
|
378
|
+
id: stringValue(row.id), categoryKey: analyticsBusinessCategoryKeyValue(row.categoryKey), categoryLabel: stringValue(row.categoryLabel), referenceNo: stringValue(row.referenceNo),
|
|
379
|
+
...(row.subjectName == null ? {} : { subjectName: stringValue(row.subjectName) }),
|
|
380
|
+
...(row.businessDate == null ? {} : { businessDate: stringValue(row.businessDate) }),
|
|
381
|
+
...(row.reason == null ? {} : { reason: stringValue(row.reason) }),
|
|
382
|
+
...(row.amountCny == null ? {} : { amountCny: moneyString(row.amountCny) }),
|
|
383
|
+
status: stringValue(row.status), action: analyticsDetailActionValue(action)
|
|
384
|
+
};
|
|
385
|
+
}
|
|
386
|
+
function analyticsDetailActionValue(value) {
|
|
387
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
388
|
+
throw new DomainError(DomainErrorCodes.INVALID_INPUT, '分析明细操作无效');
|
|
389
|
+
const action = value;
|
|
390
|
+
const id = requiredId(action.id, '分析明细操作标识');
|
|
391
|
+
if (action.kind === 'CUSTOMER_CONTRACT_REVIEW')
|
|
392
|
+
return { kind: action.kind, label: '审核', id, importTaskId: requiredId(action.importTaskId, '客户合同任务标识') };
|
|
393
|
+
if (action.kind === 'PURCHASE_CONTRACT_REVIEW')
|
|
394
|
+
return { kind: action.kind, label: '审核', id, importTaskId: requiredId(action.importTaskId, '采购合同任务标识') };
|
|
395
|
+
if (action.kind === 'PURCHASE_ORDER_VIEW')
|
|
396
|
+
return { kind: action.kind, label: analyticsDetailActionLabel(action.label), id, purchaseOrderId: requiredId(action.purchaseOrderId, '采购订单标识'), supplierId: requiredId(action.supplierId, '供应商标识') };
|
|
397
|
+
if (action.kind === 'SALES_ORDER_VIEW')
|
|
398
|
+
return { kind: action.kind, label: analyticsDetailActionLabel(action.label), id, customerId: requiredId(action.customerId, '客户标识'), customerContractId: requiredId(action.customerContractId, '客户合同标识'), purchaseOrderId: requiredId(action.purchaseOrderId, '采购订单标识') };
|
|
399
|
+
if (action.kind === 'SALES_FILE_REVIEW') {
|
|
400
|
+
const fileType = ['PI', 'CI', 'PL', 'CUSTOMS_DECLARATION'].find(item => item === action.fileType);
|
|
401
|
+
const editVersion = Number(action.editVersion);
|
|
402
|
+
if (!fileType || !Number.isInteger(editVersion) || editVersion < 0)
|
|
403
|
+
throw new DomainError(DomainErrorCodes.INVALID_INPUT, '销售文件复核操作无效');
|
|
404
|
+
return { kind: action.kind, label: '复核', id, customerContractId: requiredId(action.customerContractId, '客户合同标识'), purchaseOrderId: requiredId(action.purchaseOrderId, '采购订单标识'), salesOrderNoSnapshot: requiredId(action.salesOrderNoSnapshot, '销售订单编号'), fileType, fileName: requiredId(action.fileName, '文件名'), storageFileId: requiredId(action.storageFileId, '存储文件标识'), editVersion, generatedAt: requiredId(action.generatedAt, '生成时间') };
|
|
405
|
+
}
|
|
406
|
+
throw new DomainError(DomainErrorCodes.INVALID_INPUT, '分析明细操作类型无效', { kind: action.kind });
|
|
407
|
+
}
|
|
408
|
+
function analyticsDetailActionLabel(value) {
|
|
409
|
+
if (value === '查看' || value === '跟进')
|
|
410
|
+
return value;
|
|
411
|
+
throw new DomainError(DomainErrorCodes.INVALID_INPUT, '分析明细操作名称无效', { value });
|
|
412
|
+
}
|
|
413
|
+
export function analyticsDashboardFromRaw(row) {
|
|
414
|
+
const parsed = parseAnalyticsDashboard(row?.dashboardJson);
|
|
415
|
+
if (parsed)
|
|
416
|
+
return parsed;
|
|
417
|
+
return {
|
|
418
|
+
kpis: {
|
|
419
|
+
purchaseTotalCny: '0.00', salesTotalCny: '0.00', receivedCny: '0.00', unreceivedCny: '0.00',
|
|
420
|
+
paidCny: '0.00', unpaidCny: '0.00', refundedCny: '0.00', unrefundedCny: '0.00',
|
|
421
|
+
estimatedGrossProfitCny: '0.00',
|
|
422
|
+
estimatedGrossProfitRate: '0.00000000',
|
|
423
|
+
complianceTodoCount: 0,
|
|
424
|
+
riskExposureCny: '0.00'
|
|
425
|
+
},
|
|
426
|
+
monthlyTrend: [],
|
|
427
|
+
todos: [],
|
|
428
|
+
riskDistribution: [],
|
|
429
|
+
customerValueTop5: [],
|
|
430
|
+
receivableRiskTop5: [],
|
|
431
|
+
supplierRiskTop5: [],
|
|
432
|
+
salesFileReviews: [],
|
|
433
|
+
riskOrders: [],
|
|
434
|
+
dataSources: []
|
|
435
|
+
};
|
|
436
|
+
}
|
|
437
|
+
function parseAnalyticsDashboard(value) {
|
|
438
|
+
const source = typeof value === 'string' ? safeJsonParse(value) : value;
|
|
439
|
+
if (!source || typeof source !== 'object')
|
|
440
|
+
return null;
|
|
441
|
+
const dashboard = source;
|
|
442
|
+
if (!dashboard.kpis || typeof dashboard.kpis !== 'object')
|
|
443
|
+
return null;
|
|
444
|
+
const rawTodos = arrayOf(dashboard.todos);
|
|
445
|
+
const riskDistribution = arrayOf(dashboard.riskDistribution).flatMap(item => {
|
|
446
|
+
const key = complianceCategoryKeyValue(item.key);
|
|
447
|
+
return key ? [{ key, label: stringValue(item.label), count: numberValue(item.count), amountCny: item.amountCny == null ? undefined : moneyString(item.amountCny) }] : [];
|
|
448
|
+
});
|
|
449
|
+
const todos = riskDistribution.map(risk => {
|
|
450
|
+
const todo = rawTodos.find(item => item.key === risk.key);
|
|
451
|
+
return { ...risk, priority: priorityValue(todo?.priority), description: stringValue(todo?.description) };
|
|
452
|
+
});
|
|
453
|
+
return {
|
|
454
|
+
kpis: {
|
|
455
|
+
purchaseTotalCny: moneyString(dashboard.kpis.purchaseTotalCny),
|
|
456
|
+
salesTotalCny: moneyString(dashboard.kpis.salesTotalCny),
|
|
457
|
+
receivedCny: moneyString(dashboard.kpis.receivedCny),
|
|
458
|
+
unreceivedCny: moneyString(dashboard.kpis.unreceivedCny),
|
|
459
|
+
paidCny: moneyString(dashboard.kpis.paidCny),
|
|
460
|
+
unpaidCny: moneyString(dashboard.kpis.unpaidCny),
|
|
461
|
+
refundedCny: moneyString(dashboard.kpis.refundedCny),
|
|
462
|
+
unrefundedCny: moneyString(dashboard.kpis.unrefundedCny),
|
|
463
|
+
estimatedGrossProfitCny: moneyString(dashboard.kpis.estimatedGrossProfitCny),
|
|
464
|
+
estimatedGrossProfitRate: decimalString(dashboard.kpis.estimatedGrossProfitRate),
|
|
465
|
+
complianceTodoCount: riskDistribution.reduce((sum, item) => sum + item.count, 0),
|
|
466
|
+
riskExposureCny: moneyString(dashboard.kpis.riskExposureCny)
|
|
467
|
+
},
|
|
468
|
+
monthlyTrend: arrayOf(dashboard.monthlyTrend).map(item => ({
|
|
469
|
+
month: numberValue(item.month),
|
|
470
|
+
purchaseAmountCny: moneyString(item.purchaseAmountCny),
|
|
471
|
+
salesAmountCny: moneyString(item.salesAmountCny),
|
|
472
|
+
estimatedGrossProfitCny: moneyString(item.estimatedGrossProfitCny),
|
|
473
|
+
receivedCny: moneyString(item.receivedCny)
|
|
474
|
+
})),
|
|
475
|
+
todos,
|
|
476
|
+
riskDistribution,
|
|
477
|
+
customerValueTop5: arrayOf(dashboard.customerValueTop5).map(item => ({
|
|
478
|
+
customerId: stringValue(item.customerId),
|
|
479
|
+
customerName: stringValue(item.customerName),
|
|
480
|
+
salesAmountCny: moneyString(item.salesAmountCny),
|
|
481
|
+
estimatedGrossProfitCny: moneyString(item.estimatedGrossProfitCny),
|
|
482
|
+
estimatedGrossProfitRate: decimalString(item.estimatedGrossProfitRate),
|
|
483
|
+
unreceivedCny: moneyString(item.unreceivedCny),
|
|
484
|
+
riskStatus: riskStatusValue(item.riskStatus)
|
|
485
|
+
})),
|
|
486
|
+
receivableRiskTop5: arrayOf(dashboard.receivableRiskTop5).map(item => ({
|
|
487
|
+
customerId: stringValue(item.customerId),
|
|
488
|
+
customerName: stringValue(item.customerName),
|
|
489
|
+
unreceivedCny: moneyString(item.unreceivedCny),
|
|
490
|
+
orderCount: numberValue(item.orderCount),
|
|
491
|
+
latestOrderDate: item.latestOrderDate == null ? null : stringValue(item.latestOrderDate)
|
|
492
|
+
})),
|
|
493
|
+
supplierRiskTop5: arrayOf(dashboard.supplierRiskTop5).map(item => ({
|
|
494
|
+
supplierId: stringValue(item.supplierId),
|
|
495
|
+
supplierName: stringValue(item.supplierName),
|
|
496
|
+
unpaidCny: moneyString(item.unpaidCny),
|
|
497
|
+
sanctionHitCount: numberValue(item.sanctionHitCount),
|
|
498
|
+
productRiskHitCount: numberValue(item.productRiskHitCount),
|
|
499
|
+
purchaseOrderCount: numberValue(item.purchaseOrderCount)
|
|
500
|
+
})),
|
|
501
|
+
salesFileReviews: arrayOf(dashboard.salesFileReviews).map(item => ({
|
|
502
|
+
fileId: stringValue(item.fileId),
|
|
503
|
+
fileName: stringValue(item.fileName),
|
|
504
|
+
fileType: stringValue(item.fileType),
|
|
505
|
+
salesOrderNo: stringValue(item.salesOrderNo),
|
|
506
|
+
reason: stringValue(item.reason),
|
|
507
|
+
generatedAt: stringValue(item.generatedAt)
|
|
508
|
+
})),
|
|
509
|
+
riskOrders: arrayOf(dashboard.riskOrders).map(item => ({
|
|
510
|
+
orderKey: stringValue(item.orderKey),
|
|
511
|
+
orderNo: stringValue(item.orderNo),
|
|
512
|
+
partyName: stringValue(item.partyName),
|
|
513
|
+
riskType: stringValue(item.riskType),
|
|
514
|
+
riskAmountCny: moneyString(item.riskAmountCny),
|
|
515
|
+
status: stringValue(item.status)
|
|
516
|
+
})),
|
|
517
|
+
dataSources: arrayOf(dashboard.dataSources).map(item => ({ label: stringValue(item.label), description: stringValue(item.description) }))
|
|
518
|
+
};
|
|
519
|
+
}
|
|
520
|
+
const exportComplianceCategories = {
|
|
521
|
+
'customer-contract-review': { label: '客户合同待审核', priority: 'high', status: '待审核' },
|
|
522
|
+
'purchase-contract-review': { label: '采购合同待审核', priority: 'high', status: '待审核' },
|
|
523
|
+
'product-risk-review': { label: '商品风险待确认', priority: 'medium', status: '待确认' },
|
|
524
|
+
'sanction-review': { label: '制裁公司待复核', priority: 'high', status: '待复核' },
|
|
525
|
+
'sales-file-review': { label: '销售文件待复核', priority: 'medium', status: '待复核' }
|
|
526
|
+
};
|
|
527
|
+
export function analyticsExportFundRisksFromRaw(rows) {
|
|
528
|
+
return exportRows(rows, '资金风险').map(row => {
|
|
529
|
+
const riskType = exactValue(row.riskType, ['未收款风险', '未付款风险'], '资金风险类型');
|
|
530
|
+
const reason = requiredExportString(row.reason, '资金风险异常原因');
|
|
531
|
+
if (reason !== (riskType === '未收款风险' ? '未收款' : '未付款'))
|
|
532
|
+
exportInputError('资金风险异常原因无效', { riskType, reason });
|
|
533
|
+
return {
|
|
534
|
+
riskType,
|
|
535
|
+
orderNo: requiredExportString(row.orderNo, '资金风险订单编号'),
|
|
536
|
+
partyName: requiredExportString(row.partyName, '资金风险业务对象'),
|
|
537
|
+
orderDate: requiredExportString(row.orderDate, '资金风险订单日期'),
|
|
538
|
+
orderAmountCny: strictMoneyString(row.orderAmountCny, '资金风险订单金额'),
|
|
539
|
+
settledAmountCny: strictMoneyString(row.settledAmountCny, '资金风险结算金额'),
|
|
540
|
+
riskGapCny: strictMoneyString(row.riskGapCny, '资金风险缺口'),
|
|
541
|
+
reason,
|
|
542
|
+
status: exactValue(row.status, ['待跟进'], '资金风险状态')
|
|
543
|
+
};
|
|
544
|
+
});
|
|
545
|
+
}
|
|
546
|
+
export function analyticsExportComplianceFromRaw(rows) {
|
|
547
|
+
return exportRows(rows, '合规事项').map(row => {
|
|
548
|
+
const categoryKey = exactValue(row.categoryKey, AnalyticsComplianceCategoryKeys, '合规事项分类');
|
|
549
|
+
const category = exportComplianceCategories[categoryKey];
|
|
550
|
+
const categoryLabel = exactValue(row.categoryLabel, [category.label], '合规事项分类名称');
|
|
551
|
+
const priority = exactValue(row.priority, [category.priority], '合规事项优先级');
|
|
552
|
+
const status = exactValue(row.status, [category.status], '合规事项状态');
|
|
553
|
+
const common = {
|
|
554
|
+
categoryKey,
|
|
555
|
+
categoryLabel,
|
|
556
|
+
referenceNo: requiredExportString(row.referenceNo, '合规事项业务编号'),
|
|
557
|
+
subjectName: requiredExportString(row.subjectName, '合规事项业务对象'),
|
|
558
|
+
businessDate: requiredExportString(row.businessDate, '合规事项业务日期'),
|
|
559
|
+
priority,
|
|
560
|
+
status
|
|
561
|
+
};
|
|
562
|
+
if (categoryKey === 'product-risk-review' || categoryKey === 'sanction-review') {
|
|
563
|
+
return {
|
|
564
|
+
...common,
|
|
565
|
+
reason: requiredExportString(row.reason, '合规事项异常原因'),
|
|
566
|
+
amountCny: strictMoneyString(row.amountCny, '合规事项关联金额')
|
|
567
|
+
};
|
|
568
|
+
}
|
|
569
|
+
if (categoryKey === 'sales-file-review') {
|
|
570
|
+
nonApplicableExportValue(row.amountCny, '销售文件关联金额');
|
|
571
|
+
return { ...common, reason: requiredExportString(row.reason, '销售文件真实异常原因') };
|
|
572
|
+
}
|
|
573
|
+
nonApplicableExportValue(row.reason, '合同审核异常原因');
|
|
574
|
+
nonApplicableExportValue(row.amountCny, '合同审核关联金额');
|
|
575
|
+
return common;
|
|
576
|
+
});
|
|
577
|
+
}
|
|
578
|
+
export function analyticsExportCustomersFromRaw(rows) {
|
|
579
|
+
return exportRows(rows, '客户经营').map(row => ({
|
|
580
|
+
customerName: requiredExportString(row.customerName, '客户名称'),
|
|
581
|
+
salesAmountCny: strictMoneyString(row.salesAmountCny, '客户销售额'),
|
|
582
|
+
purchaseCostCny: strictMoneyString(row.purchaseCostCny, '客户采购成本'),
|
|
583
|
+
estimatedGrossProfitCny: strictMoneyString(row.estimatedGrossProfitCny, '客户预计毛利'),
|
|
584
|
+
estimatedGrossProfitRate: strictDecimalString(row.estimatedGrossProfitRate, '客户预计毛利率'),
|
|
585
|
+
receivedCny: strictMoneyString(row.receivedCny, '客户已收款'),
|
|
586
|
+
unreceivedCny: strictMoneyString(row.unreceivedCny, '客户未收款'),
|
|
587
|
+
orderCount: strictCount(row.orderCount, '客户销售订单数'),
|
|
588
|
+
...optionalExportStringProperty('latestOrderDate', row.latestOrderDate),
|
|
589
|
+
riskStatus: exactValue(row.riskStatus, ['HIGH', 'MEDIUM', 'LOW'], '客户风险等级')
|
|
590
|
+
}));
|
|
591
|
+
}
|
|
592
|
+
export function analyticsExportSuppliersFromRaw(rows) {
|
|
593
|
+
return exportRows(rows, '供应商风险').map(row => ({
|
|
594
|
+
supplierName: requiredExportString(row.supplierName, '供应商名称'),
|
|
595
|
+
purchaseAmountCny: strictMoneyString(row.purchaseAmountCny, '供应商采购额'),
|
|
596
|
+
paidCny: strictMoneyString(row.paidCny, '供应商已付款'),
|
|
597
|
+
unpaidCny: strictMoneyString(row.unpaidCny, '供应商未付款'),
|
|
598
|
+
purchaseOrderCount: strictCount(row.purchaseOrderCount, '供应商采购订单数'),
|
|
599
|
+
...optionalExportStringProperty('latestOrderDate', row.latestOrderDate),
|
|
600
|
+
sanctionHitCount: strictCount(row.sanctionHitCount, '供应商制裁命中数'),
|
|
601
|
+
productRiskHitCount: strictCount(row.productRiskHitCount, '供应商商品风险命中数')
|
|
602
|
+
}));
|
|
603
|
+
}
|
|
604
|
+
export function analyticsExportSalesFileReviewsFromRaw(rows) {
|
|
605
|
+
return exportRows(rows, '文件复核').map(row => ({
|
|
606
|
+
fileName: requiredExportString(row.fileName, '文件名称'),
|
|
607
|
+
fileType: requiredExportString(row.fileType, '文件类型'),
|
|
608
|
+
salesOrderNo: requiredExportString(row.salesOrderNo, '销售订单号'),
|
|
609
|
+
customerName: requiredExportString(row.customerName, '文件客户名称'),
|
|
610
|
+
generatedAt: requiredExportString(row.generatedAt, '文件生成时间'),
|
|
611
|
+
reason: requiredExportString(row.reason, '文件真实异常原因'),
|
|
612
|
+
status: exactValue(row.status, ['待复核'], '文件复核状态')
|
|
613
|
+
}));
|
|
614
|
+
}
|
|
615
|
+
function exportRows(rows, label) {
|
|
616
|
+
if (!Array.isArray(rows))
|
|
617
|
+
exportInputError(`${label}原始数据无效`, { rows });
|
|
618
|
+
return rows.map((row, index) => {
|
|
619
|
+
if (!row || typeof row !== 'object' || Array.isArray(row))
|
|
620
|
+
exportInputError(`${label}原始行无效`, { index, row });
|
|
621
|
+
return row;
|
|
622
|
+
});
|
|
623
|
+
}
|
|
624
|
+
function requiredExportString(value, label) {
|
|
625
|
+
if (typeof value !== 'string')
|
|
626
|
+
exportInputError(`${label}无效`, { value });
|
|
627
|
+
const text = value.trim();
|
|
628
|
+
if (!text)
|
|
629
|
+
exportInputError(`缺少${label}`, { value });
|
|
630
|
+
return text;
|
|
631
|
+
}
|
|
632
|
+
function optionalExportStringProperty(key, value) {
|
|
633
|
+
if (value == null)
|
|
634
|
+
return {};
|
|
635
|
+
return { [key]: requiredExportString(value, key) };
|
|
636
|
+
}
|
|
637
|
+
function nonApplicableExportValue(value, label) {
|
|
638
|
+
if (value != null)
|
|
639
|
+
exportInputError(`${label}不适用`, { value });
|
|
640
|
+
}
|
|
641
|
+
function strictMoneyString(value, label) {
|
|
642
|
+
const decimal = strictDecimal(value, label);
|
|
643
|
+
return decimal.toFixed(2);
|
|
644
|
+
}
|
|
645
|
+
function strictDecimalString(value, label) { return strictDecimal(value, label).toString(); }
|
|
646
|
+
function strictDecimal(value, label) {
|
|
647
|
+
if ((typeof value !== 'string' && typeof value !== 'number') || value === '')
|
|
648
|
+
exportInputError(`${label}无效`, { value });
|
|
649
|
+
try {
|
|
650
|
+
const decimal = new Decimal(value);
|
|
651
|
+
if (!decimal.isFinite())
|
|
652
|
+
exportInputError(`${label}无效`, { value });
|
|
653
|
+
return decimal;
|
|
654
|
+
}
|
|
655
|
+
catch {
|
|
656
|
+
return exportInputError(`${label}无效`, { value });
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
function strictCount(value, label) {
|
|
660
|
+
if (typeof value !== 'string' && typeof value !== 'number')
|
|
661
|
+
exportInputError(`${label}无效`, { value });
|
|
662
|
+
if (typeof value === 'string' && !/^\d+$/.test(value))
|
|
663
|
+
exportInputError(`${label}无效`, { value });
|
|
664
|
+
const count = Number(value);
|
|
665
|
+
if (!Number.isSafeInteger(count) || count < 0)
|
|
666
|
+
exportInputError(`${label}无效`, { value });
|
|
667
|
+
return count;
|
|
668
|
+
}
|
|
669
|
+
function exactValue(value, allowed, label) {
|
|
670
|
+
if (!allowed.includes(value))
|
|
671
|
+
exportInputError(`${label}无效`, { value });
|
|
672
|
+
return value;
|
|
673
|
+
}
|
|
674
|
+
function exportInputError(message, details) {
|
|
675
|
+
throw new DomainError(DomainErrorCodes.INVALID_INPUT, message, details);
|
|
676
|
+
}
|
|
677
|
+
function safeJsonParse(value) { try {
|
|
678
|
+
return JSON.parse(value);
|
|
679
|
+
}
|
|
680
|
+
catch {
|
|
681
|
+
return null;
|
|
682
|
+
} }
|
|
683
|
+
function arrayOf(value) { return Array.isArray(value) ? value.filter((item) => Boolean(item) && typeof item === 'object') : []; }
|
|
684
|
+
function complianceCategoryKeyValue(value) { return AnalyticsComplianceCategoryKeys.find(key => key === value); }
|
|
685
|
+
function analyticsBusinessCategoryKeyValue(value) {
|
|
686
|
+
const complianceKey = complianceCategoryKeyValue(value);
|
|
687
|
+
if (complianceKey)
|
|
688
|
+
return complianceKey;
|
|
689
|
+
if (value === 'risk-order')
|
|
690
|
+
return value;
|
|
691
|
+
throw new DomainError(DomainErrorCodes.INVALID_INPUT, '分析明细业务分类无效', { value });
|
|
692
|
+
}
|
|
693
|
+
function stringValue(value) { return value == null ? '' : String(value); }
|
|
694
|
+
function numberValue(value) { const number = Number(value); return Number.isFinite(number) ? number : 0; }
|
|
695
|
+
export function analyticsDetailTotalFromRaw(value) { return numberValue(value); }
|
|
696
|
+
function priorityValue(value) { return value === 'high' || value === 'medium' || value === 'low' ? value : 'medium'; }
|
|
697
|
+
function riskStatusValue(value) { return value === 'HIGH' || value === 'MEDIUM' || value === 'LOW' ? value : 'LOW'; }
|
|
698
|
+
function moneyString(value) { try {
|
|
699
|
+
return new Decimal(value == null ? 0 : value).toFixed(2);
|
|
700
|
+
}
|
|
701
|
+
catch {
|
|
702
|
+
return '0.00';
|
|
703
|
+
} }
|
|
704
|
+
function decimalString(value) { try {
|
|
705
|
+
return new Decimal(value == null ? 0 : value).toString();
|
|
706
|
+
}
|
|
707
|
+
catch {
|
|
708
|
+
return '0';
|
|
709
|
+
} }
|
|
710
|
+
//# sourceMappingURL=analytics-query.js.map
|