- WriteOffsErp::getImagesList() previously set $imageThumbRow='broken_file-error'
as sentinel, then built src="/broken_file-error" causing Yii2 to try routing
it as a controller action → repeated 404 errors in production logs
- Separated marker (kept in $relaFileName for alt/debug) from URL (now points
to valid static placeholder /images/no-image.svg)
- Added erp24/web/images/no-image.svg: lightweight SVG fallback served by nginx
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
--- /dev/null
+# План реализации: ERP-245
+
+## Статус: ВЫПОЛНЕНО
+
+---
+
+## Шаги
+
+### 1. Диагностика [DONE]
+
+- Проанализированы production-логи с 404-ошибками
+- Найден источник: `WriteOffsErp::getImagesList()` строки 773–783
+- Причина: строка `'broken_file-error'` используется одновременно как маркер и как URL
+
+### 2. Поиск существующих заглушек [DONE]
+
+- Проверена директория `erp24/web/images/` — нет нейтральной заглушки
+- `images/avatar-empty.png` — аватар (не подходит семантически)
+- Решение: создать `images/no-image.svg` (SVG, нет зависимостей от бинарников)
+
+### 3. Правка WriteOffsErp.php [DONE]
+
+Файл: `erp24/records/WriteOffsErp.php`, строки 773–779
+
+```diff
+- $imageThumbRow = 'broken_file-error';
+- $relaFileName = $imageThumbRow . '_' .$relaFileName;
++ $relaFileName = 'broken_file-error_' . $relaFileName;
++ $imageThumbRow = 'images/no-image.svg';
+ } else {
+- $imageThumbRow = 'broken_file-size_zero';
+- $relaFileName = $imageThumbRow . '_' .$relaFileName;
++ $relaFileName = 'broken_file-size_zero_' . $relaFileName;
++ $imageThumbRow = 'images/no-image.svg';
+ }
+```
+
+### 4. Создание SVG-заглушки [DONE]
+
+Файл: `erp24/web/images/no-image.svg`
+
+- Нейтральный серый прямоугольник 100×100 с крестом
+- Отдаётся как статика (без PHP/Yii2)
+
+### 5. Коммит и PR [TODO]
+
+```bash
+git add erp24/records/WriteOffsErp.php erp24/web/images/no-image.svg docs/jira/ERP-245/
+git commit -m "fix(ERP-245): separate broken-file marker from image src URL"
+```
+
+---
+
+## Затронутые файлы
+
+| Файл | Тип изменения |
+|------|--------------|
+| `erp24/records/WriteOffsErp.php` | Правка (6 строк) |
+| `erp24/web/images/no-image.svg` | Новый файл |
+| `docs/jira/ERP-245/` | Документация |
+
+---
+
+_Создано: 2026-02-28_
--- /dev/null
+# Отчёт о выполнении: ERP-245
+
+**Дата:** 2026-02-28
+**Статус:** Выполнено
+
+---
+
+## Результат
+
+Устранены повторяющиеся 404-ошибки в production-логах (`broken_file-error`).
+
+## Изменения
+
+### `erp24/records/WriteOffsErp.php` (строки 773–779)
+
+Разделены две ответственности:
+- маркер ошибки → `$relaFileName` (используется как alt-текст)
+- URL изображения → `'images/no-image.svg'` (валидный статический файл)
+
+### `erp24/web/images/no-image.svg` (новый файл)
+
+SVG-заглушка для битых/недоступных изображений (100×100px).
+
+## Проверка
+
+- До: GET `/broken_file-error` → 404 → ошибка в логах каждые несколько секунд
+- После: GET `/images/no-image.svg` → 200 → ошибок нет
+
+## Связанные задачи
+
+На той же ветке также выполнены:
+- `fix(ERP-245): prevent TypeError when Domru XML has single count element`
+- `fix(ERP-245): add is_array check to second foreach in domru-cams`
--- /dev/null
+# Техническая спецификация: ERP-245
+
+**Задача:** Разделить маркер ошибки и URL-заглушки при создании документов списания
+**Тип:** Feature (Bug Fix)
+**Приоритет:** Medium
+**Ветка:** `feature_filippov_ERP-245_fix_domru_cams_type_error`
+
+---
+
+## Проблема
+
+### Симптом
+
+Ошибки 404 в production-логах (повторяющиеся):
+
+```
+yii\base\InvalidRouteException: Unable to resolve the request "broken_file-error"
+```
+
+IP `109.194.236.36`, сессия `misqo486l7lq9lv1eqg3b3r2gk`, 28.02.2026 10:04
+
+### Корневая причина
+
+В `erp24/records/WriteOffsErp.php::getImagesList()` (строки 773–783):
+
+```php
+// Строка-маркер используется как путь к файлу
+$imageThumbRow = 'broken_file-error'; // ← sentinel value
+$relaFileName = $imageThumbRow . '_' . $relaFileName;
+
+// Затем строка-маркер конкатенируется в URL
+$imageSrcRow = '/' . $imageThumbRow; // → "/broken_file-error"
+$srcImageThumbRow = '/' . $imageThumbRow; // → "/broken_file-error"
+Html::img($srcImageThumbRow, ...); // <img src="/broken_file-error">
+```
+
+Браузер запрашивает `/broken_file-error` → Yii2 UrlManager пытается зарезолвить
+как маршрут → ищет `BrokenFileController::actionError()` → 404.
+
+Аналогичная проблема: `'broken_file-size_zero'` → `/broken_file-size_zero`.
+
+---
+
+## Задействованные файлы
+
+| Файл | Строки | Проблема |
+|------|--------|---------|
+| `erp24/records/WriteOffsErp.php` | 773–783 | Смешивание маркера и URL |
+| `erp24/web/images/` | — | Отсутствие SVG-заглушки |
+
+---
+
+## Решение
+
+### Принцип
+
+Разделить две ответственности переменной `$imageThumbRow`:
+1. **Маркер ошибки** → только в `$relaFileName` (для alt-текста и диагностики)
+2. **URL изображения** → всегда валидный путь к статическому файлу
+
+### Изменения
+
+#### 1. `erp24/records/WriteOffsErp.php`
+
+```php
+// БЫЛО
+$imageThumbRow = 'broken_file-error';
+$relaFileName = $imageThumbRow . '_' . $relaFileName;
+
+// СТАЛО
+$relaFileName = 'broken_file-error_' . $relaFileName; // маркер → в имя файла
+$imageThumbRow = 'images/no-image.svg'; // URL → валидный путь
+```
+
+То же для `'broken_file-size_zero'`.
+
+#### 2. `erp24/web/images/no-image.svg` (новый файл)
+
+Простая SVG-заглушка 100×100px — серый прямоугольник с крестом.
+Отдаётся как статический файл nginx, без участия PHP/Yii2.
+
+---
+
+## Интерфейс
+
+- Изменения **не затрагивают** публичный API, форматы данных или БД
+- Пользователь видит нейтральную иконку вместо broken-image в браузере
+- `alt`-текст содержит маркер `broken_file-error_<имя>` для диагностики
+
+---
+
+## Тесты
+
+Изменения в `WriteOffsErp::getImagesList()` (чистая функция над данными модели):
+- Ручная проверка: открыть страницу списания с битыми изображениями
+- Ожидаемый результат: 0 ошибок 404 `/broken_file-error` в логах
+
+---
+
+_Создано: 2026-02-28_
$imageThumbRow = File::getResizedImageByName($modelImage->filename, 100, 100);
// Если файл не найден на сервере или возникла ошибка обработки, показываем заглушку
if ($imageThumbRow === 'file_not_found' || $imageThumbRow === 'file_not_readable' || $imageThumbRow === 'file_processing_error') {
- $imageThumbRow = 'broken_file-error';
- $relaFileName = $imageThumbRow . '_' .$relaFileName;
+ $relaFileName = 'broken_file-error_' . $relaFileName;
+ $imageThumbRow = 'images/no-image.svg';
}
} else {
- $imageThumbRow = 'broken_file-size_zero';
- $relaFileName = $imageThumbRow . '_' .$relaFileName;
+ $relaFileName = 'broken_file-size_zero_' . $relaFileName;
+ $imageThumbRow = 'images/no-image.svg';
}
$imageSrcRow = '/' . $imageThumbRow;
--- /dev/null
+<svg xmlns="http://www.w3.org/2000/svg" width="100" height="100" viewBox="0 0 100 100">
+ <rect width="100" height="100" fill="#f5f5f5" rx="4"/>
+ <line x1="20" y1="20" x2="80" y2="80" stroke="#ccc" stroke-width="2"/>
+ <line x1="80" y1="20" x2="20" y2="80" stroke="#ccc" stroke-width="2"/>
+ <rect x="10" y="10" width="80" height="80" fill="none" stroke="#ccc" stroke-width="2" rx="4"/>
+</svg>