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

Раздел «Приём платежей»

Бэк-офисное рабочее место бухгалтера/менеджера: список всех непогашенных документов корпорации, по которым нужно получить оплату от контрагентов, — расходные накладные (Invoice.type = OUTGOING) и акты оказания услуг (ServiceAct). Приходные накладные (Invoice.type = INCOMING — наш долг поставщикам) в этот список не попадают: они обслуживаются отдельным сценарием «Оплата поставщикам». Из этого экрана оператор:

  • видит, кто и сколько должен корпорации, по каким документам, в каком статусе;
  • принимает оплату по одному или нескольким документам сразу (в т.ч. от разных контрагентов одним батчем);
  • печатает приходные кассовые ордера (ПКО);
  • открывает карточку контрагента/документа для деталей;
  • выгружает выборку в Excel.

Связанная веха: LOCALIOFFICE-968 — Ревизия accounting.


Customer Journey Map

┌──────────────────────────────────────────────┐
│ Список «Приём платежей» │
│ GET /accounting/payable-documents │
│ (фильтры: период, контрагент, тип, статус, │
│ поиск, сортировка, пагинация) │
└─┬──────────────────────┬─────────────┬───────┘
│ │ │
клик по строке (документ) клик по имени «Download»
│ контрагента в строке │
▼ ▼ ▼
Карточка документа Модалка GET /accounting/
(вне этого раздела — «Персональная payable-documents/export
Invoice/ServiceAct) карточка» (Excel)
GET /counterparties/:id
┌──────────────────────────────────────────────┐
│ чекбоксы на строках + «Принять оплату» │
│ │
▼ │
┌──────────────────────────────────────┐ │
│ Модалка «Приём оплаты» │ │
│ (отметить документы, выбрать │ │
│ счета, дату, ДДС, флаг ПКО) │ │
│ │ │
│ ┌───── «+» возле счёта ─────┐ │ │
│ ▼ │ │ │
│ Модалка «Новый счёт» │ │ │
│ POST /accounting/accounts │ │ │
│ (после создания — новый счёт │ │ │
│ доступен в селекторе) │ │ │
│ ─────────────────────────────── │ │
│ │ │
│ Кнопка «Оплатить» │ │
│ POST /accounting/payments/batch │ │
│ → { payments[], paymentReceipts[] } │ │
└────────────────────┬─────────────────┘ │
│ │
для каждого ПКО (если был флаг printPkoPerCounterparty) │
▼ │
GET /accounting/payment-receipts/:id/pdf │
(StreamableFile, PDF) │

Подкомпонент в модалке «Новый счёт» — quick-add внутри потока приёма оплаты. Та же модалка вызывается из основного раздела «План счетов», но описана здесь, потому что именно тут она триггерится частее.


Эндпоинты раздела

Основные

МетодМаршрутНазначениеКонтроллер
GET/accounting/payable-documentsСписок документов к оплате с фильтрами и итогами по выборкеpayable-document.controller.ts:83
GET/accounting/payable-documents/exportЭкспорт текущей выборки в Excelpayable-document.controller.ts:32
POST/accounting/payments/batchСоздание batch-платежа (один или несколько документов, опционально ПКО)payment.controller.ts:74
GET/accounting/payment-receipts/:id/pdfPDF приходного кассового ордераpayment-receipt.controller.ts:53
POST/accounting/payments/:id/cancelОтменить ранее проведённый платёжpayment.controller.ts:92

Селекторы и справочники

МетодМаршрутИспользуется для
GET/organization/counterpartiesСелектор «Контрагент» в шапке (мультивыбор)
GET/organization/counterparties/:idОткрытие модалки «Персональная карточка»
GET/organization/counterparties/search-by-cardСабмодалка «Считайте штрих-код» (поиск по карте)
GET/organization/subdivisionsСелекторы подразделения/склада
GET/organization/legal-entitiesСелектор юр.лица корпорации
GET/organization/conceptsСелектор концепции
GET/organization/price-categoriesСелектор ценовой категории
GET/accounting/accountsСелекторы счетов (дебет/кредит/ДДС/счёт выручки)
POST/accounting/accountsQuick-add нового счёта из модалки «Новый счёт»
GET/staffing/employeesСелектор «Ответственные лица» в модалке «Новый счёт» (⚠️ см. LOCALIOFFICE-976)

