Перейти к основному содержимому

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 это равенство ломает.

Рассмотренные варианты​

  1. Уложить ручные операции в существующий AccountingCategory. Плюс: тип category остаётся перечислением. Минус, закрывающий вопрос: перечень выводится из типов начислений WB и закрыт по построению — «аренду склада» пришлось бы класть в «прочие удержания», теряя смысл статьи.

  2. Свободная категория + source у строки реестра.

  3. Включить ручные операции в сверку. Отвергнут: оцифровка про эти деньги ничего не знает, и balanced стал бы всегда ложным у любого продавца, который ведёт учёт закупок. Сверка перестала бы что-либо значить.

  4. Отдельный эндпоинт «реестр с ручными операциями». Отвергнут: два реестра с разными итогами — гарантированный вопрос «а какой из них правильный».

Решение​

Выбраны вариант 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, справочник статей продавца.