2016-06-04 12 views
0

Ich habe ein Problem, dass ich denke, kann keine Antwort haben andere dann völlig überdenken die Anwendung, aber hoffentlich können Sie mir beweisen, falsch!Fehler bei der Währungsumrechnung bei mehrzeiligen Transaktionen

Ich habe eine Anwendung, die einen Wechselkurs verwendet, um einen Basiswert von einem Währungswert auf vier Dezimalstellen zurück zu berechnen. Die Berechnung ist recht einfach und übergibt alle relevanten Unit-Tests korrekte Ergebnisse jedes Mal geben:

public static decimal GetBaseValue(decimal currencyAmount, RateOperator rateOperator, 
     double exchangeRate) 
    { 
     if (exchangeRate <= 0) 
     { 
      throw new ArgumentException(ErrorType. 
       ExchangeRateLessThanOrEqualToZero.ErrorInfo().Description); 
     } 

     decimal baseValue = 0; 

     if (currencyAmount != 0) 
     { 
      switch (rateOperator) 
      { 
       case RateOperator.Divide: 
        baseValue = Math.Round(currencyAmount * Convert.ToDecimal(exchangeRate), 
         4, MidpointRounding.AwayFromZero); 
        break; 
       case RateOperator.Multiply: 
        baseValue = Math.Round(currencyAmount/Convert.ToDecimal(exchangeRate), 
         4, MidpointRounding.AwayFromZero); 
        break; 
       default: 
        throw new ArgumentOutOfRangeException(nameof(rateOperator)); 
      } 
     } 

     return baseValue; 
    } 

Ich habe auch die entsprechende Währung von der Basis zu berechnen, nicht um das Problem zu vermeiden gezeigt verwirrend und in jedem Fall so ziemlich identischer Code abgesehen von Parameternamen und der Umkehrung der math-Operatoren in der switch-Anweisung.

Mein Problem kommt, wenn ich diesen Prozess auf eine Transaktion anwenden muss, die mehrere Zeilen hat. Die Regel ist, dass die Summe der Belastungen gleich der Summe der Kredite sein muss, aber einige Zahlen sind um einen Bruchteil eines Pennys hinaus. Dies liegt daran, dass wir das Ergebnis von zwei oder mehr individuell konvertierten Zahlen gegen eine einzige Umwandlung zusammenrechnen, und ich glaube, dass die akkumulierte Rundung den Fehler verursacht.

Nehmen wir an, den folgenden:

  1. Artikel Wert von 1,00 $
  2. MwSt Wert von $ 0,20 (20%)
  3. Wechselkurs von 1,4540 Dollar für das Pfund

Nun lassen wir überprüfen eine Beispieltransaktion:

Line # Debit Credit 
1    $1.20 
2  $1.00 
3  $0.20 

Dieser Test besteht den Test, da die Gesamtguthaben den Gesamtbelastungen entsprechen.

Wenn wir (teilen jeden Dollar-Wert von 1,454) umwandeln, sehen wir das Problem:

Line # Debit Credit 
1    £0.8253 
2  £0.6878 
3  £0.1376 
======================== 
Total £0.8254 £0.8253 
======================== 

Dies scheitert und bricht die Regel, glaube ich, als Folge der beiden Sätze in der Belastungs Spalte Rundungs und nur ein Satz in der Kreditspalte. Aber ich muss in der Lage sein, mit Genauigkeit genau hin und her zu rechnen, und ich muss sicherstellen, dass die Transaktion in allen Linien in Währung und in Basis ausgeglichen ist.

Also zu meiner Frage: Wie kann man dieses Problem am besten angehen? Hat jemand etwas Ähnliches erlebt, und wenn es Ihnen etwas ausmacht, zu teilen, wie Sie es gelöst haben?

EDIT - Die Lösung I

Gebrauchte

Im Anschluss an die Antwort von Charles, die mich absolut in die richtige Richtung wies ich meine Arbeitsweise zugunsten von jemand anders bin Entsendung, die ein ähnliches Problem konfrontiert sein könnten. Während des Verständnis, dass dieser spezifische Code nicht für eine direkte Kopieren und Einfügen Lösung geeignet sein kann, hoffe ich, dass die Kommentare und das angewandte Verfahren wird eine Hilfe sein:

