> ## Documentation Index
> Fetch the complete documentation index at: https://docs.yume.cloud/llms.txt
> Use this file to discover all available pages before exploring further.

# Ресурсы и склад

> Складские ресурсы (тип штучный/количественный, мягкое удаление) и позиции задач мастерской, сервис складского учёта со списанием, возвратом и пересчётом под блокировкой строки, перевод складских ошибок в валидацию на границе формата и правило: возврат срабатывает при удалении позиции, а списание — только через API.

Полный перечень полей — в [глоссарии](/ru/logic/workshop/resources-glossary).

## Что описывает эта страница

Раздел отвечает за расходные материалы мастерской и их складской учёт. Здесь два тесно связанных объекта: **Складской ресурс** — сам расходник с запасом и остатком, и **Ресурс задачи мастерской** — строка привязки ресурса (или единицы инвентаря) к конкретной ремонтной задаче. Отдельный **Сервис складского учёта** отвечает за то, чтобы остаток корректно уменьшался при использовании расходника в задаче и возвращался при отмене.

Главная идея: остаток на складе — это «живое» число, которое автоматически меняется по мере того, как расходники добавляются в ремонтные задачи и убираются из них. При этом все проверки (хватает ли остатка, допустимо ли дробное количество) выполняются под блокировкой строки, чтобы два одновременных списания не «ушли в минус».

## Модель данных

### Складской ресурс

Расходный материал с учётом запаса. Хранит полный объём закупки и текущий доступный остаток.

| Поле              | Тип             | Назначение                                                                                                  |
| ----------------- | --------------- | ----------------------------------------------------------------------------------------------------------- |
| Название          | текст           | Наименование ресурса                                                                                        |
| Цена              | число (2 знака) | Цена за единицу                                                                                             |
| Тип учёта         | перечисление    | Штучный (целые единицы) или количественный (дробный)                                                        |
| Общее количество  | число (2 знака) | Полный запас на складе                                                                                      |
| Остаток           | число (2 знака) | Доступный остаток; при сохранении справочника проверяется, что он не меньше 0 и не больше общего количества |
| Удалён            | флаг            | Мягкое удаление: запись скрывается из списков, но физически остаётся                                        |
| Компания          | связь           | Арендатор-владелец записи                                                                                   |
| Создан / Обновлён | дата/время      | Служебные отметки времени                                                                                   |

**Тип учёта** определяет поведение количеств:

* **Штучный (целые единицы)** — количество всегда округляется до целого, дробные значения запрещены (например, «3 фильтра», но не «3,5»).
* **Количественный (дробный)** — допускается произвольное дробное значение (например, «2,75 литра краски»).

<Note>
  Удаление ресурса — мягкое: при удалении через интерфейс запись лишь помечается как удалённая и пропадает из списков и из выборки для чтения/редактирования. Сама строка и её остаток сохраняются в базе.
</Note>

### Ресурс задачи мастерской

Строка, которая связывает складской ресурс либо единицу/группу инвентаря с конкретной ремонтной задачей, фиксируя количество, стоимость позиции и статус подтверждения.

| Поле              | Тип             | Назначение                                                                             |
| ----------------- | --------------- | -------------------------------------------------------------------------------------- |
| Задача            | связь           | Ремонтная задача, к которой привязана позиция (обязательна)                            |
| Складской ресурс  | связь           | Списываемый со склада расходник (может отсутствовать; при удалении ресурса обнуляется) |
| Единица инвентаря | связь           | Привязанная единица инвентаря (может отсутствовать)                                    |
| Группа инвентаря  | связь           | Привязанная группа инвентаря (может отсутствовать)                                     |
| Тариф             | связь           | Тариф инвентаря; обнуляется при удалении тарифа                                        |
| Количество        | число (2 знака) | Списываемое/возвращаемое количество, по умолчанию 1                                    |
| Цена              | число (2 знака) | Стоимость позиции в задаче                                                             |
| Подтверждён       | флаг            | Признак подтверждения позиции                                                          |
| Кем подтверждён   | связь           | Пользователь, подтвердивший позицию                                                    |
| Доп. данные       | словарь (JSON)  | Произвольные дополнительные данные                                                     |
| Компания          | связь           | Арендатор-владелец записи                                                              |
| Создан / Обновлён | дата/время      | Служебные отметки времени                                                              |

