Сверка с МойСкладом (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>
14 KiB
МойСклад — опорные факты для модуля «Деньги»
Собрано 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. Используется для:
- стартовых остатков по долгам (начали работать с поставщиком, должны 500к — заводим корректировку);
- ручного исправления разошедшихся расчётов.
У нас: применяем для простого случая «начальный долг поставщику»; знак определяется интерфейсом («мы должны» / «нам должны»).
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— не нужно, у магазина нет документооборота с банком.
Эти поля не создаём в простом режиме, но резервируем места в именованиях колонок на будущее.