food-market/docs/supply-inventory.md
nns c17660c4a1 docs(s31): user-facing supply-inventory.md
Полная инструкция по трём новым возможностям: приёмка по фото
(статусы, экран проверки, запоминание сопоставлений), приёмка
сканированием (камера непрерывная, аппаратный сканер, last-purchase
price), мобильная инвентаризация (3 stage, offline-каталог, drift-
плашка, фильтр расхождений).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-10-04 21:47:19 +05:00

159 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Приёмка и инвентаризация с телефона
С версии Sprint 31 в food-market три новых способа работы с товаром:
по фото накладной, сканированием штрихкодов, и мобильная инвентаризация
с офлайн-каталогом. Все три видны в **простом режиме**, доступны ролям
**Администратор** и **Кладовщик**.
## 1. Приёмка по фото/PDF накладной
**Когда использовать:** поставщик привёз накладную в бумаге или в PDF —
не хочется набивать строки руками.
**Как:**
1. `/purchases/invoices` → «Снять камерой» (с телефона) или «Загрузить
файл» (image/* или PDF, до 60 МБ).
2. Загрузка попадает в очередь. Статус-бейдж на карточке: `В очереди
→ Распознаётся → На проверке | Ошибка`. Страница обновляется сама
каждые 3 секунды, пока есть распознающиеся.
3. Открываем карточку «На проверке» → экран проверки:
- **Шапка** (редактируемая): поставщик, №, дата, итого.
- **Строки** карточками. Сопоставлено по штрихкоду или по памяти
прошлых накладных → зелёная плашка `→ Название товара`. Не
сопоставлено → карточка оранжевым, предупреждение сверху.
- **Общий сканер** (поле «Отсканировать…»): скан → +1 к первой
несопоставленной строке или новая строка. Удобно когда вы по
бумажке проходитесь сканером.
- **Для каждой строки** можно «Найти товар» (поиск по каталогу)
или удалить. Если товара нет — ссылка «Создать товар».
- Сумма строк сравнивается с «Итого» в накладной; если
отличается больше чем на 1 — предупреждение.
4. «Сохранить правки» — обновляет документ, автоматически запоминает
все ручные сопоставления. **На следующей накладной того же
поставщика** matcher уже найдёт эти товары автоматически.
5. «Создать приёмку» → диалог: поставщик (поиск по БИН/имени,
можно создать нового), склад, валюта → `POST /apply` создаёт
нашу Supply (Draft). Проведение — обычной кнопкой в приёмке.
**Как это распознаётся:** локальный Claude Code в headless-режиме
(`claude -p --output-format json`, промпт сверяет «Итого», отличает
штрихкод от кода поставщика, не угадывает — пустые поля = null).
Нагрузка списывается с подписки, не с API-ключа. На сервере нужен
установленный `claude` CLI и `claude login` от того же пользователя, что
запускает API. Можно пустить модель на отдельной машине через
`Invoices:ClaudeCli:RemoteHost` (ssh + scp — всё внутри сервиса).
**Хранение файла:** `uploads/invoices/{yyyy-MM}/{guid}.ext`. Удаляется
по истечении `Invoices:KeepFilesDays` (default 7). Распознанные строки
остаются навсегда — по ним всегда видно, что было в накладной.
## 2. Приёмка сканированием
**Когда:** поступил товар россыпью, по бумажке лень набивать.
**Как:** `/purchases/supplies/scan` → поставщик + склад + валюта →
«Сканировать камерой». Камера непрерывная, окно не закрывается между
сканами. Товар за товаром:
- +1 к существующей строке, если такой уже есть; иначе — новая строка
сверху;
- цена подставляется **последняя закупочная** (из последней Posted
Supply);
- вибрация + вспышка на удачный скан; красным — если штрихкод не
в каталоге;
- аппаратный сканер тоже работает (details → поле + Enter).
«Создать черновик» → наш Supply (Draft) в указанном складе; проверить
и провести — на обычной странице приёмки.
**Прогресс** сохраняется в localStorage (`fm.supply-scan.rows`) — закрыли
вкладку случайно, вернулись — всё на месте. Очищается после сохранения.
## 3. Инвентаризация с телефона
**Когда:** регулярный пересчёт склада или проверка одной группы.
**Как:** `/inventory/mobile` → три шага:
### Setup
- Выбор склада.
- Опционально — **группа** (= частичная инвентаризация, непосчитанные
товары НЕ обнулятся).
- «Загрузить каталог на телефон» — компактный слепок троек
`[barcode, productId, name]` в IndexedDB. 30 тыс. штрихкодов ≈ 3 МБ.
После загрузки работает **без интернета**.
### Scan
- Камера непрерывная или аппаратный сканер; +1 за скан;
- неизвестный штрихкод — в отдельный список «Нет в каталоге» с
количеством (не попадёт в документ, нужно сначала создать карточку);
- офлайн-плашка: нет сети = сканы остаются в телефоне, уйдут на сервер
когда сеть появится. Прогресс сохраняется в localStorage.
### Review
- При первом переходе создаётся наш `InventoryDoc` в Draft, `bookQty`
**каждой позиции** = текущий Stock склада (без ручной «Пересчитать»).
- Отображается сводка: `Излишек +XX ₸` / `Недостача -YY ₸` / количество
расхождений.
- Фильтр «только расхождения» (по умолчанию выключен — показываем всё).
- **Drift-плашка:** если учётный остаток ПОПЛЫЛ с момента создания
документа (были продажи или приёмки), сверху показывается список
изменившихся позиций: `Молоко: 10 → 7`. Это ВИДНО до нажатия
«Провести», не молча.
- «Провести» → `POST /post` создаёт корректирующие движения
`InventoryAdjustment` по строкам с diff ≠ 0.
## Безопасность
- Все эндпоинты требуют permission `SuppliesEdit` или `InventoryEdit`
(Admin + Storekeeper).
- Mutli-tenant: все запросы фильтруются по `OrganizationId` текущего
пользователя. Фоновый `InvoiceWorker` работает без HTTP-контекста
и использует явный `IgnoreQueryFilters + Where OrgId` чтобы не
утечь между орг.
- Файл накладной отдаётся только владеющему org'у через `/file`.
## Файлы
| Что | Где |
|---|---|
| Домен | `src/food-market.domain/Purchases/InvoiceUpload.cs` |
| Миграция | `src/food-market.infrastructure/Persistence/Migrations/20261004170000_Phase31a_InvoiceUploads.cs` |
| Extractor + Matcher + Worker | `src/food-market.api/Infrastructure/Invoices/` |
| API накладных | `src/food-market.api/Controllers/Purchases/InvoicesController.cs` |
| API инвентаризации | `src/food-market.api/Controllers/Inventory/InventoriesController.cs` (mobile-catalog, add-line, book-drift) |
| Web — список накладных | `src/food-market.web/src/pages/InvoicesPage.tsx` |
| Web — проверка накладной | `src/food-market.web/src/pages/InvoiceReviewPage.tsx` |
| Web — scan приёмка | `src/food-market.web/src/pages/SupplyScanPage.tsx` |
| Web — mobile инвентаризация | `src/food-market.web/src/pages/InventoryMobilePage.tsx` |
| Камера | `src/food-market.web/src/components/CameraScanner.tsx` (@zxing/browser) |
| Offline-каталог | `src/food-market.web/src/lib/offlineCatalog.ts` (IndexedDB) |
| Тесты | `tests/food-market.UnitTests/InvoiceMatcherTests.cs`, `ClaudeCliInvoiceExtractorParseTests.cs`, `tests/food-market.IntegrationTests/InvoicePipelineTests.cs` |
## Конфиг
```json
"Invoices": {
"ClaudeCli": {
"Executable": "claude",
"Model": "claude-sonnet-4-6",
"TimeoutSeconds": 180,
"RemoteHost": null, // или "user@host" — модель на другой машине
"RemoteTmpDir": "/tmp/food-market-invoices"
},
"KeepFilesDays": 7
}
```
## Что НЕ сделано (откладывается до запроса)
- Создание товара прямо в экране проверки накладной — пока уходим на
`/products/new` (future wiring в onPick из search dialog).
- PWA manifest + ServiceWorker офлайн — IndexedDB работает без них, но
установка «На главный экран» потребует manifest.json. Отдельная
задача, если понадобится.
- Автоподтверждение частичной vs полной инвентаризации при проведении
(«обнулять ли непосчитанные») — сейчас сервер не обнуляет непосчитанные
в принципе (строки с diff=0 не создают движений), что совпадает с
поведением частичной. Полная «обнулить все непосчитанные» требует
явного шага в UI «добавить все остальные с actual=0» — пока нет.