feat: импорт собственного CSV QuoteForge + fix обновления цен

Добавлен парсер собственного CSV-экспорта QuoteForge (IsQuoteForgeCSV /
parseQuoteForgeCSV). Формат: UTF-8 BOM + заголовок Line;Type;p/n;...,
блоки сервер → компоненты. DirectItems создаются напрямую без прохода
через VendorSpecResolver. Модальное окно импорта принимает .csv/.txt/.xml.

Fix кнопки «Обновить цены» на странице варианта: после синхронизации
прайс-листов запрашивается актуальный estimate-прайслист и передаётся
явным pricelist_id в каждый POST /api/configs/:uuid/refresh-prices.
Ранее использовался устаревший ID, сохранённый в конфигурации.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-24 18:54:08 +03:00
parent 6b56cad248
commit 5d4e1b44f6
3 changed files with 289 additions and 10 deletions

View File

@@ -108,14 +108,14 @@
<div id="vendor-import-modal" class="fixed inset-0 bg-black bg-opacity-50 hidden items-center justify-center z-50">
<div class="bg-white rounded-lg shadow-xl w-full max-w-lg mx-4 p-6">
<h2 class="text-xl font-semibold mb-4">Импорт выгрузки вендора</h2>
<h2 class="text-xl font-semibold mb-4">Импорт конфигураций</h2>
<div class="space-y-4">
<div class="text-sm text-gray-600">
Загружает `CFXML`-выгрузку в текущий проект и создаёт несколько конфигураций, если они есть в файле.
Поддерживаемые форматы: CFXML-выгрузка вендора (.xml), собственный CSV-экспорт QuoteForge (.csv), текстовый BOM Inspur (.txt).
</div>
<div>
<label for="vendor-import-file" class="block text-sm font-medium text-gray-700 mb-1">Файл выгрузки</label>
<input id="vendor-import-file" type="file" accept=".xml,text/xml,application/xml"
<label for="vendor-import-file" class="block text-sm font-medium text-gray-700 mb-1">Файл</label>
<input id="vendor-import-file" type="file" accept=".xml,.csv,.txt,text/xml,application/xml,text/csv,text/plain"
class="w-full px-3 py-2 border rounded focus:ring-2 focus:ring-amber-500 focus:border-amber-500">
</div>
<div id="vendor-import-status" class="hidden text-sm rounded border px-3 py-2"></div>
@@ -911,7 +911,7 @@ async function importVendorWorkspace() {
const input = document.getElementById('vendor-import-file');
const submit = document.getElementById('vendor-import-submit');
if (!input || !input.files || !input.files[0]) {
setVendorImportStatus('Выберите XML-файл выгрузки', 'error');
setVendorImportStatus('Выберите файл для импорта', 'error');
return;
}
@@ -1611,11 +1611,30 @@ async function refreshAllPrices() {
serverSyncSkipped = true;
}
// Resolve latest estimate pricelist ID to pass explicitly, so each config
// is updated to the newest pricelist rather than the one stored in the config.
let latestEstimatePricelistId = null;
try {
const plResp = await fetch('/api/pricelists?active_only=true&source=estimate&per_page=1');
if (plResp.ok) {
const plData = await plResp.json();
const list = plData.pricelists || plData.items || plData;
if (Array.isArray(list) && list.length > 0 && list[0].id) {
latestEstimatePricelistId = Number(list[0].id);
}
}
} catch (_) {}
let failed = 0;
let newTotalSum = 0;
for (const cfg of configs) {
try {
const resp = await fetch('/api/configs/' + cfg.uuid + '/refresh-prices', { method: 'POST' });
const body = latestEstimatePricelistId ? JSON.stringify({ pricelist_id: latestEstimatePricelistId }) : undefined;
const resp = await fetch('/api/configs/' + cfg.uuid + '/refresh-prices', {
method: 'POST',
headers: body ? { 'Content-Type': 'application/json' } : {},
body,
});
if (!resp.ok) { failed++; continue; }
const updated = await resp.json();
if (updated && updated.total_price != null) {