526 lines
23 KiB
C#
526 lines
23 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
|
|
namespace Lskj.CommandKernel
|
|
{
|
|
public static class PurchaseSourceDocumentContract
|
|
{
|
|
public const int MaximumCount = 3;
|
|
public const long MaximumSizeBytes = 12L * 1024L * 1024L;
|
|
public const string ImagePreprocessContract = "minimax_vlm_0.0.4";
|
|
public const string PdfPreprocessContract = "pdfium_minimax_pages_v1";
|
|
public const string CsvPreprocessContract = "document_sandbox_csv_v1";
|
|
public const string XlsxPreprocessContract = "document_sandbox_xlsx_v1";
|
|
|
|
public static bool Matches(
|
|
string kind,
|
|
string filename,
|
|
string preprocessContract)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(filename)
|
|
|| string.IsNullOrWhiteSpace(preprocessContract))
|
|
return false;
|
|
string name = filename.Trim();
|
|
if (kind == "image")
|
|
{
|
|
return preprocessContract == ImagePreprocessContract
|
|
&& (name.EndsWith(".png", StringComparison.OrdinalIgnoreCase)
|
|
|| name.EndsWith(".jpg", StringComparison.OrdinalIgnoreCase)
|
|
|| name.EndsWith(".jpeg", StringComparison.OrdinalIgnoreCase)
|
|
|| name.EndsWith(".webp", StringComparison.OrdinalIgnoreCase));
|
|
}
|
|
if (kind != "file") return false;
|
|
if (name.EndsWith(".pdf", StringComparison.OrdinalIgnoreCase))
|
|
return preprocessContract == PdfPreprocessContract;
|
|
if (name.EndsWith(".csv", StringComparison.OrdinalIgnoreCase))
|
|
return preprocessContract == CsvPreprocessContract;
|
|
if (name.EndsWith(".xlsx", StringComparison.OrdinalIgnoreCase))
|
|
return preprocessContract == XlsxPreprocessContract;
|
|
return false;
|
|
}
|
|
}
|
|
|
|
public enum InvoiceLineMatchStatus
|
|
{
|
|
Exact = 0,
|
|
Ambiguous = 1,
|
|
Unmatched = 2,
|
|
Invalid = 3,
|
|
Conflict = 4
|
|
}
|
|
|
|
public enum InvoiceLineAmountMode
|
|
{
|
|
None = 0,
|
|
TaxExclusive = 1,
|
|
TaxInclusive = 2
|
|
}
|
|
|
|
public sealed class PurchaseInvoiceDraft
|
|
{
|
|
public PurchaseInvoiceDraft()
|
|
{
|
|
Lines = new List<PurchaseInvoiceLine>();
|
|
SourceDocuments = new List<PurchaseSourceDocument>();
|
|
}
|
|
|
|
public string SupplierCode { get; set; }
|
|
public string CurrencyCode { get; set; }
|
|
public string InvoiceNumber { get; set; }
|
|
public DateTime InvoiceDate { get; set; }
|
|
public decimal TotalWithoutTax { get; set; }
|
|
public decimal TaxAmount { get; set; }
|
|
public decimal TotalWithTax { get; set; }
|
|
public IList<PurchaseInvoiceLine> Lines { get; private set; }
|
|
public IList<PurchaseSourceDocument> SourceDocuments { get; private set; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// 由受信任 AstrBot Tool 从本次消息附件生成的来源凭据。它会进入采购
|
|
/// 解析凭证、命令输入指纹和客户写入 payload,模型不能自行声明来源。
|
|
/// </summary>
|
|
public sealed class PurchaseSourceDocument
|
|
{
|
|
public string Kind { get; set; }
|
|
public string Filename { get; set; }
|
|
public string Sha256 { get; set; }
|
|
public long SizeBytes { get; set; }
|
|
public string ExtractionSha256 { get; set; }
|
|
public string PreprocessContract { get; set; }
|
|
}
|
|
|
|
public sealed class PurchaseInvoiceLine
|
|
{
|
|
public string LineId { get; set; }
|
|
public string MaterialCode { get; set; }
|
|
public string Unit { get; set; }
|
|
public string SourceOrderHint { get; set; }
|
|
public decimal Quantity { get; set; }
|
|
public decimal UnitPrice { get; set; }
|
|
public decimal TaxRate { get; set; }
|
|
public decimal TaxAmount { get; set; }
|
|
public decimal LineAmount { get; set; }
|
|
}
|
|
|
|
public sealed class PurchaseSourceLine
|
|
{
|
|
public string SourceLineId { get; set; }
|
|
public string SourceOrderId { get; set; }
|
|
public string SourceOrderNumber { get; set; }
|
|
public string SupplierCode { get; set; }
|
|
public string CurrencyCode { get; set; }
|
|
public string MaterialCode { get; set; }
|
|
public string Unit { get; set; }
|
|
public decimal RemainingQuantity { get; set; }
|
|
public decimal UnitPrice { get; set; }
|
|
public decimal TaxRate { get; set; }
|
|
public decimal ExchangeRate { get; set; }
|
|
public bool Closed { get; set; }
|
|
|
|
public string UniqueKey
|
|
{
|
|
get { return (SourceOrderId ?? string.Empty) + "\u001f" + (SourceLineId ?? string.Empty); }
|
|
}
|
|
}
|
|
|
|
public sealed class PurchaseInvoiceMatchOptions
|
|
{
|
|
public PurchaseInvoiceMatchOptions()
|
|
{
|
|
QuantityTolerance = 0.0001m;
|
|
UnitPriceAbsoluteTolerance = 0.01m;
|
|
UnitPriceRelativeTolerance = 0.0001m;
|
|
TaxRateTolerance = 0.0001m;
|
|
LineAmountTolerance = 0.02m;
|
|
HeaderAmountTolerance = 0.05m;
|
|
CurrencyScale = 2;
|
|
LineAmountMode = InvoiceLineAmountMode.None;
|
|
}
|
|
|
|
public decimal QuantityTolerance { get; set; }
|
|
public decimal UnitPriceAbsoluteTolerance { get; set; }
|
|
public decimal UnitPriceRelativeTolerance { get; set; }
|
|
public decimal TaxRateTolerance { get; set; }
|
|
public decimal LineAmountTolerance { get; set; }
|
|
public decimal HeaderAmountTolerance { get; set; }
|
|
public int CurrencyScale { get; set; }
|
|
public InvoiceLineAmountMode LineAmountMode { get; set; }
|
|
}
|
|
|
|
public sealed class PurchaseInvoiceLineMatch
|
|
{
|
|
public PurchaseInvoiceLineMatch(PurchaseInvoiceLine invoiceLine)
|
|
{
|
|
InvoiceLine = invoiceLine;
|
|
Candidates = new List<PurchaseSourceLine>();
|
|
Issues = new List<string>();
|
|
}
|
|
|
|
public PurchaseInvoiceLine InvoiceLine { get; private set; }
|
|
public InvoiceLineMatchStatus Status { get; set; }
|
|
public PurchaseSourceLine SelectedSource { get; set; }
|
|
public IList<PurchaseSourceLine> Candidates { get; private set; }
|
|
public IList<string> Issues { get; private set; }
|
|
}
|
|
|
|
public sealed class PurchaseInvoiceMatchPlan
|
|
{
|
|
public PurchaseInvoiceMatchPlan()
|
|
{
|
|
Lines = new List<PurchaseInvoiceLineMatch>();
|
|
Issues = new List<string>();
|
|
}
|
|
|
|
public bool Executable { get; set; }
|
|
public decimal CalculatedTotalWithoutTax { get; set; }
|
|
public decimal CalculatedTaxAmount { get; set; }
|
|
public decimal CalculatedTotalWithTax { get; set; }
|
|
public IList<PurchaseInvoiceLineMatch> Lines { get; private set; }
|
|
public IList<string> Issues { get; private set; }
|
|
}
|
|
|
|
public static class PurchaseInvoiceMatcher
|
|
{
|
|
public const decimal MaximumQuantityTolerance = 0.01m;
|
|
public const decimal MaximumUnitPriceAbsoluteTolerance = 1m;
|
|
public const decimal MaximumUnitPriceRelativeTolerance = 0.01m;
|
|
public const decimal MaximumTaxRateTolerance = 0.001m;
|
|
public const decimal MaximumLineAmountTolerance = 1m;
|
|
public const decimal MaximumHeaderAmountTolerance = 5m;
|
|
private const decimal ExchangeRateTolerance = 0.000001m;
|
|
|
|
public static PurchaseInvoiceMatchPlan Match(
|
|
PurchaseInvoiceDraft invoice,
|
|
IEnumerable<PurchaseSourceLine> sourceLines,
|
|
PurchaseInvoiceMatchOptions options)
|
|
{
|
|
if (invoice == null) throw new ArgumentNullException("invoice");
|
|
if (sourceLines == null) throw new ArgumentNullException("sourceLines");
|
|
if (options == null) throw new ArgumentNullException("options");
|
|
ValidateOptions(options);
|
|
|
|
PurchaseInvoiceMatchPlan plan = new PurchaseInvoiceMatchPlan();
|
|
List<PurchaseSourceLine> sources = sourceLines.Where(item => item != null).ToList();
|
|
IList<PurchaseInvoiceLine> invoiceLines = invoice.Lines ?? new List<PurchaseInvoiceLine>();
|
|
if (!SafeText(invoice.SupplierCode, 64))
|
|
plan.Issues.Add("供应商必须先解析为 ERP 供应商编码,禁止用名称模糊猜测。");
|
|
if (!SafeText(invoice.CurrencyCode, 64))
|
|
plan.Issues.Add("币种编码不能为空。");
|
|
if (!SafeText(invoice.InvoiceNumber, 128))
|
|
plan.Issues.Add("发票号码不能为空。");
|
|
if (invoice.InvoiceDate == DateTime.MinValue
|
|
|| invoice.InvoiceDate.Kind != DateTimeKind.Unspecified
|
|
|| invoice.InvoiceDate.TimeOfDay != TimeSpan.Zero)
|
|
plan.Issues.Add("发票日期必须是不含时间或时区的日期。");
|
|
if (invoiceLines.Count == 0)
|
|
plan.Issues.Add("发票至少需要一行有效明细。");
|
|
if (invoiceLines.Where(item => item != null && !string.IsNullOrWhiteSpace(item.LineId))
|
|
.GroupBy(item => item.LineId.Trim(), StringComparer.OrdinalIgnoreCase)
|
|
.Any(group => group.Count() > 1))
|
|
{
|
|
plan.Issues.Add("发票明细行 ID 必须唯一,禁止同一行被重复匹配。");
|
|
}
|
|
if (sources.Any(item => !ValidSource(item))
|
|
|| sources.Where(item => ValidSource(item))
|
|
.GroupBy(item => item.UniqueKey, StringComparer.OrdinalIgnoreCase)
|
|
.Any(group => group.Count() > 1))
|
|
{
|
|
plan.Issues.Add("采购来源返回了无效或重复的来源契约,已拒绝匹配。");
|
|
sources.Clear();
|
|
}
|
|
|
|
foreach (PurchaseInvoiceLine line in invoiceLines)
|
|
{
|
|
PurchaseInvoiceLineMatch match = MatchLine(invoice, line, sources, options);
|
|
plan.Lines.Add(match);
|
|
}
|
|
ValidateFinancialTotals(invoice, plan, options);
|
|
ApplyAggregateQuantityRules(plan, options);
|
|
ApplyExchangeRateRules(plan);
|
|
plan.Executable = plan.Issues.Count == 0
|
|
&& plan.Lines.Count > 0
|
|
&& plan.Lines.All(item => item.Status == InvoiceLineMatchStatus.Exact);
|
|
return plan;
|
|
}
|
|
|
|
private static PurchaseInvoiceLineMatch MatchLine(
|
|
PurchaseInvoiceDraft invoice,
|
|
PurchaseInvoiceLine line,
|
|
IEnumerable<PurchaseSourceLine> sources,
|
|
PurchaseInvoiceMatchOptions options)
|
|
{
|
|
PurchaseInvoiceLineMatch match = new PurchaseInvoiceLineMatch(line);
|
|
ValidateInvoiceLine(line, options, match.Issues);
|
|
if (match.Issues.Count > 0)
|
|
{
|
|
match.Status = InvoiceLineMatchStatus.Invalid;
|
|
return match;
|
|
}
|
|
|
|
List<PurchaseSourceLine> candidates = sources.Where(source =>
|
|
!source.Closed
|
|
&& !string.IsNullOrWhiteSpace(source.SourceOrderId)
|
|
&& !string.IsNullOrWhiteSpace(source.SourceLineId)
|
|
&& Same(source.SupplierCode, invoice.SupplierCode)
|
|
&& Same(source.CurrencyCode, invoice.CurrencyCode)
|
|
&& Same(source.MaterialCode, line.MaterialCode)
|
|
&& Same(source.Unit, line.Unit)
|
|
&& source.ExchangeRate > 0
|
|
&& source.RemainingQuantity + options.QuantityTolerance >= line.Quantity
|
|
&& PriceMatches(source.UnitPrice, line.UnitPrice, options)
|
|
&& Math.Abs(source.TaxRate - line.TaxRate) <= options.TaxRateTolerance)
|
|
.ToList();
|
|
|
|
foreach (PurchaseSourceLine candidate in candidates) match.Candidates.Add(candidate);
|
|
if (candidates.Count == 0)
|
|
{
|
|
match.Status = InvoiceLineMatchStatus.Unmatched;
|
|
match.Issues.Add("没有找到同时满足供应商、币种、物料、单位、汇率、未开票数量、单价和税率的来源行。");
|
|
return match;
|
|
}
|
|
|
|
if (!string.IsNullOrWhiteSpace(line.SourceOrderHint))
|
|
{
|
|
List<PurchaseSourceLine> hinted = candidates.Where(source =>
|
|
Same(source.SourceOrderId, line.SourceOrderHint)
|
|
|| Same(source.SourceOrderNumber, line.SourceOrderHint)).ToList();
|
|
if (hinted.Count == 1)
|
|
{
|
|
match.Status = InvoiceLineMatchStatus.Exact;
|
|
match.SelectedSource = hinted[0];
|
|
return match;
|
|
}
|
|
if (hinted.Count == 0)
|
|
match.Issues.Add("发票给出的来源单提示与候选采购单不一致。");
|
|
else
|
|
match.Issues.Add("来源单提示仍对应多个候选明细行。");
|
|
}
|
|
|
|
if (candidates.Count == 1 && string.IsNullOrWhiteSpace(line.SourceOrderHint))
|
|
{
|
|
match.Status = InvoiceLineMatchStatus.Exact;
|
|
match.SelectedSource = candidates[0];
|
|
return match;
|
|
}
|
|
|
|
match.Status = InvoiceLineMatchStatus.Ambiguous;
|
|
match.Issues.Add("存在多个合法来源行,必须由用户选择,模型不得猜测。");
|
|
return match;
|
|
}
|
|
|
|
private static void ValidateInvoiceLine(
|
|
PurchaseInvoiceLine line,
|
|
PurchaseInvoiceMatchOptions options,
|
|
IList<string> issues)
|
|
{
|
|
if (line == null)
|
|
{
|
|
issues.Add("发票明细不能为空。");
|
|
return;
|
|
}
|
|
if (!SafeText(line.LineId, 128)) issues.Add("发票明细行 ID 不能为空或格式无效。");
|
|
if (!SafeText(line.MaterialCode, 256)) issues.Add("物料必须先解析为有效的 ERP 物料编码。");
|
|
if (!SafeText(line.Unit, 64)) issues.Add("计量单位必须由 ERP 物料主数据唯一解析。");
|
|
if (!string.IsNullOrWhiteSpace(line.SourceOrderHint)
|
|
&& !SafeText(line.SourceOrderHint, 128))
|
|
issues.Add("来源采购单提示格式无效。");
|
|
if (line.Quantity <= 0) issues.Add("数量必须大于零。");
|
|
if (line.UnitPrice < 0) issues.Add("单价不能小于零。");
|
|
if (line.TaxRate < 0 || line.TaxRate > 1) issues.Add("税率必须使用 0 到 1 的小数格式。");
|
|
if (line.TaxAmount < 0) issues.Add("行税额不能小于零。");
|
|
if (line.LineAmount < 0) issues.Add("行金额不能小于零。");
|
|
|
|
if (issues.Count == 0 && options.LineAmountMode != InvoiceLineAmountMode.None)
|
|
{
|
|
decimal expectedLine;
|
|
decimal expectedTax;
|
|
try
|
|
{
|
|
expectedLine = Money(line.Quantity * line.UnitPrice, options.CurrencyScale);
|
|
expectedTax = options.LineAmountMode == InvoiceLineAmountMode.TaxInclusive
|
|
? Money(
|
|
line.TaxRate == 0
|
|
? 0
|
|
: line.LineAmount
|
|
- (line.LineAmount / (1 + line.TaxRate)),
|
|
options.CurrencyScale)
|
|
: Money(line.LineAmount * line.TaxRate, options.CurrencyScale);
|
|
}
|
|
catch (OverflowException)
|
|
{
|
|
issues.Add("数量、单价或税额计算溢出。");
|
|
return;
|
|
}
|
|
if (Math.Abs(expectedLine - line.LineAmount) > options.LineAmountTolerance)
|
|
issues.Add("行金额与数量、单价计算结果超出配置容差。");
|
|
if (Math.Abs(expectedTax - line.TaxAmount) > options.LineAmountTolerance)
|
|
issues.Add("行税额与金额、税率计算结果超出配置容差。");
|
|
}
|
|
}
|
|
|
|
private static void ValidateFinancialTotals(
|
|
PurchaseInvoiceDraft invoice,
|
|
PurchaseInvoiceMatchPlan plan,
|
|
PurchaseInvoiceMatchOptions options)
|
|
{
|
|
if (options.LineAmountMode == InvoiceLineAmountMode.None) return;
|
|
if (invoice.TotalWithoutTax < 0 || invoice.TaxAmount < 0
|
|
|| invoice.TotalWithTax <= 0)
|
|
{
|
|
plan.Issues.Add("发票不含税金额、税额或价税合计无效。");
|
|
return;
|
|
}
|
|
try
|
|
{
|
|
decimal withoutTax = 0;
|
|
decimal tax = 0;
|
|
decimal withTax = 0;
|
|
foreach (PurchaseInvoiceLine line in invoice.Lines.Where(item => item != null))
|
|
{
|
|
tax += line.TaxAmount;
|
|
if (options.LineAmountMode == InvoiceLineAmountMode.TaxInclusive)
|
|
{
|
|
withTax += line.LineAmount;
|
|
withoutTax += line.LineAmount - line.TaxAmount;
|
|
}
|
|
else
|
|
{
|
|
withoutTax += line.LineAmount;
|
|
withTax += line.LineAmount + line.TaxAmount;
|
|
}
|
|
}
|
|
plan.CalculatedTotalWithoutTax = Money(withoutTax, options.CurrencyScale);
|
|
plan.CalculatedTaxAmount = Money(tax, options.CurrencyScale);
|
|
plan.CalculatedTotalWithTax = Money(withTax, options.CurrencyScale);
|
|
if (Math.Abs(invoice.TotalWithoutTax - plan.CalculatedTotalWithoutTax)
|
|
> options.HeaderAmountTolerance)
|
|
plan.Issues.Add("发票不含税金额与明细汇总不一致。");
|
|
if (Math.Abs(invoice.TaxAmount - plan.CalculatedTaxAmount)
|
|
> options.HeaderAmountTolerance)
|
|
plan.Issues.Add("发票税额与明细税额汇总不一致。");
|
|
if (Math.Abs(invoice.TotalWithTax - plan.CalculatedTotalWithTax)
|
|
> options.HeaderAmountTolerance)
|
|
plan.Issues.Add("发票价税合计与明细汇总不一致。");
|
|
if (Math.Abs(
|
|
invoice.TotalWithoutTax + invoice.TaxAmount - invoice.TotalWithTax)
|
|
> options.HeaderAmountTolerance)
|
|
plan.Issues.Add("发票头不含税金额加税额不等于价税合计。");
|
|
}
|
|
catch (OverflowException)
|
|
{
|
|
plan.Issues.Add("发票金额汇总计算溢出。");
|
|
}
|
|
}
|
|
|
|
private static void ApplyAggregateQuantityRules(
|
|
PurchaseInvoiceMatchPlan plan,
|
|
PurchaseInvoiceMatchOptions options)
|
|
{
|
|
IEnumerable<IGrouping<string, PurchaseInvoiceLineMatch>> groups = plan.Lines
|
|
.Where(item => item.Status == InvoiceLineMatchStatus.Exact && item.SelectedSource != null)
|
|
.GroupBy(item => item.SelectedSource.UniqueKey, StringComparer.OrdinalIgnoreCase);
|
|
foreach (IGrouping<string, PurchaseInvoiceLineMatch> group in groups)
|
|
{
|
|
PurchaseSourceLine source = group.First().SelectedSource;
|
|
decimal requested = group.Sum(item => item.InvoiceLine.Quantity);
|
|
if (requested <= source.RemainingQuantity + options.QuantityTolerance) continue;
|
|
foreach (PurchaseInvoiceLineMatch item in group)
|
|
{
|
|
item.Status = InvoiceLineMatchStatus.Conflict;
|
|
item.Issues.Add("多行发票累计占用数量超过来源采购行的未开票数量。");
|
|
}
|
|
}
|
|
}
|
|
|
|
private static void ApplyExchangeRateRules(PurchaseInvoiceMatchPlan plan)
|
|
{
|
|
List<PurchaseInvoiceLineMatch> selected = plan.Lines
|
|
.Where(item => item.Status == InvoiceLineMatchStatus.Exact
|
|
&& item.SelectedSource != null)
|
|
.ToList();
|
|
if (selected.Count <= 1) return;
|
|
decimal minimum = selected.Min(item => item.SelectedSource.ExchangeRate);
|
|
decimal maximum = selected.Max(item => item.SelectedSource.ExchangeRate);
|
|
if (minimum > 0 && maximum - minimum <= ExchangeRateTolerance) return;
|
|
foreach (PurchaseInvoiceLineMatch item in selected)
|
|
{
|
|
item.Status = InvoiceLineMatchStatus.Conflict;
|
|
item.Issues.Add("同一张发票匹配到了不同汇率的采购来源,必须拆单或由财务确认汇率。");
|
|
}
|
|
}
|
|
|
|
private static bool PriceMatches(
|
|
decimal sourcePrice,
|
|
decimal invoicePrice,
|
|
PurchaseInvoiceMatchOptions options)
|
|
{
|
|
decimal allowed = Math.Max(
|
|
options.UnitPriceAbsoluteTolerance,
|
|
Math.Abs(sourcePrice) * options.UnitPriceRelativeTolerance);
|
|
return Math.Abs(sourcePrice - invoicePrice) <= allowed;
|
|
}
|
|
|
|
private static bool Same(string left, string right)
|
|
{
|
|
return string.Equals(
|
|
(left ?? string.Empty).Trim(),
|
|
(right ?? string.Empty).Trim(),
|
|
StringComparison.OrdinalIgnoreCase);
|
|
}
|
|
|
|
private static bool ValidSource(PurchaseSourceLine source)
|
|
{
|
|
return source != null
|
|
&& SafeText(source.SourceOrderId, 128)
|
|
&& SafeText(source.SourceOrderNumber, 128)
|
|
&& SafeText(source.SourceLineId, 128)
|
|
&& SafeText(source.SupplierCode, 64)
|
|
&& SafeText(source.CurrencyCode, 64)
|
|
&& SafeText(source.MaterialCode, 256)
|
|
&& SafeText(source.Unit, 64)
|
|
&& source.RemainingQuantity >= 0
|
|
&& source.UnitPrice >= 0
|
|
&& source.TaxRate >= 0
|
|
&& source.TaxRate <= 1
|
|
&& source.ExchangeRate > 0;
|
|
}
|
|
|
|
private static bool SafeText(string value, int maximum)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(value) || value.Length > maximum)
|
|
return false;
|
|
return !value.Any(char.IsControl);
|
|
}
|
|
|
|
private static void ValidateOptions(PurchaseInvoiceMatchOptions options)
|
|
{
|
|
if (options.QuantityTolerance < 0
|
|
|| options.QuantityTolerance > MaximumQuantityTolerance
|
|
|| options.UnitPriceAbsoluteTolerance < 0
|
|
|| options.UnitPriceAbsoluteTolerance > MaximumUnitPriceAbsoluteTolerance
|
|
|| options.UnitPriceRelativeTolerance < 0
|
|
|| options.UnitPriceRelativeTolerance > MaximumUnitPriceRelativeTolerance
|
|
|| options.TaxRateTolerance < 0
|
|
|| options.TaxRateTolerance > MaximumTaxRateTolerance
|
|
|| options.LineAmountTolerance < 0
|
|
|| options.LineAmountTolerance > MaximumLineAmountTolerance
|
|
|| options.HeaderAmountTolerance < 0
|
|
|| options.HeaderAmountTolerance > MaximumHeaderAmountTolerance
|
|
|| options.CurrencyScale < 0
|
|
|| options.CurrencyScale > 6
|
|
|| !Enum.IsDefined(typeof(InvoiceLineAmountMode), options.LineAmountMode))
|
|
{
|
|
throw new ArgumentOutOfRangeException(
|
|
"options",
|
|
"采购匹配容差或币种精度超出商用安全范围。");
|
|
}
|
|
}
|
|
|
|
private static decimal Money(decimal value, int scale)
|
|
{
|
|
return Math.Round(value, scale, MidpointRounding.AwayFromZero);
|
|
}
|
|
}
|
|
}
|