@chenchaolong/plugin-trade-compliance-workbench 1.0.151 → 1.0.153
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-query.d.ts.map +1 -1
- package/dist/lib/analytics-query.js +21 -5
- package/dist/lib/analytics-query.js.map +1 -1
- package/dist/lib/domain/default-settings.d.ts.map +1 -1
- package/dist/lib/domain/default-settings.js +2 -0
- package/dist/lib/domain/default-settings.js.map +1 -1
- package/dist/lib/domain/finance.d.ts +19 -0
- package/dist/lib/domain/finance.d.ts.map +1 -1
- package/dist/lib/domain/finance.js +17 -0
- package/dist/lib/domain/finance.js.map +1 -1
- package/dist/lib/entities/customer.entity.d.ts +2 -0
- package/dist/lib/entities/customer.entity.d.ts.map +1 -1
- package/dist/lib/entities/customer.entity.js +8 -0
- package/dist/lib/entities/customer.entity.js.map +1 -1
- package/dist/lib/entities/settings.entity.d.ts +1 -1
- package/dist/lib/entities/settings.entity.d.ts.map +1 -1
- package/dist/lib/entities/settings.entity.js +2 -2
- package/dist/lib/entities/settings.entity.js.map +1 -1
- package/dist/lib/remote-components/trade-compliance-workbench/app.js +8 -8
- package/dist/lib/remote-ui/app-source.js +12 -7
- package/dist/lib/remote-ui/app-source.js.map +1 -1
- package/dist/lib/remote-ui/pages/files.d.ts.map +1 -1
- package/dist/lib/remote-ui/pages/files.js +11 -3
- package/dist/lib/remote-ui/pages/files.js.map +1 -1
- package/dist/lib/remote-ui/pages/sales.d.ts +32 -4
- package/dist/lib/remote-ui/pages/sales.d.ts.map +1 -1
- package/dist/lib/remote-ui/pages/sales.js +51 -10
- package/dist/lib/remote-ui/pages/sales.js.map +1 -1
- package/dist/lib/remote-ui/pages/settings.d.ts.map +1 -1
- package/dist/lib/remote-ui/pages/settings.js +5 -3
- package/dist/lib/remote-ui/pages/settings.js.map +1 -1
- package/dist/lib/remote-ui/sales-contracts.d.ts +13 -6
- package/dist/lib/remote-ui/sales-contracts.d.ts.map +1 -1
- package/dist/lib/remote-ui/settings-contracts.d.ts +1 -1
- package/dist/lib/remote-ui/settings-contracts.d.ts.map +1 -1
- package/dist/lib/remote-ui/settings-contracts.js +1 -1
- package/dist/lib/remote-ui/settings-contracts.js.map +1 -1
- package/dist/lib/runtime-validation.d.ts +1 -1
- package/dist/lib/sales-file.service.d.ts.map +1 -1
- package/dist/lib/sales-file.service.js +9 -1
- package/dist/lib/sales-file.service.js.map +1 -1
- package/dist/lib/schema-maintenance.d.ts.map +1 -1
- package/dist/lib/schema-maintenance.js +5 -1
- package/dist/lib/schema-maintenance.js.map +1 -1
- package/dist/lib/types.d.ts +21 -5
- package/dist/lib/types.d.ts.map +1 -1
- package/dist/lib/types.js +7 -2
- package/dist/lib/types.js.map +1 -1
- package/dist/lib/workbench-view.provider.d.ts +45 -19
- package/dist/lib/workbench-view.provider.d.ts.map +1 -1
- package/dist/lib/workbench.service.d.ts +47 -19
- package/dist/lib/workbench.service.d.ts.map +1 -1
- package/dist/lib/workbench.service.js +125 -22
- package/dist/lib/workbench.service.js.map +1 -1
- package/package.json +24 -23
|
@@ -13,7 +13,7 @@ import { createHash, randomUUID } from 'node:crypto';
|
|
|
13
13
|
import { BankAccountSettings, CatalogReviewBlock, CompanySanctionMatch, CompanySettings, ControlledCatalogBatch, ControlledCatalogImportIssue, ControlledGoodsRecord, Customer, CustomerContract, CustomerContractAnalysisRun, CustomerContractClause, CustomerContractClauseAnalysis, DerivedSalesOrderManualData, GeneratedSalesFile, HsCodeReferenceSnapshot, ImportTask, ManagedAsset, MonthlySafeExchangeRate, ProductComplianceMatch, ProfitRuleSettings, PurchaseOrder, PurchaseOrderLine, SanctionCatalogBatch, SanctionCatalogImportIssue, SanctionedCompanyAlias, SanctionedCompanyRecord, SettingsHistory, Supplier } from './entities/index.js';
|
|
14
14
|
import { CATALOG_REVIEW_BATCH_LIMIT, CATALOG_REVIEW_LEASE_MS, CATALOG_TRANSLATION_REVIEW_LEASE_MS } from './constants.js';
|
|
15
15
|
import { CatalogTypes, ClauseTypes, CustomerProfitRateStatuses, DomainError, DomainErrorCodes, RefundValidationMethods, RiskLevels, SalesPaymentStatuses, SanctionHitStatuses, StalledCatalogImportDecisions } from './types.js';
|
|
16
|
-
import { deriveSalesLine, assertReceivedAmount } from './domain/finance.js';
|
|
16
|
+
import { deriveSalesLine, assertReceivedAmount, calculateSalesLineFromRate } from './domain/finance.js';
|
|
17
17
|
import { assertYcNumberContractNo, parseCustomerContractNo, parsePurchaseContractNo } from './domain/contract-number.js';
|
|
18
18
|
import { isProductNameMatch, normalizeExactName, normalizeForProductMatch, normalizeHsCode, normalizeSanctionSource } from './domain/normalization.js';
|
|
19
19
|
import { importTaskTimedOut, nextSettingsRevision, normalizeOptionalDecimalInput } from './domain/workflow.js';
|
|
@@ -3461,11 +3461,9 @@ let TradeComplianceWorkbenchService = class TradeComplianceWorkbenchService {
|
|
|
3461
3461
|
continue;
|
|
3462
3462
|
const manual = manualMap.get(derivedOrderKey(contract.id, purchase.id));
|
|
3463
3463
|
const effectiveRate = manual?.pricingProfitRateOverride ?? (customer.pricingProfitRateStatus === 'CONFIGURED' ? customer.defaultPricingProfitRate : null);
|
|
3464
|
-
|
|
3465
|
-
continue;
|
|
3466
|
-
const source = manual?.pricingProfitRateOverride != null ? 'ORDER_OVERRIDE' : 'CUSTOMER_DEFAULT';
|
|
3464
|
+
const lineOverrides = linePricingOverrides(manual?.linePricingOverrides);
|
|
3467
3465
|
const derivedLines = (linesByPurchase.get(purchase.id) ?? []).map(line => {
|
|
3468
|
-
const pricing =
|
|
3466
|
+
const pricing = deriveSalesPricingLine(line, lineOverrides[line.id], effectiveRate);
|
|
3469
3467
|
return {
|
|
3470
3468
|
purchaseOrderLineId: line.id, productName: line.productName, englishName: line.englishName, productDescription: line.productDescription,
|
|
3471
3469
|
modelSpecification: line.modelSpecification, unit: line.unit, quantity: line.quantity, hsCode: line.hsCode,
|
|
@@ -3485,9 +3483,10 @@ let TradeComplianceWorkbenchService = class TradeComplianceWorkbenchService {
|
|
|
3485
3483
|
})
|
|
3486
3484
|
};
|
|
3487
3485
|
});
|
|
3488
|
-
const
|
|
3486
|
+
const pricedLines = derivedLines.filter(line => line.pricingStatus === 'PRICED' && line.salesLineAmountCny != null);
|
|
3487
|
+
const amount = manual?.orderAmountCnyOverride != null ? new Decimal(manual.orderAmountCnyOverride) : pricedLines.reduce((sum, line) => sum.plus(line.salesLineAmountCny ?? 0), new Decimal(0));
|
|
3489
3488
|
const received = new Decimal(manual?.receivedAmountCny ?? 0);
|
|
3490
|
-
results.push({ customerContractId: contract.id, purchaseOrderId: purchase.id, customerId: customer.id, salesOrderNo: parsePurchaseContractNo(purchase.contractNo).salesOrderNo, customerContractNo: contract.contractNo, purchaseContractNo: purchase.contractNo, orderDate: purchase.orderDate, currencyCode: 'CNY', pricingProfitRateSource:
|
|
3489
|
+
results.push({ customerContractId: contract.id, purchaseOrderId: purchase.id, customerId: customer.id, salesOrderNo: parsePurchaseContractNo(purchase.contractNo).salesOrderNo, customerContractNo: contract.contractNo, purchaseContractNo: purchase.contractNo, orderDate: purchase.orderDate, currencyCode: 'CNY', pricingProfitRateSource: orderPricingSource(derivedLines, manual?.pricingProfitRateOverride, customer.pricingProfitRateStatus === 'CONFIGURED' ? customer.defaultPricingProfitRate : null), effectivePricingProfitRate: effectiveRate, pricingStatus: orderPricingStatus(derivedLines), missingPricingFields: orderMissingPricingFields(derivedLines), orderAmountCny: amount.toFixed(manual?.orderAmountCnyOverride != null ? 4 : 0), receivedAmountCny: received.toFixed(2), unreceivedAmountCny: Decimal.max(amount.minus(received), 0).toFixed(2), lines: derivedLines });
|
|
3491
3490
|
}
|
|
3492
3491
|
}
|
|
3493
3492
|
return results.sort(compareDerivedSalesOrders);
|
|
@@ -3499,7 +3498,6 @@ let TradeComplianceWorkbenchService = class TradeComplianceWorkbenchService {
|
|
|
3499
3498
|
.leftJoin(DerivedSalesOrderManualData, 'manual', 'manual."customerContractId" = contract.id AND manual."purchaseOrderId" = purchase.id AND manual."tenantId" = contract."tenantId" AND manual."organizationId" = contract."organizationId" AND manual."deletedAt" IS NULL')
|
|
3500
3499
|
.leftJoin(Supplier, 'supplier', 'supplier.id = purchase."supplierId" AND supplier."tenantId" = purchase."tenantId" AND supplier."organizationId" = purchase."organizationId" AND supplier."deletedAt" IS NULL')
|
|
3501
3500
|
.where('contract."tenantId" = :tenantId AND contract."organizationId" = :organizationId AND contract."deletedAt" IS NULL', activeScope(scope))
|
|
3502
|
-
.andWhere("(manual.\"pricingProfitRateOverride\" IS NOT NULL OR (customer.\"pricingProfitRateStatus\" = 'CONFIGURED' AND customer.\"defaultPricingProfitRate\" IS NOT NULL))")
|
|
3503
3501
|
.select('contract.id', 'customerContractId').addSelect('purchase.id', 'purchaseOrderId');
|
|
3504
3502
|
if (customerId)
|
|
3505
3503
|
qb.andWhere('customer.id = :customerId', { customerId });
|
|
@@ -3517,8 +3515,7 @@ let TradeComplianceWorkbenchService = class TradeComplianceWorkbenchService {
|
|
|
3517
3515
|
qb.andWhere('purchase."orderDate" >= :orderDateFrom', { orderDateFrom: date.from });
|
|
3518
3516
|
if (hasQueryValue(date.to))
|
|
3519
3517
|
qb.andWhere('purchase."orderDate" <= :orderDateTo', { orderDateTo: date.to });
|
|
3520
|
-
const
|
|
3521
|
-
const amount = `(SELECT COALESCE(SUM(ROUND(ROUND(sales_line."unitPriceTaxIncluded" * (1 + ${effectiveRate}), 0) * sales_line.quantity, 0)), 0) FROM plugin_trade_compliance_purchase_order_line sales_line WHERE sales_line."purchaseOrderId" = purchase.id AND sales_line."tenantId" = purchase."tenantId" AND sales_line."organizationId" = purchase."organizationId" AND sales_line."deletedAt" IS NULL)`;
|
|
3518
|
+
const amount = derivedSalesOrderAmountSql('sales_line', 'manual."linePricingOverrides"', 'COALESCE(manual."pricingProfitRateOverride", customer."defaultPricingProfitRate")');
|
|
3522
3519
|
const received = 'COALESCE(manual."receivedAmountCny", 0)';
|
|
3523
3520
|
if (parameters.paymentStatus === 'UNPAID')
|
|
3524
3521
|
qb.andWhere(`${received} <= 0`);
|
|
@@ -3548,7 +3545,8 @@ let TradeComplianceWorkbenchService = class TradeComplianceWorkbenchService {
|
|
|
3548
3545
|
return { items: items, total, page, pageSize };
|
|
3549
3546
|
}
|
|
3550
3547
|
async saveCustomerProfitRate(scope, customerId, rate) {
|
|
3551
|
-
|
|
3548
|
+
const normalizedRate = normalizeOptionalDecimalInput(rate);
|
|
3549
|
+
if (normalizedRate != null && new Decimal(normalizedRate).isNegative())
|
|
3552
3550
|
throw new DomainError(DomainErrorCodes.INVALID_INPUT, '客户默认利润率不能小于 0');
|
|
3553
3551
|
return this.dataSource.transaction(async (manager) => {
|
|
3554
3552
|
const customerRepo = manager.getRepository(Customer);
|
|
@@ -3563,13 +3561,13 @@ let TradeComplianceWorkbenchService = class TradeComplianceWorkbenchService {
|
|
|
3563
3561
|
const contractIds = contracts.map(contract => contract.id);
|
|
3564
3562
|
const manuals = contractIds.length && purchaseIds.length ? await manager.getRepository(DerivedSalesOrderManualData).find({ where: { ...activeScope(scope), customerContractId: In(contractIds), purchaseOrderId: In(purchaseIds), deletedAt: IsNull() }, order: { customerContractId: 'ASC', purchaseOrderId: 'ASC' }, lock: { mode: 'pessimistic_write' } }) : [];
|
|
3565
3563
|
const linesByPurchase = groupBy(lines, line => line.purchaseOrderId);
|
|
3566
|
-
for (const manual of manuals.filter(item => item.pricingProfitRateOverride == null)) {
|
|
3567
|
-
const orderAmount =
|
|
3564
|
+
for (const manual of manuals.filter(item => item.pricingProfitRateOverride == null && manual.orderAmountCnyOverride == null && normalizedRate != null)) {
|
|
3565
|
+
const orderAmount = deriveOrderPricingAmount(linesByPurchase.get(manual.purchaseOrderId) ?? [], manual.linePricingOverrides, normalizedRate);
|
|
3568
3566
|
assertReceivedAmount(orderAmount, manual.receivedAmountCny);
|
|
3569
3567
|
}
|
|
3570
|
-
customer.defaultPricingProfitRate =
|
|
3568
|
+
customer.defaultPricingProfitRate = normalizedRate;
|
|
3571
3569
|
customer.updatedById = scope.userId ?? null;
|
|
3572
|
-
customer.pricingProfitRateStatus = 'CONFIGURED';
|
|
3570
|
+
customer.pricingProfitRateStatus = normalizedRate == null ? 'MISSING' : 'CONFIGURED';
|
|
3573
3571
|
return customerRepo.save(customer);
|
|
3574
3572
|
});
|
|
3575
3573
|
}
|
|
@@ -3599,19 +3597,28 @@ let TradeComplianceWorkbenchService = class TradeComplianceWorkbenchService {
|
|
|
3599
3597
|
const where = { ...activeScope(scope), customerContractId, purchaseOrderId, deletedAt: IsNull() };
|
|
3600
3598
|
let row = await repo.findOne({ where, lock: { mode: 'pessimistic_write' } });
|
|
3601
3599
|
if (!row) {
|
|
3602
|
-
await repo.createQueryBuilder().insert().into(DerivedSalesOrderManualData).values({ ...auditScope(scope), customerContractId, purchaseOrderId, pricingProfitRateOverride: null, receivedAmountCny: '0' }).orIgnore().execute();
|
|
3600
|
+
await repo.createQueryBuilder().insert().into(DerivedSalesOrderManualData).values({ ...auditScope(scope), customerContractId, purchaseOrderId, pricingProfitRateOverride: null, orderAmountCnyOverride: null, linePricingOverrides: null, receivedAmountCny: '0' }).orIgnore().execute();
|
|
3603
3601
|
row = await required(repo.findOne({ where, lock: { mode: 'pessimistic_write' } }), '销售订单人工维护数据创建失败');
|
|
3604
3602
|
}
|
|
3605
3603
|
const override = input.pricingProfitRateOverride === undefined ? row.pricingProfitRateOverride : normalizeOptionalDecimalInput(input.pricingProfitRateOverride);
|
|
3606
3604
|
if (override != null && new Decimal(override).isNegative())
|
|
3607
3605
|
throw new DomainError(DomainErrorCodes.INVALID_INPUT, '订单利润率不能小于 0');
|
|
3606
|
+
const orderAmountCnyOverride = input.orderAmountCnyOverride === undefined ? row.orderAmountCnyOverride : normalizeOptionalDecimalInput(input.orderAmountCnyOverride);
|
|
3607
|
+
if (orderAmountCnyOverride != null && new Decimal(orderAmountCnyOverride).isNegative())
|
|
3608
|
+
throw new DomainError(DomainErrorCodes.INVALID_INPUT, '销售订单总额不能小于 0');
|
|
3609
|
+
const nextLineOverrides = input.linePricingOverrides === undefined
|
|
3610
|
+
? row.linePricingOverrides
|
|
3611
|
+
: normalizeLinePricingOverrides({ ...linePricingOverrides(row.linePricingOverrides), ...linePricingOverrides(input.linePricingOverrides) });
|
|
3608
3612
|
const effectiveRate = override ?? (customer.pricingProfitRateStatus === 'CONFIGURED' ? customer.defaultPricingProfitRate : null);
|
|
3609
|
-
if (effectiveRate == null)
|
|
3610
|
-
throw new DomainError(DomainErrorCodes.CUSTOMER_PROFIT_RATE_REQUIRED, '客户默认利润率未维护');
|
|
3611
3613
|
const received = input.receivedAmountCny ?? row.receivedAmountCny;
|
|
3612
|
-
|
|
3614
|
+
const finalAmount = orderAmountCnyOverride ?? deriveOrderPricingAmount(lines, nextLineOverrides, effectiveRate);
|
|
3615
|
+
assertReceivedAmount(finalAmount, received);
|
|
3613
3616
|
if (input.pricingProfitRateOverride !== undefined)
|
|
3614
3617
|
row.pricingProfitRateOverride = override;
|
|
3618
|
+
if (input.orderAmountCnyOverride !== undefined)
|
|
3619
|
+
row.orderAmountCnyOverride = orderAmountCnyOverride;
|
|
3620
|
+
if (input.linePricingOverrides !== undefined)
|
|
3621
|
+
row.linePricingOverrides = nextLineOverrides;
|
|
3615
3622
|
if (input.receivedAmountCny !== undefined)
|
|
3616
3623
|
row.receivedAmountCny = input.receivedAmountCny;
|
|
3617
3624
|
row.updatedById = scope.userId ?? null;
|
|
@@ -4292,7 +4299,7 @@ let TradeComplianceWorkbenchService = class TradeComplianceWorkbenchService {
|
|
|
4292
4299
|
const received = new Decimal(order.receivedAmountCny);
|
|
4293
4300
|
const total = new Decimal(order.orderAmountCny);
|
|
4294
4301
|
const paymentStatus = received.lte(0) ? 'UNPAID' : received.gte(total) ? 'PAID' : 'PARTIAL';
|
|
4295
|
-
return { ...order, customerName: customerNames.get(order.customerId) ?? '', supplierName: supplierNames.get(purchaseSuppliers.get(order.purchaseOrderId) ?? '') ?? '', pricingProfitRateStatus: 'CONFIGURED', paymentStatus, lines: order.lines.map(line => ({ ...line, complianceCategories: [...new Set(line.complianceMatches.map(match => match.catalogType))] })) };
|
|
4302
|
+
return { ...order, customerName: customerNames.get(order.customerId) ?? '', supplierName: supplierNames.get(purchaseSuppliers.get(order.purchaseOrderId) ?? '') ?? '', pricingProfitRateStatus: order.pricingStatus === 'UNPRICED' ? 'MISSING' : 'CONFIGURED', paymentStatus, lines: order.lines.map(line => ({ ...line, complianceCategories: [...new Set(line.complianceMatches.map(match => match.catalogType))] })) };
|
|
4296
4303
|
});
|
|
4297
4304
|
}
|
|
4298
4305
|
async purchaseOrderDtos(scope, orders) {
|
|
@@ -4797,6 +4804,100 @@ function groupBy(rows, keyOf) { const grouped = new Map(); for (const row of row
|
|
|
4797
4804
|
} return grouped; }
|
|
4798
4805
|
function compareDerivedSalesOrders(left, right) { return left.salesOrderNo.localeCompare(right.salesOrderNo) || left.purchaseOrderId.localeCompare(right.purchaseOrderId) || left.customerContractId.localeCompare(right.customerContractId); }
|
|
4799
4806
|
function derivedOrderAmount(lines, rate) { return lines.reduce((total, line) => total.plus(deriveSalesLine({ purchaseUnitPriceCny: line.unitPriceTaxIncluded, quantity: line.quantity }, rate).salesLineAmountCny), new Decimal(0)).toFixed(0); }
|
|
4807
|
+
function derivedSalesOrderAmountSql(lineAlias, overridesExpression, effectiveRateExpression) {
|
|
4808
|
+
const override = `${overridesExpression} -> ${lineAlias}.id::text`;
|
|
4809
|
+
const lineRate = `COALESCE(NULLIF(BTRIM(${override}->>'pricingProfitRate'), '')::numeric, ${effectiveRateExpression})`;
|
|
4810
|
+
const lineAmount = `COALESCE(NULLIF(BTRIM(${override}->>'salesLineAmountCny'), '')::numeric, NULLIF(BTRIM(${override}->>'salesUnitPriceCny'), '')::numeric * ${lineAlias}.quantity, CASE WHEN COALESCE(${override}->>'pricingMethod', 'TAX_INCLUDED_MARKUP') = 'TAX_INCLUDED_MARKUP' AND ${lineAlias}."unitPriceTaxIncluded" IS NOT NULL AND ${lineRate} IS NOT NULL THEN ROUND(ROUND(${lineAlias}."unitPriceTaxIncluded" * (1 + ${lineRate}), 0) * ${lineAlias}.quantity, 0) ELSE NULL END, 0)`;
|
|
4811
|
+
return `COALESCE(manual."orderAmountCnyOverride", (SELECT COALESCE(SUM(${lineAmount}), 0) FROM plugin_trade_compliance_purchase_order_line ${lineAlias} WHERE ${lineAlias}."purchaseOrderId" = purchase.id AND ${lineAlias}."tenantId" = purchase."tenantId" AND ${lineAlias}."organizationId" = purchase."organizationId" AND ${lineAlias}."deletedAt" IS NULL))`;
|
|
4812
|
+
}
|
|
4813
|
+
function linePricingOverrides(value) {
|
|
4814
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
4815
|
+
return {};
|
|
4816
|
+
const entries = Object.entries(value).filter((entry) => Boolean(entry[1]) && typeof entry[1] === 'object' && !Array.isArray(entry[1]));
|
|
4817
|
+
return Object.fromEntries(entries);
|
|
4818
|
+
}
|
|
4819
|
+
function normalizeLinePricingOverrides(value) {
|
|
4820
|
+
const overrides = linePricingOverrides(value);
|
|
4821
|
+
const normalized = {};
|
|
4822
|
+
for (const [lineId, override] of Object.entries(overrides)) {
|
|
4823
|
+
const next = {};
|
|
4824
|
+
for (const key of ['salesLineAmountCny', 'salesUnitPriceCny', 'pricingProfitRate']) {
|
|
4825
|
+
const raw = override[key];
|
|
4826
|
+
if (raw != null && typeof raw !== 'string')
|
|
4827
|
+
throw new DomainError(DomainErrorCodes.INVALID_INPUT, `${key}无效`);
|
|
4828
|
+
const value = normalizeOptionalDecimalInput(raw);
|
|
4829
|
+
if (value != null) {
|
|
4830
|
+
if (new Decimal(value).isNegative())
|
|
4831
|
+
throw new DomainError(DomainErrorCodes.INVALID_INPUT, `${key}不能小于 0`);
|
|
4832
|
+
next[key] = value;
|
|
4833
|
+
}
|
|
4834
|
+
}
|
|
4835
|
+
if (override.pricingMethod != null)
|
|
4836
|
+
next.pricingMethod = pricingMethod(override.pricingMethod);
|
|
4837
|
+
if (Object.keys(next).length)
|
|
4838
|
+
normalized[lineId] = next;
|
|
4839
|
+
}
|
|
4840
|
+
return Object.keys(normalized).length ? normalized : null;
|
|
4841
|
+
}
|
|
4842
|
+
function optionalPricingText(value) { return value == null || value === '' ? null : String(value); }
|
|
4843
|
+
function pricingMethod(value) { return value === 'TAX_EXCLUDED_MARKUP' ? 'TAX_EXCLUDED_MARKUP' : 'TAX_INCLUDED_MARKUP'; }
|
|
4844
|
+
function deriveSalesPricingLine(line, override, orderRate) {
|
|
4845
|
+
const quantity = optionalPricingText(line.quantity);
|
|
4846
|
+
const manualAmount = optionalPricingText(override?.salesLineAmountCny);
|
|
4847
|
+
if (manualAmount != null) {
|
|
4848
|
+
const amount = new Decimal(manualAmount);
|
|
4849
|
+
const unit = quantity && !new Decimal(quantity).isZero() ? amount.div(quantity).toDecimalPlaces(4, Decimal.ROUND_HALF_UP).toFixed(4) : null;
|
|
4850
|
+
return { salesUnitPriceCny: unit, salesLineAmountCny: amount.toFixed(4), pricingProfitRate: null, pricingMethod: null, pricingSource: 'MANUAL_AMOUNT', pricingStatus: amount.isNegative() ? 'INVALID' : 'PRICED', missingPricingFields: [] };
|
|
4851
|
+
}
|
|
4852
|
+
const manualUnit = optionalPricingText(override?.salesUnitPriceCny);
|
|
4853
|
+
if (manualUnit != null) {
|
|
4854
|
+
const unit = new Decimal(manualUnit);
|
|
4855
|
+
const amount = quantity == null ? null : unit.mul(quantity).toDecimalPlaces(4, Decimal.ROUND_HALF_UP);
|
|
4856
|
+
return { salesUnitPriceCny: unit.toFixed(4), salesLineAmountCny: amount == null ? null : amount.toFixed(4), pricingProfitRate: null, pricingMethod: null, pricingSource: 'MANUAL_UNIT_PRICE', pricingStatus: unit.isNegative() || amount == null || amount.isNegative() ? 'INVALID' : 'PRICED', missingPricingFields: amount == null ? ['quantity'] : [] };
|
|
4857
|
+
}
|
|
4858
|
+
const lineRate = optionalPricingText(override?.pricingProfitRate);
|
|
4859
|
+
const rate = lineRate ?? orderRate;
|
|
4860
|
+
const method = pricingMethod(override?.pricingMethod);
|
|
4861
|
+
if (rate == null)
|
|
4862
|
+
return { salesUnitPriceCny: null, salesLineAmountCny: null, pricingProfitRate: null, pricingMethod: method, pricingSource: 'UNPRICED', pricingStatus: 'UNPRICED', missingPricingFields: ['salesAmountOrPricingInputs'] };
|
|
4863
|
+
const calculated = calculateSalesLineFromRate({
|
|
4864
|
+
quantity,
|
|
4865
|
+
purchaseUnitPriceTaxIncluded: optionalPricingText(line.unitPriceTaxIncluded),
|
|
4866
|
+
purchaseUnitPriceTaxExcluded: optionalPricingText(line.unitPriceTaxExcludedCny ?? line.unitPriceTaxExcluded),
|
|
4867
|
+
pricingProfitRate: rate,
|
|
4868
|
+
pricingMethod: method
|
|
4869
|
+
});
|
|
4870
|
+
if ('invalid' in calculated)
|
|
4871
|
+
return { salesUnitPriceCny: null, salesLineAmountCny: null, pricingProfitRate: rate, pricingMethod: method, pricingSource: lineRate != null ? 'LINE_RATE' : orderRate ? 'ORDER_OVERRIDE' : 'UNPRICED', pricingStatus: 'INVALID', missingPricingFields: [] };
|
|
4872
|
+
if (!('salesLineAmountCny' in calculated))
|
|
4873
|
+
return { salesUnitPriceCny: null, salesLineAmountCny: null, pricingProfitRate: rate, pricingMethod: method, pricingSource: 'UNPRICED', pricingStatus: 'UNPRICED', missingPricingFields: calculated.missingFields };
|
|
4874
|
+
return { salesUnitPriceCny: calculated.salesUnitPriceCny, salesLineAmountCny: calculated.salesLineAmountCny, pricingProfitRate: rate, pricingMethod: method, pricingSource: lineRate != null ? 'LINE_RATE' : orderRate ? 'ORDER_OVERRIDE' : 'UNPRICED', pricingStatus: 'PRICED', missingPricingFields: [] };
|
|
4875
|
+
}
|
|
4876
|
+
function orderPricingStatus(lines) {
|
|
4877
|
+
if (lines.some(line => line.pricingStatus === 'INVALID'))
|
|
4878
|
+
return 'INVALID';
|
|
4879
|
+
const priced = lines.filter(line => line.pricingStatus === 'PRICED').length;
|
|
4880
|
+
return priced === lines.length && lines.length > 0 ? 'COMPLETE' : priced > 0 ? 'PARTIAL' : 'UNPRICED';
|
|
4881
|
+
}
|
|
4882
|
+
function orderPricingSource(lines, orderRate, customerRate) {
|
|
4883
|
+
const sources = new Set(lines.map(line => line.pricingSource).filter(source => source !== 'UNPRICED'));
|
|
4884
|
+
if (sources.size === 0)
|
|
4885
|
+
return 'NONE';
|
|
4886
|
+
if (sources.size > 1)
|
|
4887
|
+
return 'MIXED';
|
|
4888
|
+
const [source] = [...sources];
|
|
4889
|
+
if (source === 'LINE_RATE')
|
|
4890
|
+
return 'LINE_OVERRIDE';
|
|
4891
|
+
if (source === 'MANUAL_AMOUNT' || source === 'MANUAL_UNIT_PRICE')
|
|
4892
|
+
return 'MANUAL_AMOUNT';
|
|
4893
|
+
return orderRate != null ? 'ORDER_OVERRIDE' : customerRate != null ? 'CUSTOMER_DEFAULT' : 'NONE';
|
|
4894
|
+
}
|
|
4895
|
+
function orderMissingPricingFields(lines) { return [...new Set(lines.flatMap(line => line.missingPricingFields))]; }
|
|
4896
|
+
function deriveOrderPricingAmount(lines, overrides, effectiveRate) {
|
|
4897
|
+
const overrideMap = linePricingOverrides(overrides);
|
|
4898
|
+
const priced = lines.map(line => deriveSalesPricingLine(line, overrideMap[line.id], effectiveRate)).filter(line => line.pricingStatus === 'PRICED' && line.salesLineAmountCny != null);
|
|
4899
|
+
return priced.reduce((sum, line) => sum.plus(line.salesLineAmountCny ?? 0), new Decimal(0)).toFixed(4);
|
|
4900
|
+
}
|
|
4800
4901
|
function assertOptionalEnum(value, allowed, label) { if (hasQueryValue(value) && !allowed.includes(value))
|
|
4801
4902
|
throw new DomainError(DomainErrorCodes.INVALID_INPUT, `${label}无效`); }
|
|
4802
4903
|
function fuzzyValue(value, filter) { return !hasQueryValue(filter) || String(value ?? '').toLocaleLowerCase().includes(String(filter).trim().toLocaleLowerCase()); }
|
|
@@ -4806,8 +4907,10 @@ function salesMetrics(rows) {
|
|
|
4806
4907
|
const currentYearRows = rows.filter(row => String(row.orderDate ?? '').slice(0, 4) === currentYear);
|
|
4807
4908
|
const productAmounts = new Map();
|
|
4808
4909
|
for (const row of rows)
|
|
4809
|
-
for (const line of row.lines)
|
|
4810
|
-
|
|
4910
|
+
for (const line of row.lines) {
|
|
4911
|
+
if (line.salesLineAmountCny != null)
|
|
4912
|
+
productAmounts.set(line.productName, (productAmounts.get(line.productName) ?? new Decimal(0)).plus(line.salesLineAmountCny));
|
|
4913
|
+
}
|
|
4811
4914
|
return {
|
|
4812
4915
|
orderCount: rows.length,
|
|
4813
4916
|
salesAmountCny: sum(rows, 'orderAmountCny'),
|