@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.
Files changed (71) hide show
  1. package/dist/lib/analytics-query.d.ts.map +1 -1
  2. package/dist/lib/analytics-query.js +29 -9
  3. package/dist/lib/analytics-query.js.map +1 -1
  4. package/dist/lib/catalog-import/docx.parser.d.ts.map +1 -1
  5. package/dist/lib/catalog-import/docx.parser.js +1 -4
  6. package/dist/lib/catalog-import/docx.parser.js.map +1 -1
  7. package/dist/lib/domain/contract-number.d.ts +1 -1
  8. package/dist/lib/domain/contract-number.d.ts.map +1 -1
  9. package/dist/lib/domain/contract-number.js +17 -2
  10. package/dist/lib/domain/contract-number.js.map +1 -1
  11. package/dist/lib/domain/finance.d.ts +19 -0
  12. package/dist/lib/domain/finance.d.ts.map +1 -1
  13. package/dist/lib/domain/finance.js +17 -0
  14. package/dist/lib/domain/finance.js.map +1 -1
  15. package/dist/lib/entities/catalog.entity.d.ts +2 -1
  16. package/dist/lib/entities/catalog.entity.d.ts.map +1 -1
  17. package/dist/lib/entities/catalog.entity.js +5 -1
  18. package/dist/lib/entities/catalog.entity.js.map +1 -1
  19. package/dist/lib/entities/customer.entity.d.ts +2 -0
  20. package/dist/lib/entities/customer.entity.d.ts.map +1 -1
  21. package/dist/lib/entities/customer.entity.js +8 -0
  22. package/dist/lib/entities/customer.entity.js.map +1 -1
  23. package/dist/lib/entities/settings.entity.d.ts +1 -1
  24. package/dist/lib/entities/settings.entity.d.ts.map +1 -1
  25. package/dist/lib/entities/settings.entity.js +2 -2
  26. package/dist/lib/entities/settings.entity.js.map +1 -1
  27. package/dist/lib/remote-components/trade-compliance-workbench/app.css +1 -1
  28. package/dist/lib/remote-components/trade-compliance-workbench/app.js +8 -8
  29. package/dist/lib/remote-ui/app-source.js +12 -7
  30. package/dist/lib/remote-ui/app-source.js.map +1 -1
  31. package/dist/lib/remote-ui/contracts.d.ts +1 -0
  32. package/dist/lib/remote-ui/contracts.d.ts.map +1 -1
  33. package/dist/lib/remote-ui/pages/catalogs.d.ts +1 -0
  34. package/dist/lib/remote-ui/pages/catalogs.d.ts.map +1 -1
  35. package/dist/lib/remote-ui/pages/catalogs.js +14 -9
  36. package/dist/lib/remote-ui/pages/catalogs.js.map +1 -1
  37. package/dist/lib/remote-ui/pages/procurement.d.ts.map +1 -1
  38. package/dist/lib/remote-ui/pages/procurement.js +13 -3
  39. package/dist/lib/remote-ui/pages/procurement.js.map +1 -1
  40. package/dist/lib/remote-ui/pages/sales-files.css +5 -0
  41. package/dist/lib/remote-ui/pages/sales.d.ts +30 -3
  42. package/dist/lib/remote-ui/pages/sales.d.ts.map +1 -1
  43. package/dist/lib/remote-ui/pages/sales.js +58 -12
  44. package/dist/lib/remote-ui/pages/sales.js.map +1 -1
  45. package/dist/lib/remote-ui/procurement-contracts.d.ts +7 -1
  46. package/dist/lib/remote-ui/procurement-contracts.d.ts.map +1 -1
  47. package/dist/lib/remote-ui/procurement-contracts.js.map +1 -1
  48. package/dist/lib/remote-ui/sales-contracts.d.ts +20 -7
  49. package/dist/lib/remote-ui/sales-contracts.d.ts.map +1 -1
  50. package/dist/lib/remote-ui/styles.css +1 -0
  51. package/dist/lib/sales-file.service.d.ts.map +1 -1
  52. package/dist/lib/sales-file.service.js +10 -2
  53. package/dist/lib/sales-file.service.js.map +1 -1
  54. package/dist/lib/schema-maintenance.d.ts.map +1 -1
  55. package/dist/lib/schema-maintenance.js +13 -5
  56. package/dist/lib/schema-maintenance.js.map +1 -1
  57. package/dist/lib/types.d.ts +22 -4
  58. package/dist/lib/types.d.ts.map +1 -1
  59. package/dist/lib/types.js +7 -1
  60. package/dist/lib/types.js.map +1 -1
  61. package/dist/lib/workbench-view.provider.d.ts +42 -243
  62. package/dist/lib/workbench-view.provider.d.ts.map +1 -1
  63. package/dist/lib/workbench.middleware.d.ts +35 -28
  64. package/dist/lib/workbench.middleware.d.ts.map +1 -1
  65. package/dist/lib/workbench.middleware.js +1 -1
  66. package/dist/lib/workbench.middleware.js.map +1 -1
  67. package/dist/lib/workbench.service.d.ts +59 -89
  68. package/dist/lib/workbench.service.d.ts.map +1 -1
  69. package/dist/lib/workbench.service.js +186 -45
  70. package/dist/lib/workbench.service.js.map +1 -1
  71. 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 duplicate = await repo.findOne({ where: { ...activeScope(scope), lifecycleStatus: 'ACTIVE', normalizedCompanyName, normalizedSanctionListSource, deletedAt: IsNull() } });
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 duplicate = await recordRepo.findOne({ where: { ...activeScope(scope), lifecycleStatus: 'ACTIVE', normalizedCompanyName, normalizedSanctionListSource, deletedAt: IsNull() } });
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 sales_contract."normalizedContractNo" = regexp_replace(sales_purchase."normalizedContractNo", '(-[0-9]+)?C$', '') || 'X'
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
- if (effectiveRate == null)
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 = deriveSalesLine({ purchaseUnitPriceCny: line.unitPriceTaxIncluded, quantity: line.quantity }, effectiveRate);
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 amount = derivedLines.reduce((sum, line) => sum.plus(line.salesLineAmountCny), new Decimal(0));
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: source, effectivePricingProfitRate: effectiveRate, orderAmountCny: amount.toFixed(0), receivedAmountCny: received.toFixed(2), unreceivedAmountCny: Decimal.max(amount.minus(received), 0).toFixed(2), lines: derivedLines });
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 contract."normalizedContractNo" = regexp_replace(purchase."normalizedContractNo", '(-[0-9]+)?C$', '') || 'X'`)
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 effectiveRate = 'COALESCE(manual."pricingProfitRateOverride", customer."defaultPricingProfitRate")';
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
- if (new Decimal(rate).isNegative())
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 = derivedOrderAmount(linesByPurchase.get(manual.purchaseOrderId) ?? [], rate);
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 = rate;
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
- assertReceivedAmount(derivedOrderAmount(lines, effectiveRate), received);
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 = ids.length ? await this.dataSource.getRepository(CompanySanctionMatch).find({ where: { ...activeScope(scope), subjectType: 'SUPPLIER', supplierId: In(ids), deletedAt: IsNull() } }) : [];
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 = ids.length ? await this.dataSource.getRepository(CompanySanctionMatch).find({ where: { ...activeScope(scope), subjectType: 'CUSTOMER', customerId: In(ids), deletedAt: IsNull() } }) : [];
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
- productAmounts.set(line.productName, (productAmounts.get(line.productName) ?? new Decimal(0)).plus(line.salesLineAmountCny));
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 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 linked_contract."normalizedContractNo" = regexp_replace(${alias}."normalizedContractNo", '(-[0-9]+)?C$', '') || 'X'`; }
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 }; }