Задача и границы#
EDWH должен сохранить исходную денежную сумму и объяснить каждое изменение при расчёте. Точный тип хранения сам по себе не определяет, когда округлять, куда относить остаток и какой результат сверять с источником.
Здесь рассматривается контракт расчёта и контроля, а не нормативное правило бухгалтерского учёта. Курсы валют и переоценка требуют отдельной модели. Примеры синтетические; статья имеет статус knowledge и не описывает опыт автора или новую реализацию лаборатории.
Зерно и контракт суммы#
Исходное зерно - одна строка проводки конкретного документа, юридического лица и книги учёта. Ключ включает систему-источник, идентификатор документа и номер строки. Сумма неотрицательна; для знакового расчёта дебет положителен, кредит отрицателен. Валюта обязательна: складывать значения разных валют нельзя.
business_date определяет учётный период, observed_at - момент известности строки EDWH. Для повторного расчёта фиксируют оба среза, версию входных данных и версию политики округления. Новая политика не должна незаметно менять ранее сформированный результат.
| Элемент контракта |
Что фиксировать |
| Исходная сумма |
Значение и единицу источника до преобразования |
| Валюта и масштаб |
Код, число десятичных разрядов хранения, версию справочника |
| Расчёт |
Формулу, границы входных величин, рабочую точность |
| Округление |
Шаг, режим, этап и зерно применения |
| Результат |
Единицу, диапазон, правило обработки остатка и lineage |
Масштаб валюты и масштаб вычисления#
SIX публикует текущие и исторические списки ISO 4217 с данными о minor unit. Ссылку на актуальный список полезно дополнять сохранённой версией справочника. Нельзя подставлять два разряда для любой валюты или трактовать отсутствие применимого значения как нулевой масштаб.
В предлагаемом контракте отдельно задают справочную минимальную единицу, единицу исходной суммы, масштаб хранения, рабочую точность и шаг окончательного округления. Таблица валют не заменяет договорную или учётную политику расчёта.
Для целого представления связь имеет вид amount_major = amount_minor / 10^s, где s - явно заданный масштаб. Например, учебные 12345 копеек означают 123.45 RUB при s = 2. Процентная доля этой суммы может содержать дробную копейку: её нельзя заранее отбросить только потому, что проводка хранится целым числом.
Где возникает округление#
Нужно выбрать зерно: строка, документ или агрегат отчёта. Обычно sum(round(x)) и round(sum(x)) дают разные результаты. Это разные алгоритмы, а не ошибка агрегирования.
Синтетический пример: две рассчитанные строки по 0.005 RUB, окончательный шаг 0.01 RUB, режим ближайшего значения с выбором чётного при равном расстоянии (half-even). Каждая строка даёт 0.00 RUB; округление суммы 0.010 RUB даёт 0.01 RUB. При half-up обе строки дали бы по 0.01 RUB. Ни один режим здесь не объявляется обязательным для реального учёта.
| Вход, RUB |
До 0.01, half-even |
До 0.01, half-up |
| 1.005 |
1.00 |
1.01 |
| -1.005 |
-1.00 |
-1.01 |
| 1.015 |
1.02 |
1.02 |
Двойное округление тоже меняет итог: 1.2451 при half-even сначала до 0.001, затем до 0.01 становится 1.24; напрямую до 0.01 получается 1.25. Промежуточное округление должно быть предусмотрено контрактом, а не случайно возникать при записи во временную таблицу.
Представление и промежуточные вычисления#
Целые минимальные единицы удобны для сложения уже зафиксированных проводок. Для долей нужны дробные промежуточные значения; диапазон надо проверять и у отдельных сумм, и у агрегатов. В SQLite sum() целых может завершиться ошибкой переполнения, а total() возвращает floating point. Замена функции ради обхода ошибки нарушает контракт точности. Документация SQLite.
В PostgreSQL 18 numeric(p,s) при записи округляет к заданному масштабу и проверяет диапазон. numeric округляет середину от нуля; это надо согласовать с политикой приложения. Двоичные real и double precision неточны. Документация PostgreSQL 18. Это описание СУБД, не результат запуска PostgreSQL-тестов.
В Python 3.14 для Decimal вход передают строкой, избегая предварительного float. Контекст задаёт рабочую точность, а quantize и выбранный режим - окончательный масштаб. Сигнал Inexact позволяет обнаружить потерю ненулевых разрядов. Документация Python.
Рабочую точность выбирают по максимальным входам и цепочке операций, а не одним универсальным числом. Например, деление может не иметь конечного десятичного результата. В предлагаемом процессе неожиданный Inexact до разрешённого этапа округления останавливает расчёт; ожидаемый на финальном этапе записывается в контроль. Точный тип не восстанавливает разряды, потерянные источником.
Остаток распределения и контроль#
Пусть 0.01 RUB нужно распределить поровну между тремя строками. Независимое округление долей до копейки даст три нуля. Если контракт требует сохранения общей суммы, можно распределять целые минимальные единицы: сначала целые части долей, затем остаток по убыванию дробных частей, при равенстве - по стабильному ключу строки. Получится одна копейка у одной строки и нули у двух других. Это предложенный алгоритм, не требование стандарта. Отрицательные суммы требуют отдельного знакового правила; например, распределять модуль, затем возвращать знак.
Псевдокод последовательности, без привязки к СУБД:
Сохранить исходные значения и версию контракта.
Проверить валюту, единицу, конечность и допустимый диапазон.
Вычислить на выбранном рабочем масштабе без скрытого округления.
Округлить только на согласованном зерне.
Применить утверждённое правило остатка, если оно требуется.
Сохранить результат, разницу и происхождение корректировки.
Сверить строки и документ в каждой валюте и временном срезе.
Контроль округления хранит исходный расчёт, результат и разницу result - unrounded, идентификатор политики и причину разрешённой потери. Для распределения проверяют точное равенство суммы долей целевой сумме. Для проводок проверяют баланс документа по валюте и книге учёта, а полноту набора проверяют отдельно. Общий ноль не исключает две компенсирующие потери.
Допуск нужен только там, где его допускает контракт, с явной единицей и областью применения. Он не должен скрывать потерянные проводки. До приёмки нужны сценарии неизвестного масштаба, смешанных валют, отрицательной середины, двойного округления, переполнения, нечислового значения, повторного расчёта и изменения политики. Для каждого заранее задают ожидаемый результат или отказ.
Связанные темы и вопросы#
Основы зерна описаны в проводках и документах, агрегирование - в финансовом SQL, полнота - в сверках. Лаборатория использует целые копейки RUB и не реализует описанную политику дробных расчётов.
- Какой результат должен совпасть с источником: сумма округлённых строк или округлённая сумма документа?
- Как доказать, что при изменении рабочей точности не изменился окончательный результат?
- Какие версии данных и правил нужны для повторения отчёта через год?
Problem and scope#
An EDWH must preserve the source monetary amount and explain every change made during calculation. An exact storage type alone does not determine when to round, how to allocate a remainder, or which result to reconcile with the source.
This article addresses a calculation and control contract, rather than a mandatory accounting rule. Exchange rates and revaluation need a separate model. Examples are synthetic; the article has knowledge status and describes neither the author's experience nor a new lab implementation.
Grain and amount contract#
The input grain is one journal line in a specific document, legal entity, and ledger. Its key includes the source system, document identifier, and line number. The amount is nonnegative; for signed calculations, debit is positive and credit is negative. Currency is mandatory: values in different currencies cannot be added together.
business_date determines the accounting period; observed_at records when the EDWH learned about the line. Reproduction requires both cutoffs, the input version, and the rounding-policy version. A new policy must not silently change a previously generated result.
| Contract element |
What to record |
| Source amount |
Value and source unit before transformation |
| Currency and scale |
Code, storage decimal places, reference-data version |
| Calculation |
Formula, input bounds, working precision |
| Rounding |
Increment, mode, stage, and grain |
| Result |
Unit, range, remainder handling, and lineage |
Currency scale and calculation scale#
SIX publishes current and historical ISO 4217 lists with minor-unit information. Retain a reference-data version alongside the link to the current list. Do not assume two decimal places for every currency or interpret an inapplicable value as a zero scale.
The proposed contract distinguishes the reference minor unit, source unit, storage scale, working precision, and final rounding increment. A currency table does not replace a contractual or accounting calculation policy.
For integer representation, amount_major = amount_minor / 10^s, where s is the explicitly specified scale. For example, an educational amount of 12345 kopecks means 123.45 RUB at s = 2. A percentage of this amount can contain a fraction of a kopeck; it must not be discarded early just because journal amounts are stored as integers.
Where rounding occurs#
Choose the rounding grain: line, document, or report aggregate. In general, sum(round(x)) and round(sum(x)) produce different results. These are different algorithms, rather than an aggregation bug.
Synthetic example: two calculated lines of 0.005 RUB each, a final increment of 0.01 RUB, and rounding to the nearest value with ties going to even (half-even). Each line becomes 0.00 RUB; rounding the sum of 0.010 RUB gives 0.01 RUB. With half-up, both lines would become 0.01 RUB each. Neither mode is presented here as mandatory for real accounting.
| Input, RUB |
To 0.01, half-even |
To 0.01, half-up |
| 1.005 |
1.00 |
1.01 |
| -1.005 |
-1.00 |
-1.01 |
| 1.015 |
1.02 |
1.02 |
Double rounding also changes the result: rounding 1.2451 half-even first to 0.001 and then to 0.01 gives 1.24; rounding directly to 0.01 gives 1.25. Intermediate rounding must be part of the contract, rather than an accidental effect of writing to a temporary table.
Representation and intermediate calculations#
Integer minor units are convenient for adding finalized journal amounts. Fractions need fractional intermediate values; check the range of both individual amounts and aggregates. SQLite's integer sum() can fail with overflow, while total() returns floating point. Switching functions to bypass the error violates the precision contract. SQLite documentation.
In PostgreSQL 18, writing to numeric(p,s) rounds to the declared scale and checks the range. numeric rounds ties away from zero; align this with the application policy. Binary real and double precision are inexact. PostgreSQL 18 documentation. This describes database behavior, rather than a PostgreSQL test execution.
In Python 3.14, pass strings to Decimal, avoiding an intermediate float. The context sets working precision, while quantize and the chosen mode determine the final scale. The Inexact signal detects the loss of nonzero digits. Python documentation.
Choose working precision from maximum inputs and the operation chain, rather than a universal number. Division, for example, may have no finite decimal result. In the proposed process, unexpected Inexact before the permitted rounding stage stops the calculation; an expected signal at the final stage is recorded in the control result. An exact type cannot restore digits already lost by the source.
Allocation remainders and controls#
Suppose 0.01 RUB must be split equally among three lines. Independently rounding the shares to kopecks gives three zeros. If the contract requires preserving the total, allocate integer minor units: first assign the whole parts of the shares, then allocate the remainder by descending fractional part, breaking ties by a stable line key. One line receives a kopeck and the other two receive zero. This is a proposed algorithm, rather than a standard requirement. Negative amounts need a separate sign rule; one option is to allocate the absolute amount and then restore its sign.
Sequence pseudocode, independent of any database:
Preserve source values and the contract version.
Validate currency, unit, finiteness, and permitted range.
Calculate at the chosen working scale without hidden rounding.
Round only at the agreed grain.
Apply the approved remainder rule where required.
Save the result, difference, and adjustment lineage.
Reconcile lines and documents in each currency and time snapshot.
A rounding control retains the unrounded calculation, result, and difference result - unrounded, policy identifier, and reason for permitted loss. For allocations, verify that the shares sum exactly to the target amount. For journal entries, check document balance by currency and ledger, and check set completeness separately. An overall zero does not exclude two offsetting losses.
Use a tolerance only where the contract permits it, with an explicit unit and scope. It must not conceal missing entries. Before acceptance, cover unknown scale, mixed currencies, negative ties, double rounding, overflow, nonnumeric values, repeated calculations, and policy changes. Define an expected result or rejection for each scenario in advance.
Related topics and questions#
For grain fundamentals, see journal entries and documents; for aggregation, see financial SQL; for completeness, see reconciliation. The lab uses integer RUB kopecks and does not implement the fractional calculation policy described here.
- Which result must match the source: the sum of rounded lines or the rounded document total?
- How can you show that changing working precision has not changed the final result?
- Which data and rule versions are needed to reproduce a report a year later?