Импорт оборудования из Excel
Чтобы не собирать список оборудования руками, его можно загрузить из .xlsx прямо в проект. Импорт создаёт проектную заявку на оборудование из строк файла, сопоставляя каждую строку с каталогом по названию и автоматически создавая недостающие позиции.
Эндпоинты
| Метод | Путь | Назначение |
|---|---|---|
GET | /api/v1/projects/{id}/equipment-requests/import/template | Скачать .xlsx-шаблон с примером. |
POST | /api/v1/projects/{id}/equipment-requests/import | Загрузить заполненный .xlsx. |
Доступ — как к логистическому разделу проекта (та же проверка, что и у создания заявки на оборудование вручную).
Формат файла
Первая строка — заголовки, далее строки данных. Колонки (порядок свободный, заголовки сопоставляются гибко, рус/англ):
| Заголовок | Обязателен | Пример | Поле заявки |
|---|---|---|---|
Название | да | Acer Nitro 5 AN515-44-R34J | сопоставление |
Цена / день / Цена в проекте | нет | 5500 или 5 500 ₽ | unit_price |
Кол-во | да | 1 | requested_qty |
Дней | нет | 1 (по умолчанию 1) | rental_days |
Числа терпимы к рублёвому символу, пробелам-разделителям тысяч и запятой как десятичному разделителю. Полностью пустые строки пропускаются.
Цена в проекте vs цена каталога
Цена из Excel — это «цена в проекте» (unit_price): она переопределяет базовую цену из каталога при расчёте сметы позиции. Если колонка пуста, в расчёте используется базовая цена каталога (equipment.cost). При авто-создании позиции цена из Excel становится и базовой ценой новой позиции каталога. То же поле unit_price можно передать при ручном добавлении позиции (POST .../equipment-requests).
Логика сопоставления
Для каждой строки:
- Поиск активной позиции каталога по названию (без учёта регистра и пробелов).
- Если найдено — используется существующая позиция (
matched). - Если не найдено — создаётся новая позиция каталога из названия и цены за день (
created_equipment), категория — «Импорт».
Дубликаты (одна и та же позиция в нескольких строках) отбрасываются с ошибкой строки.
Отчёт об импорте
Ответ — отчёт, а не «тихая» загрузка: видно, что сопоставлено, что создано и какие строки не прошли.
{
"project_id": 7,
"matched": 1,
"created_equipment": 1,
"items": [
{ "row": 2, "name": "Acer Nitro 5", "equipment_id": 1, "created_equipment": false, "requested_qty": 1, "rental_days": 1 },
{ "row": 3, "name": "HDMI кабель", "equipment_id": 42, "created_equipment": true, "requested_qty": 5, "rental_days": 1 }
],
"errors": [
{ "row": 4, "message": "quantity must be positive" }
],
"request": { "id": 10, "name": "Импорт оборудования", "items": [ /* ... */ ] }
}- При наличии хотя бы одной валидной строки создаётся заявка (
request), HTTP201. - Если валидных строк нет —
requestотсутствует, возвращаются толькоerrors, HTTP200. - Стоимость позиций считается штатной тарифной логикой заявки (
estimateRentalCost), как и при ручном добавлении.
Итоги заявки (totals) — считает сервер
Заявка (request, а также GET /equipment-requests/:id и список с include_items=true) несёт блок totals — готовый расчёт «Базовая сумма / Наценка / НДС / Итого» по действующему НДС проекта и наценке бюджета. Фронт ничего не пересчитывает:
"totals": {
"base": 5500.00, // Базовая сумма (Σ estimated_cost)
"markup_percent": 0,
"markup_amount": 0.00, // Сумма наценки
"vat_mode": "included",
"vat_rate": 22,
"vat_amount": 916.67, // Сумма НДС
"total_net": 4583.33,
"total_with_vat": 5500.00 // Итоговая сумма
}Арифметика — та же, что в сводке бюджета (money.ComputeVAT), копейка-в-копейку. Для заявок без проекта vat_mode = "none", наценка 0.
Если нужен только расчёт без полного тела заявки — есть лёгкий эндпоинт:
GET /api/v1/equipment-requests/{id}/totals → объект totalsГде смотреть в коде
- Парсер и сервис импорта:
internal/modules/equipmentrequests/import.go. - Порт к каталогу (
EquipmentCatalog) и адаптер:import.go+internal/app/equipment_catalog_adapter.go. - Сопоставление по названию:
internal/modules/equipment/(FindByName/FindActiveByName). - HTTP-обработчики:
internal/modules/equipmentrequests/transport_http.go(Import,ImportTemplate). - Парсинг
.xlsx—github.com/xuri/excelize/v2.