--- /dev/null
+# Product1cReplacementController — Таблица экшенов
+
+| № | Экшен | HTTP | Назначение | Параметры | Возврат | Особенности |
+|---|-------|------|------------|-----------|---------|-------------|
+| 1 | `actionIndex()` | GET/POST | Список замен с группировкой по GUID | - | `render('index')` | POST: загрузка XLSX через `Product1cReplacementService::uploadTemplateReplacement()` |
+| 2 | `actionView($id)` | GET | Просмотр товара и всех его замен | `$id` (GUID товара) | `render('view')` | Расчёт разницы цен, загрузка последних логов, информация об администраторах |
+| 3 | `actionCreate()` | GET/POST | Создание новых замен для товара | - | `render('create')` или `redirect(['index'])` | Множественное создание в транзакции, логирование каждой записи |
+| 4 | `actionSearch($q, $exclude, $guid)` | GET | AJAX-поиск товаров для Select2 | `$q`, `$exclude`, `$guid` | JSON: `{items: [{id, text}]}` | Исключение существующих замен и основных товаров |
+| 5 | `actionUpdate($id)` | GET/POST | Редактирование замен товара | `$id` (GUID товара) | `render('update')` или `redirect(['view'])` | Обновление/создание в одной транзакции, логирование только изменений |
+| 6 | `actionLogHistory($guid)` | GET | История изменений замен для товара | `$guid` | `render('log-history')` | Пагинация 20 записей, сортировка ASC по дате |
+| 7 | `actionDelete($id)` | POST | Мягкое удаление замены | `$id` (ID записи) | JSON: `{success, message, redirect}` | Логирование удаления, `redirect: true` если удалена последняя замена |
+
+## Вспомогательные методы
+
+| Метод | Область | Назначение | Параметры | Возврат |
+|-------|---------|------------|-----------|---------|
+| `findModel($id)` | protected | Поиск записи по ID | `$id` | `Product1cReplacement` или 404 |
+| `findModelByGuid($id)` | protected | Поиск записи по GUID | `$id` (GUID) | `Product1cReplacement` или 404 |
+| `logReplacementAction($replacementId, $stateBefore, $stateAfter)` | public static | Запись изменений в лог | `$replacementId`, `$stateBefore`, `$stateAfter` | void (throws Exception при ошибке) |
+
+## Маршруты (примеры)
+
+```
+GET /product1c-replacement/index
+POST /product1c-replacement/index (загрузка XLSX)
+GET /product1c-replacement/view?id={guid}
+GET /product1c-replacement/create
+POST /product1c-replacement/create
+GET /product1c-replacement/search?q={query}&exclude={ids}&guid={guid}
+GET /product1c-replacement/update?id={guid}
+POST /product1c-replacement/update?id={guid}
+GET /product1c-replacement/log-history?guid={guid}
+POST /product1c-replacement/delete?id={id}
+```
+
+## Формат данных POST-запросов
+
+### actionCreate / actionUpdate
+```php
+[
+ 'Product1cReplacement' => [
+ 'guid' => 'основной-товар-guid',
+ 'guid_replacement' => [
+ 'замена-1-guid',
+ 'замена-2-guid',
+ 'замена-3-guid'
+ ],
+ 'replacement_ids' => [ // только для update
+ '42', // ID существующей записи
+ '', // пустое значение = создать новую
+ '43'
+ ]
+ ]
+]
+```
+
+### actionIndex (загрузка файла)
+```
+POST /product1c-replacement/index
+Content-Type: multipart/form-data
+
+myfile: template_replacement.xlsx
+```
+
+## Формат JSON-ответов
+
+### actionSearch
+```json
+{
+ "items": [
+ {
+ "id": "123-guid-abc",
+ "text": "Молоко 3.2% (ART-12345)"
+ },
+ {
+ "id": "456-guid-def",
+ "text": "Кефир 2.5% (ART-67890)"
+ }
+ ]
+}
+```
+
+### actionDelete
+```json
+{
+ "success": true,
+ "message": "Запись успешно удалена",
+ "redirect": false
+}
+```
+
+**При ошибке:**
+```json
+{
+ "success": false,
+ "message": "Ошибка при удалении записи."
+}
+```
+
+## Особенности работы экшенов
+
+### actionIndex
+- **Двойной режим:** GET (список) / POST (загрузка XLSX)
+- **Группировка:** Выбирает уникальные `guid`, затем для каждого загружает все `guid_replacement`
+- **Результат группировки:** `['guid-A' => ['guid-B', 'guid-C'], 'guid-D' => ['guid-E']]`
+
+### actionView
+- **Комплексная загрузка:** модель + замены + логи + цены + администраторы
+- **Расчёт разницы:** `priceDifference = priceReplacement - priceOriginal`
+- **Структура lastLogs:**
+ ```php
+ [
+ 42 => [
+ 'lastLog' => Product1cReplacementLog,
+ 'adminName' => 'Иван Иванов',
+ 'productReplacementPrice' => Prices,
+ 'priceDifference' => 150
+ ]
+ ]
+ ```
+
+### actionCreate
+- **Транзакция:** Все замены создаются атомарно
+- **Логирование:** Каждая запись получает лог `'Запись создана' → guid_replacement`
+- **Rollback:** При ошибке в любой замене откатываются все
+
+### actionSearch
+- **Три режима исключений:**
+ 1. `$guid = "guid"` → исключить все основные товары
+ 2. `$guid = {конкретный GUID}` → исключить замены для этого товара
+ 3. `$exclude = "1,2,3"` → исключить конкретные ID
+- **Лимит:** 20 результатов
+- **Поиск:** ILIKE по названию товара
+
+### actionUpdate
+- **Гибридная логика:**
+ - Если `replacement_ids[$index]` заполнен → UPDATE
+ - Если пуст → INSERT
+- **Логирование:** Только при изменении `guid_replacement`
+- **Транзакция:** Все изменения атомарны
+
+### actionLogHistory
+- **Агрегация:** Находит все ID замен для товара, затем их логи
+- **Сортировка:** ASC (от старых к новым)
+- **Дополнительно:** Получает название товара из `Products1c`
+
+### actionDelete
+- **Soft Delete:** Установка `deleted_at`
+- **Подсчёт остатка:** Определяет, нужен ли редирект на index
+- **JSON ответ:** Для AJAX-обработки на клиенте
+- **Логирование:** `guid_replacement → 'Запись удалена'`
+
+## Зависимости между экшенами
+
+```
+index → create → index (redirect после создания)
+index → view → update → view (redirect после обновления)
+view → delete (AJAX) → view/index (по условию)
+view → log-history (отдельная страница)
+create/update → search (AJAX для Select2)
+```
+
+## Patterns использования
+
+### 1. Создание замен
+```
+GET /product1c-replacement/create
+ ↓ (выбор товаров через search)
+POST /product1c-replacement/create (с массивом guid_replacement)
+ ↓
+redirect /product1c-replacement/index
+```
+
+### 2. Редактирование замен
+```
+GET /product1c-replacement/view?id={guid}
+ ↓
+GET /product1c-replacement/update?id={guid}
+ ↓ (изменение через search)
+POST /product1c-replacement/update?id={guid}
+ ↓
+redirect /product1c-replacement/view?id={guid}
+```
+
+### 3. Просмотр истории
+```
+GET /product1c-replacement/view?id={guid}
+ ↓ (клик на "История")
+GET /product1c-replacement/log-history?guid={guid}
+```
+
+### 4. Удаление замены
+```
+GET /product1c-replacement/view?id={guid}
+ ↓ (клик "Удалить" на конкретной замене)
+AJAX POST /product1c-replacement/delete?id={replacement_id}
+ ↓ (ответ JSON)
+if (redirect) → redirect /product1c-replacement/index
+else → reload view
+```
--- /dev/null
+# Product1cReplacementController — Анализ
+
+## Общая информация
+
+**Файл:** `erp24/controllers/crud/Product1cReplacementController.php`
+**Пространство имён:** `yii_app\controllers\crud`
+**Родительский класс:** `yii\web\Controller`
+**Размер:** 480 строк
+
+## Назначение
+
+CRUD-контроллер для управления **заменами товаров из 1С**. Позволяет связывать товары с их заменителями, отслеживать изменения через систему логирования, загружать шаблоны замен и управлять историей изменений.
+
+---
+
+## Основные особенности
+
+### 1. **Групповое управление заменами**
+- Один товар может иметь несколько заменителей
+- Система автоматически группирует замены по GUID
+- Поддержка множественного выбора заменителей
+
+### 2. **Система логирования**
+- Все изменения записываются в `Product1cReplacementLog`
+- Отслеживание `state_before` → `state_after`
+- Привязка к администратору, выполнившему действие
+- История изменений с отображением автора
+
+### 3. **Загрузка шаблонов**
+- Импорт замен через XLSX файлы
+- Обработка через `Product1cReplacementService::uploadTemplateReplacement()`
+- Валидация данных при загрузке
+
+### 4. **Расчёт разницы цен**
+- Автоматический расчёт разницы между основным товаром и заменой
+- Интеграция с `Prices` для получения актуальных цен
+- Отображение разницы в интерфейсе
+
+### 5. **Soft Delete**
+- Мягкое удаление записей (`deleted_at`)
+- Сохранение истории удалений в логах
+- Автоматический редирект при удалении последней замены
+
+---
+
+## Структура экшенов
+
+### 1. `actionIndex()`
+**Назначение:** Список всех замен с группировкой по GUID
+
+**Особенности:**
+- Обработка POST-запросов для загрузки XLSX
+- Группировка замен по `guid` с выборкой `guid_replacement`
+- Поиск через `Product1cReplacementSearch`
+- Передача `groupedReplacements` для отображения связей
+
+**Логика:**
+```php
+$models = $dataProvider->query->select(['guid'])->distinct()->all();
+$groupedReplacements = [];
+
+foreach ($models as $model) {
+ $groupedReplacements[$model->guid] = Product1cReplacement::find()
+ ->select('guid_replacement')
+ ->where(['guid' => $model->guid])
+ ->andWhere(['deleted_at' => null])
+ ->column();
+}
+```
+
+---
+
+### 2. `actionView($id)`
+**Назначение:** Просмотр товара и всех его замен
+
+**Параметры:**
+- `$id` — GUID основного товара
+
+**Функциональность:**
+- Получение всех активных замен для товара
+- Загрузка последнего лога для каждой замены
+- Расчёт разницы цен между основным товаром и заменами
+- Получение имени администратора из `Admin`
+
+**Логика расчёта разницы:**
+```php
+$priceDifference = null;
+if ($productPrice !== null && $productReplacementPrice !== null) {
+ $priceDifference = (int)($productReplacementPrice->price) - (int)($productPrice->price);
+}
+```
+
+**Передаваемые данные:**
+- `model` — основной товар
+- `replacements` — ActiveDataProvider с заменами
+- `lastLogs` — массив последних логов с информацией об администраторах
+- `productPrice` — цена основного товара
+
+---
+
+### 3. `actionCreate()`
+**Назначение:** Создание новых замен для товара
+
+**Логика:**
+1. Получение массива `guid_replacement` из формы
+2. Начало транзакции
+3. Создание отдельной записи для каждого заменителя
+4. Логирование создания каждой записи
+5. Commit транзакции или rollback при ошибке
+
+**Код создания:**
+```php
+foreach ($guidReplacements as $replacementGuid) {
+ $replacementModel = new Product1cReplacement([
+ 'guid' => $model->guid,
+ 'guid_replacement' => $replacementGuid,
+ ]);
+ if (!$replacementModel->save()) {
+ throw new \Exception('Ошибка сохранения замены.');
+ }
+ self::logReplacementAction(
+ $replacementModel->id,
+ 'Запись создана',
+ $replacementGuid
+ );
+}
+```
+
+**Особенности:**
+- Использование транзакций для целостности данных
+- Автоматическое логирование при создании
+- Редирект на index после успеха
+
+---
+
+### 4. `actionSearch($q, $exclude, $guid)`
+**Назначение:** AJAX-поиск товаров для Select2
+
+**Параметры:**
+- `$q` — строка поиска по названию
+- `$exclude` — список ID для исключения (через запятую)
+- `$guid` — GUID товара для исключения уже добавленных замен
+
+**Логика исключений:**
+```php
+if ($guid) {
+ if ($guid == "guid") {
+ // Исключить товары, которые уже являются основными
+ $existingGuids = Product1cReplacement::find()
+ ->select(['guid'])
+ ->distinct()
+ ->column();
+ $query->andWhere(['not in', 'id', $existingGuids]);
+ } else {
+ // Исключить уже добавленные замены
+ $existingReplacements = Product1cReplacement::find()
+ ->select('guid_replacement')
+ ->where(['guid' => $guid])
+ ->column();
+ $query->andWhere(['not in', 'id', $existingReplacements]);
+ }
+}
+```
+
+**Формат ответа:**
+```json
+{
+ "items": [
+ {
+ "id": "guid",
+ "text": "Название товара (артикул)"
+ }
+ ]
+}
+```
+
+---
+
+### 5. `actionUpdate($id)`
+**Назначение:** Редактирование замен товара
+
+**Параметры:**
+- `$id` — GUID основного товара
+
+**Логика:**
+1. Загрузка всех активных замен
+2. Получение массивов `guid_replacement` и `replacement_ids` из POST
+3. Для каждой замены:
+ - Если `replacement_ids[$index]` заполнен → обновление существующей
+ - Если пуст → создание новой записи
+4. Логирование только при изменении данных
+
+**Код обновления:**
+```php
+foreach ($replacementData as $index => $replacementGuid) {
+ if (!empty($replacementIds[$index])) {
+ // Обновление существующей
+ $replacementModel = Product1cReplacement::findOne($replacementIds[$index]);
+ if ($replacementModel && $replacementModel->guid_replacement !== $replacementGuid) {
+ $stateBefore = $replacementModel->guid_replacement;
+ $replacementModel->guid_replacement = $replacementGuid;
+ if ($replacementModel->save()) {
+ self::logReplacementAction(
+ $replacementModel->id,
+ $stateBefore,
+ $replacementGuid
+ );
+ }
+ }
+ } else {
+ // Создание новой
+ $replacementModel = new Product1cReplacement([
+ 'guid' => $model->guid,
+ 'guid_replacement' => $replacementGuid,
+ ]);
+ if ($replacementModel->save()) {
+ self::logReplacementAction(
+ $replacementModel->id,
+ 'Запись создана',
+ $replacementGuid
+ );
+ }
+ }
+}
+```
+
+---
+
+### 6. `actionLogHistory($guid)`
+**Назначение:** Просмотр истории изменений замен для товара
+
+**Параметры:**
+- `$guid` — GUID основного товара
+
+**Функциональность:**
+- Получение всех ID замен для товара
+- Выборка логов из `Product1cReplacementLog`
+- Сортировка по дате (ASC)
+- Пагинация по 20 записей
+
+**Передаваемые данные:**
+- `dataProvider` — логи изменений
+- `guid` — идентификатор товара
+- `name` — название товара
+
+---
+
+### 7. `actionDelete($id)`
+**Назначение:** Мягкое удаление замены
+
+**Параметры:**
+- `$id` — ID записи замены
+
+**Формат ответа:** JSON
+
+**Логика:**
+1. Подсчёт оставшихся активных замен для товара
+2. Логирование удаления
+3. Вызов `softDelete()`
+4. Возврат JSON с флагом `redirect` (если удалена последняя замена)
+
+**Пример ответа:**
+```json
+{
+ "success": true,
+ "message": "Запись успешно удалена",
+ "redirect": false
+}
+```
+
+---
+
+## Вспомогательные методы
+
+### 1. `findModel($id)`
+Поиск записи по ID с выбросом 404 при отсутствии.
+
+### 2. `findModelByGuid($id)`
+Поиск первой записи по GUID (для получения основного товара).
+
+### 3. `logReplacementAction($replacementId, $stateBefore, $stateAfter)` (static)
+**Назначение:** Запись изменений в лог
+
+**Логика:**
+1. Проверка существования идентичного лога (дедупликация)
+2. Создание записи `Product1cReplacementLog`
+3. Запись ошибок в лог при неудаче
+
+**Код:**
+```php
+$existingLog = Product1cReplacementLog::find()
+ ->where([
+ 'replacement_id' => $replacementId,
+ 'state_before' => $stateBefore,
+ 'state_after' => $stateAfter,
+ 'admin_id' => Yii::$app->user->id,
+ ])
+ ->exists();
+
+if ($existingLog) {
+ return;
+}
+
+$log = new Product1cReplacementLog([
+ 'replacement_id' => $replacementId,
+ 'state_before' => $stateBefore,
+ 'state_after' => $stateAfter,
+ 'date' => date('Y-m-d H:i:s'),
+ 'admin_id' => Yii::$app->user->id,
+]);
+
+if (!$log->save()) {
+ Yii::error('Ошибка записи в лог: ' . json_encode($log->errors), __METHOD__);
+ throw new \Exception('Ошибка записи в лог.');
+}
+```
+
+---
+
+## Зависимости
+
+### Модели
+- `Product1cReplacement` — основная модель замен
+- `Product1cReplacementSearch` — модель поиска
+- `Product1cReplacementLog` — логи изменений
+- `Products1c` — справочник товаров 1С
+- `Prices` — цены товаров
+- `Admin` — администраторы системы
+
+### Сервисы
+- `Product1cReplacementService::uploadTemplateReplacement()` — загрузка XLSX
+
+### Behaviors
+- `TimestampBehavior` — автоматические метки времени
+- `VerbFilter` — (не используется, пустой массив actions)
+
+---
+
+## Особенности реализации
+
+### 1. **Множественные замены**
+Система хранит каждую замену как отдельную запись:
+```
+guid = "A" → guid_replacement = "B"
+guid = "A" → guid_replacement = "C"
+guid = "A" → guid_replacement = "D"
+```
+
+### 2. **Транзакционность**
+Все операции создания/обновления обёрнуты в транзакции для целостности данных.
+
+### 3. **AJAX интеграция**
+- `actionSearch` для Select2
+- `actionDelete` возвращает JSON
+
+### 4. **Дедупликация логов**
+Предотвращение повторных идентичных записей в логах.
+
+### 5. **Soft Delete Pattern**
+Использование `deleted_at` вместо физического удаления.
+
+---
+
+## Безопасность
+
+### Проблемы:
+❌ **Отсутствие RBAC-проверок**
+❌ **Нет `accessRules` или фильтров доступа**
+❌ **Любой авторизованный пользователь может управлять заменами**
+
+### Рекомендации:
+1. Добавить `AccessControl` behavior
+2. Реализовать проверку прав для каждого экшена
+3. Валидировать права на изменение критичных данных
+
+---
+
+## Производительность
+
+### Узкие места:
+⚠️ **N+1 запросы в `actionView()`** (цикл по заменам + запросы логов и цен)
+⚠️ **Множественные запросы в `actionIndex()`** (цикл группировки замен)
+
+### Оптимизации:
+1. Использовать `joinWith()` для загрузки связанных данных
+2. Кэшировать результаты группировки
+3. Добавить индексы на `guid`, `guid_replacement`, `deleted_at`
+
+---
+
+## Примеры использования
+
+### 1. Создание замены
+```
+POST /product1c-replacement/create
+Body:
+ Product1cReplacement[guid] = "123-guid"
+ Product1cReplacement[guid_replacement][] = "456-guid"
+ Product1cReplacement[guid_replacement][] = "789-guid"
+```
+
+### 2. Поиск товаров
+```
+GET /product1c-replacement/search?q=молоко&guid=123-guid
+Response: {"items": [{"id": "999-guid", "text": "Молоко 3.2% (ART123)"}]}
+```
+
+### 3. Удаление замены
+```
+POST /product1c-replacement/delete?id=42
+Response: {"success": true, "message": "Запись успешно удалена", "redirect": false}
+```
+
+---
+
+## Связанные компоненты
+
+- **Модели:** `Product1cReplacement`, `Product1cReplacementLog`, `Products1c`, `Prices`
+- **Сервисы:** `Product1cReplacementService`
+- **Views:** `index`, `view`, `create`, `update`, `log-history`
+- **Функциональность:** CRUD, логирование, поиск, импорт, расчёт цен
+
+---
+
+## Выводы
+
+**Product1cReplacementController** — сложный CRUD-контроллер с расширенной функциональностью:
+- ✅ Множественные связи
+- ✅ Полное логирование изменений
+- ✅ AJAX-поиск
+- ✅ Импорт данных
+- ✅ Расчёт разницы цен
+- ✅ Транзакционность
+
+**Требует доработки:**
+- ❌ Отсутствие контроля доступа
+- ⚠️ Оптимизация запросов
+- ⚠️ Добавление валидации прав
--- /dev/null
+# Product1cReplacementController — Быстрый справочник
+
+## Основная информация
+
+**Контроллер:** `Product1cReplacementController`
+**Namespace:** `yii_app\controllers\crud`
+**Путь:** `erp24/controllers/crud/Product1cReplacementController.php`
+**Родитель:** `yii\web\Controller`
+**Назначение:** CRUD управление заменами товаров из 1С с логированием изменений
+
+---
+
+## Быстрый доступ
+
+| Действие | URL | Метод |
+|----------|-----|-------|
+| Список замен | `/product1c-replacement/index` | GET |
+| Загрузка XLSX | `/product1c-replacement/index` | POST |
+| Просмотр товара | `/product1c-replacement/view?id={guid}` | GET |
+| Создание замен | `/product1c-replacement/create` | GET/POST |
+| Редактирование | `/product1c-replacement/update?id={guid}` | GET/POST |
+| История логов | `/product1c-replacement/log-history?guid={guid}` | GET |
+| Поиск товаров | `/product1c-replacement/search?q={query}` | GET |
+| Удаление замены | `/product1c-replacement/delete?id={id}` | POST (AJAX) |
+
+---
+
+## Ключевые экшены
+
+### 1. **actionIndex** — Список и загрузка
+- **GET:** Показывает список замен с группировкой по GUID
+- **POST:** Загружает XLSX файл с шаблоном замен
+- **Особенность:** Группирует замены: `['guid-A' => ['guid-B', 'guid-C']]`
+
+### 2. **actionView** — Детальный просмотр
+- Показывает товар + все замены + историю + разницу цен
+- Загружает последний лог для каждой замены
+- Отображает имена администраторов, выполнивших изменения
+
+### 3. **actionCreate** — Создание замен
+- Позволяет выбрать несколько заменителей для одного товара
+- Использует транзакцию для атомарности
+- Логирует каждую созданную запись
+
+### 4. **actionSearch** — AJAX-поиск
+- Для Select2 виджета
+- Исключает уже добавленные замены
+- Формат: `{items: [{id, text}]}`
+
+### 5. **actionUpdate** — Редактирование
+- Обновляет существующие замены
+- Добавляет новые замены
+- Логирует только изменения
+
+### 6. **actionLogHistory** — История изменений
+- Показывает все логи для товара
+- Пагинация 20 записей
+- Сортировка от старых к новым
+
+### 7. **actionDelete** — Мягкое удаление
+- Устанавливает `deleted_at`
+- Возвращает JSON с флагом `redirect`
+- Логирует удаление
+
+---
+
+## Модели и зависимости
+
+### Модели
+- **Product1cReplacement** — основная модель замен
+- **Product1cReplacementLog** — логи изменений
+- **Product1cReplacementSearch** — модель поиска
+- **Products1c** — справочник товаров 1С
+- **Prices** — цены товаров
+- **Admin** — администраторы
+
+### Сервисы
+- **Product1cReplacementService::uploadTemplateReplacement()** — загрузка XLSX
+
+---
+
+## Система логирования
+
+### Метод: `logReplacementAction($replacementId, $stateBefore, $stateAfter)`
+
+**Записывает в лог:**
+- ID замены
+- Состояние до изменения
+- Состояние после изменения
+- Дата и время
+- ID администратора
+
+**Предотвращает дубликаты:** Проверяет существование идентичного лога перед сохранением.
+
+**События логирования:**
+| Событие | state_before | state_after |
+|---------|--------------|-------------|
+| Создание | `'Запись создана'` | `guid_replacement` |
+| Изменение | старый `guid_replacement` | новый `guid_replacement` |
+| Удаление | старый `guid_replacement` | `'Запись удалена'` |
+
+---
+
+## Особенности реализации
+
+### 1. Множественные замены
+Один товар может иметь несколько замен, каждая хранится отдельной записью:
+```
+guid = "A" → guid_replacement = "B"
+guid = "A" → guid_replacement = "C"
+guid = "A" → guid_replacement = "D"
+```
+
+### 2. Транзакционность
+Все операции создания/обновления выполняются в транзакциях:
+```php
+$transaction = Yii::$app->db->beginTransaction();
+try {
+ // операции
+ $transaction->commit();
+} catch (\Exception $e) {
+ $transaction->rollBack();
+ throw $e;
+}
+```
+
+### 3. Расчёт разницы цен
+Автоматически вычисляется разница между ценой основного товара и замены:
+```php
+$priceDifference = (int)($productReplacementPrice->price) - (int)($productPrice->price);
+```
+
+### 4. Soft Delete
+Используется мягкое удаление через `deleted_at`:
+```php
+$model->softDelete(); // вместо $model->delete()
+```
+
+### 5. AJAX интеграция
+- `actionSearch` — для Select2 виджета
+- `actionDelete` — возвращает JSON для AJAX-обработки
+
+---
+
+## Примеры использования
+
+### Создание замены
+```php
+POST /product1c-replacement/create
+
+Product1cReplacement[guid] = "123-guid"
+Product1cReplacement[guid_replacement][] = "456-guid"
+Product1cReplacement[guid_replacement][] = "789-guid"
+```
+
+### Поиск товаров
+```php
+GET /product1c-replacement/search?q=молоко&guid=123-guid
+
+Response:
+{
+ "items": [
+ {"id": "999-guid", "text": "Молоко 3.2% (ART123)"}
+ ]
+}
+```
+
+### Удаление замены
+```javascript
+$.post('/product1c-replacement/delete?id=42', function(response) {
+ if (response.success) {
+ if (response.redirect) {
+ window.location.href = '/product1c-replacement/index';
+ } else {
+ location.reload();
+ }
+ }
+});
+```
+
+### Загрузка XLSX
+```html
+<form method="post" enctype="multipart/form-data">
+ <input type="file" name="myfile">
+ <button type="submit">Загрузить</button>
+</form>
+```
+
+---
+
+## Безопасность
+
+### ⚠️ Проблемы:
+- **Нет RBAC-проверок**
+- **Нет `AccessControl` behavior**
+- **Любой авторизованный пользователь может управлять заменами**
+
+### ✅ Рекомендации:
+1. Добавить `AccessControl` behavior
+2. Реализовать проверку прав для каждого экшена
+3. Валидировать права на изменение критичных данных
+
+---
+
+## Производительность
+
+### ⚠️ Узкие места:
+- **N+1 запросы в `actionView()`** (цикл по заменам + запросы логов и цен)
+- **Множественные запросы в `actionIndex()`** (цикл группировки замен)
+
+### ✅ Оптимизации:
+1. Использовать `joinWith()` для загрузки связанных данных
+2. Кэшировать результаты группировки
+3. Добавить индексы на `guid`, `guid_replacement`, `deleted_at`
+
+---
+
+## Диаграмма потока данных
+
+```mermaid
+graph TD
+ A[Index] -->|Создать| B[Create]
+ A -->|Просмотр| C[View]
+ A -->|Загрузить XLSX| D[uploadTemplateReplacement]
+
+ B -->|POST| E[Транзакция]
+ E -->|Создание замен| F[logReplacementAction]
+ F -->|Redirect| A
+
+ C -->|Редактировать| G[Update]
+ C -->|История| H[LogHistory]
+ C -->|Удалить| I[Delete]
+
+ G -->|POST| J[Транзакция]
+ J -->|Обновление/создание| F
+ J -->|Redirect| C
+
+ I -->|softDelete| F
+ I -->|JSON| K[AJAX Response]
+ K -->|redirect=true| A
+ K -->|redirect=false| C
+
+ B -.->|AJAX| L[Search]
+ G -.->|AJAX| L
+```
+
+---
+
+## Связанные компоненты
+
+### Контроллеры
+- `Product1cReplacementController` (текущий)
+
+### Модели
+- `Product1cReplacement`
+- `Product1cReplacementLog`
+- `Product1cReplacementSearch`
+- `Products1c`
+- `Prices`
+- `Admin`
+
+### Сервисы
+- `Product1cReplacementService`
+
+### Views
+- `views/product1c-replacement/index.php`
+- `views/product1c-replacement/view.php`
+- `views/product1c-replacement/create.php`
+- `views/product1c-replacement/update.php`
+- `views/product1c-replacement/log-history.php`
+
+---
+
+## Чеклист разработчика
+
+### При создании замен:
+- [ ] Проверить наличие товара в `Products1c`
+- [ ] Убедиться, что заменители существуют
+- [ ] Использовать транзакции
+- [ ] Логировать каждую операцию
+
+### При обновлении:
+- [ ] Проверить права доступа
+- [ ] Логировать только изменения
+- [ ] Обрабатывать создание новых и обновление существующих
+
+### При удалении:
+- [ ] Использовать soft delete
+- [ ] Логировать удаление
+- [ ] Проверить количество оставшихся замен
+- [ ] Вернуть корректный JSON
+
+### При разработке новых функций:
+- [ ] Добавить RBAC-проверки
+- [ ] Оптимизировать запросы (избегать N+1)
+- [ ] Использовать транзакции для критичных операций
+- [ ] Логировать все изменения данных
+
+---
+
+## FAQ
+
+**Q: Как добавить несколько замен одновременно?**
+A: В форме создания/редактирования используется Select2 с множественным выбором. Массив `guid_replacement[]` обрабатывается циклом.
+
+**Q: Почему при удалении последней замены происходит редирект?**
+A: Чтобы избежать ошибки 404 на странице `view`, если у товара не осталось замен.
+
+**Q: Как работает дедупликация логов?**
+A: Перед сохранением проверяется существование записи с идентичными `replacement_id`, `state_before`, `state_after`, `admin_id`.
+
+**Q: Можно ли восстановить удалённую замену?**
+A: Да, через SQL: `UPDATE product1c_replacement SET deleted_at = NULL WHERE id = ?`
+
+**Q: Как посмотреть полную историю изменений?**
+A: Перейти в `actionLogHistory` для конкретного товара (GUID).
+
+---
+
+## Полезные SQL-запросы
+
+### Все замены для товара
+```sql
+SELECT * FROM product1c_replacement
+WHERE guid = 'your-guid' AND deleted_at IS NULL;
+```
+
+### История изменений замены
+```sql
+SELECT l.*, a.name as admin_name
+FROM product1c_replacement_log l
+LEFT JOIN admin a ON l.admin_id = a.id
+WHERE l.replacement_id = 42
+ORDER BY l.date ASC;
+```
+
+### Товары без замен
+```sql
+SELECT p.* FROM products1c p
+LEFT JOIN product1c_replacement r ON p.id = r.guid
+WHERE r.id IS NULL AND p.tip = 'products';
+```
+
+### Замены с разницей цен > 100
+```sql
+SELECT
+ r.guid,
+ r.guid_replacement,
+ p1.price as original_price,
+ p2.price as replacement_price,
+ (p2.price - p1.price) as difference
+FROM product1c_replacement r
+JOIN prices p1 ON r.guid = p1.product_id
+JOIN prices p2 ON r.guid_replacement = p2.product_id
+WHERE (p2.price - p1.price) > 100
+AND r.deleted_at IS NULL;
+```
+
+---
+
+## Ключевые выводы
+
+✅ **Сильные стороны:**
+- Полное логирование изменений
+- Транзакционность операций
+- AJAX интеграция
+- Импорт данных через XLSX
+- Расчёт разницы цен
+
+❌ **Требует доработки:**
+- Отсутствие контроля доступа
+- Оптимизация запросов (N+1)
+- Валидация прав пользователей