Ошибки при конвертации валюты при многострочной транзакции

У меня есть проблема, которая, я думаю, может не дать ответа, кроме как полностью переосмыслить приложение, но, надеюсь, вы, ребята, можете доказать, что я не прав!

У меня есть приложение, которое использует обменный курс для обратного расчета базового значения от значения валюты до четырех знаков после запятой. Расчет довольно прост и проходит все соответствующие модульные тесты, каждый раз давая правильные результаты:

    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;
    }

У меня также есть эквивалент для вычисления валюты из базы, не показанный, чтобы избежать путаницы в проблеме и в любом случае в значительной степени идентичный код, кроме имен параметров и обращения математических операторов в операторе switch.

Моя проблема возникает, когда мне нужно применить этот процесс к транзакции, которая имеет несколько строк. Правило состоит в том, что сумма дебетов должна быть равна сумме кредитов, но некоторые цифры выходят за долю копейки. Это потому, что мы суммируем результат двух или более индивидуально преобразованных чисел против одного преобразования, и я считаю, что накопленное округление вызывает ошибку.

Давайте предположим следующее:

  1. Стоимость товара 1,00 $
  2. НДС в размере $0,20 (20%)
  3. Обменный курс 1,4540 долларов за фунт

Теперь давайте рассмотрим пример транзакции:

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

Это проходит тест, поскольку общее количество кредитов совпадает с общим количеством дебетов.

Когда мы конвертируем (делим каждое значение в долларах на 1.454), мы видим проблему:

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

Это не работает и нарушает правило, как я полагаю, в результате двух наборов округления в столбце дебета и только одного набора в столбце кредита. Тем не менее, мне нужно иметь возможность рассчитывать с точностью до наоборот, и мне нужно убедиться, что транзакция балансирует в валюте и в базе по всем строкам.

Итак, на мой вопрос: как лучше решить эту проблему? Кто-нибудь сталкивался с чем-то похожим, и если да, то не могли бы вы рассказать, как вы решили это?

РЕДАКТИРОВАТЬ - Решение, которое я использовал

После ответа Чарльза, который указал мне абсолютно верное направление, я публикую свой метод работы в интересах любого, кто может столкнуться с подобной проблемой. Понимая, что этот конкретный код может не подходить для прямого копирования и вставки решения, я надеюсь, что комментарии и последующая процедура помогут:

    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;
                    }
                }                    
            }
        }
    }

2 ответа

Решение

Скорее, это зависит от ситуации, но одним из вариантов для этого является только преобразование итогов, а затем пропорциональное распределение их для каждой части с использованием метода уменьшающего сальдо. Это гарантирует, что сумма частей всегда будет точно равна сумме.

Ваши дебетовые и кредитные столбцы в сумме составляют 1,20 доллара, поэтому вы конвертируете это по своей ставке, получая 0,8253 фунта.

Затем вы пропорционально распределяете это по своим дебетовым суммам от самых маленьких до самых больших. Теория, лежащая в основе сортировки, заключается в том, что вы реже будете заботиться о лишних / недостающих копейках большего числа.

Таким образом, вы начинаете с общей суммы в 1,20 доллара США и 0,6878 фунтов стерлингов, а затем рассчитываете долю вашего конвертированного баланса, которая относится к вашей минимальной сумме в долларах:

$0.20 / $1.20 * 0.8253 = £0.1376

Затем вы вычитаете суммы из ваших итоговых сумм (это часть "сокращающего баланса"):

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

А затем вычислите следующий по величине (поскольку у вас есть только еще 1 сумма в этом примере, это тривиально):

$1.00 / $1.00 * £0.6877 = £0.6877

Так что это дает вам:

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

Обычно это бизнес-проблема, а не программная. Результаты, которые вы видите, являются результатом математического округления. В финансовых расчетах предприятие будет решать, когда будет применяться обменный курс, и его следует применять один раз. Например, в вашем случае может быть решено применить его к стоимости товара, а затем умножить на НДС, чтобы получить общую сумму. Это также обычно присутствует в ваших регионах, принятых бухгалтерских / финансовых стандартов.

Другие вопросы по тегам