pdfgen/lib/pdf-fill-data/src/pdfFillDataPGE.js
2025-08-16 07:28:01 +00:00

548 lines
26 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// pdfFillDataPGE.js
const fs = require('fs');
const path = require('path');
const cheerio = require('cheerio'); // Библиотека для парсинга
const pdfFillData = require('./pdfFillData'); // Родительский класс
const { rgb } = require('pdf-lib');
const randomData = require('./randomDataGenerator');
class pdfFillDataPGE extends pdfFillData {
constructor() {
super('PGE');
this.baselineCorrectionFactor = 0.85;
// --- Свойства для динамических данных (СТР. 1) ---
this.service_for_name = '';
this.service_for_address = '';
this.account_no = '';
this.statement_date = '';
this.due_date = '';
// Account Summary
this.previous_statement = '';
this.payment_received = '';
this.previous_unpaid_balance = '';
this.pge_delivery_charges = '';
this.svce_generation_charges = '';
this.total_amount_due_date = ''; // e.g., "08/28/2019"
this.total_amount_due = '';
// Remittance Slip (отрывной купон)
this.remit_account_number = '';
this.remit_due_date = '';
this.remit_total_amount_due = '';
// --- ДАННЫЕ ДЛЯ ГРАФИКОВ ---
this.monthly_billing_history = []; // Массив объектов { month, year, electric, gas }
this.daily_usage_electric = {}; // { year_ago, last_period, current_period }
this.daily_usage_gas = {}; // { year_ago, last_period, current_period }
this.electric_conservation_incentive = '';
this.electric_transmission = '';
this.electric_distribution = '';
this.electric_public_purpose = '';
this.electric_nuclear_decommissioning = '';
this.electric_dwr_bond = '';
this.electric_ctc = '';
this.electric_ecra = '';
this.electric_pcia = '';
this.electric_taxes_and_other = '';
this.electric_total_charges = '';
// --- СВОЙСТВА ДЛЯ СТР. 3 ---
this.delivery_charges_period = '';
this.service_agreement_id = '';
this.rate_schedule = '';
this.meter_number = '';
this.current_meter_reading = '';
this.prior_meter_reading = '';
this.total_usage_kwh = '';
this.baseline_territory = '';
this.heat_source = '';
this.tier1_allowance_detail = '';
this.tier1_usage_amount = '';
this.tier2_usage_amount = '';
this.generation_credit = '';
this.pcia_adjustment = '';
this.franchise_fee_surcharge = '';
this.electric_usage_period_title = '';
this.daily_electric_usage_history = [];
// В constructor()
this.average_daily_usage = 0;
this.svce_rate_schedule = '';
this.svce_generation_detail = '';
this.svce_generation_amount = '';
this.svce_energy_surcharge = '';
this.svce_total_charges = '';
// --- СВОЙСТВА ДЛЯ СТР. 5 (Gas Charges) ---
this.gas_charges_period = '';
this.gas_service_agreement_id = '';
this.gas_rate_schedule = '';
this.gas_meter_number = '';
this.gas_current_reading = '';
this.gas_prior_reading = '';
this.gas_difference = '';
this.gas_multiplier = '';
this.gas_total_usage = '';
this.gas_baseline_territory = '';
this.gas_serial = '';
this.gas_tier1_allowance_detail = '';
this.gas_tier1_usage_detail = '';
this.gas_tier1_usage_amount = '';
this.gas_ppp_surcharge_detail = '';
this.gas_ppp_surcharge_amount = '';
this.gas_total_charges = '';
this.gas_procurement_period = '';
this.gas_procurement_amount = '';
this.gas_usage_period_title = '';
this.gas_average_daily_usage = 0;
this.daily_gas_usage_history = [];
this.fontMap = {
// "Имя шрифта из SVG-атрибута font-family": индекс_из_routes.js
'Helvetica': 0, // Базовый шрифт, всегда индекс 0
'Arial-BoldMT': 1,
'Arial': 2,
'ArialNarrow-Bold': 3,
'ArialNarrow': 4,
'Arial-ItalicMT': 5,
'MyriadPro-Regular': 2,
// Добавьте другие шрифты, если они есть в SVG
};
}
async setFields(fields) {
if (!fields || Object.keys(fields).length === 0) {
fields = randomData.pge(); // <-- Замените на entergy(), aep_ohio() и т.д.
}
this.service_for_name = fields.service_for_name;
this.service_for_address = fields.service_for_address;
this.account_no = fields.account_no;
this.statement_date = fields.statement_date;
this.due_date = fields.due_date;
this.previous_statement = fields.previous_statement;
this.payment_received = fields.payment_received;
this.previous_unpaid_balance = fields.previous_unpaid_balance;
this.pge_delivery_charges = fields.pge_delivery_charges;
this.svce_generation_charges = fields.svce_generation_charges;
this.total_amount_due_date = fields.total_amount_due_date;
this.total_amount_due = fields.total_amount_due;
this.remit_account_number = fields.remit_account_number;
this.remit_due_date = fields.remit_due_date;
this.remit_total_amount_due = fields.remit_total_amount_due;
// Данные для графиков
this.monthly_billing_history = fields.monthly_billing_history || [];
this.daily_usage_electric = fields.daily_usage_electric || {};
this.daily_usage_gas = fields.daily_usage_gas || {};
this.electric_conservation_incentive = fields.electric_conservation_incentive;
this.electric_transmission = fields.electric_transmission;
this.electric_distribution = fields.electric_distribution;
this.electric_public_purpose = fields.electric_public_purpose;
this.electric_nuclear_decommissioning = fields.electric_nuclear_decommissioning;
this.electric_dwr_bond = fields.electric_dwr_bond;
this.electric_ctc = fields.electric_ctc;
this.electric_ecra = fields.electric_ecra;
this.electric_pcia = fields.electric_pcia;
this.electric_taxes_and_other = fields.electric_taxes_and_other;
this.electric_total_charges = fields.electric_total_charges;
// --- ДАННЫЕ ДЛЯ СТР. 3 ---
this.delivery_charges_period = fields.delivery_charges_period;
this.service_agreement_id = fields.service_agreement_id;
this.rate_schedule = fields.rate_schedule;
this.meter_number = fields.meter_number;
this.current_meter_reading = fields.current_meter_reading;
this.prior_meter_reading = fields.prior_meter_reading;
this.total_usage_kwh = fields.total_usage_kwh;
this.baseline_territory = fields.baseline_territory;
this.heat_source = fields.heat_source;
this.tier1_allowance_detail = fields.tier1_allowance_detail;
this.tier1_usage_amount = fields.tier1_usage_amount;
this.tier2_usage_amount = fields.tier2_usage_amount;
this.generation_credit = fields.generation_credit;
this.pcia_adjustment = fields.pcia_adjustment;
this.franchise_fee_surcharge = fields.franchise_fee_surcharge;
this.electric_usage_period_title = fields.electric_usage_period_title;
this.svce_rate_schedule = fields.svce_rate_schedule;
this.svce_generation_detail = fields.svce_generation_detail;
this.svce_generation_amount = fields.svce_generation_amount;
this.svce_energy_surcharge = fields.svce_energy_surcharge;
this.svce_total_charges = fields.svce_total_charges;
// --- ДАННЫЕ ДЛЯ СТР. 5 ---
this.gas_charges_period = fields.gas_charges_period;
this.gas_service_agreement_id = fields.gas_service_agreement_id;
this.gas_rate_schedule = fields.gas_rate_schedule;
this.gas_meter_number = fields.gas_meter_number;
this.gas_current_reading = fields.gas_current_reading;
this.gas_prior_reading = fields.gas_prior_reading;
this.gas_difference = fields.gas_difference;
this.gas_multiplier = fields.gas_multiplier;
this.gas_total_usage = fields.gas_total_usage;
this.gas_baseline_territory = fields.gas_baseline_territory;
this.gas_serial = fields.gas_serial;
this.gas_tier1_allowance_detail = fields.gas_tier1_allowance_detail;
this.gas_tier1_usage_detail = fields.gas_tier1_usage_detail;
this.gas_tier1_usage_amount = fields.gas_tier1_usage_amount;
this.gas_ppp_surcharge_detail = fields.gas_ppp_surcharge_detail;
this.gas_ppp_surcharge_amount = fields.gas_ppp_surcharge_amount;
this.gas_total_charges = fields.gas_total_charges;
this.gas_procurement_period = fields.gas_procurement_period;
this.gas_procurement_amount = fields.gas_procurement_amount;
this.gas_usage_period_title = fields.gas_usage_period_title;
this.gas_average_daily_usage = parseFloat(fields.gas_average_daily_usage) || 0;
// В файле pdfFillDataPGE.js, метод setFields()
// ... (после строки this.gas_average_daily_usage = ...)
// --- НОВАЯ, УМНАЯ ПРОВЕРКА И ПАРСИНГ ---
if (typeof fields.daily_gas_usage_history === 'string') {
// Если пришли данные из формы (строка), парсим их
try {
this.daily_gas_usage_history = JSON.parse(fields.daily_gas_usage_history || '[]');
} catch (e) {
this.daily_gas_usage_history = [];
console.error("Error parsing daily_gas_usage_history JSON from string");
}
} else {
// Если пришли данные из генератора (уже массив), просто присваиваем
this.daily_gas_usage_history = fields.daily_gas_usage_history || [];
}
if (typeof fields.daily_electric_usage_history === 'string') {
try {
this.daily_electric_usage_history = JSON.parse(fields.daily_electric_usage_history || '[]');
} catch (e) {
this.daily_electric_usage_history = [];
console.error("Error parsing daily_electric_usage_history JSON from string");
}
} else {
this.daily_electric_usage_history = fields.daily_electric_usage_history || [];
}
// Удаляем старый `try/catch` для average_daily_usage, он уже обработан
this.average_daily_usage = parseFloat(fields.average_daily_usage) || 0;
}
/**
* Универсальный метод для отрисовки столбчатых диаграмм.
* Имеет два режима:
* 1. ТОЧНЫЙ: Если находит в SVG метки id="chart-label-start" и id="chart-label-end",
* строит сетку между их центрами.
* 2. ЗАПАСНОЙ: Если метки не найдены, строит равномерную сетку по всей ширине
* контейнера id="chart-area".
* @param {object} options - Объект с параметрами для диаграммы.
*/
async _drawBarChart(options) {
const { ipage, $, historyData, maxAxisValue, barWidth, electricColor, gasColor } = options;
if (!historyData || historyData.length === 0) return;
const page = this.getPageByIndex(ipage);
if (!page) return;
const pageHeight = this.getPageHeight(ipage);
const chartRect = $('#chart-area');
if (chartRect.length === 0) {
console.error(`Could not find boundary element with id='chart-area' in SVG for page ${ipage}.`);
return;
}
const chartArea = {
x_ill: parseFloat(chartRect.attr('x')),
y_ill: parseFloat(chartRect.attr('y')),
width: parseFloat(chartRect.attr('width')),
height: parseFloat(chartRect.attr('height'))
};
if (Object.values(chartArea).some(isNaN)) {
console.error(`Invalid attributes for #chart-area in SVG for page ${ipage}.`);
return;
}
// --- АЛГОРИТМ ВЫБОРА РЕЖИМА ---
const startLabel = $('#chart-label-start');
const endLabel = $('#chart-label-end');
const numBars = historyData.length;
let startPointX, stepX;
if (startLabel.length > 0 && endLabel.length > 0) {
// --- РЕЖИМ 1: ТОЧНЫЙ (по меткам) ---
console.log(`Page ${ipage} chart: Using 'chart-label-start/end' for precise layout.`);
const getLabelCenter = (element) => {
const transformAttr = element.attr('transform');
const matrix = transformAttr ? transformAttr.match(/matrix\(([^)]+)\)/) : null;
if (!matrix) return NaN;
const [scaleX, , , , x_svg] = matrix[1].split(/[ ,]+/).map(parseFloat);
const font = this.getCustomerFontByIndex(0);
const textWidth = font.widthOfTextAtSize(element.text(), 7 * scaleX);
return x_svg + (textWidth / 2);
};
startPointX = getLabelCenter(startLabel);
const endPointX = getLabelCenter(endLabel);
if (isNaN(startPointX) || isNaN(endPointX)) {
console.error("Could not parse coordinates from start/end labels.");
return;
}
stepX = (numBars > 1) ? (endPointX - startPointX) / (numBars - 1) : 0;
} else {
// --- РЕЖИМ 2: ЗАПАСНОЙ (по ширине chart-area) ---
console.log(`Page ${ipage} chart: Using '#chart-area' for uniform layout.`);
const spacePerBar = chartArea.width / (numBars + 1);
startPointX = chartArea.x_ill + spacePerBar;
stepX = spacePerBar;
}
// --- ОБЩАЯ ЛОГИКА ОТРИСОВКИ ---
historyData.forEach((dataPoint, index) => {
const electricValue = parseFloat(dataPoint.electric || dataPoint.kwh) || 0;
const gasValue = parseFloat(dataPoint.gas) || 0;
const totalValue = electricValue + gasValue;
if (totalValue <= 0) return;
const electricBarHeight = (electricValue / maxAxisValue) * chartArea.height;
const gasBarHeight = (gasValue / maxAxisValue) * chartArea.height;
const barCenterX = startPointX + (index * stepX);
const barX = barCenterX - (barWidth / 2);
const baseY = pageHeight - chartArea.y_ill - chartArea.height;
if (gasValue > 0 && gasColor) {
page.drawRectangle({ x: barX, y: baseY, width: barWidth, height: gasBarHeight, color: gasColor });
}
if (electricValue > 0 && electricColor) {
page.drawRectangle({ x: barX, y: baseY + gasBarHeight, width: barWidth, height: electricBarHeight, color: electricColor });
}
});
}
/**
* Рисует пунктирную линию среднего потребления на графике.
* @param {number} ipage - Индекс страницы.
* @param {CheerioAPI} $ - Распарсенный Cheerio объект с содержимым SVG.
*/
async _drawAverageLine(ipage, $, average_daily = this.average_daily_usage, max = 30) {
if (!average_daily || average_daily <= 0) return;
const page = this.getPageByIndex(ipage);
if (!page) return;
const pageHeight = this.getPageHeight(ipage);
const chartRect = $('#chart-area');
if (chartRect.length === 0) {
console.error("Could not find #chart-area for average line.");
return;
}
const chartArea = {
x_ill: parseFloat(chartRect.attr('x')),
y_ill: parseFloat(chartRect.attr('y')),
width: parseFloat(chartRect.attr('width')),
height: parseFloat(chartRect.attr('height'))
};
if (Object.values(chartArea).some(isNaN)) {
console.error("Invalid attributes for #chart-area for average line.");
return;
}
// Вычисляем Y-координату для линии
const avgLineY = (pageHeight - chartArea.y_ill - chartArea.height) + (average_daily / max) * chartArea.height;
// Рисуем линию по всей ширине графика
page.drawLine({
start: { x: chartArea.x_ill, y: avgLineY },
end: { x: chartArea.x_ill + chartArea.width, y: avgLineY },
thickness: 0.5,
color: rgb(0, 0, 0),
dashArray: [3, 3],
});
}
// --- Новый главный метод отрисовки ---
// В файле pdfFillDataPGE.js
// --- ПОЛНОСТЬЮ ЗАМЕНИТЕ СТАРЫЙ МЕТОД draw() НА ЭТОТ ---
async draw() {
const svgDir = path.join(__dirname, `../../../public/template/svg/pge/`);
const replacements = {
// Заголовок
'1234567890-1': this.account_no,
'09/07/2019': this.statement_date,
'09/28/2019': this.due_date,
// Service For
'SPARKY JOULE': this.service_for_name,
'12345 ENERGY CT': this.service_for_address,
// Account Summary
'$91.57': "$" + this.previous_statement,
'-91.57': this.payment_received,
'$0.00': "$" + this.previous_unpaid_balance,
'$55.66': "$" + this.pge_delivery_charges,
'$32.48': "$" + this.svce_generation_charges,
'08/28/2019': this.total_amount_due_date,
'$88.14': "$" + this.total_amount_due,
// Remittance Slip (отрывной купон)
// Примечание: '123456789-1' в SVG может быть другим текстом, чем '1234567890-1'
// Проверьте SVG и используйте правильный ключ. Для примера я использую два разных.
'123456789-1': this.remit_account_number,
// Дата и сумма на купоне могут совпадать с основными, но могут и отличаться,
// поэтому используем отдельные переменные.
// '09/28/2019': this.remit_due_date, // Конфликтует с Due Date в заголовке
// '$88.14': this.remit_total_amount_due, // Конфликтует с Total Amount Due выше
// Daily Usage Comparison (маленькие графики)
'12.50': this.daily_usage_gas.year_ago,
'12.16': this.daily_usage_gas.last_period,
'12.67': this.daily_usage_gas.current_period,
'0.12': this.daily_usage_electric.year_ago,
'0.16': this.daily_usage_electric.last_period,
'0.17': this.daily_usage_electric.current_period,
// --- НОВЫЕ ПЛЕЙСХОЛДЕРЫ СО СТРАНИЦЫ 2 ---
'-$9.50': "-$" + this.electric_conservation_incentive * -1,
'12.42': this.electric_transmission,
'35.08': this.electric_distribution,
'4.71': this.electric_public_purpose,
'0.33': this.electric_nuclear_decommissioning,
'1.91': this.electric_dwr_bond,
'0.42': this.electric_ctc,
'-0.22': this.electric_ecra,
'10.26': this.electric_pcia,
'0.25': this.electric_taxes_and_other,
// --- ПЛЕЙСХОЛДЕРЫ СО СТР. 3 ---
'08/02/2019 - 08/31/2019 (30 billing days)': this.delivery_charges_period,
'Service For: 12345 ENERGY CT': `Service For: ${this.service_for_address}`,
'Service Agreement ID: 111111111': `Service Agreement ID: ${this.service_agreement_id}`,
'Rate Schedule: E1 X Residential Service': `Rate Schedule: ${this.rate_schedule}`,
'1111111111': this.meter_number,
'37,710': this.current_meter_reading,
'37,330': this.prior_meter_reading,
'380.000000 kWh': this.total_usage_kwh,
'X': this.baseline_territory,
'B - Not Electric': this.heat_source,
'297.00 kWh (30 days x 9.9 kWh/day)': this.tier1_allowance_detail,
'$66.46': this.tier1_usage_amount,
'23.37': this.tier2_usage_amount,
'-44.68': this.generation_credit,
'10.26': this.pcia_adjustment,
'0.25': this.franchise_fee_surcharge,
'Electric Usage This Period: 380.000000 kWh, 30 billing days': this.electric_usage_period_title,
// --- НОВЫЕ ПЛЕЙСХОЛДЕРЫ СО СТРАНИЦЫ 4 ---
'E-1': this.svce_rate_schedule,
'380.000000 kWh @ $0.08519': this.svce_generation_detail,
'380.000000 kWh': `${this.svce_generation_detail.split(" ")[0]} kWh`,
'$32.37': "$" + this.svce_generation_amount,
'32.37': this.svce_generation_amount,
'0.11': this.svce_energy_surcharge,
'$32.48': "$" + this.svce_total_charges,
// --- ПЛЕЙСХОЛДЕРЫ СО СТРАНИЦЫ 5 ---
'08/02/2019 - 08/31/2019 (30 billing days)': this.gas_charges_period,
'Service Agreement ID: 1111111111': `Service Agreement ID: ${this.gas_service_agreement_id}`,
'Rate Schedule: G1 X Residential Service': `Rate Schedule: ${this.gas_rate_schedule}`,
'11111111': this.gas_meter_number,
'2,588': this.gas_current_reading,
'2,583': this.gas_prior_reading,
'diff5': this.gas_difference,
'1.031647': this.gas_multiplier,
'5.000000 Therms': this.gas_total_usage,
// 'X': this.gas_baseline_territory, // "X" слишком общий, может вызвать проблемы
// 'G': this.gas_serial, // "G" слишком общий
'17.70 Therms (30 days x 0.59 Therms/day)': this.gas_tier1_allowance_detail,
'5.000000 Therms @ $1.28395': this.gas_tier1_usage_detail,
'$6.42': "$" + this.gas_tier1_usage_amount,
'($0.09047 /Therm)': "$" + this.gas_ppp_surcharge_detail,
'0.45': this.gas_ppp_surcharge_amount,
'$6.87': "$" + this.gas_total_charges,
'07/02/2019 - 07/31/2019': this.gas_procurement_period,
'$0.28462': "$" + this.gas_procurement_amount,
'Gas Usage This Period: 5.000000 Therms, 30 billing days': this.gas_usage_period_title,
};
for (let i = 0; i < this.countPages(); i++) {
const svgPath = path.join(svgDir, `${i + 1}.svg`);
if (fs.existsSync(svgPath)) {
console.log(`Processing all elements for page ${i}: ${svgPath}`);
// --- ПАРСИМ SVG ОДИН РАЗ НА СТРАНИЦУ ---
const svgContent = fs.readFileSync(svgPath, 'utf8');
const $ = cheerio.load(svgContent, { xmlMode: true });
// --- ВЫЗЫВАЕМ СПЕЦИФИЧНЫЕ ДЛЯ СТРАНИЦЫ ФУНКЦИИ ---
// Графики на первой странице (индекс 0)
// График на первой странице
if (i === 0) {
await this._drawBarChart({
ipage: i,
$: $,
historyData: this.monthly_billing_history,
maxAxisValue: 200,
barWidth: 6.5,
electricColor: rgb(0, 0, 0),
gasColor: rgb(0.8, 0.8, 0.8)
});
}
// Графики на третьей странице (индекс 2)
if (i === 2) {
await this._drawBarChart({
ipage: i,
$: $, // Передаем распарсенный объект
historyData: this.daily_electric_usage_history,
maxAxisValue: 30,
barWidth: 4,
electricColor: rgb(0, 0, 0),
gasColor: null
});
await this._drawAverageLine(i, $); // Вызываем отрисовку средней линии
}
if (i === 4) { // График на пятой странице (индекс 4)
await this._drawBarChart({
ipage: i,
$: $,
historyData: this.daily_gas_usage_history,
maxAxisValue: 5, // Максимум на оси Y для газа
barWidth: 4,
electricColor: null,
gasColor: rgb(0.8, 0.8, 0.8)
});
// Рисуем линию среднего, если нужно
await this._drawAverageLine(i, $, this.gas_average_daily_usage, 5);
}
// Текст рисуется для каждой страницы в конце
await super.parseAndDrawSVG(i, $, replacements);
}
}
}
}
module.exports = pdfFillDataPGE;