docs(s31): user-facing supply-inventory.md
Полная инструкция по трём новым возможностям: приёмка по фото (статусы, экран проверки, запоминание сопоставлений), приёмка сканированием (камера непрерывная, аппаратный сканер, last-purchase price), мобильная инвентаризация (3 stage, offline-каталог, drift- плашка, фильтр расхождений). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
457d8f5877
commit
c17660c4a1
158
docs/supply-inventory.md
Normal file
158
docs/supply-inventory.md
Normal file
|
|
@ -0,0 +1,158 @@
|
||||||
|
# Приёмка и инвентаризация с телефона
|
||||||
|
|
||||||
|
С версии 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» — пока нет.
|
||||||
Loading…
Reference in a new issue