Касса, платежи и взаиморасчёты
Учёт реального движения денег: расчётные счета и кассы, реестр платежей, частичные оплаты документов и взаиморасчёты с контрагентами. Это первый (фундаментальный) блок АРМ бухгалтера и юриста.
Реализовано: модуль
internal/modules/payments, миграции0109(схема) и0110(RBAC).
Зачем
До этого блока «оплата» в системе была лишь статусом документа (paid): не было записей о деньгах, частичных оплатах, остатках по счетам и взаиморасчётов с контрагентами. Теперь бухгалтер ведёт реальный денежный контур.
Сущности
Счёт / касса (Account)
Расчётный счёт банка или наличная касса организации.
| Поле | Описание |
|---|---|
name | Название («Основной р/с», «Касса») |
kind | bank или cash |
company_bank_account_id | Необязательная связь с банковским счётом компании |
currency | Валюта (по умолчанию RUB) |
opening_balance | Начальный остаток (точные деньги, см. money) |
is_active | Активен ли счёт |
is_payroll_default | «Счёт для зарплаты»: на него встают выплаты по табелям. Не больше одного — PATCH {is_payroll_default: true} снимает признак с прежнего счёта |
Остаток считается как opening_balance + Σ(incoming) − Σ(outgoing) по платежам в статусе completed.
Счёт правится (PATCH /api/v1/accounts/:id): название, связь с банковским счётом, начальный остаток. Вид и валюта не меняются — по счёту уже могут быть платежи. is_active=false отключает счёт: новых платежей по нему не создать (400 payments_account_inactive), запланированные можно провести или отменить, история и остаток сохраняются, счёт можно включить обратно. Перед отключением интерфейс предупреждает об остатке и числе запланированных платежей; в форме нового платежа отключённых счетов нет, в фильтре реестра они подписаны «(отключён)».
Платёж (Payment)
Факт движения денег.
| Поле | Описание |
|---|---|
account_id | По какому счёту/кассе прошёл платёж |
direction | incoming (поступление) / outgoing (выплата) |
amount | Сумма (> 0) |
vat_amount | НДС в сумме платежа (опц., ≤ amount) |
date | Дата платежа (YYYY-MM-DD) |
counterparty_type | company / contact / employee / supplier / none (supplier ставит оплата по документу поставщика) |
counterparty_id | ID контрагента (обязателен, если тип не none) |
purpose | Назначение платежа |
method | bank / cash / card / offset (взаимозачёт) |
status | planned / completed / cancelled |
allocations | Разнесение на документы (частичные оплаты) |
supplier_document_id | Оплата по документу поставщика: только исходящий, документ согласован, сумма не больше остатка |
timesheet_id | Выплата по табелю: создаётся передачей табеля, проведение закрывает табель, отменить можно только возвратом табеля с причиной (409 payments_payroll_cancel_forbidden) |
task_id | Задача финансам «Выплатить зарплату» по выплате табеля: проведение её закрывает, отмена выплаты — отменяет |
counterparty_name | Имя сотрудника для выплаты сотруднику (только чтение) |
Статусы: planned → completed | cancelled. completed и cancelled — терминальные. В остаток счёта и взаиморасчёты попадают только completed-платежи.
Разнесение (Allocation)
Связь платежа с документом из модуля documents. Один счёт можно закрыть несколькими платежами; сумма разнесений по платежу не может превышать его amount. Аванс = сумма платежа сверх разнесённого.
Поступление разносится на счёт прямо из формы платежа: необязательные проект, документ проекта и сумма разнесения (по умолчанию — вся сумма платежа). Для поступления поля выплаты (обязательство, статья затрат) не проверяются. В списке — только выставленные неоплаченные счета проекта.
Счёт закрывается сам. Когда проведённые платежи (при проведении или при создании сразу проведённым) разнесли на выставленный счёт не меньше его суммы, счёт переходит в «Оплачен» обычным путём модуля documents — там же фиксируется выручка проекта. Оплачена часть — счёт «Частично оплачен» (partially_paid), выручка проекта — оплаченная часть и растёт с каждым платежом. Выручка пишется по документу идемпотентно (finance.UpsertRevenue: сумма, а не прибавка), поэтому повтор и ручная отметка «Оплачен» её не удваивают. «Частично оплачен» вручную не ставится — только платежами; остаток можно отметить «Оплачен» вручную. Счета, оплаченные до этих правил, при старте сервера проходит backfillPaidInvoices («Выставлен» и «Частично оплачен» с разнесёнными оплатами).
Проведённый платёж — дальше по цепочке. После проведения (payments.PaymentObserver): документ поставщика пересчитывает оплату, статус и факт проекта; обязательство, оплаченное проведёнными платежами целиком, получает дату факта = дату платежа (постоянный платёж заводит следующий период). Сбой этих реакций платёж не отменяет — пишется в журнал, пересчёт идемпотентен.
Плитка «Проведено» в реестре считает только проведённые платежи (поступления минус выплаты); запланированные выплаты — в плитке «К оплате».
Проведение платежа (planned → completed) в интерфейсе требует подтверждения: проведённый платёж меняет остаток счёта и больше не отменяется.
Взаиморасчёты и акт сверки
Для контрагента-компании система сводит:
- Выставлено (
invoiced) — сумма биллинговых документов (invoice,act), кроме черновиков и отменённых. - Оплачено (
paid) — сумма разнесений поcompleted-платежам. - Баланс (
balance) = выставлено − оплачено. Положительный — контрагент должен нам (дебиторка), отрицательный — переплата/аванс.
Акт сверки (reconciliation) детализирует баланс построчно по каждому документу: его сумма, оплачено, остаток (outstanding).
Документы привязаны к компании (documents.company_id), поэтому взаиморасчёты считаются только для counterparty_type=company.
API
Все списки — в каноническом конверте {items, meta} с заголовком X-List-Contract-Version.
| Метод | Путь | Право | Описание |
|---|---|---|---|
GET | /api/v1/accounts | accounting.view | Список счетов/касс (?active=true — только активные) |
POST | /api/v1/accounts | accounting.manage | Создать счёт/кассу |
PATCH | /api/v1/accounts/:id | accounting.manage | Изменить или отключить (is_active) счёт/кассу |
GET | /api/v1/accounts/:id | accounting.view | Счёт |
GET | /api/v1/accounts/:id/balance | accounting.view | Остаток по счёту |
GET | /api/v1/payments | accounting.view | Реестр платежей (фильтры ниже; kind=payroll — выплаты по табелям) |
POST | /api/v1/payments | accounting.manage | Создать платёж (+ разнесения) |
GET | /api/v1/payments/:id | accounting.view | Платёж с разнесениями |
PATCH | /api/v1/payments/:id/status | accounting.manage | Изменить статус; при проведении — account_id счёта |
POST | /api/v1/payments/complete | accounting.manage | Провести несколько запланированных платежей: {payment_ids, account_id?}, всё или ничего |
GET | /api/v1/counterparties/:type/:id/balance | accounting.view | Взаиморасчёты |
GET | /api/v1/counterparties/:type/:id/reconciliation | accounting.view | Акт сверки |
GET | /api/v1/reports/cashflow | accounting.view | ДДС (движение денежных средств) |
GET | /api/v1/reports/receivables | accounting.view | Сводная дебиторка/авансы |
GET | /api/v1/reports/vat | accounting.view | НДС-регистр (исходящий/входящий) |
Фильтры реестра платежей: account_id, direction, status, counterparty_type, counterparty_id, date_from, date_to (YYYY-MM-DD), limit, offset.
Пример создания платежа
POST /api/v1/payments
{
"account_id": 1,
"direction": "incoming",
"amount": 50000.0,
"vat_amount": 9016.39,
"date": "2026-06-15",
"counterparty_type": "company",
"counterparty_id": 7,
"purpose": "Оплата по счёту №1",
"method": "bank",
"status": "completed",
"allocations": [
{ "document_id": 101, "amount": 50000.0 }
]
}Денежные поля сериализуются как JSON-числа (контракт number), внутри хранятся точным десятичным типом money.Money и колонками NUMERIC(14,2).
Отчётность (ДДС и дебиторка)
Поверх реестра платежей строятся два org-wide отчёта (право accounting.view, суммы — точные money.Money):
- ДДС (
GET /api/v1/reports/cashflow?from=&to=&account_id=) — движение денежных средств по месяцам: приход, расход и сальдо (net) поcompleted-платежам, плюс итоги за период. Фильтры: счёт и диапазон дат (YYYY-MM-DD). - Дебиторка/авансы (
GET /api/v1/reports/receivables) — сводный баланс по каждому контрагенту-компании:invoiced − paid. Положительный баланс — дебиторка (нам должны), отрицательный — полученный аванс. Итоги:total_receivableиtotal_advances.
Полная кредиторка (наши обязательства перед поставщиками) появится вместе с учётом входящих счетов в последующих фазах; сейчас «кому должны мы» отражается только как полученные авансы.
НДС-регистр
GET /api/v1/reports/vat?from=&to= агрегирует НДС по месяцам из completed-платежей с указанным vat_amount:
- Исходящий НДС (
output_vat) — с поступлений (incoming), НДС с реализации; - Входящий НДС (
input_vat) — с выплат (outgoing), к вычету; - К уплате (
net = output − input) по периодам и итогом (net_payable).
Сводный P&L по организации
Сводный отчёт о прибылях и убытках по всей организации отдаёт существующий эндпоинт GET /api/v1/reports/financial_overview (без фильтров — по всем проектам): плановая/фактическая выручка, затраты и прибыль. Фонд оплаты труда (payroll) будет включён в P&L после интеграции начислений в finance.
Доступ (RBAC)
Новая роль accountant и права accounting.view / accounting.manage (seeded в migrations/0110). admin обходит проверку, director видит данные через system.global_read. Подробнее — rbac_matrix.md.
Что дальше
Следующие блоки АРМ опираются на этот контур:
- ДДС / дебиторка-кредиторка — отчётность поверх реестра платежей.
- Зарплата — переданный табель создаёт запланированный
outgoing-платёж сотруднику (counterparty_type=employee,timesheet_id), его проведение закрывает табель. - Авто-смена статуса документа — сделана: «Оплачен» / «Частично оплачен» по разнесённым проведённым платежам.
Дорожная карта целиком — в АРМ бухгалтера и юриста.
Ворота ТЗ — только для расходов. Поступление от клиента принимается по проекту и без утверждённого ТЗ: блокировать приход денег смысла нет (payments_spec_required проверяется только для direction=outgoing, в том числе при оплате по обязательству — по проекту обязательства). Форма выплаты не отправляет то, что сервер отклонит: кнопка «Добавить платёж» выключена с причиной. Охват «свой» (руководитель дивизиона, менеджер) разносит поступление только на счёт своего проекта — в форме проект и документ для него обязательны.