Таблица «Приём платежей»

Параметры запроса GET /accounting/payable-documents

Breaking changes LOCALIOFFICE-969:

  • параметр counterpartyId (одиночный UUID) удалён, используйте counterpartyIds[] (мультивыбор);
  • старые вызовы с counterpartyId=… молча игнорируют фильтр (благодаря глобальному whitelist: true в ValidationPipe), фронту необходима миграция;
  • аналогично появился subdivisionIds[] вместо одиночного фильтра подразделения (раньше его не было — не breaking, но семантика мультивыбора зафиксирована).

Breaking changes LOCALIOFFICE-969 для GET /organization/counterparties:

  • ответ изменился с массива CounterpartyResponseDto[] на envelope { items, total, page, limit };
  • параметр type (singular) удалён, используйте types[] (мультивыбор).
ПараметрТипНазначение
dateFrom, dateToISO dateФильтр периода по documentDate
counterpartyIdsUUID[]Мультивыбор контрагентов. Передаётся как повторяемый параметр (?counterpartyIds=…&counterpartyIds=…) или через запятую. До 100 значений.
subdivisionIdsUUID[]Мультивыбор подразделения/склада. При заданном фильтре акты услуг исключаются (у них нет склада).
revenueAccountIdsUUID[]Мультивыбор счёта выручки. Для накладных — по счёту из автоматической проводки документа; для актов услуг — по полю revenueAccountId.
paymentStatusenum PaymentStatusUNPAID / PARTIALLY_PAID / PAID
documentTypeenum PayableDocumentTypeINVOICE / SERVICE_ACT
statusesPayableDocumentStatus[]Мультивыбор визуального статуса строки (PAID / OVERDUE / DUE_SOON / WITH_DEBT / UNPAID). Применяется после вычисления статуса — выборка соответствует тем же правилам, что и поле status в ответе.
searchстрокаПоиск по номеру документа или имени контрагента
showPaidbooleanПо умолчанию скрываем оплаченные
sortByстрокаdocumentDate / documentNumber / totalAmount / remainingAmount / counterpartyName / dueDate / paymentDate / subdivisionName / revenueAccountName / vatAmount / counterpartyDebt. null-значения уходят в конец независимо от направления.
sortOrderasc / descПо умолчанию desc
page, limitintПагинация

Маппинг колонок UI ↔ полей DTO

PayableDocumentDto (payable-document.mapper.ts:13):

Колонка UIПоле DTOПримечание
ДатаdocumentDateISO date
Дата оплатыpaymentDateДата последнего успешного (не отменённого) платежа или null, если оплат ещё не было
Срок оплатыdueDatenull допустим
№документаdocumentNumber
Тип документаdocumentTypeINVOICE / SERVICE_ACT
КонтрагентcounterpartyName (counterpartyId для drill-down)
Сумма, ₽totalAmountstring
Оплатить (чекбокс)Клиентский, не отправляем на бэк
Осталось, ₽remainingAmountstring
Оста. (долг по контрагенту)counterpartyDebtСуммарный остаток по всем непогашенным документам этого контрагента в корпорации (Invoice + ServiceAct). Дублируется в каждой строке.
СкладsubdivisionName (subdivisionId)У актов услуг — null
Счёт выручкиrevenueAccountName (revenueAccountId)
В т.ч НДСvatAmountstring
Комментарийcomment
(цвет строки)statusСм. секцию ниже

Итоги в подвале

В ответе списка: total, totalAmount, totalRemaining, page, limit. Итоги пересчитываются по тому же набору фильтров.

Поле «Оста.» в подвале (Кол-во: 187)

Клиентский счётчик отмеченных чекбоксами строк, на бэке не считается.


Семантика статусов и цветов

Поле status в PayableDocumentDto — визуальный статус строки. Вычисляется в payable-document.mapper.ts:74 с приоритетом: оплачено → просрочено → подходит срок → задолженность.

statusЦвет на UIУсловие
PAID🟢 зелёныйpaymentStatus === "PAID"
OVERDUE🔴 красныйесть dueDate и она в прошлом
DUE_SOON🟠 оранжевыйесть dueDate, до неё ≤ 7 дней
WITH_DEBT⬛ чёрныйpaymentStatus === "PARTIALLY_PAID", срок не просрочен
UNPAID⬛ чёрныйвсё остальное (нет платежей, нет срока)