private static void SetBaseValues(Transaction transaction) 
    { 
     // Get the initial currency totals 
     decimal currencyDebitRunningTotal = transaction.CurrencyDebitTotal; 
     decimal currencyCreditRunningTotal = transaction.CurrencyCreditTotal; 

     // Only one conversion, but we do one per column 
     // Note that the values should be the same anyway 
     // or the transaction would be invalid 
     decimal baseDebitRunningTotal = 
      Functions.GetBaseValue(currencyDebitRunningTotal, 
      transaction.MasterLine.RateOperator, 
      transaction.MasterLine.ExchangeRate); 
     decimal baseCreditRunningTotal = 
      Functions.GetBaseValue(currencyCreditRunningTotal, 
      transaction.MasterLine.RateOperator, 
      transaction.MasterLine.ExchangeRate); 

     // Create a list of transaction lines that belong to this transaction 
     List<TransactionLineBase> list = new List<TransactionLineBase> 
     { transaction.MasterLine }; 
     list.AddRange(transaction.TransactionLines); 

     // If there is no tax line, don't add a null entry 
     // as that would cause conversion failure 
     if (transaction.TaxLine != null) 
     { 
      list.Add(transaction.TaxLine); 
     } 

     // Sort the list ascending by value 
     var workingList = list.OrderBy(
      x => x.CurrencyCreditAmount ?? 0 + x.CurrencyDebitAmount ?? 0).ToList(); 

     // Iterate the lines excluding any entries where Credit and Debit 
     // values are both null (this is possible on some rows on 
     // some transactions types e.g. Reconciliations 
     foreach (var line in workingList.Where(
      line => line.CurrencyCreditAmount != null || 
      line.CurrencyDebitAmount != null)) 
     { 
      if (transaction.CanConvertCurrency) 
      { 
       SetBaseValues(line); 
      } 
      else 
      { 
       var isDebitLine = line.CurrencyCreditAmount == null; 

       if (isDebitLine) 
       { 
        if (line.CurrencyDebitAmount != 0) 
        { 
         line.BaseDebitAmount = 
          line.CurrencyDebitAmount ?? 0/
          currencyDebitRunningTotal * baseDebitRunningTotal; 
         currencyDebitRunningTotal -= 
          line.CurrencyDebitAmount ?? 0; 
         baseDebitRunningTotal -= line.BaseDebitAmount ?? 0;        
        } 
       } 
       else 
       { 
        if (line.CurrencyCreditAmount != 0) 
        { 
         line.BaseCreditAmount = 
          line.CurrencyCreditAmount ?? 0/ 
         currencyCreditRunningTotal*baseCreditRunningTotal; 
         currencyCreditRunningTotal -= line.CurrencyCreditAmount ?? 0; 
         baseCreditRunningTotal -= line.BaseCreditAmount ?? 0; 
        } 
       }      
      } 
     } 
    } 

Antwort

1

Dies hängt eher von der Situation ab, aber eine Möglichkeit besteht darin, die Summen nur zu konvertieren und dann anteilig auf jedes der Teile mit einer reduzierenden Bilanzmethode zu verteilen. Dies stellt sicher, dass die Summe der Teile immer genau der Summe entspricht.

Ihre Debit- und Kreditspalten summieren sich auf $ 1,20, also konvertieren Sie diese zu Ihrer Rate und Sie erhalten £ 0,8253.

Dann ordnen Sie diese anteilig Ihren Sollbeträgen von den kleinsten bis zu den größten zu. Die Theorie hinter der Sortierung ist, dass es weniger wahrscheinlich ist, dass Sie sich um die zusätzlichen/fehlenden Pennies einer größeren Anzahl kümmern.

So beginnen Sie mit den Summen von $ 1,20 und 0,6878 £ und dann den Anteil der konvertierten Balance zu berechnen, die auf Ihre kleinsten Dollar-Betrag gilt:

$0.20/$1.20 * 0.8253 = £0.1376 

Sie dann die Beträge von Ihrem Summen abziehen (dies ist der Teil) 'Balance reduzieren':

$1.20 - $0.20 = $1.00 
£0.8253 - £0.1376 = £0.6877 

Und dann berechnet die nächstgrößere (wie Sie nur 1 mehr Menge in diesem Beispiel haben, ist dies trivial):

So

das gibt Ihnen:

Line # Debit Credit 
1    £0.8253 
2  £0.6877 
3  £0.1376 
======================== 
Total £0.8253 £0.8253 
======================== 
+0

Hallo Charles, Vielen Dank für Ihren interessanten Beitrag. Die Theorie macht Sinn, wäre aber nicht genau reversibel und würde nicht in die aktuelle Verarbeitungsmethode passen, die im Grunde die Zeilen in der Reihenfolge bearbeitet. Das heißt nicht, dass es nicht erreicht werden könnte, aber es würde eine vollständige Änderung des Systems bedeuten. Ich werde darüber noch einmal nachdenken und mit den Mächtigen darüber diskutieren, ob dies ein möglicher Weg nach vorne ist. Ich werde posten, sobald ich irgendeine Form der Entscheidung habe. Nochmals vielen Dank dafür, dass Sie sich die Zeit und Mühe genommen haben, zu antworten. – oldcoder

