food-market/docs/money-moysklad-reference.md
nns a25b5b3277
Some checks are pending
Auto-tag / Create date-tag (push) Waiting to run
CI / Backend (.NET 8) (push) Waiting to run
CI / Web (React + Vite) (push) Waiting to run
CI / POS (WPF, Windows) (push) Waiting to run
Docker API / Build + push API (push) Waiting to run
Docker API / Deploy API on stage (push) Blocked by required conditions
feat(money): бэкенд модуля «Деньги» для простого режима (пп.0–6)
Сверка с МойСкладом (docs/money-moysklad-reference.md) + домен/инфра/API:

- Счета и кассы (MoneyAccount: Cash/Safe/Bank/Acquiring, начальный остаток,
  привязка к магазину, IsDefault/IsActive, инвариант CurrentBalance).
- Денежные документы (MoneyTransaction: PaymentIn/Out, CashIn/Out, переводы
  парой TransferOut↔TransferIn; статьи расходов ExpenseItem; проведение/отмена
  в Serializable-транзакции через MoneyService).
- Оплата поставщикам: Supply/SupplierReturn.PayedSum + MoneyTransactionLink,
  SuppliesController отдаёт PaymentStatus + фильтр paymentStatus.
- Взаиморасчёты: CounterpartyAdjustment + MoneyReportsController.SupplierDebts.
- Смены кассы: CashShift + CashDrawerOperation (open/close, expected↔actual,
  Z-отчёт), запрет розничной продажи без открытой смены в RetailSalesController.
- Отчёт «Деньги»: balances/movement с проверкой инварианта остатков.

Права: RolePermissions +MoneyView/MoneyEdit/CashShiftOperate; сидер раздаёт
кассиру CashShiftOperate+MoneyView, кладовщику — нет; 5 системных статей
расходов на новую орг. Миграция Phase33a (ручная, с [Migration]+[DbContext]).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-07 21:52:04 +05:00

