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
Сверка с МойСкладом (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>
258 lines
14 KiB
Markdown
258 lines
14 KiB
Markdown
# МойСклад — опорные факты для модуля «Деньги»
|
||
|
||
Собрано 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` — не нужно, у магазина нет
|
||
документооборота с банком.
|
||
|
||
Эти поля не создаём в простом режиме, но резервируем места в именованиях
|
||
колонок на будущее.
|