docs(money): модель «деньги смены → остатки» + МойСклад reference + прогресс
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
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
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
005c4ac5bf
commit
0977c639cd
|
|
@ -1,257 +1,39 @@
|
|||
# МойСклад — опорные факты для модуля «Деньги»
|
||||
# МойСклад — розничные деньги (reference для модели смены)
|
||||
|
||||
Собрано 2026-10-07 через headless Playwright со страниц
|
||||
`https://dev.moysklad.ru/doc/api/remap/1.2/#/…`. Код скрейпера:
|
||||
`tests/e2e/scrape-moysklad.ts`, сырые дампы: `/tmp/moysklad-dump/*.txt`.
|
||||
Как в МойСклад розничные операции отражаются в деньгах (по
|
||||
`https://dev.moysklad.ru/doc/api/remap/1.2/documents/` и известной семантике;
|
||||
страница docs отдаётся JS-ом, поэтому ниже — сводка модели, а не дословная цитата).
|
||||
|
||||
Задача этого документа — зафиксировать **именно те поля и связи
|
||||
МойСклада**, которые мы должны уметь воспроизводить. Своими названиями
|
||||
колонок/эндпоинтов пользуемся по смыслу их, чтобы потом синхронизация
|
||||
легла без хирургии.
|
||||
## Сущности
|
||||
- **Розничная смена** `retailshift` — контейнер смены на точке (ККМ). Хранит
|
||||
начальный остаток (размен), суммы продаж нал/безнал, возвраты, внесения/изъятия,
|
||||
итоговые суммы на закрытие.
|
||||
- **Розничная продажа** `retaildemand` — чек. Внутри — оплата: `cashSum` (нал) и
|
||||
`noCashSum`/`bankPercent` (безнал, эквайринг). Привязана к смене.
|
||||
- **Розничный возврат** `retailsalesreturn` — возврат покупателю (нал/безнал).
|
||||
- **Внесение/Изъятие** `retaildrawercashin` / `retaildrawercashout` — движение
|
||||
наличных в кассе ККМ внутри смены (внесение разменом, изъятие инкассацией).
|
||||
- **Приходный/расходный ордер** `cashin` / `cashout`, платёж `paymentin`/`paymentout`
|
||||
— общие денежные документы (не розничные), влияют на остаток счёта/кассы.
|
||||
|
||||
---
|
||||
## Как попадает в деньги
|
||||
- Касса (денежный счёт типа «касса») = размен + Σ продаж нал − Σ возвратов нал +
|
||||
Σ внесений − Σ изъятий. Безнал (карты) идёт на привязанный **расчётный счёт/
|
||||
эквайринг**, отдельно от наличной кассы.
|
||||
- Каждый `retaildemand`/`retailsalesreturn`/`drawercashin|out` — это движение по
|
||||
счёту; в отчёте «Деньги» (движение ДС) они видны по типам.
|
||||
- **Закрытие смены**: фиксируется фактический остаток наличных; **расхождение**
|
||||
(факт − расчёт) оформляется как излишек/недостача — отдельное движение кассы.
|
||||
- Размен (начальный остаток смены) — НЕ новое поступление: это те же деньги, что
|
||||
уже в кассе; он не задваивается как приход.
|
||||
|
||||
## 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` — не нужно, у магазина нет
|
||||
документооборота с банком.
|
||||
|
||||
Эти поля не создаём в простом режиме, но резервируем места в именованиях
|
||||
колонок на будущее.
|
||||
## Наша адаптация (food-market, простой режим)
|
||||
- Материализуем движения смены в `money_transactions` ПРИ ЗАКРЫТИИ (а не по каждому
|
||||
чеку) — проще для простого магазина, остаток кассы/эквайринга становится верным
|
||||
сразу после закрытия. Типы — `MoneyTransactionCategory` (RetailSalesCash/…/ShiftSurplus).
|
||||
- Карта → отдельный счёт «Эквайринг» смены (как расчётный счёт в МойСклад).
|
||||
- Размен не материализуем (деньги уже в кассе). Итог кассы после закрытия = факту
|
||||
наличных, если по кассе не было других операций.
|
||||
- Отличие: МойСклад проводит `retaildemand` в деньги сразу (живой остаток в течение
|
||||
смены); у нас остаток кассы меняется при закрытии, но «живой» нал смены для
|
||||
контроля выплат считается на лету (размен + продажи − возвраты + внесения − выплаты).
|
||||
|
|
|
|||
79
docs/money-shift-balances-progress.md
Normal file
79
docs/money-shift-balances-progress.md
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
# Деньги смены → остатки счетов (прогресс)
|
||||
|
||||
Автономная задача владельца (08.10.2026, нашёл Mac-Claude на стенде). Деньги смены
|
||||
(продажи нал/карта, возвраты, внесения/выплаты, расхождение) НЕ попадают в остаток
|
||||
счёта. Делать ДО кассы — касса опирается на эти остатки.
|
||||
|
||||
## Факты со стенда (Демо после B)
|
||||
- «Касса (e2e)»: нач. 50 000, текущий −98 812 (= 50 000 − 148 812 оплата приёмки нал).
|
||||
Смена С-2026-000001: продажи нал 3 775, возвраты нал 1 500, внесение 10 000, выплата 3 000,
|
||||
факт 14 775 — НИЧЕГО не попало в остаток счёта. Эквайринг: 0 при продажах картой 2 325.
|
||||
- Оплата 148 812 нал при остатке 50 000 → касса в минус, никто не остановил.
|
||||
- Инвариант теста «счёт ≡ начальный + Σ операций» прошёл только потому, что движения смены
|
||||
не считаются операциями счёта — дыра в тесте.
|
||||
|
||||
## Текущая модель (как есть)
|
||||
- Остаток счёта = `OpeningBalance + Σ(PaymentIn/CashIn/TransferIn) − Σ(PaymentOut/CashOut/TransferOut)`
|
||||
по Posted `money_transactions` (MoneyAccountsController.List + MoneyReportsController.Balances).
|
||||
- Смена (`CashShift`) хранит SalesCash/SalesCard/ReturnsCash/ReturnsCard/DrawerIn/DrawerOut/
|
||||
ExpectedCash/ActualCash/CashDifference — считаются ПРИ ЗАКРЫТИИ из связанных retail_sales +
|
||||
cash_drawer_operations. В `money_transactions` НЕ пишутся → в остаток счёта не входят.
|
||||
|
||||
## Выбранная модель (материализация движений смены при закрытии)
|
||||
Единый реестр — `money_transactions`. При закрытии смены создаём Posted-транзакции на счета смены,
|
||||
и существующая формула остатка автоматически их учитывает; отчёт «Движение» показывает их по типам.
|
||||
- Касса смены (CashAccount): `+SalesCash` (Розничные продажи), `−ReturnsCash` (Возвраты),
|
||||
`+DrawerIn` (Внесение), `−DrawerOut` (Выплата), `±CashDifference` (Излишек/Недостача).
|
||||
- Эквайринг смены (AcquiringAccount): `+SalesCard` (Продажи картой), `−ReturnsCard` (Возвраты по карте).
|
||||
- Размен (OpeningFloat) — НЕ движение (деньги уже в кассе). Итог: остаток кассы после закрытия =
|
||||
старый остаток + (SalesCash−ReturnsCash+DrawerIn−DrawerOut+Diff) = ActualCash, если до смены на
|
||||
кассе был только размен. Σ по типам = ActualCash − OpeningFloat.
|
||||
Движения помечаем `Category` (enum) + `CashShiftId` (идемпотентность). Создаются только ненулевые.
|
||||
|
||||
## Отрицательный остаток
|
||||
- Расход (CashOut/PaymentOut/TransferOut при post money-tx) и Выплата из смены (drawer Out) больше
|
||||
доступных денег → 409 «На счёте «…» недостаточно денег: остаток X ₸, нужно Y ₸».
|
||||
- Настройка орг `AllowNegativeBalance` (по умолч. выкл., менять только админ).
|
||||
- Выплата из смены сверяется с ЖИВЫМ налом смены (OpeningFloat + SalesCash−ReturnsCash+DrawerIn−DrawerOut
|
||||
на текущий момент), расход money-tx — с остатком счёта.
|
||||
|
||||
## План
|
||||
1. [x] МойСклад reference → `docs/money-moysklad-reference.md`.
|
||||
2. [x] Миграция `Phase34_ShiftMoneyMovements` (manual, idempotent): `money_transactions.Category`,
|
||||
`.CashShiftId`, `organizations.AllowNegativeBalance`. Применилась на стенде (в history + колонки есть).
|
||||
3. [x] `MoneyService.CloseShiftAsync` → `MaterializeShiftMovementsAsync` (Posted-движения по типам, link на смену, идемпотентно).
|
||||
4. [x] Guard: `PostAsync` (расход > остатка счёта → InsufficientFunds 409) + `AddDrawerOperationAsync` (выплата > нала смены);
|
||||
настройка `AllowNegativeBalance` в OrgSettings (PUT только Admin); InsufficientFunds→409 в MapResult/Drawer.
|
||||
5. [x] Остатки/отчёты правильные автоматически (всё читает money_transactions); `Category` в DTO для типов.
|
||||
6. [x] `docs/money.md` — раздел «Как деньги смены попадают в остатки» + «Отрицательный остаток».
|
||||
7. [x] Остаток вычисляемый → пересчёт не нужен; новые смены материализуются при закрытии.
|
||||
(Историч. закрытые смены прошлых тест-орг не бэкфиллим — чистятся prune'ом; демо пересоздан прогоном B.)
|
||||
8. [x] full-chain: `indepBalance()` — независимый расчёт (нач + платежи + смены: касса += факт−размен,
|
||||
эквайринг += картой−возвраты), сверяется с API /balances; шаг 8 — оплата ≤ остатка + отказ при превышении.
|
||||
Unit (MoneyMathTests, +1 = 26 ✓) + integration (MoneyModuleTests, +2 = 10 ✓).
|
||||
9. [x] deploy-stage; post-deploy C 16/16 зелёный; репро до деплоя — 4 шага красные (подтверждено).
|
||||
10. [ ] Коммиты в main + push (после 2×C + B зелёных).
|
||||
|
||||
## Найденное/исправленное
|
||||
1. **Движения смены не попадали в остаток счёта** (касса/эквайринг игнорировали продажи/возвраты/
|
||||
внесения/выплаты/расхождение). Фикс: материализация при закрытии смены в money_transactions по типам.
|
||||
2. **Касса уходила в минус без предупреждения** (расход больше остатка). Фикс: guard InsufficientFunds (409)
|
||||
на расход money-tx и выплату из смены; настройка орг «Разрешить отрицательные остатки» (по умолч. выкл., Admin).
|
||||
3. **Дыра в тесте**: инвариант сверял остаток с суммой из того же API. Фикс: независимый расчёт в тесте.
|
||||
|
||||
## Демо после фикса (прогон B, проверено руками)
|
||||
- Касса: нач. 50 000 → текущий **31 275** (= 50 000 − 30 000 оплата приёмки нал
|
||||
+ 9 775 смена С-001 (факт14775−размен5000) + 1 500 смена С-003). Раньше было −98 812.
|
||||
- Касса-2: 3 000 → **4 550** (+1 550 смена С-004). Банк: 200 000 → **181 610** (−18 390 оплата №2).
|
||||
- Эквайринг: 0 → **2 325** (продажи картой смены С-001). Раньше 0. Эквайринг-2: 0.
|
||||
- Отрицательных остатков нет. 8 движений смен материализованы по типам (продажи нал/карта,
|
||||
возвраты, внесение, выплата, излишек). Отказ при оплате больше остатка кассы — 409.
|
||||
|
||||
## Статус: ГОТОВО ✅
|
||||
Движения смены попадают в остатки; отрицательный остаток запрещён (с настройкой). B 16/16 (36 скринов),
|
||||
2× C подряд зелёные (16/16). unit 26 ✓, integration money 10 ✓.
|
||||
|
||||
## Журнал
|
||||
- 08.10 — старт; модель (материализация при закрытии); миграция+код+guard+настройка+отчёт; docs.
|
||||
Репро до деплоя: 4 шага красные. После деплоя: C 16/16 зелёный; unit 26 ✓, integration money 10 ✓.
|
||||
Демо B зелёный, остатки сверены (см. выше). 2× C подряд зелёные.
|
||||
|
|
@ -151,3 +151,30 @@
|
|||
**Ошибся в смене/платеже.** Платёж можно «Отменить проведение» и исправить.
|
||||
Закрытая смена неизменяема — расхождение фиксируется как разница (недостача/
|
||||
излишек) и видно в Z-отчёте.
|
||||
|
||||
## Как деньги смены попадают в остатки счетов (с 08.10.2026)
|
||||
|
||||
Раньше продажи/возвраты/внесения/выплаты смены не влияли на остаток кассы — это
|
||||
была ошибка. Теперь при **закрытии смены** эти движения автоматически попадают в
|
||||
деньги по типам:
|
||||
|
||||
- **Касса смены:** + Розничные продажи (нал) − Возвраты покупателям (нал)
|
||||
+ Внесения − Выплаты ± Излишек/Недостача.
|
||||
- **Эквайринг смены:** + Продажи картой − Возвраты по карте.
|
||||
- **Размен** (начальный остаток смены) движением не считается — это те же деньги,
|
||||
что уже были в кассе. Поэтому остаток кассы после закрытия = фактической сумме
|
||||
наличных, если по кассе не было других операций.
|
||||
|
||||
Эти строки видны в отчёте «Деньги» (движение) и в списке «Платежи и ордера»
|
||||
(с пометкой смены). Остаток каждого счёта = Начальный остаток + все проведённые
|
||||
движения (платежи/ордера/переводы + движения смен).
|
||||
|
||||
## Отрицательный остаток
|
||||
|
||||
По умолчанию счёт/касса **не может уйти в минус**: если расход (оплата, перевод,
|
||||
выплата из смены) больше остатка — операция отклоняется с сообщением
|
||||
«На счёте «…» недостаточно денег: остаток X ₸, нужно Y ₸». Выплата из смены
|
||||
сверяется с наличными, которые реально есть в кассе на этот момент.
|
||||
|
||||
Включить минус может только администратор: Настройки организации →
|
||||
«Разрешить отрицательные остатки».
|
||||
|
|
|
|||
Loading…
Reference in a new issue