258 lines
14 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# МойСклад — опорные факты для модуля «Деньги»
Собрано 2026-10-07 через headless Playwright со страниц
`https://dev.moysklad.ru/doc/api/remap/1.2/#/…`. Код скрейпера:
`tests/e2e/scrape-moysklad.ts`, сырые дампы: `/tmp/moysklad-dump/*.txt`.
Задача этого документа — зафиксировать **именно те поля и связи
МойСклада**, которые мы должны уметь воспроизводить. Своими названиями
колонок/эндпоинтов пользуемся по смыслу их, чтобы потом синхронизация
легла без хирургии.
---
## 1. Юрлицо → счета (`organization.accounts`)
Юрлицо (`organization`) содержит коллекцию банковских счетов (`accounts`).
Один помечен «основным» (`isDefault`). Поля счёта:
| Поле | Тип | Назначение |
|---|---|---|
| `id` | UUID | — |
| `accountNumber` | String(255), **required** | номер счёта |
| `bankName` | String(255) | наименование банка |
| `bankLocation` | String(255) | адрес банка |
| `bic` | String(255) | БИК/BIC |
| `correspondentAccount` | String(255) | корр. счёт |
| `currency` | Meta, expand | валюта счёта |
| `isDefault` | Boolean, **required** | основной ли |
**Следствие для нас:** на одну организацию у магазина может быть
несколько банковских счетов (Kaspi, Halyk и т.п.) + «касса» как отдельная
сущность. МойСклад касса/сейф — это уже свойство ордеров, а не поле
юрлица. У нас решено объединить это в единый справочник «Счета и кассы»
с типом `Bank | Cash | Safe | Acquiring` — так удобнее для простого
магазина, при синхронизации кассы маппим в документы cashin/cashout
«без закрывающего `organizationAccount`», а банк — в payment с
`organizationAccount` = нашим `MoneyAccountId`.
---
## 2. Входящий платёж (`paymentin`) и исходящий (`paymentout`) — **безнал**
Основные поля (общие для in/out):
| Поле | Тип | Назначение |
|---|---|---|
| `agent` | Meta, **required** | контрагент |
| `agentAccount` | Meta | счёт контрагента |
| `organization` | Meta, **required** | наше юрлицо |
| `organizationAccount` | Meta | **наш** банковский счёт (списание / зачисление) |
| `moment` | DateTime, **required** | дата документа |
| `sum` | Float, **required** | сумма (в копейках у МойСклада; у нас — decimal в тенге) |
| `rate` | Object | валюта + курс |
| `paymentPurpose` | String(255), **required** | назначение платежа |
| `incomingDate` / `incomingNumber` | — | исходящий номер/дата платёжки (для входящих) |
| `applicable` | Boolean, **required** | «проведено» |
| `expenseItem` | Meta (**только у paymentout и cashout**) | статья расходов |
| `operations` | Array(Meta + `linkedSum`) | **привязка к документам** |
| `vatSum` | Float | НДС |
### Привязка `operations`
Это ключевая вещь: один платёж/ордер привязывается к N документам,
на каждый — своя `linkedSum`.
**paymentin / cashin — что может оплачивать:** `customerorder`,
`purchasereturn`, `demand`, `invoiceout`, `commissionreportin`, `retailShift`.
→ оплата **от покупателя** (включая возврат поставщику — деньги возвращаются
поставщиком обратно к нам).
**paymentout / cashout — что может оплачивать:** `supply` (приёмка),
`purchaseorder`, `invoicein`, `commissionreportout`, `customerreturn`
(возврат покупателю).
→ оплата **поставщикам и клиентам (возвраты)**.
### `noClosingDocs`
У `paymentout`/`cashout` есть флаг `noClosingDocs`. При `true` — платёж
«без закрывающих документов», `operations` должен быть пуст. При `false` —
`operations` обязателен для разноски. Нельзя передать `true` и
непустой `operations` одновременно.
### Валидация валюты платежа
Если задан `organizationAccount` в валюте, отличной от валюты учёта
компании, валюта документа **должна совпадать с валютой счёта** (иначе
ошибка 22004). У приёмки `cashin`/`cashout` этой валидации нет.
---
## 3. Приходный / расходный ордер (`cashin`, `cashout`) — **наличные**
Поля в целом совпадают с paymentin/paymentout, но:
- **нет `organizationAccount`** — наличные деньги у МойСклада не
привязаны к конкретной «кассе», а трекаются суммарно по организации.
У нас это неудобно (магазин хочет видеть «касса магазина Абая», «касса
магазина Жибек Жолы», «сейф директора») — поэтому вводим внутреннее
поле `MoneyAccountId` обязательным для cash-документов тоже.
- `paymentPurpose` называется «Основание».
- У `cashout` есть `expenseItem` и `noClosingDocs`.
---
## 4. Внесение/выплата кассы смены (`retaildrawercashin`, `retaildrawercashout`)
Отдельная сущность — именно операции кассира внутри открытой смены
(«разменять 50к из сейфа», «отдать 10к курьеру из кассы»).
Ключевые поля:
| Поле | Тип | Назначение |
|---|---|---|
| `agent` | Meta, **required** | **сотрудник** (не контрагент!) — кассир |
| `organization` | Meta, **required** | юрлицо |
| `sum` | Float, **required**, read-only | сумма |
| `moment` | DateTime | момент |
| `retailShift` | Meta, **required** | смена, в рамках которой операция |
| `description` | String(4096) | комментарий (у нас — «Причина») |
| `applicable` | Boolean | проведено |
У МойСклада эти документы «только для чтения `sum`» — считаются от
исходной операции. У нас будет проще: кассир вручную вводит сумму +
причину, валидация >0.
---
## 5. Розничная смена (`retailshift`)
| Поле | Тип | Назначение |
|---|---|---|
| `retailStore` | Meta, **required** | точка продаж (у нас RetailPoint) |
| `store` | Meta | склад (авто из retailStore если не задан) |
| `organization` | Meta, **required** | юрлицо |
| `organizationAccount` | Meta | счёт для безнал. выручки (эквайринг) |
| `acquire` | Meta | **банк-эквайер** по картам |
| `qrAcquire` | Meta | банк-эквайер по QR-коду |
| `bankPercent` | Double | % комиссии эквайера по картам |
| `bankComission` | Double | **сумма** комиссии (≤ выручки по картам) |
| `qrBankPercent`, `qrBankComission` | — | то же для QR |
| `proceedsCash` | Float, read-only | **выручка наличными** |
| `proceedsNoCash` | Float, read-only | **выручка безнал** |
| `receivedCash` | Float, read-only | получено наличными (с учётом внесений/выплат) |
| `receivedNoCash` | Float, read-only | получено безнал |
| `closeDate` | DateTime | момент закрытия смены |
| `operations` | Array | связанные операции (продажи, возвраты) |
| `paymentOperations` | Array, read-only | связанные платежи |
| `cheque.start / cheque.end` | Object | ККТ-признаки открытия/закрытия: `fnNumber`, `kktRegNumber`, `fiscalDocNumber`, `fiscalDocSign`, `shiftNumber`, `time`, (end) `chequesTotal`, `fiscalDocsTotal`. |
**Схема расчёта наличных в смене (повторяем в своём коде):**
```
expectedCash = openingFloat // «разменник» на старте
+ proceedsCash // продажи наличными
- cashRefunds // возвраты наличными
+ Σ retaildrawercashin // внесения
- Σ retaildrawercashout // выплаты
```
«По факту» вводит кассир при закрытии; разница =
`actualCash - expectedCash`, подсвечиваем в UI.
Выручка по картам отражается на счёте `organizationAccount` смены
(= наш «счёт эквайринга»). Комиссия банка — отдельный расход
(ежемесячно/ежедневно, зависит от договора).
---
## 6. Корректировка взаиморасчётов (`counterpartyadjustment`)
Простой документ: `agent` (контрагент), `organization`, `moment`,
`sum`, `applicable`, `description`. Используется для:
1. **стартовых остатков по долгам** (начали работать с поставщиком,
должны 500к — заводим корректировку);
2. ручного исправления разошедшихся расчётов.
У нас: применяем для простого случая «начальный долг поставщику»; знак
определяется интерфейсом («мы должны» / «нам должны»).
---
## 7. Отчёт «Деньги» (`/report/money/plotseries`)
Разделяется на две части:
- **plotseries** — график движения за период: `credit` (приход),
`debit` (расход), и массив `series` с точками `date / credit / debit / balance`.
Параметры: `momentFrom`, `momentTo`, `interval` ∈ {hour, day, month}.
- (вторая часть — `/report/money` — остатки по счетам, в нашем дампе
обрезано, но из UI МойСклада знаем: массив объектов `{account, sum}`,
суммарный баланс.)
Фильтры: `organization`, `project`.
**Для нас:** реализуем два эндпоинта в одном контроллере —
`GET /api/money/report/balances` и `GET /api/money/report/movement?from=&to=&interval=`,
второй отдаёт credit/debit/balance в формате plotseries.
---
## 8. Отчёт «Показатели контрагентов» (`/report/counterparty`) — долг
В одном ряду по контрагенту:
| Поле | Назначение |
|---|---|
| `balance` | **баланс взаиморасчётов**: >0 — контрагент должен нам, <0 — мы должны ему (знак зависит от типа) |
| `demandsCount`, `demandsSum` | продажи |
| `returnsCount`, `returnsSum` | возвраты |
| `averageReceipt` | средний чек |
| `profit` | прибыль |
| `firstDemandDate`, `lastDemandDate` | — |
Для простого режима нам нужен **только `balance`** и аналог
`oldestUnpaidDate` (у МойСклада нет этого поля в отчёте, но мы считаем
сами из `supplyPayedSum < supplyTotal`).
Формула долга поставщику у нас:
```
debtToSupplier(supplierId)
= Σ supplies.total (status = Posted, supplierId = X)
− Σ supplierReturns.total (аналогично)
− Σ supplierPayments (paymentout + cashout по этому поставщику)
+ Σ counterpartyAdjustments.sumSigned (стартовые долги)
```
---
## 9. `payedSum` на документах закупки/продажи
Эмпирически у МойСклада на `supply`, `demand`, `purchasereturn`,
`customerreturn`, `invoiceout`, `invoicein` есть вычисляемое поле
`payedSum` — «сколько из суммы документа уже оплачено привязанными
платежами/ордерами». Оно не передаётся в POST, апдейтится сервером
на каждое изменение `operations/linkedSum` платежа.
**У нас:** добавляем `PayedSum` на `Supply` и `SupplierReturn`,
пересчитываем в `MoneyService` при проведении/отмене платежа/ордера,
статус оплаты вычисляется в SQL/контроллере:
`unpaid | partial | paid`.
---
## 10. Что НЕ берём в простой режим
- `project`, `salesChannel`, `contract`, `state`, `group`, `attributes`,
`files`, `syncId`, `externalCode` — всё для расширенного/интеграции.
- `vatSum` — у магазина простого режима НДС считается общий, а не на
уровне платежа; поле опустим в UI, в БД оставим `nullable` на будущее.
- `invoiceout`/`invoicein` (счета) — у нас нет сущности «Счёт», только
документы.
- `incomingDate`/`incomingNumber` — не нужно, у магазина нет
документооборота с банком.
Эти поля не создаём в простом режиме, но резервируем места в именованиях
колонок на будущее.