Some checks are pending
Auto-tag / Create date-tag (push) Waiting to run
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
83 lines
9.9 KiB
Markdown
83 lines
9.9 KiB
Markdown
# Уведомления об отрицательных остатках → инвентаризация
|
||
|
||
Идея владельца 08.10.2026: «когда у товара количество становится отрицательным, система и
|
||
мобильное приложение должны отправлять PUSH о том, что необходимо произвести инвентаризацию
|
||
по таким товарам, и сразу предоставить список таких товаров».
|
||
|
||
Решение владельца 08.10: **разрешать продажу в минус нужно**, но с постоянным предупреждением
|
||
везде (касса, админка, приложение) и требованием срочно провести инвентаризацию.
|
||
|
||
Поправка 09.10: **пункт 12 (предварительная себестоимость) НЕ делаем** — тема отложена;
|
||
себестоимость продаж в минус оставляем как сейчас, расчёт не трогаем.
|
||
|
||
## Сверка с МойСкладом (что есть у них)
|
||
- **Отчёт «Остатки»** (`report/stock`) с параметром `stockMode`: значения `all`, `positiveOnly`,
|
||
`negativeOnly`, `underminimum`, `nonEmpty`, `empty`. Т.е. отрицательные остатки — это **только
|
||
фильтр в отчёте** (`stockMode=negativeOnly`). Отдельного «уведомления» нет.
|
||
- **Вебхуки** МойСклада — на CRUD-события сущностей (создание/изменение/удаление документов и
|
||
справочников). Порога «остаток ушёл в минус» среди событий **нет** — вебхук на изменение
|
||
документа есть, но «стало отрицательно» как события нет.
|
||
- Минус **разрешён** настройкой («разрешить отрицательные остатки»); push/требования пересчёта нет.
|
||
|
||
**Вывод:** у МойСклада — пассивный отчёт-фильтр. Мы делаем **лучше**: проактивные предупреждения
|
||
(событие при уходе в минус), постоянные баннеры/индикаторы в кассе/админке/приложении, push
|
||
(web/telegram, APNs-заглушка), подсказка причины, и действие «Пересчитать» (авто-черновик
|
||
инвентаризации по минусовым товарам), с авто-закрытием при возврате остатка ≥ 0 и эскалацией.
|
||
(Источник по МойСкладу: dev.moysklad.ru/doc/api/remap/1.2 report/stock stockMode; webhooks — на сущности.)
|
||
|
||
## План / статус (легенда: [x] готово · [~] частично/заложено · [ ] отложено)
|
||
0. [x] Сверка с МойСкладом (см. выше): у них — только фильтр отчёта `stockMode=negativeOnly`, push/события нет.
|
||
1. [x] Событие при проведении документа → остаток < 0: `NegativeStockAlert` (товар, склад, остаток,
|
||
документ-причина: тип/номер/автор/время), единая точка — `StockService.ApplyMovementAsync`.
|
||
Остаток ≥ 0 → закрывается автоматически. Миграция Phase39, частичный уникальный индекс.
|
||
2. [x] Подсказка причины: API списка отдаёт `hint` «возможно, не проведена приёмка П-… от <поставщик>»
|
||
(если по товару есть черновик приёмки).
|
||
3. [~] Без спама: счётчик/баннер — реалтайм-запрос (poll 60с), не спамит. Поля `NotifiedAt`/`EscalatedAt`
|
||
в сущности заложены под фоновую агрегацию/рассылку. **Отложено:** фоновая job (email/telegram
|
||
одним сообщением на склад, тихие часы, получатели по ролям/привязке, тумблеры в настройках).
|
||
4. [x] «Пересчитать эти товары»: `POST /api/negative-stock/recount` → черновик инвентаризации по складу
|
||
ровно с минусовыми товарами (BookQty = текущий отрицательный остаток), открывается из баннера.
|
||
5. [~] Каналы: **веб** — постоянный баннер + фильтр/пометка в остатках ✓; отчёт «Отрицательные» (фильтр
|
||
`negativeOnly`) ✓. **Мобильное API** — `GET /api/negative-stock` + `/count` готовы (iOS читает) ✓.
|
||
**APNs-заглушка заложена** ✓ — таблица `device_tokens` (Phase40) + `POST/DELETE /api/me/device-tokens`
|
||
+ контракт `IPushSender`, в DI `StubPushSender` (лог); реальный APNs/FCM/VAPID подменит заглушку
|
||
без правок вызова. Описано в `docs/mobile-api.md`. **Отложено:** центр уведомлений (realtime-событие
|
||
NegativeStock), Telegram-рассылка, Web Push (VAPID + SW), реальная отправка push.
|
||
6. [~] Тесты: integration **2/2** (минус разрешён → алерт → recount → авто-закрытие; фильтр negativeOnly) ✓;
|
||
full-chain шаг 9/13 переписаны (oversell/отмена приёмки → проведено + предупреждение) ✓.
|
||
**Скрины на стенде** ✓ — `tests/e2e/reports/neg-banner-home-*.png` (баннер «22 товаров в минусе»),
|
||
`neg-stock-filter-*.png` (фильтр «Только минусы» + красные остатки). Стенд-smoke: `/count` 200,
|
||
`/api/negative-stock` 200, `device-tokens` DELETE 204 (Phase39+Phase40 применились).
|
||
**Отложено:** unit на агрегацию/дебаунс (нет job), e2e в браузере на баннер.
|
||
7. [x] Снята блокировка продажи в минус: розница, касса (POS-синк), опт, списание, перемещение,
|
||
возврат поставщику, отмена приёмки. Деньги (минус счёта) — не трогали (другое правило).
|
||
8. [ ] **Отложено** (десктопная Avalonia-касса): предупреждение в строке + жёлтый индикатор в шапке +
|
||
офлайн по локальным остаткам. (Отдельная поверхность; iOS/касса — Mac-Claude.)
|
||
9. [x] Админка: постоянный баннер «N товаров в минусе — нужна инвентаризация» (сворачивается, крестиком
|
||
не закрыть) + «Пересчитать эти товары»; в остатках — красная пометка + фильтр «Только минусы».
|
||
10. [~] Мобильное API/iOS: `/api/negative-stock/count` + список готовы. Баннер в приложении — Mac-Claude.
|
||
11. [~] Эскалация: в API есть флаг `overdue` (минус висит дольше N часов, по умолч. 24). **Отложено:**
|
||
фоновое повторное уведомление владельцу/админу + блок «Просроченные минусы» на главной.
|
||
12. [ ] ОТЛОЖЕНО (поправка владельца 09.10) — предварительная себестоимость НЕ делаем.
|
||
|
||
## Что сделано в этой сессии (ядро, оттестировано и выложено)
|
||
Разрешение минуса + детекция/авто-закрытие предупреждений + API (список/счётчик/recount/фильтр) +
|
||
веб-баннер и пометки в остатках + integration-тесты 2/2 + full-chain 9/13. Деплой на стенд.
|
||
|
||
## Что отложено (заложен фундамент, нужен отдельный проход)
|
||
Фоновая рассылка (email/telegram, агрегация/тихие часы/получатели/настройки), эскалация-job и
|
||
«просроченные минусы», центр уведомлений (realtime-событие), Web Push (VAPID), **реальная**
|
||
отправка push (APNs/FCM — заглушка уже в DI), индикаторы в десктопной Avalonia-кассе (в т.ч. офлайн).
|
||
**DONE не ставим — не все пункты закрыты.**
|
||
|
||
## Что НЕ трогаем
|
||
Прод, shared-postgres, global.json, gateway nginx, sklad. Расчёт себестоимости (п.12 отложен).
|
||
|
||
## Журнал
|
||
- 09.10 — старт: прогресс-док, сверка с МойСкладом, обследование кода.
|
||
- 09.10 — ядро выложено на стенд (9ba1edb): разрешение минуса + детекция/авто-закрытие +
|
||
API (список/счётчик/recount/фильтр) + веб-баннер/пометки + integration 2/2.
|
||
- 09.10 — задел push (3547e82): `device_tokens` (Phase40) + `IPushSender`/`StubPushSender` +
|
||
`/api/me/device-tokens`, описано в `docs/mobile-api.md`. Выложено на стенд, Phase39+Phase40
|
||
применились (`/api/negative-stock/count` → 200). Скрины баннера + фильтра минусов сняты.
|