<Info>
  Позиция может ссылаться на складской расходник, на единицу инвентаря или на группу инвентаря — поля независимы и все допускают пустое значение. Складской остаток трогается только тогда, когда заполнен именно **складской ресурс**; для «инвентарных» позиций списание со склада не выполняется.
</Info>

**Уникальность.** Для одной компании нельзя дважды привязать одну и ту же единицу инвентаря к одной и той же задаче — на уровне базы действует ограничение на сочетание «компания + единица инвентаря + задача», и оно применяется только когда единица инвентаря заполнена. Позиции без единицы инвентаря этим ограничением не связаны.

## Складской учёт: списание, возврат, пересчёт

Все операции над остатком собраны в едином **Сервисе складского учёта**. Каждая из них выполняется в транзакции и перед изменением остатка берёт **блокировку строки** ресурса — то есть до конца операции другая параллельная операция не сможет одновременно изменить этот же остаток. Это защищает от гонок при одновременном списании.

### Списание

Уменьшает остаток ресурса на заданное количество.

Порядок проверок:

1. Если ресурс не указан или количество не положительное — операция ничего не делает и молча завершается.
2. Строка ресурса блокируется и перечитывается из базы (берётся актуальный остаток, а не тот, что был в памяти).
3. Для штучного ресурса проверяется целостность количества: дробное значение приводит к **ошибке «дробное количество недопустимо»**.
4. Если доступный остаток меньше запрошенного — возникает **ошибка «недостаточно остатка»** с указанием имени ресурса, доступного и требуемого количества.
5. Иначе остаток уменьшается на запрошенное количество и сохраняется.

### Возврат

Возвращает количество в остаток. Новый остаток вычисляется как текущий остаток плюс возвращаемое количество, но **не выше общего количества** ресурса — то есть остаток никогда не превысит полный запас на складе. Пустой ресурс или неположительное количество игнорируются.

### Пересчёт

Применяется при **правке** позиции: сначала выполняется возврат старого количества старого ресурса, затем списание нового количества нового ресурса. Так корректно обрабатывается и смена ресурса, и изменение количества в уже существующей строке. Обе операции идут в одной транзакции, поэтому если новое списание не проходит (не хватает остатка или дробное штучное количество), уже сделанный возврат откатывается вместе с ним.

## Где и когда меняется остаток

Это ключевой и неочевидный момент раздела: списание/возврат остатка привязаны к разным точкам жизненного цикла позиции.

* **Списание** и **пересчёт** вызываются только внутри формата при создании и правке позиции через API. То есть остаток уменьшается ровно в момент, когда позицию добавляют к задаче или редактируют через соответствующий адрес.
* **Возврат** вызывается автоматически при удалении позиции. Поэтому когда удаляют саму ремонтную задачу, её позиции удаляются каскадно, для каждой срабатывает возврат, и остаток корректно возвращается на склад.

<Warning>
  Из-за этой асимметрии создание или изменение позиции **в обход API** (например, напрямую в базе, через админку или из служебного кода) **не спишет** остаток со склада. Возврат при этом всё равно сработает при удалении, поскольку он завязан на удаление записи, а не на адрес. Меняйте позиции только через штатные адреса, иначе учёт остатка разъедется.
</Warning>

Помимо складского учёта, при любом сохранении и удалении позиции автоматически запускается **пересчёт стоимости задачи** — суммарная стоимость ремонтной задачи всегда остаётся согласованной с набором её позиций.

## Обработка ошибок склада

