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
Docker Web / Build + push Web (push) Waiting to run
Docker Web / Deploy Web on stage (push) Blocked by required conditions
- InventoryEditPage: «Завершить подсчёт» вместо «Провести»; на завершённой — кнопки «Создать оприходование излишков» / «Создать списание недостачи» (блокируются, если нет разницы или документ уже создан) + блок «Связанные документы» + «Вернуть в черновик». - InventoriesPage: статус «Завершена»; колонка «Результат» (опр./спис. № или «не применено»). - Enter/LossEditPage: «На основании инвентаризации №…» со ссылкой. - types.ts: InventoryStatus Draft/Completed; related в InventoryDto; результат в списке; inventoryDocId/Number в Enter/Loss DTO. - docs/inventory-moysklad-reference.md (сверка), inventory-model-progress.md, supply-inventory.md («Как провести инвентаризацию», 5 шагов), mobile-api.md (compat post/unpost). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
98 lines
7.3 KiB
Markdown
98 lines
7.3 KiB
Markdown
# API для мобильного «FM Склад»
|
||
|
||
Серверные возможности, добавленные для iOS-приложения (стенд `api/*`). Авторизация —
|
||
OpenIddict password/refresh grant, `client_id=food-market-web`, Bearer-токен. Все
|
||
ответы и ошибки — JSON; бизнес-ошибки отдают `{ error, field?, code? }`.
|
||
|
||
## 1. Остатки по группе товаров (с вложенными)
|
||
`GET /api/inventory/stock?groupId={guid}&storeId=&search=&includeZero=&page=&pageSize=&sort=&order=`
|
||
|
||
- `groupId` — фильтр по группе товаров **включая все вложенные подгруппы**
|
||
(по денормализованному `Path`). Комбинируется с `storeId`, `search`, сортировкой.
|
||
- Ответ: `PagedResult<StockRow>` — `{ items:[{ productId, productName, article, unitSymbol,
|
||
storeId, storeName, quantity, reservedQuantity, available }], total, page, pageSize }`.
|
||
|
||
## 2. Добавить штрихкод к товару
|
||
`POST /api/catalog/products/{id}/barcodes` — право `ProductsEdit`.
|
||
|
||
Тело: `{ "code": "4870001112223", "kind": "ean13" }`.
|
||
`kind` ∈ `ean13|ean8|code128|code39|upca|upce|other` (регистр не важен); если не задан —
|
||
тип определяется по длине/цифрам кода.
|
||
|
||
Ответы:
|
||
- `201 Created` → `{ id, productId, code, type, isPrimary }` (первый штрихкод товара
|
||
становится primary).
|
||
- `200 OK` — тот же код уже у **этого** товара (идемпотентно), тело как у 201.
|
||
- `409 Conflict` → `{ error:"Штрихкод уже у товара «…».", code:"barcode-duplicate",
|
||
conflictProduct:"…", field:"code" }` — код занят **другим** товаром организации.
|
||
- `400` — пустой/слишком длинный код; `404` — товар не найден.
|
||
|
||
Уникальность проверяется в пределах организации (`(OrganizationId, Code)`).
|
||
|
||
## 3. Права текущего пользователя
|
||
`GET /api/me` теперь возвращает `permissions` — эффективные права (имя флага →
|
||
bool) **с учётом роли и простого/расширенного режима**. Мобильное приложение и веб
|
||
гейтят UI по одному источнику (скрывают одно и то же).
|
||
|
||
```json
|
||
{
|
||
"sub":"…","name":"…","email":"…","roles":["Admin"],"orgId":"…",
|
||
"hasLiveOrg":true,"hasActiveEmployee":true,"advancedMode":false,
|
||
"permissions":{ "ProductsView":true,"ProductsEdit":true,"StocksView":true,
|
||
"DemandsView":false,"TransferEdit":false, … }
|
||
}
|
||
```
|
||
|
||
Правила: SuperAdmin — все права без ограничений; Admin — все, но в **простом режиме**
|
||
advanced-only скрыты; обычная роль — её флаги (в простом режиме advanced-only тоже
|
||
обнуляются). Advanced-only: `DemandsView/Edit/Post`, `TransferEdit`, `LoyaltyManage`,
|
||
`PromotionsManage`, `PriceTypesManage` (зеркалит серверный `[RequiresAdvancedMode]`).
|
||
Имена флагов — из `RolePermissions`. Веб использует тот же источник (`useCan()`).
|
||
|
||
## 4. Весовой признак в офлайн-каталоге
|
||
`GET /api/pos/v1/sync` (каталог для сканирования/офлайна) в `ProductSyncDto` отдаёт:
|
||
- `packaging` (1=штучный, 2=весовой, 3=разливной) и **`isWeighed`** (явный bool `packaging==2`),
|
||
- `unitCode` — единица измерения (шт/кг/л…).
|
||
|
||
## 5. Весовые этикетки (внутримагазинные EAN-13)
|
||
Как в МойСкладе: внутримагазинные EAN-13 с префиксом из диапазона 20–29, где зашиты
|
||
«код товара» (PLU, N цифр) и величина (вес в граммах → кг, либо цена).
|
||
|
||
**Настройка организации** — `GET/PUT /api/organization/settings` (PUT — право Admin):
|
||
- `weightBarcodeEnabled` (bool, по умолчанию true),
|
||
- `weightBarcodeWeightPrefixes` (CSV префиксов веса, null → `20,21,22,23,24`),
|
||
- `weightBarcodePricePrefixes` (CSV префиксов цены, null → `25,26,27,28,29`),
|
||
- `weightBarcodeCodeDigits` (длина PLU, по умолчанию 5).
|
||
Блок применяется, только если в PUT передан `weightBarcodeEnabled` (иначе настройки не трогаются).
|
||
|
||
**Разбор при поиске** — `GET /api/catalog/products/by-barcode/{value}`:
|
||
- Обычный штрихкод → товар (или `{ items:[…] }` при нескольких совпадениях), `404` если нет.
|
||
- **Весовая этикетка** (префикс весовой/ценовой) → `{ product:{…}, weighted:true,
|
||
valueKind:"WeightKg"|"Price", quantity:1.5, embeddedPrice:null }`. Товар ищется по
|
||
встроенному PLU (как короткий штрихкод товара либо как артикул); `quantity` — вес (кг),
|
||
`embeddedPrice` — зашитая цена (для ценовых этикеток).
|
||
|
||
Пример: этикетка `2112345015007` при дефолтных настройках → PLU `12345`, вес `1.500` кг.
|
||
|
||
Разбор единый для сервера и кассы — `foodmarket.Shared.Barcodes.WeightedBarcode`.
|
||
|
||
## Инвентаризация: post/unpost (временная совместимость)
|
||
|
||
С переходом на модель МойСклада инвентаризация сама остатки **не двигает** (см.
|
||
`docs/inventory-moysklad-reference.md`). Веб использует: `complete` → `create-enter` /
|
||
`create-loss`, затем проводит документы.
|
||
|
||
Мобильное приложение «FM Склад» исторически вызывает `post`/`unpost` инвентаризации.
|
||
Чтобы не ломать установленные приложения, эндпоинты **оставлены как совместимые**:
|
||
|
||
- `POST /api/inventory/inventories/{id}/post` — **= завершить + создать и ПРОВЕСТИ**
|
||
Оприходование (излишки) и Списание (недостача) одним вызовом. Идемпотентно: если
|
||
связанные документы уже созданы — не дублирует.
|
||
- `POST /api/inventory/inventories/{id}/unpost` — отменить результаты: отменить
|
||
проведение и удалить связанные Оприходование/Списание (остатки вернутся), вернуть
|
||
инвентаризацию в черновик.
|
||
|
||
**Статус: временное.** Рекомендуемый путь для новых версий мобильного приложения —
|
||
`complete` + `create-enter`/`create-loss` (как в вебе), чтобы кладовщик мог проверить
|
||
документы перед проведением. `post`/`unpost` будут помечены deprecated после обновления iOS.
|