Два значения (WITH_DEBT и UNPAID) маппятся в один и тот же чёрный цвет. Это осознанно: бэк сохраняет различение «частично оплачено» vs «не оплачивалось», UI рисует одинаково.


Селекторы раздела

Шапка таблицы

СелекторИсточникПараметры запроса
За период (Открытый/Сегодня/Неделя/Месяц/…)клиентский пресетфронт сам разворачивает в dateFrom / dateTo
Контрагент (мультивыбор)GET /organization/counterpartiestypes[], search (по подстроке имени, регистронезависимо), page/limit. Ответ — { items, total, page, limit }.
Поискстрока в запросеsearch
Показывать оплаченныефлаг в запросеshowPaid
Автообновлениеклиентскийполлинг, бэк не нужен

Внутри модалки «Приём оплаты»

СелекторИсточникФильтр
Датаdatepicker
Дебет счётGET /accounting/accounts?role=CASHСчета денежных средств (касса/банк)
Кредит счётGET /accounting/accounts?role=COUNTERPARTY_SETTLEMENTSРасчёты с контрагентами (60/62)
Статья ДДСGET /accounting/accounts?role=CASH_FLOW_ITEMСтатьи ДДС

Дефолтные значения трёх селекторов берутся из настроек корпорации (defaultPaymentDebitAccountId, defaultPaymentCreditAccountId, defaultPaymentDdsAccountId). После прогона сидера типового плана счетов они проставлены автоматически (Расчётный счёт / Расчёты с покупателями / ДДС «Поступления от продаж») и редактируются через PATCH /organization/corporation. Если поле = null, фронт показывает селектор без предзаполнения.

Внутри модалки «Новый счёт»

СелекторИсточник
ТипGET /accounting/account-types
Счёт (родитель)GET /accounting/accounts
Ответственные лицаGET /staffing/employees (⚠️ интегрируется задачей LOCALIOFFICE-976)

В ответе AccountType есть поле isSystemAccount: boolean. Системные типы (создаются сидером типового плана счетов) защищены на бэке: переименование и смена superType отдают 400, удаление — 400. Менять role у системного типа разрешено. По isSystemAccount=true фронт должен дизейблить кнопки «Переименовать» и «Удалить» в админке справочника типов. Аналогично у Account есть isSystemAccount — системные счета (Касса, 62 и др.) полностью защищены от редактирования и удаления.

Внутри модалки «Персональная карточка»

СелекторИсточник
Полenum CounterpartyGender (хардкод на фронте)
Тип поставщикаenum SupplierType (хардкод)
Ценовая категорияGET /organization/price-categories
ПодразделениеGET /organization/subdivisions
Юр лицо корпорацииGET /organization/legal-entities
КонцепцияGET /organization/concepts
Система EDIenum EdiSystem (хардкод)
Счёт ДДС (банк. выписка)GET /accounting/accounts?role=CASH_FLOW_ITEM
Корр. счёт (банк. выписка)GET /accounting/accounts?role=COUNTERPARTY_SETTLEMENTS
Действие при отклонении ценыenum PriceDeviationAction (хардкод)
День платежаenum PayWeekday (хардкод)

Enum’ы фронт берёт из Swagger-схемы, отдельных reference-эндпоинтов в проекте нет.


Модалка «Приём оплаты»

Триггер: кнопка «Принять оплату» на странице. Перед открытием UI собирает идентификаторы отмеченных в таблице документов.

Поля формы

Поле UIПоле DTO CreateBatchPaymentDto
ДатаpaymentDate (ISO date)
Дебет счётaccountId (UUID)
Кредит счётcreditAccountId (UUID)
Статья ДДСddsAccountId (UUID, опционально)
Таблица «№ + Сумма»items[]: { documentType, documentId, amount }
⚪ Распечатать ПКО для каждого контрагентаprintPkoPerCounterparty (boolean)

DTO: create-batch-payment.dto.ts.

Сценарий «Оплатить»

