116 lines
4.4 KiB
JavaScript
116 lines
4.4 KiB
JavaScript
|
|
const csv = require('csv-parser');
|
|||
|
|
const fs = require('fs');
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* Парсер отчёта по задолженности (CSV с разделителем ;).
|
|||
|
|
* Строки данных: № п/п (0), Лицевой счёт (1), Ответственный (4), Объект учета (5), Количество месяцев (8), последняя колонка — Общая задолженность.
|
|||
|
|
*/
|
|||
|
|
class DebtorReportProcessor {
|
|||
|
|
/**
|
|||
|
|
* Нормализация числа из отчёта: пробелы убрать, запятую заменить на точку.
|
|||
|
|
*/
|
|||
|
|
parseAmount(str) {
|
|||
|
|
if (str === undefined || str === null || str === '') return null;
|
|||
|
|
const s = String(str).replace(/\s/g, '').replace(',', '.').trim();
|
|||
|
|
if (s === '' || s === '-') return null;
|
|||
|
|
const n = parseFloat(s);
|
|||
|
|
return Number.isFinite(n) ? n : null;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* Проверка: строка похожа на строку данных (колонка 0 — число, колонка 1 — лицевой счёт вида 00-000000001).
|
|||
|
|
*/
|
|||
|
|
isDataRow(cells) {
|
|||
|
|
if (!cells || cells.length < 6) return false;
|
|||
|
|
const col0 = String(cells[0] || '').trim();
|
|||
|
|
const col1 = String(cells[1] || '').trim();
|
|||
|
|
if (col1 === '' || col1.toLowerCase() === 'итого') return false;
|
|||
|
|
const num0 = parseInt(col0, 10);
|
|||
|
|
const isNum = col0 !== '' && !Number.isNaN(num0) && num0 >= 1;
|
|||
|
|
const looksLikeAccount = /^[\d\-]+$/.test(col1) || /^\d{2}-\d{9}$/.test(col1.replace(/\s/g, ''));
|
|||
|
|
return isNum && (looksLikeAccount || col1.length >= 5);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* Парсинг одной строки данных в объект для БД.
|
|||
|
|
*/
|
|||
|
|
parseDataRow(cells, rowIndex) {
|
|||
|
|
const account = String(cells[1] || '').trim();
|
|||
|
|
const responsibleName = String(cells[4] || '').trim();
|
|||
|
|
const objectAddress = String(cells[5] || '').trim();
|
|||
|
|
const monthsRaw = String(cells[8] || '').trim();
|
|||
|
|
const monthsDebt = monthsRaw === '' ? null : (parseInt(monthsRaw, 10) || null);
|
|||
|
|
const lastCol = cells[cells.length - 1];
|
|||
|
|
const totalDebt = this.parseAmount(lastCol);
|
|||
|
|
return {
|
|||
|
|
row_index: rowIndex,
|
|||
|
|
account,
|
|||
|
|
responsible_name: responsibleName,
|
|||
|
|
object_address: objectAddress,
|
|||
|
|
months_debt: monthsDebt,
|
|||
|
|
total_debt: totalDebt != null ? totalDebt : 0,
|
|||
|
|
};
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* Парсинг CSV-файла отчёта по задолженности.
|
|||
|
|
* Возвращает массив объектов { row_index, account, responsible_name, object_address, months_debt, total_debt }.
|
|||
|
|
*/
|
|||
|
|
async parseDebtorReportCSV(filePath) {
|
|||
|
|
return new Promise((resolve, reject) => {
|
|||
|
|
const rows = [];
|
|||
|
|
let rowIndex = 0;
|
|||
|
|
|
|||
|
|
fs.createReadStream(filePath, { encoding: 'utf8' })
|
|||
|
|
.pipe(csv({ separator: ';', headers: false }))
|
|||
|
|
.on('data', (row) => {
|
|||
|
|
const cells = Object.values(row);
|
|||
|
|
if (rowIndex === 0 && cells[0] !== undefined) {
|
|||
|
|
const first = String(cells[0]).replace(/^\uFEFF/, '');
|
|||
|
|
cells[0] = first;
|
|||
|
|
}
|
|||
|
|
if (this.isDataRow(cells)) {
|
|||
|
|
rows.push(this.parseDataRow(cells, rowIndex));
|
|||
|
|
}
|
|||
|
|
rowIndex++;
|
|||
|
|
})
|
|||
|
|
.on('end', () => resolve(rows))
|
|||
|
|
.on('error', reject);
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* Парсинг XLSX: чтение первого листа, поиск строк данных по тем же правилам.
|
|||
|
|
*/
|
|||
|
|
async parseDebtorReportXLSX(filePath) {
|
|||
|
|
const XLSX = require('xlsx');
|
|||
|
|
const workbook = XLSX.readFile(filePath);
|
|||
|
|
const sheetName = workbook.SheetNames[0];
|
|||
|
|
const worksheet = workbook.Sheets[sheetName];
|
|||
|
|
const rawData = XLSX.utils.sheet_to_json(worksheet, { header: 1, defval: '' });
|
|||
|
|
const rows = [];
|
|||
|
|
for (let i = 0; i < rawData.length; i++) {
|
|||
|
|
const cells = rawData[i] || [];
|
|||
|
|
if (this.isDataRow(cells)) {
|
|||
|
|
rows.push(this.parseDataRow(cells, i));
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
return rows;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* Единая точка входа: парсинг по типу файла.
|
|||
|
|
*/
|
|||
|
|
async parseDebtorReport(filePath, fileType) {
|
|||
|
|
if (fileType === 'CSV') {
|
|||
|
|
return this.parseDebtorReportCSV(filePath);
|
|||
|
|
}
|
|||
|
|
if (fileType === 'XLSX') {
|
|||
|
|
return this.parseDebtorReportXLSX(filePath);
|
|||
|
|
}
|
|||
|
|
throw new Error('Поддерживаются только CSV и XLSX');
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
module.exports = DebtorReportProcessor;
|