Распределение затрат: пул, драйвер и сохранение суммы
Как закрепить пул затрат и версию драйвера, обработать нулевую базу и отрицательную сумму и получить воспроизводимые доли получателей.
7 мин чтения·База знаний
Справочный материал. Контракты данных, архитектурные решения и ограничения применения. Источники приведены в материалах, где используются внешние основания.
Нужно распределить общий пул затрат между центрами затрат в управленческой витрине, сохранив исходную сумму и объяснив долю каждого получателя. Распределение создаёт производный факт, а не меняет исходные проводки. Рассматривается один этап с неотрицательной базой драйвера; взаимные услуги, циклические распределения, валютная конвертация и формирование бухгалтерских проводок исключены.
Это предлагаемый проектный контракт со статусом knowledge. Выбор драйвера и допустимость распределения утверждаются владельцем финансовой методологии. Алгоритм не является требованием IFRS, не описывает опыт автора и не реализован в лаборатории. Денежная точность объясняет масштаб и округление, а отображение счетов - классификацию без весов; здесь главное - состав пула, база получателей и воспроизводимость распределения.
Зерно пула - одна сумма по юридическому лицу, регистру, периоду, категории затрат и валюте в выбранной версии входных данных. pool_id идентифицирует эту группу, а pool_version_id - её неизменяемый снимок. Каждая выбранная исходная строка должна иметь определённую принадлежность пулу; случайное включение одной строки дважды запрещено. Сохраняются ссылки на версии исходных строк и правило отбора.
Сумма P знаковая: в этом контракте затраты положительны, их уменьшение отрицательно. Единица - целая копейка RUB с масштабом 2 только для синтетического примера. Другие валюты и масштабы образуют отдельные пулы. Смешивать валюты или распределять сумму после неявной конвертации нельзя.
Зерно драйвера - один получатель внутри pool_version_id и driver_snapshot_id. Ключ получателя включает юридическое лицо и стабильный идентификатор центра затрат. Драйвером могут быть часы обслуживания, площадь или количество операций, но все веса одного пула измеряются в одной закреплённой единице. Десятичные веса переводят точно в целые единицы общего масштаба, например часы в сотые часа; отрицательные, отсутствующие и нечисловые веса отклоняют. Нулевой вес допустим и даёт нулевую долю.
business_date исходных строк определяет период затрат; observed_at и cutoff определяют доступные версии. Отдельно фиксируют период измерения драйвера, cutoff его снимка, версию методики и состав допустимых получателей. Время запуска не заменяет эти параметры. Новая версия драйвера создаёт новый расчёт, а не переписывает предыдущий выпуск.
Сначала отбирают версии исходных строк и агрегируют сумму до зерна пула. Драйвер независимо приводят к одной строке на получателя. Затем проверяют покрытие: каждый ожидаемый получатель присутствует ровно один раз; лишние и отсутствующие ключи требуют решения до расчёта. Соединение детальных проводок с детальными измерениями драйвера до агрегации способно размножить обе меры.
Идентичность расчёта задаёт набор (pool_version_id, driver_snapshot_id, policy_version_id). Выходное зерно - этот набор плюс ключ получателя. run_id обозначает попытку исполнения, но не новую экономическую версию результата. В витрину входит только один выбранный успешный выпуск для заданного контекста отчёта.
При сумме весов W = 0 распределение не определено, даже если P = 0. В предлагаемом контракте расчёт блокируется и сохраняется диагностический статус; его нельзя выдавать за успешный результат из нулей. Альтернативы - оставить сумму в отдельном нераспределённом пуле или применить резервный драйвер - требуют явной версии политики и отдельного контроля. Отсутствующий вес нельзя автоматически подменять нулём.
При W > 0 распределяют модуль A = abs(P). Для каждого целого веса w_i вычисляют q_i = floor(A * w_i / W) и остаток r_i = A * w_i - q_i * W. Затем R = A - sum(q_i) единиц добавляют первым R получателям по убыванию r_i. При равенстве используют возрастающий стабильный ключ с закреплённым порядком сравнения, независимым от локали. Знак P возвращают после распределения модуля.
Псевдокод, не исполняемый SQL или Python:
Validate immutable inputs, unique recipient keys and nonnegative integer weights.
W := sum(weights); if W = 0: stop with a diagnostic status.
A := abs(P)
For each recipient i: (q_i, r_i) := integer quotient and remainder of A*w_i by W.
R := A - sum(q_i)
Sort recipients by remainder descending, then stable key ascending.
Add one unit to q_i for each of the first R recipients.
Restore the sign of P; retain zero shares and the allocation audit fields.
Check all invariants; publish the complete result as one selected release.
Так как 0 <= r_i < W, получаем 0 <= R < n, где n - число получателей. Если остаток есть, положительных остатков достаточно для его распределения; нулевой вес не получает добавочную единицу. Каждая доля отличается от точной пропорции менее чем на одну минимальную единицу, а сумма долей точно равна P. Это свойства данного алгоритма, а не допуск финансового стандарта.
Операции частного и остатка описаны в документации Python 3.14. В реализации нужно исключить переполнение произведения A*w_i. В PostgreSQL 18 точный numeric отличается от неточных floating point типов; запись в numeric(p,s) может округлить значение к масштабу столбца. Документация PostgreSQL 18. Она подтверждает свойства типов, но не проверку этого алгоритма на PostgreSQL. Здесь приведён только псевдокод; исполнительных тестов распределения не заявлено.
Пул равен 100 копейкам RUB, получатели A, B, C имеют веса 1, 1, 1. Ключи сравниваются в порядке A < B < C.
Получатель
Целая доля q
Остаток r при W = 3
Добавка
Итог, копейки
A
33
1
1
34
B
33
1
0
33
C
33
1
0
33
При P = -100 те же веса дают -34, -33, -33; при P = 0 и положительном W все доли нулевые. Если веса меняются на 1, 2, 0, доли становятся 33, 67, 0: большую дробную часть получает B. Это заданные учебные числа, а не реальные данные.
Независимое округление долей может нарушить сумму пула. Отнесение всего остатка на заранее выбранного получателя проще, но способно сильнее отклонить его долю от пропорции. Метод наибольших остатков требует сортировки и стабильного правила равенства; один ключ может регулярно получать лишнюю единицу. Ротация приоритетов возможна только как другая версионируемая политика. При изменении суммы пула на единицу доля отдельного получателя не обязана возрастать, поэтому алгоритм не обещает монотонности.
Для каждой версии пула сохраняют исходную сумму, список получателей, веса, W, q_i, r_i, добавку, итог и версии правил. Контроль включает:
точное равенство sum(allocated_minor) = P отдельно по пулу, валюте и выпуску;
совпадение набора выходных ключей с закреплённым набором получателей, включая нулевые доли;
уникальность выходного зерна и отсутствие отрицательного веса;
нулевую долю при нулевом весе и соответствие знака ненулевой доли знаку пула;
тот же результат при перестановке входных строк и повторе с теми же версиями;
отказ при пустом наборе, нулевом знаменателе, дубликате ключа, неизвестной валюте или выходе за диапазон реализации.
Одного равенства сумм недостаточно: потерянный получатель может компенсироваться завышенной долей другого. Исходный пул и производные доли нельзя складывать в одном показателе затрат. При аналитическом переносе между центрами отдельно определяют снятие суммы с исходного центра; эта статья не задаёт проводки такого переноса.
Поздняя корректировка источника или драйвера создаёт новый снимок зависимых пулов и новый выпуск. Старый выпуск остаётся воспроизводимым. Частичный результат после сбоя не выбирают для отчёта; повтор той же идентичности расчёта не должен создавать двойные доли. Для закрытого периода возможность замены выпуска определяется политикой закрытия, а не алгоритмом распределения.
Для происхождения долей нужны lineage показателя и версии входов; для выбора выпуска после cutoff - закрытие периода. Эта модель дополняет денежную точность и классификацию счетов, но не заменяет их контракты.
Какие поля нужны, чтобы повторить распределение после изменения драйвера?
Почему сохранение суммы не доказывает правильность состава получателей?
Как отличить повтор запуска от нового экономического выпуска расчёта?
Allocate a shared cost pool to cost centers in a management reporting mart while preserving the source amount and explaining each recipient's share. Allocation creates a derived fact rather than modifying source journal entries. This article covers one stage with a nonnegative driver base; reciprocal services, cyclic allocations, currency conversion, and accounting entry generation are outside its scope.
This is a proposed project contract with knowledge status. The financial methodology owner approves the driver and whether allocation is appropriate. The algorithm is not an IFRS requirement, does not describe the author's experience, and is not implemented in the lab. Monetary precision covers scale and rounding, while account mapping covers classification without weights; the focus here is pool membership, the recipient base, and reproducible allocation.
The pool grain is one amount by legal entity, ledger, period, cost category, and currency in a selected input-data version. pool_id identifies that group; pool_version_id identifies its immutable snapshot. Each selected source line must have a defined pool membership; accidentally including a line twice is prohibited. Retain references to source-line versions and the selection rule.
Amount P is signed: under this contract, costs are positive and cost reductions are negative. The unit is an integer RUB kopeck with scale 2 solely for the synthetic example. Other currencies and scales form separate pools. Do not mix currencies or allocate an amount following implicit conversion.
The driver grain is one recipient within pool_version_id and driver_snapshot_id. The recipient key includes the legal entity and a stable cost-center identifier. Drivers can be service hours, floor area, or transaction counts, but all weights in a pool use one fixed unit. Convert decimal weights exactly to integers at a common scale, such as hundredths of an hour; reject negative, missing, and nonnumeric weights. A zero weight is allowed and receives a zero share.
The source lines' business_date determines the cost period; observed_at and the cutoff determine available versions. Separately fix the driver measurement period, snapshot cutoff, methodology version, and eligible recipient set. Execution time cannot replace these parameters. A new driver version creates a new calculation rather than overwriting a previous release.
First select source-line versions and aggregate the amount to pool grain. Independently reduce driver data to one row per recipient. Then check coverage: each expected recipient must appear exactly once; unexpected and missing keys require resolution before calculation. Joining detailed journal lines to detailed driver observations before aggregation can multiply both measures.
Calculation identity is the tuple (pool_version_id, driver_snapshot_id, policy_version_id). Output grain is that tuple plus the recipient key. run_id identifies an execution attempt rather than a new economic version of the result. Only one selected successful release enters the mart for a given reporting context.
When total weight W = 0, allocation is undefined even if P = 0. This contract blocks the calculation and retains a diagnostic status; a collection of zeros must not be presented as a successful result. Alternatives, such as retaining the amount in a separate unallocated pool or using a fallback driver, require an explicit policy version and separate controls. A missing weight must not automatically become zero.
For W > 0, allocate the magnitude A = abs(P). For each integer weight w_i, calculate q_i = floor(A * w_i / W) and remainder r_i = A * w_i - q_i * W. Add the remaining R = A - sum(q_i) units to the first R recipients ordered by descending r_i. Break ties by ascending stable key using a fixed comparison order independent of locale. Restore the sign of P after allocating its magnitude.
Pseudocode, not executable SQL or Python:
Validate immutable inputs, unique recipient keys and nonnegative integer weights.
W := sum(weights); if W = 0: stop with a diagnostic status.
A := abs(P)
For each recipient i: (q_i, r_i) := integer quotient and remainder of A*w_i by W.
R := A - sum(q_i)
Sort recipients by remainder descending, then stable key ascending.
Add one unit to q_i for each of the first R recipients.
Restore the sign of P; retain zero shares and the allocation audit fields.
Check all invariants; publish the complete result as one selected release.
Since 0 <= r_i < W, we have 0 <= R < n, where n is the recipient count. When units remain, there are enough positive remainders to assign them; a zero weight never receives an extra unit. Each share differs from its exact proportion by less than one minor unit, and the shares sum exactly to P. These are properties of this algorithm rather than a financial standard's tolerance.
Quotient and remainder operations are described in the Python 3.14 documentation. An implementation must prevent overflow in A*w_i. In PostgreSQL 18, exact numeric differs from inexact floating point types; writing to numeric(p,s) may round a value to the column's scale. PostgreSQL 18 documentation. This documents type behavior rather than testing this algorithm on PostgreSQL. Only pseudocode is provided here; no executed allocation tests are claimed.
The pool contains 100 RUB kopecks, and recipients A, B, C have weights 1, 1, 1. Keys are compared in the order A < B < C.
Recipient
Whole share q
Remainder r with W = 3
Extra unit
Result, kopecks
A
33
1
1
34
B
33
1
0
33
C
33
1
0
33
For P = -100, the same weights give -34, -33, -33; for P = 0 and positive W, all shares are zero. Changing weights to 1, 2, 0 produces shares of 33, 67, 0: B receives the larger fractional remainder. These are defined educational values rather than real data.
Rounding shares independently may violate the pool total. Assigning the entire remainder to a predetermined recipient is simpler but can move that share further from its proportion. The largest-remainder method requires sorting and a stable tie rule; one key may repeatedly receive an extra unit. Rotating priorities is possible only as a different versioned policy. Increasing the pool by one unit does not necessarily increase an individual recipient's share, so the algorithm does not promise monotonicity.
For each pool version, retain the source amount, recipient list, weights, W, q_i, r_i, extra unit, final share, and rule versions. Controls include:
exact equality sum(allocated_minor) = P separately by pool, currency, and release;
equality of output keys and the fixed recipient set, including zero shares;
unique output grain and no negative weights;
a zero share for a zero weight and matching signs for every nonzero share and the pool;
identical results after reordering input rows or rerunning the same versions;
rejection of an empty set, zero denominator, duplicate key, unknown currency, or values outside the implementation's range.
Equal totals alone are insufficient: a missing recipient can be offset by an overstated share for another. Do not sum the source pool and its derived shares in the same cost metric. For an analytical transfer between cost centers, separately define removal of the amount from the source center; this article does not specify accounting entries for that transfer.
A late source or driver correction creates a new snapshot of dependent pools and a new release. The old release remains reproducible. Do not select partial results after a failure for reporting; repeating the same calculation identity must not create duplicate shares. For a closed period, the closing policy determines whether a release can be replaced, rather than the allocation algorithm.
Explaining shares requires metric lineage and input versions; selecting a release after a cutoff requires period-close rules. This model complements monetary precision and account classification without replacing their contracts.
Which fields are needed to reproduce allocation after the driver changes?
Why does preserving the total fail to prove that the recipient set is correct?
How can you distinguish an execution retry from a new economic release of the calculation?