# Приёмка и инвентаризация с телефона С версии Sprint 31 в food-market три новых способа работы с товаром: по фото накладной, сканированием штрихкодов, и мобильная инвентаризация с офлайн-каталогом. Все три видны в **простом режиме**, доступны ролям **Администратор** и **Кладовщик**. ## 1. Приёмка по фото/PDF/Excel накладной **Когда использовать:** поставщик привёз накладную в бумаге, в PDF, в Excel-шаблоне или прислал фото. **Как:** 1. `/purchases/invoices` → один из четырёх способов: - «Снять камерой» — одно/несколько фото через камеру телефона (каждый файл = отдельная накладная). - «Загрузить файл» — image/*, PDF, **шаблон .xlsx** или **.csv** (`docs/supplier-invoice-template.xlsx` — заготовка для поставщиков). - «Сфотографировать листы» — камера для многостраничной накладной (все снимки идут как **одна** накладная, не отдельные). - «Фото товаров + накладная» — главный сценарий: подряд снимаете накладную и каждый товар (штрихкод + этикетка), пачка идёт одним запросом → Claude сам собирает приёмку с миниатюрой у каждой строки. Там же «Готовые фото» — выбрать уже снятые из галереи, можно смешивать со снимаемыми. 2. Загрузка попадает в очередь. Статус-бейдж на карточке: `В очереди → Распознаётся → На проверке | Ошибка`. Страница обновляется сама каждые 3 секунды, пока есть распознающиеся. 3. Открываем карточку «На проверке» → экран проверки: - **Шапка** (редактируемая): поставщик, №, дата, итого. - **Строки** карточками. Сопоставлено по штрихкоду или по памяти прошлых накладных → зелёная плашка `→ Название товара`. Не сопоставлено → карточка оранжевым, предупреждение сверху. - **Общий сканер** (поле «Отсканировать…»): скан → +1 к первой несопоставленной строке или новая строка. Удобно когда вы по бумажке проходитесь сканером. - **Для каждой строки** можно «Найти товар» (поиск по каталогу) или удалить. Если товара нет — ссылка «Создать товар». - Сумма строк сравнивается с «Итого» в накладной; если отличается больше чем на 1 — предупреждение. 4. **Автосохранение**: каждая правка (поля шапки, количество, цена, ручное сопоставление, скан) через ~700 мс сохраняется на сервере. Индикатор «сопоставлено N/M · сохраняю… / сохранено / не сохранилось» в нижней панели. Можно закрыть вкладку и открыть снова — ничего не потеряется. Ручные сопоставления запоминаются в `InvoiceAlias`: **на следующей накладной того же поставщика** matcher найдёт эти товары автоматически. 5. **Общий сканер**: если fuzzy-match по названию неуверенный, появляется панель «<товар> — в какую строку накладной?» с кнопками строк. На этой же странице работает **аппаратный сканер** целиком — не нужно кликать в поле, серия цифр + Enter подхватывается глобально (если курсор не в текстовом поле, которое читает сам). 6. **Фото-приёмка**: у каждой строки — миниатюра снимка товара, кликабельна для полного размера; пометки «цифры прочитаны неуверенно — отсканируйте» или «штрихкода нет в каталоге». 7. «Создать приёмку» → диалог: поставщик (поиск по БИН/имени, можно создать нового), склад, валюта → `POST /apply` создаёт нашу Supply (Draft). Проведение — обычной кнопкой в приёмке. **Как это распознаётся:** локальный Claude Code в headless-режиме (`claude -p --output-format json`, промпт сверяет «Итого», отличает штрихкод от кода поставщика, не угадывает — пустые поля = null). Нагрузка списывается с подписки, не с API-ключа. На сервере нужен установленный `claude` CLI и `claude login` от того же пользователя, что запускает API. Можно пустить модель на отдельной машине через `Invoices:ClaudeCli:RemoteHost` (ssh + scp — всё внутри сервиса). **Хранение файла (owner-policy):** фото накладных на сервере **не копим**. - Обычная загрузка (одно фото/PDF/Excel) — файл удаляется **сразу после распознавания**. Распознанные строки живут в БД (`ParsedJson`). - Фото-приёмка (папка со снимками) — хранится **только пока накладная на проверке**: нужна для миниатюр у строк. Удаляется при создании черновика приёмки (apply), при удалении загрузки, и через **24 часа** — если «брошено» на статусе `needs_review`. `Invoices:KeepFilesDays` больше не используется. ## 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 за скан; - **поле аппаратного сканера на виду** (не спрятано в раскрывашку) — можно просто сканировать, а также: - **глобальный keydown** ловит сканер и когда курсор не в поле (серия цифр + Enter) — удобно, если вы перешли к другому элементу страницы; - **импорт Excel/CSV** (`Загрузить из файла`) — колонки «Штрихкод» и «Количество», повторы штрихкодов суммируются, товары добавляются к текущему списку. Документ при этом **не создаётся** — можно дополнительно просканировать и уже потом провести; - неизвестный штрихкод — в отдельный список «Нет в каталоге» с количеством (не попадёт в документ, нужно сначала создать карточку); - офлайн-плашка: нет сети = сканы остаются в телефоне, уйдут на сервер когда сеть появится. Прогресс сохраняется в 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-5-5", "TimeoutSeconds": 180, "PhotoTimeoutSeconds": 600, // пачка фото — долго, отдельный тайм-аут "RemoteHost": null, // или "user@host" — модель на другой машине "RemoteTmpDir": "/tmp/food-market-invoices" } } ``` > Haiku сочиняет данные накладных — не использовать. `KeepFilesDays` > больше не читается (owner-policy: фото не копим). ## Что НЕ сделано (откладывается до запроса) - Создание товара прямо в экране проверки накладной — пока уходим на `/products/new` (future wiring в onPick из search dialog). - PWA manifest + ServiceWorker офлайн — IndexedDB работает без них, но установка «На главный экран» потребует manifest.json. Отдельная задача, если понадобится. - Автоподтверждение частичной vs полной инвентаризации при проведении («обнулять ли непосчитанные») — сейчас сервер не обнуляет непосчитанные в принципе (строки с diff=0 не создают движений), что совпадает с поведением частичной. Полная «обнулить все непосчитанные» требует явного шага в UI «добавить все остальные с actual=0» — пока нет.