POST /accounting/payments/batch запускает всю бизнес-логику в одной транзакции (payment.service.ts:94):

  1. Валидация: непустой items, корректная дата, разные дебет/кредит, отсутствие дубликатов документов.
  2. Блокирующая дата корпорации проверяется.
  3. Загружаются дебет- и кредит-счета с проверкой принадлежности корпорации.
  4. По каждому документу позиции (с сортировкой по documentId для анти-deadlock):
    • SELECT ... FOR UPDATE на документ (защита от двойной оплаты),
    • валидация суммы (amount ≤ remainingAmount, не на полностью оплаченный, без переплаты),
    • создание Posting (проводка: дебет/кредит + аналитика контрагент + ДДС),
    • создание Payment со ссылкой на Posting,
    • обновление документа: paidAmount += amount, пересчёт paymentStatus.
  5. Если printPkoPerCounterparty=true: группировка платежей по (counterpartyId, legalEntityId), создание PaymentReceipt на каждую группу (сводный если в группе >1 платежа), генерация номера.

Смешанный batch (несколько контрагентов в одном запросе) поддерживается — на каждого контрагента создаётся отдельный ПКО.

Ответ

BatchPaymentResponseDto (batch-payment-response.dto.ts):

{
"payments": [ /* PaymentResponseDto[] — по одному на позицию */ ],
"paymentReceipts": [ /* PaymentReceiptResponseDto[] — только если был флаг */ ]
}

Фронт получает paymentReceipts[] и для каждого ПКО дёргает GET /accounting/payment-receipts/:id/pdf для отображения/печати.

Ошибки

Все валидационные ошибки — 400 BadRequest с понятным русским текстом. Дата платежа в заблокированном периоде — 400. Документ из другой корпорации — 400.


Модалка «Новый счёт» (quick-add)

Триггер: кнопка «+» возле любого из трёх селекторов счетов в модалке «Приём оплаты». Также открывается из основного раздела «План счетов» — поведение и DTO одинаковые.

Поля формы

Поле UIПоле DTO CreateAccountDtoПримечание
ТипtypeIdUUID, ссылка на AccountType
Названиеname
⬜ Является подсчётом + СчётparentIdUUID родительского счёта
Комментарийcomment
Код (0516)codeстрока
⬜ (галка справа от кода)autoGenerateCode⚠️ см. LOCALIOFFICE-976
Ответственные лицаresponsibleEmployeeIds[]⚠️ см. LOCALIOFFICE-976

DTO: create-account.dto.ts.

Поведение автогенерации (после задачи 976)

  • autoGenerateCode=true + parentId задан → бэк подбирает <код родителя>.<NN>, где NN — следующий свободный двузначный суффикс среди активных детей того же родителя.
  • autoGenerateCode=true без parentId → следующий свободный целочисленный код среди корневых счетов корпорации. typeId в подборе не участвует: уникальность кода — в рамках всей корпорации.
  • autoGenerateCode=true + code в теле → code игнорируется, возвращается сгенерированный.
  • autoGenerateCode=false (или нет флага) → code берётся как есть, валидируется уникальность.

Дубликат кода — 409 Conflict.

После создания

Новый счёт сразу доступен в селекторе, из которого был открыт. Фронт обновляет соответствующий список через повторный GET /accounting/accounts с тем же фильтром.


Модалка «Персональная карточка»

Триггер: клик по имени контрагента в строке таблицы. Открывает существующую карточку контрагента (Counterparty) — ту же, что и из раздела «Контрагенты».

Вкладки и поля

DTO: create-counterparty.dto.ts, UpdateCounterpartyDto = PartialType(CreateCounterpartyDto).

Основные сведения

Поле UIПоле DTO
Имя в системеname
Фамилия / Имя / ОтчествоlastName / firstName / middleName
Компанияcompany
Телефон / Доп. телефонphone / additionalPhone
АдресactualAddress (⚠️ зафиксировано в LOCALIOFFICE-975)
Пол / Дата рожденияgender / birthDate
e-mailemail
Тип поставщикаsupplierType
Ценовая категорияpriceCategoryId
⬜ Сотрудник / Поставщик / ГостьisEmployee / isSupplier / isGuest

Чекбокс «Клиент» (isClient) в UI скрыт. Бэк сам выставляет: isClient = !(isSupplier || isEmployee || isGuest) при каждом сохранении (⚠️ LOCALIOFFICE-975).

Дополнительные сведения

Поле UIПоле DTO
ПодразделениеsubdivisionId
Юр лицоlegalEntityId
КонцепцияconceptId
Система EDIediSystem
Комментарийcomment
Счёт ДДС (банк. выписка)cashFlowAccountId
Корр. счёт (банк. выписка)bankStatementCorrAccountId
⬜ Нельзя проводить операции по счёту без прокатывания карточкиrequireCardSwipe
Номер карты (вручную / штрих-код)cardNumber

