158 lines
5.8 KiB
JavaScript
Executable File
158 lines
5.8 KiB
JavaScript
Executable File
const csv = require('csv-parser');
|
||
const XLSX = require('xlsx');
|
||
const fs = require('fs');
|
||
|
||
/**
|
||
* Процессор для обработки ОСВ по счёту 76.06 (лицевые счета жителей)
|
||
*/
|
||
class BalanceSheet76Processor {
|
||
constructor() {}
|
||
|
||
parseAmount(value) {
|
||
if (!value) return 0;
|
||
const str = String(value).replace(/\s/g, '').replace(',', '.');
|
||
const num = parseFloat(str);
|
||
return isNaN(num) ? 0 : num;
|
||
}
|
||
|
||
_isDataRow(rowArray) {
|
||
if (!rowArray || rowArray.length < 5) return false;
|
||
const firstCell = String(rowArray[0] || '').trim();
|
||
const col1 = this.parseAmount(rowArray[1]);
|
||
const col3 = this.parseAmount(rowArray[3]);
|
||
const col4 = this.parseAmount(rowArray[4]);
|
||
return firstCell.length >= 1 && (col1 > 0 || col3 > 0 || col4 > 0);
|
||
}
|
||
|
||
/**
|
||
* Извлечь номер л/с из колонки 0 (например "00-000000001, ФИО (л/с)" -> "00-000000001")
|
||
*/
|
||
extractAccountLs(firstCell) {
|
||
const s = String(firstCell || '').trim();
|
||
const comma = s.indexOf(',');
|
||
if (comma > 0) {
|
||
const prefix = s.substring(0, comma).trim();
|
||
if (/^[\d\-]+$/.test(prefix) || /^[\d\-]+\s*$/.test(prefix)) return prefix;
|
||
}
|
||
return null;
|
||
}
|
||
|
||
/**
|
||
* Парсинг CSV ОСВ 76 (UTF-8, BOM, автоопределение начала данных)
|
||
*/
|
||
async parseBalanceSheet76CSV(filePath) {
|
||
return new Promise((resolve, reject) => {
|
||
const allRows = [];
|
||
let currentRowIndex = 0;
|
||
let dataStartRowIndex = -1;
|
||
|
||
fs.createReadStream(filePath, { encoding: 'utf8' })
|
||
.pipe(csv({ separator: ';', headers: false }))
|
||
.on('data', (row) => {
|
||
const rowArray = Object.values(row);
|
||
if (currentRowIndex === 0 && rowArray[0] !== undefined) {
|
||
rowArray[0] = String(rowArray[0]).replace(/^\uFEFF/, '');
|
||
}
|
||
allRows.push({ index: currentRowIndex, data: rowArray });
|
||
currentRowIndex++;
|
||
})
|
||
.on('end', () => {
|
||
for (let i = 0; i < allRows.length; i++) {
|
||
const rowArray = allRows[i].data;
|
||
const firstCell = String(rowArray[0] || '').trim();
|
||
if (firstCell.includes('Обороты за период') || firstCell.includes('Контрагенты') || firstCell.includes('Счет,')) {
|
||
dataStartRowIndex = i + 1;
|
||
break;
|
||
}
|
||
if (this._isDataRow(rowArray)) {
|
||
dataStartRowIndex = i;
|
||
break;
|
||
}
|
||
}
|
||
if (dataStartRowIndex < 0) dataStartRowIndex = 9;
|
||
const rows = allRows.filter(r => r.index >= dataStartRowIndex);
|
||
resolve(rows);
|
||
})
|
||
.on('error', reject);
|
||
});
|
||
}
|
||
|
||
async parseBalanceSheet76XLSX(filePath) {
|
||
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: '', raw: false });
|
||
const rows = [];
|
||
for (let i = 9; i < rawData.length; i++) {
|
||
rows.push({ index: i, data: rawData[i] || [] });
|
||
}
|
||
return rows;
|
||
}
|
||
|
||
/**
|
||
* Парсинг ведомости 76 и возврат массива строк по лицевым счетам
|
||
*/
|
||
async parseBalanceSheet76(filePath, fileType) {
|
||
const rows = fileType === 'CSV'
|
||
? await this.parseBalanceSheet76CSV(filePath)
|
||
: await this.parseBalanceSheet76XLSX(filePath);
|
||
|
||
const result = [];
|
||
let rowIndex = 0;
|
||
|
||
for (const row of rows) {
|
||
const rowData = row.data;
|
||
if (!rowData || rowData.length === 0) continue;
|
||
|
||
const firstCell = String(rowData[0] || '').trim();
|
||
if (!firstCell) continue;
|
||
if (firstCell === '76.06' || firstCell === 'Итого') continue;
|
||
|
||
const saldoStartDebet = this.parseAmount(rowData[1]);
|
||
const turnoverDebet = this.parseAmount(rowData[3]);
|
||
const turnoverCredit = this.parseAmount(rowData[4]);
|
||
const saldoEndDebet = this.parseAmount(rowData[5]);
|
||
const saldoEndCredit = this.parseAmount(rowData[6]);
|
||
|
||
result.push({
|
||
row_index: rowIndex,
|
||
account_label: firstCell,
|
||
account_ls: this.extractAccountLs(firstCell),
|
||
saldo_start_debet: saldoStartDebet,
|
||
turnover_debet: turnoverDebet,
|
||
turnover_credit: turnoverCredit,
|
||
saldo_end_debet: saldoEndDebet,
|
||
saldo_end_credit: saldoEndCredit
|
||
});
|
||
rowIndex++;
|
||
}
|
||
|
||
console.log(`[BalanceSheet76Processor] Распознано строк (лицевых счетов): ${result.length}`);
|
||
return result;
|
||
}
|
||
|
||
extractPeriodFromFilename(filename) {
|
||
const yearMatch = filename.match(/20\d{2}/);
|
||
const year = yearMatch ? parseInt(yearMatch[0]) : new Date().getFullYear();
|
||
const monthMatch = filename.match(/(\d{1,2})[\.\/](\d{4})|(\d{4})[\.\/](\d{1,2})|Январь|январь/i);
|
||
if (monthMatch) {
|
||
const monthNames = { январ: 1, феврал: 2, март: 3, апрел: 4, май: 5, июн: 6, июл: 7, август: 8, сентябр: 9, октябр: 10, ноябр: 11, декабр: 12 };
|
||
const lower = filename.toLowerCase();
|
||
for (const [key, m] of Object.entries(monthNames)) {
|
||
if (lower.includes(key)) {
|
||
const startDate = new Date(year, m - 1, 1);
|
||
const endDate = new Date(year, m, 0);
|
||
return {
|
||
start: `${year}-${String(m).padStart(2, '0')}-01`,
|
||
end: `${year}-${String(m).padStart(2, '0')}-${String(endDate.getDate()).padStart(2, '0')}`,
|
||
type: 'month'
|
||
};
|
||
}
|
||
}
|
||
}
|
||
return { start: `${year}-01-01`, end: `${year}-12-31`, type: 'year' };
|
||
}
|
||
}
|
||
|
||
module.exports = BalanceSheet76Processor;
|