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