Сабмодалка «Считайте штрих-код» — UI-flow считывания. Найти контрагента по карте: GET /organization/counterparties/search-by-card?cardNumber=.... Записать новое значение — обычным сохранением карточки.

Паспорт/Лицензия

Поле UIПоле DTO
Серия / Номер / ВыданpassportSeries / passportNumber / passportIssuedBy
Дата выдачи / Действителен доpassportIssueDate / passportValidUntil
Вид деятельностиactivityType

Юр. лицо

Массив legalEntities[] в DTO заменяет текущий набор юр.лиц контрагента. На текущем этапе в UI одно юр.лицо: PATCH с массивом длиннее 1 элемента отдаёт 400 (⚠️ LOCALIOFFICE-975).

DTO одного юр.лица: create-counterparty-legal-entity.dto.ts.

Поле UIПоле DTO legalEntities[i]
Наименованиеname
Полное наименование⚠️ fullName — добавляется задачей LOCALIOFFICE-975
ОКПО / ИНН / КПП / ОКПДokpo / inn / kpp / okpd
Юр. адрес / Факт. адресlegalAddress / actualAddress
Банк / БИК / Расчётный счётbankName / bik / accountNumber
Город банка / Корр. счётbankCity / correspondentAccount
SWIFT BIC / GLN / Рег. номерswiftBic / gln / regNumber

Контроль цен

Поле UIПоле DTO
Действие при отклонении ценыpriceDeviationAction
Отсрочка платежа, днейpaymentDeferDays
День платежаpayWeekday
⬜ Запрещать запись в прайс-листdenyPriceListWrite

Фото

Не в bulk. Отдельные эндпоинты:

  • POST /organization/counterparties/:id/photo — multipart-загрузка, валидация ≥300×300 px.
  • DELETE /organization/counterparties/:id/photo — удаление.

В bulk-DTO поле photoUrl не передаётся; фронт после загрузки получает обновлённый URL отдельно.

Bulk-сохранение

Один PATCH /organization/counterparties/:id принимает все поля всех вкладок кроме фото, включая массив юр.лиц.


Известные расхождения и план доработок

Что не работает / появитсяЗадача
Карточка контрагента: fullName юр.лица, фиксация «Адрес → actualAddress», автоматический isClient, одно юр.лицоLOCALIOFFICE-975
Модалка «Новый счёт»: ответственные лица, автогенерация кодаLOCALIOFFICE-976

Что работает уже сейчас и менять не нужно

  • POST /accounting/payments/batch создаёт платежи, проводки, обновляет документы, генерирует ПКО — всё в одной транзакции с защитой от двойной оплаты (FOR UPDATE) и блокирующей датой корпорации.
  • Смешанный batch: один запрос может содержать документы разных контрагентов; при printPkoPerCounterparty=true создаётся отдельный ПКО на каждую пару (counterpartyId, legalEntityId) со сводным флагом isSummary=true, если в группе >1 платежа. В ответе для каждого платежа доступны counterpartyId, legalEntityId, invoiceId/serviceActId, documentType — фронт сопоставляет платёж с выбранной строкой таблицы. В paymentReceipts[].payments[]paymentId/invoiceId/serviceActId для построения связи «строка → ПКО».
  • Фильтр селекторов счетов по семантической роли (GET /accounting/accounts?role=CASH|COUNTERPARTY_SETTLEMENTS|CASH_FLOW_ITEM) выполняется на бэке; клиентская пост-фильтрация не нужна.
  • Корпоративные дефолты defaultPaymentDebitAccountId / defaultPaymentCreditAccountId / defaultPaymentDdsAccountId хранятся на Corporation, читаются/правятся через GET/PATCH /organization/corporation, проставляются сидером типового плана счетов; если поле null, селектор открывается без предзаполнения.
  • Валидация суммы по остатку, отказ от оплаты полностью оплаченного, отказ от переплаты — всё с понятными 400.
  • Карточка контрагента уже сохраняется одним bulk-PATCH запросом, фото — отдельно multipart, ПКО — отдельный PDF-эндпоинт.

Эти точки не закладывать в задачи на изменение — это согласованное текущее поведение.