@chenchaolong/plugin-trade-compliance-workbench 1.0.151 → 1.0.153

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