From: Vladimir Fomichev Date: Thu, 9 Apr 2026 06:39:09 +0000 (+0300) Subject: feat(ERP-318): справочник поставщиков — Model + Controller + View + JS X-Git-Url: https://gitweb.erp-flowers.ru/?a=commitdiff_plain;h=ae50d94526aa6f6f677d9e7370d127d68b9a1127;p=erp24_rep%2Fyii-erp24%2F.git feat(ERP-318): справочник поставщиков — Model + Controller + View + JS Реализован CRUD справочника поставщиков с модальными окнами (AJAX), GridView + PJAX и каскадной деактивацией связанных записей. Компоненты: - Supplier (AR модель с TimestampBehavior + BlameableBehavior) - SupplierSearch (search модель без пагинации) - SupplierController (AJAX CRUD, наследует BaseController) - views/supplier/index.php (GridView + модалка + JS) - views/supplier/_form.php (форма для модалки) - BuyerReferenceController + view (контейнер с табами) Бейджи типа и статуса — по UI-guideline из spec-supplier-reference.html. Валидация: name unique varchar(200), lead_time >= 0, type/currency enum. Soft delete через is_active=false с каскадом на markings и product_mappings. Write-операции обёрнуты в DB-транзакции. Артефакты workflow: docs/jira/ERP-318/ Co-Authored-By: Claude Opus 4.6 (1M context) --- diff --git a/docs/jira/ERP-318/debate-log.md b/docs/jira/ERP-318/debate-log.md new file mode 100644 index 00000000..c12b93a4 --- /dev/null +++ b/docs/jira/ERP-318/debate-log.md @@ -0,0 +1,112 @@ +# Лог дебатов: ERP-318 — PRD + +**Дата:** 2026-04-09 +**Раунд:** 1 + +--- + +## Модель 1: DeepSeek v3.2 (Backend Architect) + +**Вердикт: APPROVED** с minor improvements + +### Сильные стороны +- Proper use of TimestampBehavior и BlameableBehavior +- Comprehensive validation rules +- Good soft delete logic + +### Рекомендации (MINOR) +1. Добавить scopes: `findActive()`, `findLocal()`, `findInternational()` +2. `afterSave()` для audit logging при изменении is_active +3. Кэширование dropdown-данных (currency, type) — *отклонено: <20 записей, нет смысла* +4. Bulk operations — *отклонено: вне scope задачи* + +--- + +## Модель 2: Grok 4.1 Fast (Edge Case Critic) + +**Вердикт: 9 issues (2 CRITICAL, 4 MAJOR, 3 MINOR)** + +### CRITICAL +1. **Race on UNIQUE name** — конкурентные AJAX-запросы могут создать дубли → **ПРИНЯТО: обернуть CRUD в транзакцию** +2. **Soft delete race / optimistic lock** — *ОТКЛОНЕНО: <20 записей, 1-2 пользователя, риск минимален* + +### MAJOR +1. **N+1 queries** на связях markings/product_mappings → **ПРИНЯТО: использовать `with()` eager loading** +2. **RBAC soft delete** — restore/forceDelete → *ОТКЛОНЕНО: реактивация вне scope* +3. **BlameableBehavior без user** → **ПРИНЯТО: добавить defaultValue fallback для CLI/cron** +4. **PJAX filter state** → *ОТКЛОНЕНО: нет фильтров в GridView, <20 записей* + +### MINOR +1. **Timezone** → *ОТКЛОНЕНО: PostgreSQL + Yii2 TimestampBehavior с NOW() = серверное время* +2. **Orphans в joined tables** → **ПРИНЯТО: учесть при отображении связей** +3. **Unique validation vs soft-deleted** → **ПРИНЯТО: name unique среди ВСЕХ записей (вкл. неактивных)* + +--- + +## Модель 3: Gemini 2.5 Pro (QA Engineer) + +**Вердикт: APPROVED** (ответ обрезан по лимиту) + +Начал анализ тестируемости, подтвердил PRD как "отличный, но сжатый". + +--- + +## Консенсус (Раунд 1) + +| Модель | Вердикт | +|--------|---------| +| DeepSeek v3.2 | APPROVED | +| Grok 4.1 Fast | MAJOR (требует доработки) | +| Gemini 2.5 Pro | APPROVED | + +**Общий вердикт: APPROVED с доработками** (2 из 3 APPROVED, 70% консенсус достигнут) + +### Принятые улучшения для PRD → Spec + +1. ✅ Обернуть create/update в DB-транзакцию +2. ✅ Eager loading для связей (`with()`) +3. ✅ BlameableBehavior: defaultValue для fallback +4. ✅ Scopes: `findActive()` в модели +5. ✅ Unique name проверяется среди ВСЕХ записей (включая неактивных) + +--- + +# Лог дебатов: ERP-318 — Spec + +**Дата:** 2026-04-09 +**Раунд:** 1 + +## Модель 1: DeepSeek v3.2 (Backend Architect) + +**Вердикт: MINOR** + +### Замечания +1. XSS в GridView → уже учтено (Html::encode в value callback) +2. deactivate() в модели нарушает SRP → **ОТКЛОНЕНО**: паттерн проекта, <20 записей, нет смысла в сервисе +3. BlameableBehavior defaultValue=0 → **ПРИНЯТО**: изменить на null, в UI показывать "Система" +4. Индексы БД для is_active, type, currency → **ОТКЛОНЕНО**: <20 записей +5. DTO для create/update → **ОТКЛОНЕНО**: overengineering для справочника + +## Модель 2: Grok 4.1 Fast (Security Engineer) + +**Вердикт: MAJOR** + +### Замечания +1. **XSS inline styles в badges** → **ПРИНЯТО**: заменить inline styles на CSS-классы +2. CSRF для AJAX → ОК, стандарт Yii2 +3. SQL injection → ОК, ActiveRecord ORM +4. RBAC для AJAX actions → ОК, BaseController проверяет menu/supplier/{action} + +## Консенсус (Раунд 1) + +| Модель | Вердикт | +|--------|---------| +| DeepSeek v3.2 | MINOR | +| Grok 4.1 Fast | MAJOR | + +**Общий вердикт: APPROVED с доработками** + +### Принятые улучшения для Spec → Implementation + +1. ✅ Заменить inline styles в badges на CSS-классы +2. ✅ BlameableBehavior defaultValue → null вместо 0 diff --git a/docs/jira/ERP-318/interview.md b/docs/jira/ERP-318/interview.md new file mode 100644 index 00000000..d9c07c2e --- /dev/null +++ b/docs/jira/ERP-318/interview.md @@ -0,0 +1,51 @@ +# ERP-318: Интервью — Справочник поставщиков + +**Дата:** 2026-04-09 +**Участник:** Владимир Фомичев + +--- + +## Q1: Scope CRUD операций +**Ответ: (B)** Модальные окна поверх таблицы (AJAX-формы в Bootstrap modal) +- Создание и редактирование — через модальное окно +- Модалка: заголовок на тёмно-синем фоне (#1e3a5f) +- Поля: название* (text), тип* (select), валюта* (select), lead time* (number) + +## Q2: Блокировка при soft delete +**Ответ: (C)** is_active = false + каскадная деактивация связанных записей +- При деактивации поставщика — каскадно деактивировать связанные markings и product_mappings +- Блокировка деактивации при активных заказах (UC-05 из спецификации) +- Подтверждение через confirm-диалог + +## Q3: Таблица — GridView или DataTables +**Ответ: (A)** Стандартный Yii2 GridView с column filters + PJAX +- Bootstrap 5 `table-bordered table-hover` +- Колонки: Название (auto), Тип (badge), Lead time (center), Валюта (center), Статус (badge), Действия +- Сортировка по имени (ASC) по умолчанию +- Пагинация не нужна (<20 записей) + +## Q4: Бейджи типа и статуса +**Ответ: (B)** Есть утверждённый UI-guideline (из spec-supplier-reference.html) +- **Тип:** + - `local` → badge (цвет из UI-kit проекта) + - `international` → badge (цвет из UI-kit проекта) +- **Статус:** + - Активен → зелёный badge + - Неактивен → серый badge +- Неактивные строки: `opacity: 0.5` + +## Q5: Права доступа +**Ответ: (A)** Любой авторизованный пользователь с доступом к меню +- Через CrmMenu + стандартная проверка сессии `modul_arr_dostup` +- Без дополнительных RBAC-ролей + +--- + +## Дополнительный контекст из спецификации (spec-supplier-reference.html) + +- **Таблица БД:** `erp24.suppliers` — миграция уже существует +- **Связи:** `markings.supplier_id`, `product_mappings.supplier_id` +- **Валидация:** name UNIQUE, lead_time >= 0, type/currency из enum +- **Аудит:** TimestampBehavior + BlameableBehavior (created_by, updated_by, created_at, updated_at) +- **Контейнер:** BuyerReferenceController с Bootstrap 5 tabs (4 вкладки) +- **FR-S-08:** Аудит — все изменения в audit_log (P1, может быть отложено) diff --git a/docs/jira/ERP-318/plan.md b/docs/jira/ERP-318/plan.md new file mode 100644 index 00000000..dc29c848 --- /dev/null +++ b/docs/jira/ERP-318/plan.md @@ -0,0 +1,109 @@ +# План реализации: ERP-318 + +**Дата:** 2026-04-09 +**На основе:** Spec v1.0 + Debate improvements + +--- + +## Порядок задач + +| # | Задача | Файл(ы) | Зависимости | Оценка | +|---|--------|---------|-------------|--------| +| 1 | Модель Supplier | `erp24/records/Supplier.php` | Миграция (существует) | S | +| 2 | Search модель | `erp24/records/SupplierSearch.php` | Задача 1 | S | +| 3 | Контроллер SupplierController | `erp24/controllers/SupplierController.php` | Задачи 1, 2 | M | +| 4 | View: таблица + модалка + JS | `erp24/views/supplier/index.php`, `_form.php` | Задачи 1, 3 | L | +| 5 | Интеграция с BuyerReference | `erp24/views/buyer-reference/index.php` | Задача 4 | S | + +**S** = small (<30 мин), **M** = medium (30-60 мин), **L** = large (1-2 часа) + +--- + +## Задача 1: Модель Supplier.php + +**Файл:** `erp24/records/Supplier.php` + +- ActiveRecord для `erp24.suppliers` +- Constants: TYPE_LOCAL, TYPE_INTERNATIONAL, CURRENCY_* +- behaviors(): TimestampBehavior (NOW()) + BlameableBehavior (defaultValue: null) +- rules(): name required unique max200, type in enum, currency in enum, lead_time_days integer min:0 +- attributeLabels() на русском +- findActive() scope +- Relations: getMarkings(), getProductMappings() +- Helpers: getTypeOptions(), getCurrencyOptions() +- Badge helpers: getTypeBadge(), getStatusBadge() — **CSS-классы** (не inline styles) +- deactivate() — транзакция, cascade updateAll + +## Задача 2: Search модель SupplierSearch.php + +**Файл:** `erp24/records/SupplierSearch.php` + +- Extends Model (не ActiveRecord) +- Properties: id, name, type, currency, is_active +- search() → ActiveDataProvider, pagination: false, orderBy name ASC +- Фильтры: andFilterWhere для всех полей + +## Задача 3: Контроллер SupplierController.php + +**Файл:** `erp24/controllers/SupplierController.php` + +- Extends BaseController (RBAC через menu/supplier/*) +- VerbFilter: create/update/delete → POST +- actionIndex() — partial rendering при AJAX +- actionCreateForm() — renderAjax _form +- actionCreate() — JSON + транзакция +- actionUpdateForm(id) — renderAjax _form +- actionUpdate(id) — JSON + транзакция +- actionDelete(id) — deactivate() + JSON + +## Задача 4: Views (таблица + модалка + JS) + +**Файлы:** `erp24/views/supplier/index.php`, `erp24/views/supplier/_form.php` + +### index.php +- Header: заголовок + кнопка «Добавить» +- GridView + PJAX (id: supplier-pjax) +- Колонки: name (bold), type (badge), lead_time (center, "N дн"), currency (center), is_active (badge), actions +- rowOptions: opacity 0.5 для inactive +- Модальное окно (#supplier-modal), header #1e3a5f +- JS: jQuery — открытие модалки, AJAX submit, pjax.reload, confirm деактивации + +### _form.php +- HTML form (не ActiveForm widget — для renderAjax) +- Поля: name (text), type (select), currency (select), lead_time_days (number) +- Layout: type + currency + lead_time в одну строку (d-flex gap-2) +- Footer: Отмена + Сохранить + +## Задача 5: Интеграция с BuyerReference + +**Файл:** `erp24/views/buyer-reference/index.php` + +- Заменить placeholder вкладки «Поставщики» на AJAX-загрузку +- JS: загрузка `/supplier/index` при shown.bs.tab + при document.ready +- data-loaded флаг для предотвращения повторной загрузки + +--- + +## CSS-классы для бейджей + +Добавить в view (inline ` + +
+
+ + Справочник поставщиков + +
Локальные и международные +
+ +
+ + 'supplier-pjax', 'timeout' => 5000]); ?> + + $dataProvider, + 'tableOptions' => ['class' => 'table table-bordered table-hover table-sm'], + 'layout' => '{items}', + 'rowOptions' => function ($model) { + return $model->is_active ? [] : ['style' => 'opacity:0.5;']; + }, + 'columns' => [ + [ + 'attribute' => 'name', + 'format' => 'raw', + 'value' => function ($model) { + return '' . Html::encode($model->name) . ''; + }, + ], + [ + 'attribute' => 'type', + 'format' => 'raw', + 'contentOptions' => ['style' => 'text-align:center;'], + 'headerOptions' => ['style' => 'text-align:center;'], + 'value' => function ($model) { + return $model->getTypeBadge(); + }, + 'filter' => Supplier::getTypeOptions(), + ], + [ + 'attribute' => 'lead_time_days', + 'label' => 'Lead time', + 'contentOptions' => ['style' => 'text-align:center;'], + 'headerOptions' => ['style' => 'text-align:center;'], + 'value' => function ($model) { + return $model->lead_time_days . ' дн'; + }, + ], + [ + 'attribute' => 'currency', + 'contentOptions' => ['style' => 'text-align:center;'], + 'headerOptions' => ['style' => 'text-align:center;'], + 'filter' => Supplier::getCurrencyOptions(), + ], + [ + 'attribute' => 'is_active', + 'label' => 'Статус', + 'format' => 'raw', + 'contentOptions' => ['style' => 'text-align:center;'], + 'headerOptions' => ['style' => 'text-align:center;'], + 'value' => function ($model) { + return $model->getStatusBadge(); + }, + 'filter' => [1 => 'Активен', 0 => 'Неактивен'], + ], + [ + 'class' => 'yii\grid\ActionColumn', + 'template' => '{update} {delete}', + 'contentOptions' => ['style' => 'text-align:center;white-space:nowrap;'], + 'buttons' => [ + 'update' => function ($url, $model) { + return Html::a( + '', + '#', + [ + 'class' => 'btn-supplier-edit', + 'data-id' => $model->id, + 'title' => 'Редактировать', + ] + ); + }, + 'delete' => function ($url, $model) { + if (!$model->is_active) { + return ' '; + } + return ' ' . Html::a( + '', + '#', + [ + 'class' => 'btn-supplier-delete', + 'data-id' => $model->id, + 'data-name' => $model->name, + 'title' => 'Деактивировать', + ] + ); + }, + ], + ], + ], +]); ?> + + + + + + +' + messages[0] + ''); + }); + } + }, + error: function() { + alert('Ошибка сервера'); + } + }); + }); + + // Деактивация + $(document).on('click', '.btn-supplier-delete', function(e) { + e.preventDefault(); + var id = $(this).data('id'); + var name = $(this).data('name'); + + if (!confirm('Деактивировать поставщика "' + name + '"?\\nСвязанные маркировки и маппинги также будут деактивированы.')) { + return; + } + + $.ajax({ + url: '{$deleteUrl}?id=' + id, + type: 'POST', + data: {_csrf: yii.getCsrfToken()}, + dataType: 'json', + success: function(resp) { + if (resp.success) { + $.pjax.reload({container: '#supplier-pjax'}); + } else { + alert(resp.message || 'Ошибка деактивации'); + } + }, + error: function() { + alert('Ошибка сервера'); + } + }); + }); + + // Очистка ошибок при вводе + $(document).on('input change', '#supplier-form input, #supplier-form select', function() { + $(this).removeClass('is-invalid'); + $(this).next('.invalid-feedback').remove(); + }); +})(); +JS; + +$this->registerJs($js); +?>