+0

@oldcoder kein Problem.Wie Sie herausgefunden haben (und wie Peter darauf hingewiesen hat), besteht die einzige Möglichkeit darin, nur eine Zahl zu konvertieren und die anderen basierend darauf zu berechnen - entweder unter Verwendung seines spezifischen Domänenwissens wie der Mehrwertsteuerrate oder durch Verwendung diese allgemeinere Methode. Wenn Sie jede ohne Kontext konvertieren, verursachen die kumulativen Rundungsfehler kleine Diskrepanzen. –

+0

Hallo Charles, Ihre Antwort führte mich zur richtigen Lösung. Es erforderte ein wenig Reengineering, um mit vorhandenem Code arbeiten zu können, und um alle Möglichkeiten abzudecken, war es ziemlich mühsam und fehlerbehaftet, aber das Ergebnis funktioniert einwandfrei und alle zugehörigen Unit- und Integrationstests passieren 100%. Eine besondere Sache war, dass ich Credit und Debit laufen lassen musste, indem ich die Summen für die Währungs- und Basiswerte laufen ließ, um sicherzustellen, dass alle notwendigen Werte verfügbar waren. Vielen Dank für Ihre Hilfe, die sehr geschätzt wird. – oldcoder

0

Dies ist typischerweise ein geschäftliches Problem nicht um eine Programmierung. Die Ergebnisse, die Sie sehen, sind ein Ergebnis der mathematischen Rundung. In finanziellen Berechnungen wird das Geschäft entscheiden, wann der Wechselkurs angewendet wird und es muss einmal angewendet werden. Zum Beispiel könnte in Ihrem Fall entschieden werden, es auf den Artikelwert anzuwenden und dann mit der Mehrwertsteuer zu multiplizieren, um Ihren Gesamtbetrag zu erhalten.Dies ist auch in der Regel in Ihrer Region akzeptiert Accounting/Financial akzeptiert Standards.

+0

Vielen Dank für Ihre Antwort. Leider funktioniert die Anwendung (oder sollte besser funktionieren) mit jeder beliebigen Währung auf der Welt, und die Regeln, die ich befolgen muss, sind in der Frage angegeben. Ich kann nicht sehen, wie dieses Problem gelöst werden kann, da die einzelnen Linien noch berechnet werden müssen und die Basis- und Währungswerte für die weitere Verwendung verfügbar sind. Ändern der Regeln könnte funktionieren, wenn wir immer noch GAAP anwenden können, aber was ändert sich? Runden auf 2 Plätze könnte funktionieren, aber technisch bricht die Regeln und ich habe nicht genügend Tests durchgeführt, um sicher zu sein, dass es immer funktionieren würde ... – oldcoder

+0

Sie sind Fragen kapseln die Herausforderungen mit der Arbeit am Geldwechsel. In meiner Antwort würde es keine Rolle spielen, welche Währung Sie verwendet haben, wenn Sie den Umtausch nur anwenden, wenn Sie immer in der Lage sein werden, die Basiswährung mit Genauigkeit zu konvertieren. Das Runden auf die am wenigsten genaue Zahl (in diesem Fall zwei Dezimalstellen) funktioniert auch nicht. Wie gesagt, es ist eine mathematische Unmöglichkeit, mit konsistenten Ergebnissen zu erreichen, wenn Sie den Austausch an mehreren Stellen in Ihren Berechnungen anwenden. Tut mir leid, wenn es so aussieht, als wäre ich hartnäckig. Ich hatte dieses Problem, bevor ich mit finanziellen Apps arbeitete. – Peter4499

+0

Hallo Peter4499, Ja, ich sagte in meiner Frage, dass ich ziemlich sicher war, dass es mathematisch nicht praktikabel war. Es kann nur einmal berechnet werden, da jede Zeile einen Basiswert haben muss und es in einer Transaktion eine beliebige Anzahl von Zeilen geben kann - ich habe ein anschauliches, aber einfaches Beispiel für das Problem angegeben. Das gibt mir drei Möglichkeiten - das System ändern, die Regeln ändern oder den winzigen kleinen Fehler ignorieren. Ich bin mir nicht sicher, ob ich überhaupt etwas davon machen kann, aber danke für den Input, der geschätzt wird. – oldcoder