0056. Ручные операции в бухгалтерии и их место в сверке
Копия ADR из репозитория:
backend/docs/adr/0056-manual-accounting-transactions.md. Канонический текст — в репозитории; портал обновляется командойnpm run sync:adr.
- Статус: accepted
- Дата: 2026-09-18
- Связанные задачи: OP-224 (5.7); опирается на ADR-0039 (реестр доходов/расходов), ADR-0018 (P&L), OP-151/155
Контекст и проблема
Бухгалтерия была чистой проекцией отчёта WB о реализации (ADR-0039): собственного хранилища у модуля не было, реестр вычислялся на чтение. Из этого следовало ограничение, которое не лечится настройками: закупка товара, зарплата, реклама вне WB в реестр не попадали никак.
Модуль отвечал на вопрос «сколько удержал Wildberries», хотя продавцу нужен ответ на «сколько я заработал».
Осложняющее обстоятельство: /accounting/reports/reconciliation сверяет сальдо
реестра с прибылью оцифровки, и равенство net === Σ profit — не совпадение, а
свойство построения. Любая строка не из отчёта WB это равенство ломает.
Рассмотренные варианты
-
Уложить ручные операции в существующий
AccountingCategory. Плюс: типcategoryостаётся перечислением. Минус, закрывающий вопрос: перечень выводится из типов начислений WB и закрыт по построению — «аренду склада» пришлось бы класть в «прочие удержания», теряя смысл статьи. -
Свободная категория +
sourceу строки реестра. -
Включить ручные операции в сверку. Отвергнут: оцифровка про эти деньги ничего не знает, и
balancedстал бы всегда ложным у любого продавца, который ведёт учёт закупок. Сверка перестала бы что-либо значить. -
Отдельный эндпоинт «реестр с ручными операциями». Отвергнут: два реестра с разными итогами — гарантированный вопрос «а какой из них правильный».
Решение
Выбраны вариант 2 и — для сверки — разделение источников.
Своя таблица accounting_transactions — первое собственное хранилище
модуля. category — свободная строка, source различает происхождение строки
реестра (wb / manual). amount хранится положительным, знак задаёт type:
сумма со знаком плюс отдельный вид операции дали бы два независимых способа
сделать расход доходом.
Ручные операции сворачиваются по паре «статья + вид». Реестр — отчёт по статьям, а не журнал операций: десять чеков «Закупка товара» за месяц должны стать одной строкой. WB-статьи сохраняют канонический порядок, ручные дописываются следом по алфавиту — порядок не зависит от порядка ввода.
Сверка считает только WB-часть реестра. manualNet отдаётся отдельным полем
и в /ledger, и в /reconciliation — ровно на эту величину реестр расходится с
P&L, и без неё расхождение выглядело бы как ошибка категоризации.
Операции аудируются (F6): это ручное вмешательство в финансовую отчётность, и вопрос «кто внёс этот расход» обязан иметь ответ.
CSV получил защиту от formula-injection. До OP-224 её не было обоснованно —
в выгрузку попадали только статические метки и числа. Свободный текст продавца
сделал ячейку вида =HYPERLINK(...) достижимой; числа из защиты исключены явно,
чтобы ведущий - отрицательной суммы не превратился в апостроф.
Последствия
-
Положительные:
- реестр отвечает на вопрос продавца, а не только на вопрос «что удержал WB»;
- сверка сохранила смысл: она по-прежнему ловит ошибку категоризации WB-данных, а не шумит на каждой закупке;
- источник строки виден и в API, и в выгрузке.
-
Отрицательные / компромиссы:
- ломающее изменение контракта:
LedgerEntryDto.categoryперестал быть перечислением, появилисьsourceиmanualNet, в CSV — новая колонка. Фронту нужно расширить типы; - свободная категория означает разнобой в написании («Закупка» и «закупка» — две статьи). Справочник статей продавца — возможное развитие;
- ручные операции пока только создаются: правка и удаление — отдельная задача, и до неё исправление ошибки ввода возможно только через БД.
- ломающее изменение контракта:
-
Дальше: редактирование/удаление операций, фильтруемый список
GET /accounting/transactions, справочник статей продавца.