@chenchaolong/plugin-trade-compliance-workbench 1.0.152 → 1.0.154
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 +29 -9
- package/dist/lib/analytics-query.js.map +1 -1
- package/dist/lib/catalog-import/docx.parser.d.ts.map +1 -1
- package/dist/lib/catalog-import/docx.parser.js +1 -4
- package/dist/lib/catalog-import/docx.parser.js.map +1 -1
- package/dist/lib/domain/contract-number.d.ts +1 -1
- package/dist/lib/domain/contract-number.d.ts.map +1 -1
- package/dist/lib/domain/contract-number.js +17 -2
- package/dist/lib/domain/contract-number.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/catalog.entity.d.ts +2 -1
- package/dist/lib/entities/catalog.entity.d.ts.map +1 -1
- package/dist/lib/entities/catalog.entity.js +5 -1
- package/dist/lib/entities/catalog.entity.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.css +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/contracts.d.ts +1 -0
- package/dist/lib/remote-ui/contracts.d.ts.map +1 -1
- package/dist/lib/remote-ui/pages/catalogs.d.ts +1 -0
- package/dist/lib/remote-ui/pages/catalogs.d.ts.map +1 -1
- package/dist/lib/remote-ui/pages/catalogs.js +14 -9
- package/dist/lib/remote-ui/pages/catalogs.js.map +1 -1
- package/dist/lib/remote-ui/pages/procurement.d.ts.map +1 -1
- package/dist/lib/remote-ui/pages/procurement.js +13 -3
- package/dist/lib/remote-ui/pages/procurement.js.map +1 -1
- package/dist/lib/remote-ui/pages/sales-files.css +5 -0
- package/dist/lib/remote-ui/pages/sales.d.ts +30 -3
- package/dist/lib/remote-ui/pages/sales.d.ts.map +1 -1
- package/dist/lib/remote-ui/pages/sales.js +58 -12
- package/dist/lib/remote-ui/pages/sales.js.map +1 -1
- package/dist/lib/remote-ui/procurement-contracts.d.ts +7 -1
- package/dist/lib/remote-ui/procurement-contracts.d.ts.map +1 -1
- package/dist/lib/remote-ui/procurement-contracts.js.map +1 -1
- package/dist/lib/remote-ui/sales-contracts.d.ts +20 -7
- package/dist/lib/remote-ui/sales-contracts.d.ts.map +1 -1
- package/dist/lib/remote-ui/styles.css +1 -0
- package/dist/lib/sales-file.service.d.ts.map +1 -1
- package/dist/lib/sales-file.service.js +10 -2
- 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 +13 -5
- package/dist/lib/schema-maintenance.js.map +1 -1
- package/dist/lib/types.d.ts +22 -4
- package/dist/lib/types.d.ts.map +1 -1
- package/dist/lib/types.js +7 -1
- package/dist/lib/types.js.map +1 -1
- package/dist/lib/workbench-view.provider.d.ts +42 -243
- package/dist/lib/workbench-view.provider.d.ts.map +1 -1
- package/dist/lib/workbench.middleware.d.ts +35 -28
- package/dist/lib/workbench.middleware.d.ts.map +1 -1
- package/dist/lib/workbench.middleware.js +1 -1
- package/dist/lib/workbench.middleware.js.map +1 -1
- package/dist/lib/workbench.service.d.ts +59 -89
- package/dist/lib/workbench.service.d.ts.map +1 -1
- package/dist/lib/workbench.service.js +186 -45
- package/dist/lib/workbench.service.js.map +1 -1
- package/package.json +1 -1
|
@@ -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';
|
|
@@ -682,7 +682,7 @@ let TradeComplianceWorkbenchService = class TradeComplianceWorkbenchService {
|
|
|
682
682
|
}
|
|
683
683
|
async listSanctionedCompanies(scope, query = {}) {
|
|
684
684
|
const page = await this.pagedCatalogQuery(SanctionedCompanyRecord, SanctionCatalogBatch, scope, query, {
|
|
685
|
-
exact: { reviewStatus: query.parameters?.reviewStatus },
|
|
685
|
+
exact: { reviewStatus: query.parameters?.reviewStatus, subjectKind: query.parameters?.subjectKind },
|
|
686
686
|
fuzzy: {
|
|
687
687
|
companyName: query.parameters?.companyName,
|
|
688
688
|
countryOrRegion: query.parameters?.countryOrRegion,
|
|
@@ -789,10 +789,11 @@ let TradeComplianceWorkbenchService = class TradeComplianceWorkbenchService {
|
|
|
789
789
|
const companyName = input.companyName.trim();
|
|
790
790
|
const normalizedCompanyName = normalizeExactName(companyName);
|
|
791
791
|
const normalizedSanctionListSource = normalizeSanctionSource(input.sanctionListSource);
|
|
792
|
-
const
|
|
792
|
+
const subjectKind = input.subjectKind === 'PERSON' ? 'PERSON' : 'ENTITY';
|
|
793
|
+
const duplicate = await repo.findOne({ where: { ...activeScope(scope), lifecycleStatus: 'ACTIVE', subjectKind, normalizedCompanyName, normalizedSanctionListSource, deletedAt: IsNull() } });
|
|
793
794
|
if (duplicate)
|
|
794
795
|
throw new DomainError(DomainErrorCodes.DUPLICATE_CATALOG_RECORD, '相同公司名称和制裁名单来源的记录已存在', { id: duplicate.id });
|
|
795
|
-
return repo.save(repo.create({ ...auditScope(scope), lifecycleStatus: 'ACTIVE', reviewStatus: 'PENDING', sourceType: input.sourceType ?? 'MANUAL_ENTRY', companyName, normalizedCompanyName, countryOrRegion: input.countryOrRegion ?? null, sanctionListSource: input.sanctionListSource ?? null, normalizedSanctionListSource, sanctionReason: input.sanctionReason ?? null, effectiveDate: input.effectiveDate ?? null, remarks: input.remarks ?? null, sourceLocation: input.sourceLocation ?? null, sourceContent: input.sourceContent ?? null }));
|
|
796
|
+
return repo.save(repo.create({ ...auditScope(scope), lifecycleStatus: 'ACTIVE', reviewStatus: 'PENDING', sourceType: input.sourceType ?? 'MANUAL_ENTRY', subjectKind, companyName, normalizedCompanyName, countryOrRegion: input.countryOrRegion ?? null, sanctionListSource: input.sanctionListSource ?? null, normalizedSanctionListSource, sanctionReason: input.sanctionReason ?? null, effectiveDate: input.effectiveDate ?? null, remarks: input.remarks ?? null, sourceLocation: input.sourceLocation ?? null, sourceContent: input.sourceContent ?? null }));
|
|
796
797
|
};
|
|
797
798
|
const saved = await (this.dataSource.options?.type === 'postgres' ? this.dataSource.transaction(persist) : persist(this.dataSource));
|
|
798
799
|
await this.recalculateCompanyRisks(scope, 'CATALOG_CHANGED');
|
|
@@ -801,7 +802,8 @@ let TradeComplianceWorkbenchService = class TradeComplianceWorkbenchService {
|
|
|
801
802
|
async updateSanctionedCompany(scope, id, input) {
|
|
802
803
|
const repo = this.dataSource.getRepository(SanctionedCompanyRecord);
|
|
803
804
|
const item = await required(repo.findOne({ where: { ...activeScope(scope), id, lifecycleStatus: 'ACTIVE', deletedAt: IsNull() } }), '制裁公司不存在');
|
|
804
|
-
Object.assign(item, pick(input, ['companyName', 'countryOrRegion', 'sanctionListSource', 'sanctionReason', 'effectiveDate', 'remarks']));
|
|
805
|
+
Object.assign(item, pick(input, ['subjectKind', 'companyName', 'countryOrRegion', 'sanctionListSource', 'sanctionReason', 'effectiveDate', 'remarks']));
|
|
806
|
+
item.subjectKind = item.subjectKind === 'PERSON' ? 'PERSON' : 'ENTITY';
|
|
805
807
|
item.reviewStatus = 'REVIEWED';
|
|
806
808
|
item.normalizedCompanyName = normalizeExactName(item.companyName);
|
|
807
809
|
item.normalizedSanctionListSource = normalizeSanctionSource(item.sanctionListSource);
|
|
@@ -896,7 +898,8 @@ let TradeComplianceWorkbenchService = class TradeComplianceWorkbenchService {
|
|
|
896
898
|
throw new DomainError(DomainErrorCodes.INVALID_INPUT, '公司名称、生效日期和制裁原因为必填项');
|
|
897
899
|
const normalizedCompanyName = normalizeExactName(companyName);
|
|
898
900
|
const normalizedSanctionListSource = normalizeSanctionSource(payload.sanctionListSource ?? original.sanctionListSource ?? null);
|
|
899
|
-
const
|
|
901
|
+
const subjectKind = (payload.subjectKind ?? original.subjectKind) === 'PERSON' ? 'PERSON' : 'ENTITY';
|
|
902
|
+
const duplicate = await recordRepo.findOne({ where: { ...activeScope(scope), lifecycleStatus: 'ACTIVE', subjectKind, normalizedCompanyName, normalizedSanctionListSource, deletedAt: IsNull() } });
|
|
900
903
|
if (duplicate)
|
|
901
904
|
throw new DomainError(DomainErrorCodes.DUPLICATE_CATALOG_RECORD, '相同公司名称和制裁名单来源的记录已存在', { id: duplicate.id });
|
|
902
905
|
const saved = await recordRepo.save(recordRepo.create({
|
|
@@ -905,6 +908,7 @@ let TradeComplianceWorkbenchService = class TradeComplianceWorkbenchService {
|
|
|
905
908
|
lifecycleStatus: 'ACTIVE',
|
|
906
909
|
reviewStatus: 'REVIEWED',
|
|
907
910
|
sourceType: 'FILE_IMPORT',
|
|
911
|
+
subjectKind,
|
|
908
912
|
companyName,
|
|
909
913
|
normalizedCompanyName,
|
|
910
914
|
countryOrRegion: payload.countryOrRegion ?? original.countryOrRegion ?? null,
|
|
@@ -971,10 +975,11 @@ let TradeComplianceWorkbenchService = class TradeComplianceWorkbenchService {
|
|
|
971
975
|
}
|
|
972
976
|
const normalizedCompanyName = normalizeExactName(companyName);
|
|
973
977
|
const normalizedSanctionListSource = normalizeSanctionSource(row.sanctionListSource);
|
|
978
|
+
const subjectKind = row.subjectKind === 'PERSON' ? 'PERSON' : 'ENTITY';
|
|
974
979
|
const duplicate = await recordRepo.findOne({ where: [
|
|
975
|
-
{ ...activeScope(scope), batchId: result.id, lifecycleStatus: result.lifecycleStatus, normalizedCompanyName, normalizedSanctionListSource, deletedAt: IsNull() },
|
|
976
|
-
...(result.replacedBatchId ? [{ ...activeScope(scope), batchId: IsNull(), lifecycleStatus: 'ACTIVE', normalizedCompanyName, normalizedSanctionListSource, deletedAt: IsNull() }] : []),
|
|
977
|
-
{ ...activeScope(scope), lifecycleStatus: 'ACTIVE', normalizedCompanyName, normalizedSanctionListSource, deletedAt: IsNull(), ...(result.replacedBatchId ? { batchId: Not(result.replacedBatchId) } : {}) }
|
|
980
|
+
{ ...activeScope(scope), batchId: result.id, lifecycleStatus: result.lifecycleStatus, subjectKind, normalizedCompanyName, normalizedSanctionListSource, deletedAt: IsNull() },
|
|
981
|
+
...(result.replacedBatchId ? [{ ...activeScope(scope), batchId: IsNull(), lifecycleStatus: 'ACTIVE', subjectKind, normalizedCompanyName, normalizedSanctionListSource, deletedAt: IsNull() }] : []),
|
|
982
|
+
{ ...activeScope(scope), lifecycleStatus: 'ACTIVE', subjectKind, normalizedCompanyName, normalizedSanctionListSource, deletedAt: IsNull(), ...(result.replacedBatchId ? { batchId: Not(result.replacedBatchId) } : {}) }
|
|
978
983
|
] });
|
|
979
984
|
if (duplicate && await this.catalogIssueExists(issueRepo, scope, result.id, 'DUPLICATE', row))
|
|
980
985
|
continue;
|
|
@@ -985,7 +990,7 @@ let TradeComplianceWorkbenchService = class TradeComplianceWorkbenchService {
|
|
|
985
990
|
await issueRepo.save(issueRepo.create({ ...createdScope(scope), batchId: result.id, issueType: 'DUPLICATE', sourceSequence: result.totalCount, sourceLocation: row.sourceLocation ?? null, sourceContent: row.sourceContent ?? null, parsedPayload: row, message: '制裁目录中已存在相同公司和名单来源' }));
|
|
986
991
|
continue;
|
|
987
992
|
}
|
|
988
|
-
await recordRepo.save(recordRepo.create({ ...auditScope(scope), batchId: result.id, lifecycleStatus: result.lifecycleStatus, reviewStatus: 'PENDING', sourceType: 'FILE_IMPORT', companyName, normalizedCompanyName, countryOrRegion: row.countryOrRegion ?? null, sanctionListSource: row.sanctionListSource ?? null, normalizedSanctionListSource, sanctionReason: row.sanctionReason ?? null, effectiveDate: row.effectiveDate ?? null, remarks: row.remarks ?? null, sourceLocation: row.sourceLocation ?? null, sourceContent: row.sourceContent ?? null }));
|
|
993
|
+
await recordRepo.save(recordRepo.create({ ...auditScope(scope), batchId: result.id, lifecycleStatus: result.lifecycleStatus, reviewStatus: 'PENDING', sourceType: 'FILE_IMPORT', subjectKind, companyName, normalizedCompanyName, countryOrRegion: row.countryOrRegion ?? null, sanctionListSource: row.sanctionListSource ?? null, normalizedSanctionListSource, sanctionReason: row.sanctionReason ?? null, effectiveDate: row.effectiveDate ?? null, remarks: row.remarks ?? null, sourceLocation: row.sourceLocation ?? null, sourceContent: row.sourceContent ?? null }));
|
|
989
994
|
result.successCount += 1;
|
|
990
995
|
}
|
|
991
996
|
if (changed)
|
|
@@ -1758,10 +1763,11 @@ let TradeComplianceWorkbenchService = class TradeComplianceWorkbenchService {
|
|
|
1758
1763
|
const normalizedCompanyName = normalizeExactName(companyName);
|
|
1759
1764
|
const sanctionListSource = optionalString(candidate.values.sanctionListSource);
|
|
1760
1765
|
const normalizedSanctionListSource = normalizeSanctionSource(sanctionListSource);
|
|
1766
|
+
const subjectKind = candidate.values.subjectKind === 'PERSON' ? 'PERSON' : 'ENTITY';
|
|
1761
1767
|
const duplicate = await recordRepo.findOne({ where: [
|
|
1762
|
-
{ ...activeScope(scope), batchId: batch.id, lifecycleStatus: batch.lifecycleStatus, normalizedCompanyName, normalizedSanctionListSource, deletedAt: IsNull() },
|
|
1763
|
-
...(batch.replacedBatchId ? [{ ...activeScope(scope), batchId: IsNull(), lifecycleStatus: 'ACTIVE', normalizedCompanyName, normalizedSanctionListSource, deletedAt: IsNull() }] : []),
|
|
1764
|
-
{ ...activeScope(scope), lifecycleStatus: 'ACTIVE', normalizedCompanyName, normalizedSanctionListSource, deletedAt: IsNull(), ...(batch.replacedBatchId ? { batchId: Not(batch.replacedBatchId) } : {}) }
|
|
1768
|
+
{ ...activeScope(scope), batchId: batch.id, lifecycleStatus: batch.lifecycleStatus, subjectKind, normalizedCompanyName, normalizedSanctionListSource, deletedAt: IsNull() },
|
|
1769
|
+
...(batch.replacedBatchId ? [{ ...activeScope(scope), batchId: IsNull(), lifecycleStatus: 'ACTIVE', subjectKind, normalizedCompanyName, normalizedSanctionListSource, deletedAt: IsNull() }] : []),
|
|
1770
|
+
{ ...activeScope(scope), lifecycleStatus: 'ACTIVE', subjectKind, normalizedCompanyName, normalizedSanctionListSource, deletedAt: IsNull(), ...(batch.replacedBatchId ? { batchId: Not(batch.replacedBatchId) } : {}) }
|
|
1765
1771
|
] });
|
|
1766
1772
|
batch.totalCount += 1;
|
|
1767
1773
|
if (duplicate) {
|
|
@@ -1774,7 +1780,7 @@ let TradeComplianceWorkbenchService = class TradeComplianceWorkbenchService {
|
|
|
1774
1780
|
continue;
|
|
1775
1781
|
}
|
|
1776
1782
|
const record = await recordRepo.save(recordRepo.create({
|
|
1777
|
-
...auditScope(scope), batchId: batch.id, lifecycleStatus: batch.lifecycleStatus, sourceType: 'FILE_IMPORT', companyName, normalizedCompanyName,
|
|
1783
|
+
...auditScope(scope), batchId: batch.id, lifecycleStatus: batch.lifecycleStatus, sourceType: 'FILE_IMPORT', subjectKind, companyName, normalizedCompanyName,
|
|
1778
1784
|
countryOrRegion: optionalString(candidate.values.countryOrRegion), sanctionListSource,
|
|
1779
1785
|
normalizedSanctionListSource, sanctionReason: optionalString(candidate.values.sanctionReason),
|
|
1780
1786
|
effectiveDate: optionalString(candidate.values.effectiveDate), remarks: optionalString(candidate.values.remarks),
|
|
@@ -2108,7 +2114,7 @@ let TradeComplianceWorkbenchService = class TradeComplianceWorkbenchService {
|
|
|
2108
2114
|
});
|
|
2109
2115
|
for (const record of drafts) {
|
|
2110
2116
|
const conflictWhere = {
|
|
2111
|
-
...activeScope(scope), lifecycleStatus: 'ACTIVE', normalizedCompanyName: record.normalizedCompanyName,
|
|
2117
|
+
...activeScope(scope), lifecycleStatus: 'ACTIVE', subjectKind: record.subjectKind ?? 'ENTITY', normalizedCompanyName: record.normalizedCompanyName,
|
|
2112
2118
|
normalizedSanctionListSource: record.normalizedSanctionListSource, deletedAt: IsNull()
|
|
2113
2119
|
};
|
|
2114
2120
|
const conflict = await recordRepo.findOne({ where: batch.replacedBatchId
|
|
@@ -2887,7 +2893,7 @@ let TradeComplianceWorkbenchService = class TradeComplianceWorkbenchService {
|
|
|
2887
2893
|
ON sales_purchase."tenantId" = sales_contract."tenantId"
|
|
2888
2894
|
AND sales_purchase."organizationId" = sales_contract."organizationId"
|
|
2889
2895
|
AND sales_purchase."deletedAt" IS NULL
|
|
2890
|
-
AND
|
|
2896
|
+
AND ${derivedSalesContractLinkSql('sales_contract', 'sales_purchase')}
|
|
2891
2897
|
WHERE sales_contract."tenantId" = :tenantId
|
|
2892
2898
|
AND sales_contract."organizationId" = :organizationId
|
|
2893
2899
|
AND sales_contract."deletedAt" IS NULL
|
|
@@ -2907,7 +2913,7 @@ let TradeComplianceWorkbenchService = class TradeComplianceWorkbenchService {
|
|
|
2907
2913
|
}
|
|
2908
2914
|
async listSalesOrders(scope, customerId, query = {}) {
|
|
2909
2915
|
assertOptionalEnum(query.parameters?.paymentStatus, SalesPaymentStatuses, '收款状态');
|
|
2910
|
-
const customer = (await this.salesCustomerDtos(scope, [await required(this.dataSource.getRepository(Customer).findOne({ where: { ...activeScope(scope), id: customerId, deletedAt: IsNull() } }), '客户不存在')]))[0];
|
|
2916
|
+
const customer = (await this.salesCustomerDtos(scope, [await required(this.dataSource.getRepository(Customer).findOne({ where: { ...activeScope(scope), id: customerId, deletedAt: IsNull() } }), '客户不存在')], false))[0];
|
|
2911
2917
|
const page = await this.selectDerivedSalesOrderIdentities(scope, customerId, query, false);
|
|
2912
2918
|
const metricIdentities = await this.selectAllDerivedSalesOrderIdentities(scope, customerId, query.parameters ?? {}, false);
|
|
2913
2919
|
const [items, metricRows] = await Promise.all([
|
|
@@ -3461,11 +3467,9 @@ let TradeComplianceWorkbenchService = class TradeComplianceWorkbenchService {
|
|
|
3461
3467
|
continue;
|
|
3462
3468
|
const manual = manualMap.get(derivedOrderKey(contract.id, purchase.id));
|
|
3463
3469
|
const effectiveRate = manual?.pricingProfitRateOverride ?? (customer.pricingProfitRateStatus === 'CONFIGURED' ? customer.defaultPricingProfitRate : null);
|
|
3464
|
-
|
|
3465
|
-
continue;
|
|
3466
|
-
const source = manual?.pricingProfitRateOverride != null ? 'ORDER_OVERRIDE' : 'CUSTOMER_DEFAULT';
|
|
3470
|
+
const lineOverrides = linePricingOverrides(manual?.linePricingOverrides);
|
|
3467
3471
|
const derivedLines = (linesByPurchase.get(purchase.id) ?? []).map(line => {
|
|
3468
|
-
const pricing =
|
|
3472
|
+
const pricing = deriveSalesPricingLine(line, lineOverrides[line.id], effectiveRate);
|
|
3469
3473
|
return {
|
|
3470
3474
|
purchaseOrderLineId: line.id, productName: line.productName, englishName: line.englishName, productDescription: line.productDescription,
|
|
3471
3475
|
modelSpecification: line.modelSpecification, unit: line.unit, quantity: line.quantity, hsCode: line.hsCode,
|
|
@@ -3485,9 +3489,10 @@ let TradeComplianceWorkbenchService = class TradeComplianceWorkbenchService {
|
|
|
3485
3489
|
})
|
|
3486
3490
|
};
|
|
3487
3491
|
});
|
|
3488
|
-
const
|
|
3492
|
+
const pricedLines = derivedLines.filter(line => line.pricingStatus === 'PRICED' && line.salesLineAmountCny != null);
|
|
3493
|
+
const amount = manual?.orderAmountCnyOverride != null ? new Decimal(manual.orderAmountCnyOverride) : pricedLines.reduce((sum, line) => sum.plus(line.salesLineAmountCny ?? 0), new Decimal(0));
|
|
3489
3494
|
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:
|
|
3495
|
+
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
3496
|
}
|
|
3492
3497
|
}
|
|
3493
3498
|
return results.sort(compareDerivedSalesOrders);
|
|
@@ -3495,11 +3500,10 @@ let TradeComplianceWorkbenchService = class TradeComplianceWorkbenchService {
|
|
|
3495
3500
|
derivedSalesIdentityQuery(scope, customerId, parameters, includeParties) {
|
|
3496
3501
|
const qb = this.dataSource.getRepository(CustomerContract).createQueryBuilder('contract')
|
|
3497
3502
|
.innerJoin(Customer, 'customer', 'customer.id = contract."customerId" AND customer."tenantId" = contract."tenantId" AND customer."organizationId" = contract."organizationId" AND customer."deletedAt" IS NULL')
|
|
3498
|
-
.innerJoin(PurchaseOrder, 'purchase', `purchase."tenantId" = contract."tenantId" AND purchase."organizationId" = contract."organizationId" AND purchase."deletedAt" IS NULL AND
|
|
3503
|
+
.innerJoin(PurchaseOrder, 'purchase', `purchase."tenantId" = contract."tenantId" AND purchase."organizationId" = contract."organizationId" AND purchase."deletedAt" IS NULL AND ${derivedSalesContractLinkSql('contract', 'purchase')}`)
|
|
3499
3504
|
.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
3505
|
.leftJoin(Supplier, 'supplier', 'supplier.id = purchase."supplierId" AND supplier."tenantId" = purchase."tenantId" AND supplier."organizationId" = purchase."organizationId" AND supplier."deletedAt" IS NULL')
|
|
3501
3506
|
.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
3507
|
.select('contract.id', 'customerContractId').addSelect('purchase.id', 'purchaseOrderId');
|
|
3504
3508
|
if (customerId)
|
|
3505
3509
|
qb.andWhere('customer.id = :customerId', { customerId });
|
|
@@ -3517,8 +3521,7 @@ let TradeComplianceWorkbenchService = class TradeComplianceWorkbenchService {
|
|
|
3517
3521
|
qb.andWhere('purchase."orderDate" >= :orderDateFrom', { orderDateFrom: date.from });
|
|
3518
3522
|
if (hasQueryValue(date.to))
|
|
3519
3523
|
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)`;
|
|
3524
|
+
const amount = derivedSalesOrderAmountSql('sales_line', 'manual."linePricingOverrides"', 'COALESCE(manual."pricingProfitRateOverride", customer."defaultPricingProfitRate")');
|
|
3522
3525
|
const received = 'COALESCE(manual."receivedAmountCny", 0)';
|
|
3523
3526
|
if (parameters.paymentStatus === 'UNPAID')
|
|
3524
3527
|
qb.andWhere(`${received} <= 0`);
|
|
@@ -3548,7 +3551,8 @@ let TradeComplianceWorkbenchService = class TradeComplianceWorkbenchService {
|
|
|
3548
3551
|
return { items: items, total, page, pageSize };
|
|
3549
3552
|
}
|
|
3550
3553
|
async saveCustomerProfitRate(scope, customerId, rate) {
|
|
3551
|
-
|
|
3554
|
+
const normalizedRate = normalizeOptionalDecimalInput(rate);
|
|
3555
|
+
if (normalizedRate != null && new Decimal(normalizedRate).isNegative())
|
|
3552
3556
|
throw new DomainError(DomainErrorCodes.INVALID_INPUT, '客户默认利润率不能小于 0');
|
|
3553
3557
|
return this.dataSource.transaction(async (manager) => {
|
|
3554
3558
|
const customerRepo = manager.getRepository(Customer);
|
|
@@ -3563,13 +3567,13 @@ let TradeComplianceWorkbenchService = class TradeComplianceWorkbenchService {
|
|
|
3563
3567
|
const contractIds = contracts.map(contract => contract.id);
|
|
3564
3568
|
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
3569
|
const linesByPurchase = groupBy(lines, line => line.purchaseOrderId);
|
|
3566
|
-
for (const manual of manuals.filter(item => item.pricingProfitRateOverride == null)) {
|
|
3567
|
-
const orderAmount =
|
|
3570
|
+
for (const manual of manuals.filter(item => item.pricingProfitRateOverride == null && manual.orderAmountCnyOverride == null && normalizedRate != null)) {
|
|
3571
|
+
const orderAmount = deriveOrderPricingAmount(linesByPurchase.get(manual.purchaseOrderId) ?? [], manual.linePricingOverrides, normalizedRate);
|
|
3568
3572
|
assertReceivedAmount(orderAmount, manual.receivedAmountCny);
|
|
3569
3573
|
}
|
|
3570
|
-
customer.defaultPricingProfitRate =
|
|
3574
|
+
customer.defaultPricingProfitRate = normalizedRate;
|
|
3571
3575
|
customer.updatedById = scope.userId ?? null;
|
|
3572
|
-
customer.pricingProfitRateStatus = 'CONFIGURED';
|
|
3576
|
+
customer.pricingProfitRateStatus = normalizedRate == null ? 'MISSING' : 'CONFIGURED';
|
|
3573
3577
|
return customerRepo.save(customer);
|
|
3574
3578
|
});
|
|
3575
3579
|
}
|
|
@@ -3599,19 +3603,28 @@ let TradeComplianceWorkbenchService = class TradeComplianceWorkbenchService {
|
|
|
3599
3603
|
const where = { ...activeScope(scope), customerContractId, purchaseOrderId, deletedAt: IsNull() };
|
|
3600
3604
|
let row = await repo.findOne({ where, lock: { mode: 'pessimistic_write' } });
|
|
3601
3605
|
if (!row) {
|
|
3602
|
-
await repo.createQueryBuilder().insert().into(DerivedSalesOrderManualData).values({ ...auditScope(scope), customerContractId, purchaseOrderId, pricingProfitRateOverride: null, receivedAmountCny: '0' }).orIgnore().execute();
|
|
3606
|
+
await repo.createQueryBuilder().insert().into(DerivedSalesOrderManualData).values({ ...auditScope(scope), customerContractId, purchaseOrderId, pricingProfitRateOverride: null, orderAmountCnyOverride: null, linePricingOverrides: null, receivedAmountCny: '0' }).orIgnore().execute();
|
|
3603
3607
|
row = await required(repo.findOne({ where, lock: { mode: 'pessimistic_write' } }), '销售订单人工维护数据创建失败');
|
|
3604
3608
|
}
|
|
3605
3609
|
const override = input.pricingProfitRateOverride === undefined ? row.pricingProfitRateOverride : normalizeOptionalDecimalInput(input.pricingProfitRateOverride);
|
|
3606
3610
|
if (override != null && new Decimal(override).isNegative())
|
|
3607
3611
|
throw new DomainError(DomainErrorCodes.INVALID_INPUT, '订单利润率不能小于 0');
|
|
3612
|
+
const orderAmountCnyOverride = input.orderAmountCnyOverride === undefined ? row.orderAmountCnyOverride : normalizeOptionalDecimalInput(input.orderAmountCnyOverride);
|
|
3613
|
+
if (orderAmountCnyOverride != null && new Decimal(orderAmountCnyOverride).isNegative())
|
|
3614
|
+
throw new DomainError(DomainErrorCodes.INVALID_INPUT, '销售订单总额不能小于 0');
|
|
3615
|
+
const nextLineOverrides = input.linePricingOverrides === undefined
|
|
3616
|
+
? row.linePricingOverrides
|
|
3617
|
+
: normalizeLinePricingOverrides({ ...linePricingOverrides(row.linePricingOverrides), ...linePricingOverrides(input.linePricingOverrides) });
|
|
3608
3618
|
const effectiveRate = override ?? (customer.pricingProfitRateStatus === 'CONFIGURED' ? customer.defaultPricingProfitRate : null);
|
|
3609
|
-
if (effectiveRate == null)
|
|
3610
|
-
throw new DomainError(DomainErrorCodes.CUSTOMER_PROFIT_RATE_REQUIRED, '客户默认利润率未维护');
|
|
3611
3619
|
const received = input.receivedAmountCny ?? row.receivedAmountCny;
|
|
3612
|
-
|
|
3620
|
+
const finalAmount = orderAmountCnyOverride ?? deriveOrderPricingAmount(lines, nextLineOverrides, effectiveRate);
|
|
3621
|
+
assertReceivedAmount(finalAmount, received);
|
|
3613
3622
|
if (input.pricingProfitRateOverride !== undefined)
|
|
3614
3623
|
row.pricingProfitRateOverride = override;
|
|
3624
|
+
if (input.orderAmountCnyOverride !== undefined)
|
|
3625
|
+
row.orderAmountCnyOverride = orderAmountCnyOverride;
|
|
3626
|
+
if (input.linePricingOverrides !== undefined)
|
|
3627
|
+
row.linePricingOverrides = nextLineOverrides;
|
|
3615
3628
|
if (input.receivedAmountCny !== undefined)
|
|
3616
3629
|
row.receivedAmountCny = input.receivedAmountCny;
|
|
3617
3630
|
row.updatedById = scope.userId ?? null;
|
|
@@ -4261,20 +4274,48 @@ let TradeComplianceWorkbenchService = class TradeComplianceWorkbenchService {
|
|
|
4261
4274
|
}
|
|
4262
4275
|
async supplierDtos(scope, suppliers) {
|
|
4263
4276
|
const ids = suppliers.map(item => item.id);
|
|
4264
|
-
const matches =
|
|
4277
|
+
const [matches, purchases] = await Promise.all([
|
|
4278
|
+
ids.length ? this.dataSource.getRepository(CompanySanctionMatch).find({ where: { ...activeScope(scope), subjectType: 'SUPPLIER', supplierId: In(ids), deletedAt: IsNull() } }) : [],
|
|
4279
|
+
ids.length ? this.dataSource.getRepository(PurchaseOrder).find({ where: { ...activeScope(scope), supplierId: In(ids), deletedAt: IsNull() } }) : []
|
|
4280
|
+
]);
|
|
4281
|
+
const productRisks = await this.relatedProductRisks(scope, new Map(suppliers.map(supplier => [supplier.id, purchases.filter(purchase => purchase.supplierId === supplier.id).map(purchase => purchase.id)])));
|
|
4265
4282
|
return suppliers.map(supplier => {
|
|
4266
4283
|
const sanctionMatchCount = matches.filter(match => match.supplierId === supplier.id).length;
|
|
4267
|
-
return { ...supplier, sanctionMatchCount, sanctionStatus: supplier.complianceCheckedAt == null ? 'UNCHECKED' : sanctionMatchCount ? 'HIT' : 'CLEAR' };
|
|
4284
|
+
return { ...supplier, ...productRisks.get(supplier.id), sanctionMatchCount, sanctionStatus: supplier.complianceCheckedAt == null ? 'UNCHECKED' : sanctionMatchCount ? 'HIT' : 'CLEAR' };
|
|
4268
4285
|
});
|
|
4269
4286
|
}
|
|
4270
|
-
async salesCustomerDtos(scope, customers) {
|
|
4287
|
+
async salesCustomerDtos(scope, customers, includeRelatedProductRisks = true) {
|
|
4271
4288
|
const ids = customers.map(item => item.id);
|
|
4272
|
-
const matches
|
|
4289
|
+
const [matches, contracts, purchases] = await Promise.all([
|
|
4290
|
+
ids.length ? this.dataSource.getRepository(CompanySanctionMatch).find({ where: { ...activeScope(scope), subjectType: 'CUSTOMER', customerId: In(ids), deletedAt: IsNull() } }) : [],
|
|
4291
|
+
includeRelatedProductRisks && ids.length ? this.dataSource.getRepository(CustomerContract).find({ where: { ...activeScope(scope), customerId: In(ids), deletedAt: IsNull() } }) : [],
|
|
4292
|
+
includeRelatedProductRisks && ids.length ? this.dataSource.getRepository(PurchaseOrder).find({ where: { ...activeScope(scope), deletedAt: IsNull() } }) : []
|
|
4293
|
+
]);
|
|
4294
|
+
const purchaseIdsByCustomer = new Map(customers.map(customer => {
|
|
4295
|
+
const contractBaseCodes = new Set(contracts.filter(contract => contract.customerId === customer.id).map(contract => parseCustomerContractNo(contract.contractNo).baseCode));
|
|
4296
|
+
return [customer.id, purchases.filter(purchase => contractBaseCodes.has(parsePurchaseContractNo(purchase.contractNo).baseCode)).map(purchase => purchase.id)];
|
|
4297
|
+
}));
|
|
4298
|
+
const productRisks = includeRelatedProductRisks ? await this.relatedProductRisks(scope, purchaseIdsByCustomer) : new Map();
|
|
4273
4299
|
return customers.map(customer => {
|
|
4274
4300
|
const sanctionMatchCount = matches.filter(match => match.customerId === customer.id).length;
|
|
4275
|
-
return { ...customer, profitRateStatus: customer.pricingProfitRateStatus, sanctionMatchCount, sanctionStatus: customer.complianceCheckStatus === 'UNCHECKED' ? 'UNCHECKED' : sanctionMatchCount ? 'HIT' : 'CLEAR' };
|
|
4301
|
+
return { ...customer, ...productRisks.get(customer.id), profitRateStatus: customer.pricingProfitRateStatus, sanctionMatchCount, sanctionStatus: customer.complianceCheckStatus === 'UNCHECKED' ? 'UNCHECKED' : sanctionMatchCount ? 'HIT' : 'CLEAR' };
|
|
4276
4302
|
});
|
|
4277
4303
|
}
|
|
4304
|
+
async relatedProductRisks(scope, purchaseIdsByParty) {
|
|
4305
|
+
const purchaseIds = [...new Set([...purchaseIdsByParty.values()].flat())];
|
|
4306
|
+
const lines = purchaseIds.length ? await this.dataSource.getRepository(PurchaseOrderLine).find({ where: { ...activeScope(scope), purchaseOrderId: In(purchaseIds), deletedAt: IsNull() } }) : [];
|
|
4307
|
+
const lineIds = lines.map(line => line.id);
|
|
4308
|
+
const matches = lineIds.length ? await this.dataSource.getRepository(ProductComplianceMatch).find({ where: { ...activeScope(scope), purchaseOrderLineId: In(lineIds), deletedAt: IsNull() } }) : [];
|
|
4309
|
+
return new Map([...purchaseIdsByParty.entries()].map(([partyId, partyPurchaseIds]) => {
|
|
4310
|
+
const partyLines = lines.filter(line => partyPurchaseIds.includes(line.purchaseOrderId));
|
|
4311
|
+
const partyLineIds = new Set(partyLines.map(line => line.id));
|
|
4312
|
+
const dualUseLineIds = new Set(matches.filter(match => partyLineIds.has(match.purchaseOrderLineId) && match.catalogType === 'DUAL_USE').map(match => match.purchaseOrderLineId));
|
|
4313
|
+
const overseasSanctionLineIds = new Set(matches.filter(match => partyLineIds.has(match.purchaseOrderLineId) && match.catalogType === 'OVERSEAS_SANCTION').map(match => match.purchaseOrderLineId));
|
|
4314
|
+
const hitLineCount = new Set([...dualUseLineIds, ...overseasSanctionLineIds]).size;
|
|
4315
|
+
const status = hitLineCount > 0 ? 'HIT' : partyLines.some(line => line.complianceCheckedAt == null) ? 'UNCHECKED' : 'CLEAR';
|
|
4316
|
+
return [partyId, { relatedProductRiskStatus: status, relatedDualUseLineCount: dualUseLineIds.size, relatedOverseasSanctionLineCount: overseasSanctionLineIds.size, relatedProductRiskLineCount: hitLineCount }];
|
|
4317
|
+
}));
|
|
4318
|
+
}
|
|
4278
4319
|
async salesOrderDtos(scope, orders) {
|
|
4279
4320
|
if (!orders.length)
|
|
4280
4321
|
return [];
|
|
@@ -4292,7 +4333,7 @@ let TradeComplianceWorkbenchService = class TradeComplianceWorkbenchService {
|
|
|
4292
4333
|
const received = new Decimal(order.receivedAmountCny);
|
|
4293
4334
|
const total = new Decimal(order.orderAmountCny);
|
|
4294
4335
|
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))] })) };
|
|
4336
|
+
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
4337
|
});
|
|
4297
4338
|
}
|
|
4298
4339
|
async purchaseOrderDtos(scope, orders) {
|
|
@@ -4797,6 +4838,100 @@ function groupBy(rows, keyOf) { const grouped = new Map(); for (const row of row
|
|
|
4797
4838
|
} return grouped; }
|
|
4798
4839
|
function compareDerivedSalesOrders(left, right) { return left.salesOrderNo.localeCompare(right.salesOrderNo) || left.purchaseOrderId.localeCompare(right.purchaseOrderId) || left.customerContractId.localeCompare(right.customerContractId); }
|
|
4799
4840
|
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); }
|
|
4841
|
+
function derivedSalesOrderAmountSql(lineAlias, overridesExpression, effectiveRateExpression) {
|
|
4842
|
+
const override = `${overridesExpression} -> ${lineAlias}.id::text`;
|
|
4843
|
+
const lineRate = `COALESCE(NULLIF(BTRIM(${override}->>'pricingProfitRate'), '')::numeric, ${effectiveRateExpression})`;
|
|
4844
|
+
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)`;
|
|
4845
|
+
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))`;
|
|
4846
|
+
}
|
|
4847
|
+
function linePricingOverrides(value) {
|
|
4848
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
4849
|
+
return {};
|
|
4850
|
+
const entries = Object.entries(value).filter((entry) => Boolean(entry[1]) && typeof entry[1] === 'object' && !Array.isArray(entry[1]));
|
|
4851
|
+
return Object.fromEntries(entries);
|
|
4852
|
+
}
|
|
4853
|
+
function normalizeLinePricingOverrides(value) {
|
|
4854
|
+
const overrides = linePricingOverrides(value);
|
|
4855
|
+
const normalized = {};
|
|
4856
|
+
for (const [lineId, override] of Object.entries(overrides)) {
|
|
4857
|
+
const next = {};
|
|
4858
|
+
for (const key of ['salesLineAmountCny', 'salesUnitPriceCny', 'pricingProfitRate']) {
|
|
4859
|
+
const raw = override[key];
|
|
4860
|
+
if (raw != null && typeof raw !== 'string')
|
|
4861
|
+
throw new DomainError(DomainErrorCodes.INVALID_INPUT, `${key}无效`);
|
|
4862
|
+
const value = normalizeOptionalDecimalInput(raw);
|
|
4863
|
+
if (value != null) {
|
|
4864
|
+
if (new Decimal(value).isNegative())
|
|
4865
|
+
throw new DomainError(DomainErrorCodes.INVALID_INPUT, `${key}不能小于 0`);
|
|
4866
|
+
next[key] = value;
|
|
4867
|
+
}
|
|
4868
|
+
}
|
|
4869
|
+
if (override.pricingMethod != null)
|
|
4870
|
+
next.pricingMethod = pricingMethod(override.pricingMethod);
|
|
4871
|
+
if (Object.keys(next).length)
|
|
4872
|
+
normalized[lineId] = next;
|
|
4873
|
+
}
|
|
4874
|
+
return Object.keys(normalized).length ? normalized : null;
|
|
4875
|
+
}
|
|
4876
|
+
function optionalPricingText(value) { return value == null || value === '' ? null : String(value); }
|
|
4877
|
+
function pricingMethod(value) { return value === 'TAX_EXCLUDED_MARKUP' ? 'TAX_EXCLUDED_MARKUP' : 'TAX_INCLUDED_MARKUP'; }
|
|
4878
|
+
function deriveSalesPricingLine(line, override, orderRate) {
|
|
4879
|
+
const quantity = optionalPricingText(line.quantity);
|
|
4880
|
+
const manualAmount = optionalPricingText(override?.salesLineAmountCny);
|
|
4881
|
+
if (manualAmount != null) {
|
|
4882
|
+
const amount = new Decimal(manualAmount);
|
|
4883
|
+
const unit = quantity && !new Decimal(quantity).isZero() ? amount.div(quantity).toDecimalPlaces(4, Decimal.ROUND_HALF_UP).toFixed(4) : null;
|
|
4884
|
+
return { salesUnitPriceCny: unit, salesLineAmountCny: amount.toFixed(4), pricingProfitRate: null, pricingMethod: null, pricingSource: 'MANUAL_AMOUNT', pricingStatus: amount.isNegative() ? 'INVALID' : 'PRICED', missingPricingFields: [] };
|
|
4885
|
+
}
|
|
4886
|
+
const manualUnit = optionalPricingText(override?.salesUnitPriceCny);
|
|
4887
|
+
if (manualUnit != null) {
|
|
4888
|
+
const unit = new Decimal(manualUnit);
|
|
4889
|
+
const amount = quantity == null ? null : unit.mul(quantity).toDecimalPlaces(4, Decimal.ROUND_HALF_UP);
|
|
4890
|
+
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'] : [] };
|
|
4891
|
+
}
|
|
4892
|
+
const lineRate = optionalPricingText(override?.pricingProfitRate);
|
|
4893
|
+
const rate = lineRate ?? orderRate;
|
|
4894
|
+
const method = pricingMethod(override?.pricingMethod);
|
|
4895
|
+
if (rate == null)
|
|
4896
|
+
return { salesUnitPriceCny: null, salesLineAmountCny: null, pricingProfitRate: null, pricingMethod: method, pricingSource: 'UNPRICED', pricingStatus: 'UNPRICED', missingPricingFields: ['salesAmountOrPricingInputs'] };
|
|
4897
|
+
const calculated = calculateSalesLineFromRate({
|
|
4898
|
+
quantity,
|
|
4899
|
+
purchaseUnitPriceTaxIncluded: optionalPricingText(line.unitPriceTaxIncluded),
|
|
4900
|
+
purchaseUnitPriceTaxExcluded: optionalPricingText(line.unitPriceTaxExcludedCny ?? line.unitPriceTaxExcluded),
|
|
4901
|
+
pricingProfitRate: rate,
|
|
4902
|
+
pricingMethod: method
|
|
4903
|
+
});
|
|
4904
|
+
if ('invalid' in calculated)
|
|
4905
|
+
return { salesUnitPriceCny: null, salesLineAmountCny: null, pricingProfitRate: rate, pricingMethod: method, pricingSource: lineRate != null ? 'LINE_RATE' : orderRate ? 'ORDER_OVERRIDE' : 'UNPRICED', pricingStatus: 'INVALID', missingPricingFields: [] };
|
|
4906
|
+
if (!('salesLineAmountCny' in calculated))
|
|
4907
|
+
return { salesUnitPriceCny: null, salesLineAmountCny: null, pricingProfitRate: rate, pricingMethod: method, pricingSource: 'UNPRICED', pricingStatus: 'UNPRICED', missingPricingFields: calculated.missingFields };
|
|
4908
|
+
return { salesUnitPriceCny: calculated.salesUnitPriceCny, salesLineAmountCny: calculated.salesLineAmountCny, pricingProfitRate: rate, pricingMethod: method, pricingSource: lineRate != null ? 'LINE_RATE' : orderRate ? 'ORDER_OVERRIDE' : 'UNPRICED', pricingStatus: 'PRICED', missingPricingFields: [] };
|
|
4909
|
+
}
|
|
4910
|
+
function orderPricingStatus(lines) {
|
|
4911
|
+
if (lines.some(line => line.pricingStatus === 'INVALID'))
|
|
4912
|
+
return 'INVALID';
|
|
4913
|
+
const priced = lines.filter(line => line.pricingStatus === 'PRICED').length;
|
|
4914
|
+
return priced === lines.length && lines.length > 0 ? 'COMPLETE' : priced > 0 ? 'PARTIAL' : 'UNPRICED';
|
|
4915
|
+
}
|
|
4916
|
+
function orderPricingSource(lines, orderRate, customerRate) {
|
|
4917
|
+
const sources = new Set(lines.map(line => line.pricingSource).filter(source => source !== 'UNPRICED'));
|
|
4918
|
+
if (sources.size === 0)
|
|
4919
|
+
return 'NONE';
|
|
4920
|
+
if (sources.size > 1)
|
|
4921
|
+
return 'MIXED';
|
|
4922
|
+
const [source] = [...sources];
|
|
4923
|
+
if (source === 'LINE_RATE')
|
|
4924
|
+
return 'LINE_OVERRIDE';
|
|
4925
|
+
if (source === 'MANUAL_AMOUNT' || source === 'MANUAL_UNIT_PRICE')
|
|
4926
|
+
return 'MANUAL_AMOUNT';
|
|
4927
|
+
return orderRate != null ? 'ORDER_OVERRIDE' : customerRate != null ? 'CUSTOMER_DEFAULT' : 'NONE';
|
|
4928
|
+
}
|
|
4929
|
+
function orderMissingPricingFields(lines) { return [...new Set(lines.flatMap(line => line.missingPricingFields))]; }
|
|
4930
|
+
function deriveOrderPricingAmount(lines, overrides, effectiveRate) {
|
|
4931
|
+
const overrideMap = linePricingOverrides(overrides);
|
|
4932
|
+
const priced = lines.map(line => deriveSalesPricingLine(line, overrideMap[line.id], effectiveRate)).filter(line => line.pricingStatus === 'PRICED' && line.salesLineAmountCny != null);
|
|
4933
|
+
return priced.reduce((sum, line) => sum.plus(line.salesLineAmountCny ?? 0), new Decimal(0)).toFixed(4);
|
|
4934
|
+
}
|
|
4800
4935
|
function assertOptionalEnum(value, allowed, label) { if (hasQueryValue(value) && !allowed.includes(value))
|
|
4801
4936
|
throw new DomainError(DomainErrorCodes.INVALID_INPUT, `${label}无效`); }
|
|
4802
4937
|
function fuzzyValue(value, filter) { return !hasQueryValue(filter) || String(value ?? '').toLocaleLowerCase().includes(String(filter).trim().toLocaleLowerCase()); }
|
|
@@ -4806,8 +4941,10 @@ function salesMetrics(rows) {
|
|
|
4806
4941
|
const currentYearRows = rows.filter(row => String(row.orderDate ?? '').slice(0, 4) === currentYear);
|
|
4807
4942
|
const productAmounts = new Map();
|
|
4808
4943
|
for (const row of rows)
|
|
4809
|
-
for (const line of row.lines)
|
|
4810
|
-
|
|
4944
|
+
for (const line of row.lines) {
|
|
4945
|
+
if (line.salesLineAmountCny != null)
|
|
4946
|
+
productAmounts.set(line.productName, (productAmounts.get(line.productName) ?? new Decimal(0)).plus(line.salesLineAmountCny));
|
|
4947
|
+
}
|
|
4811
4948
|
return {
|
|
4812
4949
|
orderCount: rows.length,
|
|
4813
4950
|
salesAmountCny: sum(rows, 'orderAmountCny'),
|
|
@@ -4821,7 +4958,11 @@ function salesMetrics(rows) {
|
|
|
4821
4958
|
function sum(rows, key) { return rows.reduce((value, row) => value.plus(row[key] ?? 0), new Decimal(0)).toFixed(2); }
|
|
4822
4959
|
function booleanFilter(value) { return value === true || value === 'true' ? true : value === false || value === 'false' ? false : null; }
|
|
4823
4960
|
function purchaseEstimatedRefundSql(alias) { return `(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" = ${alias}.id AND refund_line."tenantId" = ${alias}."tenantId" AND refund_line."organizationId" = ${alias}."organizationId" AND refund_line."deletedAt" IS NULL)`; }
|
|
4824
|
-
function
|
|
4961
|
+
function derivedSalesContractLinkSql(contractAlias, purchaseAlias) {
|
|
4962
|
+
const base = `regexp_replace(${purchaseAlias}."normalizedContractNo", '(-[0-9]+)?C$', '')`;
|
|
4963
|
+
return `${contractAlias}."normalizedContractNo" IN (${base}, ${base} || 'X')`;
|
|
4964
|
+
}
|
|
4965
|
+
function linkedCustomerContractSql(alias) { return `SELECT 1 FROM plugin_trade_compliance_customer_contract linked_contract WHERE linked_contract."tenantId" = :tenantId AND linked_contract."organizationId" = :organizationId AND linked_contract."deletedAt" IS NULL AND ${derivedSalesContractLinkSql('linked_contract', alias)}`; }
|
|
4825
4966
|
function customerSanctionMatchSql(alias) { return `SELECT 1 FROM plugin_trade_compliance_company_sanction_match customer_match WHERE customer_match."tenantId" = :tenantId AND customer_match."organizationId" = :organizationId AND customer_match."deletedAt" IS NULL AND customer_match."subjectType" = 'CUSTOMER' AND customer_match."customerId" = ${alias}."customerId"`; }
|
|
4826
4967
|
function createdScope(scope) { return { ...activeScope(scope), createdById: scope.userId ?? null }; }
|
|
4827
4968
|
function auditScope(scope) { return { ...createdScope(scope), updatedById: scope.userId ?? null, deletedAt: null, deletedById: null, deleteOperation: null }; }
|