Сервис складского учёта намеренно не знает про HTTP и бросает собственные, «фреймворко-независимые» исключения — «недостаточно остатка» и «дробное количество недопустимо». Превращение их в понятный ответ API происходит **на границе формата**: вызовы списания и пересчёта обёрнуты в **трансляцию складских ошибок**, которая переводит их в обычную ошибку валидации с текстом на русском:

* нехватка остатка → сообщение вида «Недостаточно «`название`». Доступно: …, требуется: …»;
* дробное количество для штучного ресурса → «`название` учитывается в целых единицах».

<Tip>
  Такое разделение позволяет использовать один и тот же складской сервис и из веб-запросов, и из фонового/служебного кода: бизнес-логика склада не завязана на слой API, а перевод в HTTP-ответ добавляется только там, где он нужен.
</Tip>

## Как это используется

### Справочник складских ресурсов

Список и создание ресурсов доступны аутентифицированным пользователям; из выборки всегда исключены мягко удалённые записи. При создании нового ресурса действует правило: если остаток явно не задан, он приравнивается к общему количеству — «свежий» запас изначально доступен целиком. Удаление ресурса переводит его в состояние «удалён» (мягкое удаление), а не стирает физически.

Для штучных ресурсов при сохранении справочника дробная часть у общего количества и остатка отбрасывается (значения приводятся к целым). Дополнительно проверяется, что остаток не отрицателен и не превышает общее количество.

### Позиции задачи

Позиции читаются и создаются в контексте конкретной задачи (задача берётся из адреса запроса, а не из тела). При создании позиции, если она ссылается на единицу инвентаря, помимо стандартных проверок выполняется собственная проверка на дубликат по сочетанию «задача + единица инвентаря + группа инвентаря», и при совпадении возвращается ошибка. Для позиций без единицы инвентаря эта проверка не выполняется. После сохранения строки происходит списание остатка привязанного складского ресурса; при правке — пересчёт (возврат старого, списание нового).

<Note>
  Проверка дубликата при сохранении (по задаче, единице и группе инвентаря) и жёсткое ограничение уникальности в базе (по компании, единице инвентаря и задаче) — это два разных барьера с немного разным набором полей. Оба срабатывают только при заполненной единице инвентаря. Ограничение базы — окончательная гарантия целостности; проверка формата даёт более раннее и понятное сообщение об ошибке.
</Note>

## Связанные страницы

<CardGroup cols={2}>
  <Card title="Глоссарий: Ресурсы и склад" icon="book" href="/ru/logic/workshop/resources-glossary">
    Полный перечень полей складского ресурса и позиции задачи мастерской.
  </Card>

  <Card title="Мастерская (ремонтные задачи)" icon="screwdriver-wrench" href="/ru/logic/workshop">
    Обзорная страница модуля мастерской и навигация по всем его разделам.
  </Card>

  <Card title="Воронка и задачи" icon="diagram-project" href="/ru/logic/workshop/pipeline">
    Kanban-воронки, этапы и ремонтные задачи с их статусами, порядком и агрегатами по списку.
  </Card>

  <Card title="Услуги" icon="screwdriver" href="/ru/logic/workshop/services">
    Привязка услуг из каталога к ремонтным задачам с тарифом, ценой и длительностью.
  </Card>

  <Card title="Стоимость и оплаты" icon="money-bill-transfer" href="/ru/logic/workshop/pricing">
    Автоагрегация цены задачи из ресурсов и услуг и суммы оплат из платежей, плюс статусы оплаты.
  </Card>

  <Card title="Метрика прибыли" icon="chart-line" href="/ru/logic/workshop/profit">
    Проецирование прибыли по ресурсам и услугам завершённой задачи в общую таблицу метрик.
  </Card>

  <Card title="Автосмена состояния инвентаря" icon="arrows-rotate" href="/ru/logic/workshop/inventory-state">
    Автоматический перевод инвентаря в целевое состояние при движении задачи по воронке.
  </Card>
</CardGroup>
