mirror of
https://github.com/Nikolay-Shirokov/cc-1c-skills.git
synced 2026-08-01 01:07:46 +03:00
fix(web-test): readSpreadsheet — поддержка text-only и отчётов с числовыми шапками
Рефакторинг buildSpreadsheetMapping на 3-уровневый алгоритм. - Level 1: якорь по DCS-кодам (К1..Кn) — детерминированный для всех ФСД-отчётов, работает независимо от формата чисел (рубли/тыс/млн). - Level 2: якорь по форматированным числам (пробел-группировка, запятая-десятичка, ведущий минус) вместо общей проверки — голые целые (коды счетов "50", "51") больше не принимаются за данные. - Level 3: single-row header fallback для text-only данных и query-console. Починено: - ФСД-отчёты с числами в групповых шапках (ДДС по счетам 50/51/52/55/57) — был fallback raw rows, теперь структурированный вывод. - query() из consoleЗапросов для text-only результатов — был data=[], теперь корректно парсит headers/data. E2E проверено на titan: 4 отчёта (ДС, 45, 77, Ведомость) + 5 query-кейсов. Регрессий нет. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
82e70d2c30
commit
afdfc97fb1
@@ -1,4 +1,4 @@
|
|||||||
// web-test browser v1.7 — Playwright browser management for 1C web client
|
// web-test browser v1.8 — Playwright browser management for 1C web client
|
||||||
// Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
// Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
/**
|
/**
|
||||||
* Playwright browser management for 1C web client.
|
* Playwright browser management for 1C web client.
|
||||||
@@ -1044,33 +1044,25 @@ function buildSpreadsheetMapping(allCells) {
|
|||||||
return arr;
|
return arr;
|
||||||
});
|
});
|
||||||
|
|
||||||
// Strict numeric value check: digits with optional spaces/commas, excludes codes like "68/78"
|
// Generic numeric check: digits with optional spaces/commas, excludes codes like "68/78"
|
||||||
|
// Accepts bare integers (e.g. account codes "50", "84") — used for hasNumber / totals classification.
|
||||||
const isNumericVal = (c) => {
|
const isNumericVal = (c) => {
|
||||||
if (!c || !/\d/.test(c)) return false;
|
if (!c || !/\d/.test(c)) return false;
|
||||||
const s = c.replace(/^[-\s\u00a0]+/, '').replace(/[\s\u00a0]/g, '');
|
const s = c.replace(/^[-\s\u00a0]+/, '').replace(/[\s\u00a0]/g, '');
|
||||||
return /^\d[\d,]*$/.test(s);
|
return /^\d[\d,]*$/.test(s);
|
||||||
};
|
};
|
||||||
|
// Data-formatted numeric value: requires a formatting signal (grouping space, decimal comma, or leading minus).
|
||||||
|
// Used as the anchor for first data row — avoids false positives on bare account codes like "50", "51".
|
||||||
|
const isDataNumericVal = (c) => {
|
||||||
|
if (!isNumericVal(c)) return false;
|
||||||
|
return /[\s\u00a0,]/.test(c) || /^-/.test(c);
|
||||||
|
};
|
||||||
const hasNumber = (row) => row.some(c => isNumericVal(c));
|
const hasNumber = (row) => row.some(c => isNumericVal(c));
|
||||||
const nonEmpty = (row) => row.filter(c => c !== '').length;
|
const nonEmpty = (row) => row.filter(c => c !== '').length;
|
||||||
|
|
||||||
// Find first data row: prefer row with >=2 numeric cells, fallback to >=1
|
// Build a rich mapping (group/super/DCS) anchored at a known detailIdx + firstDataIdx.
|
||||||
let firstDataIdx = rows.length;
|
// Shared by Level 1 (DCS-code anchor) and Level 2 (formatted-number anchor).
|
||||||
for (let i = 0; i < rows.length; i++) {
|
const buildRichMapping = (detailIdx, firstDataIdx) => {
|
||||||
if (rows[i].filter(c => isNumericVal(c)).length >= 2) { firstDataIdx = i; break; }
|
|
||||||
}
|
|
||||||
if (firstDataIdx === rows.length) {
|
|
||||||
for (let i = 0; i < rows.length; i++) {
|
|
||||||
if (hasNumber(rows[i])) { firstDataIdx = i; break; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Find header rows
|
|
||||||
let detailIdx = -1;
|
|
||||||
for (let i = firstDataIdx - 1; i >= 0; i--) {
|
|
||||||
if (nonEmpty(rows[i]) >= Math.min(3, maxCol + 1)) { detailIdx = i; break; }
|
|
||||||
}
|
|
||||||
if (detailIdx === -1) return null; // no headers detected
|
|
||||||
|
|
||||||
let groupIdx = -1;
|
let groupIdx = -1;
|
||||||
if (detailIdx > 0 && nonEmpty(rows[detailIdx - 1]) >= 2) groupIdx = detailIdx - 1;
|
if (detailIdx > 0 && nonEmpty(rows[detailIdx - 1]) >= 2) groupIdx = detailIdx - 1;
|
||||||
|
|
||||||
@@ -1127,14 +1119,13 @@ function buildSpreadsheetMapping(allCells) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Column name → physical column index
|
|
||||||
const colMap = new Map();
|
const colMap = new Map();
|
||||||
for (let c = 0; c < colNames.length; c++) {
|
for (let c = 0; c < colNames.length; c++) {
|
||||||
if (colNames[c]) colMap.set(colNames[c], c);
|
if (colNames[c]) colMap.set(colNames[c], c);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Classify data rows: separate data indices and totals index
|
// Classify data rows: separate data indices and totals index
|
||||||
const dataRowIndices = []; // indices into rows[] array
|
const dataRowIndices = [];
|
||||||
let totalsRowIdx = -1;
|
let totalsRowIdx = -1;
|
||||||
for (let i = firstDataIdx; i < rows.length; i++) {
|
for (let i = firstDataIdx; i < rows.length; i++) {
|
||||||
if (!hasNumber(rows[i]) && nonEmpty(rows[i]) === 0) continue;
|
if (!hasNumber(rows[i]) && nonEmpty(rows[i]) === 0) continue;
|
||||||
@@ -1154,6 +1145,82 @@ function buildSpreadsheetMapping(allCells) {
|
|||||||
dataStartIdx: firstDataIdx, dataRowIndices, totalsRowIdx,
|
dataStartIdx: firstDataIdx, dataRowIndices, totalsRowIdx,
|
||||||
rowMap, hasNumber, nonEmpty,
|
rowMap, hasNumber, nonEmpty,
|
||||||
};
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
// --- Level 1: DCS-code row anchor ---
|
||||||
|
// ФСД / СКД-отчёты всегда содержат строку "К1, К2, ..." — rock-solid structural marker.
|
||||||
|
// Якорение через неё — детерминированное, работает даже если все данные — голые целые (отчёт в "тыс.руб").
|
||||||
|
for (let i = 0; i < rows.length; i++) {
|
||||||
|
const detailNonEmpty = rows[i].filter(c => c);
|
||||||
|
if (detailNonEmpty.length >= 2 && detailNonEmpty.every(c => /^К\d+$/.test(c))) {
|
||||||
|
// Find first non-empty row after the К-codes row as data start
|
||||||
|
let firstDataIdx = rows.length;
|
||||||
|
for (let j = i + 1; j < rows.length; j++) {
|
||||||
|
if (nonEmpty(rows[j]) > 0) { firstDataIdx = j; break; }
|
||||||
|
}
|
||||||
|
return buildRichMapping(i, firstDataIdx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Level 2: formatted-number anchor (heuristic for reports without DCS codes) ---
|
||||||
|
let firstDataIdx = rows.length;
|
||||||
|
for (let i = 0; i < rows.length; i++) {
|
||||||
|
if (rows[i].filter(c => isDataNumericVal(c)).length >= 2) { firstDataIdx = i; break; }
|
||||||
|
}
|
||||||
|
if (firstDataIdx === rows.length) {
|
||||||
|
for (let i = 0; i < rows.length; i++) {
|
||||||
|
if (rows[i].some(c => isDataNumericVal(c))) { firstDataIdx = i; break; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (firstDataIdx < rows.length) {
|
||||||
|
let detailIdx = -1;
|
||||||
|
for (let i = firstDataIdx - 1; i >= 0; i--) {
|
||||||
|
if (nonEmpty(rows[i]) >= Math.min(3, maxCol + 1)) { detailIdx = i; break; }
|
||||||
|
}
|
||||||
|
if (detailIdx !== -1) return buildRichMapping(detailIdx, firstDataIdx);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Level 3: single-row header fallback (text-only data, query console) ---
|
||||||
|
// First "wide" row (nonEmpty >= 2) = headers, rest = data. No multi-level composition.
|
||||||
|
let headerIdx = -1;
|
||||||
|
for (let i = 0; i < rows.length; i++) {
|
||||||
|
if (nonEmpty(rows[i]) >= 2) { headerIdx = i; break; }
|
||||||
|
}
|
||||||
|
// Single-column tables: accept nonEmpty >= 1
|
||||||
|
if (headerIdx === -1 && maxCol === 0) {
|
||||||
|
for (let i = 0; i < rows.length; i++) {
|
||||||
|
if (nonEmpty(rows[i]) >= 1) { headerIdx = i; break; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (headerIdx === -1) return null; // truly empty — top-level fallback to { rows, total }
|
||||||
|
|
||||||
|
const detailRow = rows[headerIdx];
|
||||||
|
const colNames = [];
|
||||||
|
for (let c = 0; c <= maxCol; c++) colNames.push(detailRow[c] || null);
|
||||||
|
const colMap = new Map();
|
||||||
|
for (let c = 0; c < colNames.length; c++) {
|
||||||
|
if (colNames[c]) colMap.set(colNames[c], c);
|
||||||
|
}
|
||||||
|
|
||||||
|
const dataRowIndices = [];
|
||||||
|
let totalsRowIdx = -1;
|
||||||
|
for (let i = headerIdx + 1; i < rows.length; i++) {
|
||||||
|
if (!hasNumber(rows[i]) && nonEmpty(rows[i]) === 0) continue;
|
||||||
|
const first = rows[i][0]?.trim().toLowerCase();
|
||||||
|
if (first === 'итого' || first === 'всего') {
|
||||||
|
totalsRowIdx = i;
|
||||||
|
} else {
|
||||||
|
dataRowIndices.push(i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
rows, sortedRows, maxCol, colNames, colMap,
|
||||||
|
headerRowIdx: headerIdx, groupRowIdx: -1, superRowIdx: -1,
|
||||||
|
dataStartIdx: headerIdx + 1, dataRowIndices, totalsRowIdx,
|
||||||
|
rowMap, hasNumber, nonEmpty,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
Reference in New Issue
Block a user