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

14 KiB
Raw Blame History

МойСклад — опорные факты для модуля «Деньги»

Собрано 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 — не нужно, у магазина нет документооборота с банком.

Эти поля не создаём в простом режиме, но резервируем места в именованиях колонок на будущее.