first commit

This commit is contained in:
Oleg 2025-08-16 07:28:01 +00:00
commit 48bb7355f9
131 changed files with 70001 additions and 0 deletions

BIN
Core/.DS_Store vendored Normal file

Binary file not shown.

21
Core/js/getcookies.js Normal file
View File

@ -0,0 +1,21 @@
// GET COOKIES
module.exports = function(req) {
const cookieHeader = req.headers.cookie;
if (cookieHeader) {
// Разбиваем строку Cookie на массив кук, разделенных "; "
const cookieArray = cookieHeader.split('; ');
// Создаем объект, чтобы хранить куки
const cookies = {};
// Парсим каждую куку и добавляем ее в объект cookies
cookieArray.forEach(cookie => {
const [name, value] = cookie.split('=');
cookies[name] = value;
});
return cookies;
} else return false;
}

29
Core/js/sendmail.js Normal file
View File

@ -0,0 +1,29 @@
const path = require('path');
const nodemailer = require("nodemailer");
var Config = require(path.join(__dirname, '../../config'));
const Sendmail = class {
constructor() {
console.log('Sendmail Controller INIT');
}
send( username, email, token ) {
const transporter = nodemailer.createTransport(Config.sendmail);
const info = transporter.sendMail({
from: '"GAMES SUPPORT" <support@iron-brain.ru>',
to: email,
subject: "Confirm registration.",
text: "Hello, "+username+". Confirm registration by link. https://pdfgen.evpak-soft.ru/api/v1/confirm/?token="+token,
html: "<p>Hello, "+username+". Confirm registration by link.</p><p>[<a href='https://pdfgen.evpak-soft.ru/api/v1/confirm/?token="+token+"'>https://pdfgen.evpak-soft.ru/api/v1/confirm/?token="+token+"</a>]</p>",
});
console.log("Message sent: %s", info.messageId);
}
}
module.exports = Sendmail;

View File

@ -0,0 +1,433 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Generate AEP Ohio PDF</title>
<style>
body { font-family: sans-serif; margin: 20px; background-color: #f4f4f4; color: #333; }
.container { background-color: #fff; padding: 20px; border-radius: 8px; box-shadow: 0 0 10px rgba(0,0,0,0.1); max-width: 600px; margin: auto; }
h1 { text-align: center; color: #00609E; } /* AEP Blue */
h2 { margin-top: 30px; border-bottom: 1px solid #eee; padding-bottom: 5px; color: #333; }
.form-group { margin-bottom: 15px; }
label { display: block; margin-bottom: 5px; font-weight: bold; }
input[type="text"] {
width: calc(100% - 18px);
padding: 8px;
border: 1px solid #ccc;
border-radius: 4px;
box-sizing: border-box;
}
button {
display: block;
width: 100%;
padding: 10px 15px;
background-color: #00609E;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 16px;
margin-top: 20px;
}
button:hover { background-color: #004c7e; }
#status { margin-top: 20px; font-weight: bold; text-align: center; }
.grid-container { display: grid; grid-template-columns: 1fr 1fr; gap: 15px; }
.full-width { grid-column: 1 / -1; }
</style>
</head>
<body>
<div class="container">
<h1>Generate AEP Ohio PDF</h1>
<form id="pdfForm">
<h2>Основная информация</h2>
<div class="grid-container">
<div class="form-group">
<label for="customer_name">Customer Name:</label>
<input type="text" id="customer_name" name="customer_name" value="MAGGIORES WHOLESALE PUBLIC SALT">
</div>
<div class="form-group">
<label for="attn_name">Attention Name (опционально):</label>
<input type="text" id="attn_name" name="attn_name" value="ATTN: TIM SALTER">
</div>
<div class="form-group full-width">
<label for="service_address">Service Address (полный адрес):</label>
<input type="text" id="service_address" name="service_address" value="2927 HARRISBURG RD NE, CANTON, OH 44705-2563">
</div>
<div class="form-group">
<label for="account_number">Account #:</label>
<input type="text" id="account_number" name="account_number" value="073-71205415">
</div>
<div class="form-group">
<label for="bill_mailing_date">Bill Mailing Date:</label>
<input type="text" id="bill_mailing_date" name="bill_mailing_date" value="Aug 15, 2018">
</div>
<div class="form-group">
<label for="amount_due_date">Amount Due Date:</label>
<input type="text" id="amount_due_date" name="amount_due_date" value="September 6, 2018">
</div>
<div class="form-group">
<label for="remit_pay_after_date_amount">Pay After Date Amount:</label>
<input type="text" id="remit_pay_after_date_amount" name="remit_pay_after_date_amount" value="Pay $533.41 after 09/06/2018">
</div>
<div class="form-group full-width">
<label for="barcode_number">Barcode Number:</label>
<input type="text" id="barcode_number" name="barcode_number" value="0000526980000533410100000000000733129541515080809011900009">
</div>
</div>
<h2>Период и потребление</h2>
<div class="grid-container">
<div class="form-group">
<label for="billing_period_from">Billing From:</label>
<input type="text" id="billing_period_from" name="billing_period_from" value="07/17/18">
</div>
<div class="form-group">
<label for="billing_period_to">Billing To:</label>
<input type="text" id="billing_period_to" name="billing_period_to" value="08/13/18">
</div>
<div class="form-group">
<label for="billing_period_days">Billing Days:</label>
<input type="text" id="billing_period_days" name="billing_period_days" value="(28 days)">
</div>
<div class="form-group">
<label for="current_charges_kwh">Current kWh:</label>
<input type="text" id="current_charges_kwh" name="current_charges_kwh" value="3,803">
</div>
</div>
<h2>Детализация сборов AEP Ohio (для Delivery Charge)</h2>
<div class="form-group">
<label for="tariff_info_line">Строка тарифа:</label>
<input type="text" id="tariff_info_line" name="tariff_info_line" value="Tariff 840 - Medium General Service 08/13/18">
</div>
<div class="form-group">
<label for="service_delivery_identifier">Service Delivery Identifier:</label>
<input type="text" id="service_delivery_identifier" name="service_delivery_identifier" value="00140060724501674">
</div>
<div class="grid-container">
<div class="form-group">
<label for="transmission_service">Transmission Service ($):</label>
<input type="text" id="transmission_service" name="transmission_service" value="86.53">
</div>
<div class="form-group">
<label for="distribution_service">Distribution Service ($):</label>
<input type="text" id="distribution_service" name="distribution_service" value="155.81">
</div>
<div class="form-group">
<label for="customer_charge_detail">Customer Charge ($):</label>
<input type="text" id="customer_charge_detail" name="customer_charge_detail" value="22.79">
</div>
<div class="form-group">
<label for="retail_stability_rider">Retail Stability Rider ($):</label>
<input type="text" id="retail_stability_rider" name="retail_stability_rider" value="27.57">
</div>
<div class="form-group">
<label for="deferred_asset_rider">Deferred Asset Rider ($):</label>
<input type="text" id="deferred_asset_rider" name="deferred_asset_rider" value="7.30">
</div>
<div class="form-group">
<label for="phase_in_recovery_rider">Phase-In Recovery Rider ($):</label>
<input type="text" id="phase_in_recovery_rider" name="phase_in_recovery_rider" value="21.11">
</div>
<div class="form-group">
<label for="power_purchase_rider">Power Purchase Rider ($):</label>
<input type="text" id="power_purchase_rider" name="power_purchase_rider" value="0.25">
</div>
</div>
<h2>Сборы поставщика (Supplier Charges)</h2>
<div class="form-group">
<label for="supplier_charges">Energy Supply ($):</label>
<input type="text" id="supplier_charges" name="supplier_charges" value="198.52">
</div>
<hr>
<h2>Предыдущие начисления (Previous Charges)</h2>
<div class="grid-container">
<div class="form-group">
<label for="total_amount_due_last_billing">Total Amount Due At Last Billing ($):</label>
<input type="text" id="total_amount_due_last_billing" name="total_amount_due_last_billing" value="782.05">
</div>
<div class="form-group">
<label for="payment_thank_you_date">Payment Date (e.g., 08/14/18):</label>
<input type="text" id="payment_thank_you_date" name="payment_thank_you_date" value="08/14/18">
</div>
<div class="form-group">
<label for="payment_thank_you_amount">Payment Received Amount ($):</label>
<input type="text" id="payment_thank_you_amount" name="payment_thank_you_amount" value="-782.05">
</div>
<div class="form-group">
<label for="late_payment_charge">Late Payment Charge ($):</label>
<input type="text" id="late_payment_charge" name="late_payment_charge" value="7.10">
</div>
<div class="form-group full-width">
<label for="previous_balance_due">Previous Balance Due ($):</label>
<input type="text" id="previous_balance_due" name="previous_balance_due" value="7.10">
</div>
</div>
<hr>
<h2>История потребления (Usage History)</h2>
<div class="form-group full-width">
<label for="usage_history_data">Данные в формате JSON (массив объектов):</label>
<textarea id="usage_history_data" name="usage_history_data" rows="8">[
{"month": "Aug", "year": 2017, "kwh": 5605},
{"month": "Sep", "year": 2017, "kwh": 4379},
{"month": "Oct", "year": 2017, "kwh": 4876},
{"month": "Nov", "year": 2017, "kwh": 4676},
{"month": "Dec", "year": 2017, "kwh": 6952},
{"month": "Jan", "year": 2018, "kwh": 8434},
{"month": "Feb", "year": 2018, "kwh": 7836},
{"month": "Mar", "year": 2018, "kwh": 6225},
{"month": "Apr", "year": 2018, "kwh": 7420},
{"month": "May", "year": 2018, "kwh": 5991},
{"month": "Jun", "year": 2018, "kwh": 4404},
{"month": "Jul", "year": 2018, "kwh": 5175},
{"month": "Aug", "year": 2018, "kwh": 3803}
]
</textarea>
</div>
<hr>
<h2>Детали на странице 3 (маленькие графики)</h2>
<h4>Usage (kWh)</h4>
<div class="grid-container">
<div class="form-group">
<label>Предыдущий год (e.g., Aug '17):</label>
<input type="text" name="usage_details_prev_year" value="5605">
</div>
<div class="form-group">
<label>Предыдущий месяц (e.g., Jul '18):</label>
<input type="text" name="usage_details_prev_month" value="5175">
</div>
<div class="form-group">
<label>Текущий месяц (e.g., Aug '18):</label>
<input type="text" name="usage_details_curr_month" value="3803">
</div>
</div>
<h4>Avg. Daily Cost ($)</h4>
<div class="grid-container">
<div class="form-group">
<label>Предыдущий год (e.g., Aug '17):</label>
<input type="text" name="cost_details_prev_year" value="11.09">
</div>
<div class="form-group">
<label>Предыдущий месяц (e.g., Jul '18):</label>
<input type="text" name="cost_details_prev_month" value="11.48">
</div>
<div class="form-group">
<label>Текущий месяц (e.g., Aug '18):</label>
<!-- В оригинале это значение не показано, вводим примерное -->
<input type="text" name="cost_details_curr_month" value="11.75">
</div>
</div>
<h4>Avg. Temperature (°F)</h4>
<div class="grid-container">
<div class="form-group">
<label>Предыдущий год (e.g., Aug '17):</label>
<input type="text" name="temp_details_prev_year" value="72">
</div>
<div class="form-group">
<label>Предыдущий месяц (e.g., Jul '18):</label>
<input type="text" name="temp_details_prev_month" value="73">
</div>
<div class="form-group">
<label>Текущий месяц (e.g., Aug '18):</label>
<input type="text" name="temp_details_curr_month" value="75">
</div>
</div>
<hr>
<h2>Квитанция о депозите (Deposit Receipt)</h2>
<div class="grid-container">
<div class="form-group">
<label for="area_office">Area Office:</label>
<input type="text" id="area_office" name="area_office" value="09210">
</div>
<div class="form-group">
<label for="cashier_number">Cashier Number:</label>
<input type="text" id="cashier_number" name="cashier_number" value="920">
</div>
<div class="form-group full-width">
<label for="deposit_number">Deposit Number:</label>
<input type="text" id="deposit_number" name="deposit_number" value="075050936 2537 003 20180813">
</div>
<div class="form-group">
<label for="deposit_date">Deposit Date:</label>
<input type="text" id="deposit_date" name="deposit_date" value="08/13/2018">
</div>
<div class="form-group">
<label for="deposit_amount">Deposit Amount ($):</label>
<input type="text" id="deposit_amount" name="deposit_amount" value="$157.00">
</div>
</div>
<hr>
<h2>Детализация потребления (Billed Usage & Meter Read)</h2>
<h4>Таблица "Billed Usage"</h4>
<div class="grid-container">
<div class="form-group">
<label>Дата (e.g., 08/18):</label>
<input type="text" name="billed_usage_date" value="08/18">
</div>
</div>
<div class="grid-container">
<div class="form-group">
<label>kWh Usage:</label>
<input type="text" name="billed_usage_kwh_usage" value="3,803">
</div>
<div class="form-group">
<label>kWh Billed Usage:</label>
<input type="text" name="billed_usage_kwh_billed" value="3,803 kWh">
</div>
<div class="form-group">
<label>kW Usage:</label>
<input type="text" name="billed_usage_kw_usage" value="16.927">
</div>
<div class="form-group">
<label>kW Billed Usage:</label>
<input type="text" name="billed_usage_kw_billed" value="16.900 kW">
</div>
</div>
<h4>Таблица "Meter Read Details"</h4>
<div class="form-group">
<label>Номер счетчика (Meter #):</label>
<input type="text" name="meter_read_details_meter_no" value="92002912">
</div>
<h5>Строка kWh</h5>
<div class="grid-container">
<div class="form-group">
<label>Previous:</label>
<input type="text" name="meter_read_kwh_previous" value="86974">
</div>
<div class="form-group">
<label>Current:</label>
<input type="text" name="meter_read_kwh_current" value="90777">
</div>
<div class="form-group">
<label>Metered:</label>
<input type="text" name="meter_read_kwh_metered" value="3803">
</div>
<div class="form-group">
<label>Usage:</label>
<input type="text" name="meter_read_kwh_usage" value="3,803 kWh">
</div>
</div>
<h5>Строка kW</h5>
<div class="grid-container">
<div class="form-group">
<label>Current:</label>
<input type="text" name="meter_read_kw_current" value="16.927">
</div>
<div class="form-group">
<label>Metered:</label>
<input type="text" name="meter_read_kw_metered" value="16.927">
</div>
<div class="form-group">
<label>Usage:</label>
<input type="text" name="meter_read_kw_usage" value="16.927 kW">
</div>
</div>
<div class="form-group">
<label>Service Period:</label>
<input type="text" name="meter_read_service_period" value="07/16 - 08/13">
</div>
<div class="form-group">
<label>Next Read Date Info:</label>
<input type="text" name="next_read_date_info" value="Next scheduled read date should be between Sep 11 and Sep 14.">
</div>
<button type="submit">Generate and Download PDF</button>
</form>
<div id="status"></div>
</div>
<script>
// ... (Ваш JavaScript код для отправки формы остается БЕЗ ИЗМЕНЕНИЙ) ...
document.getElementById('pdfForm').addEventListener('submit', async function(event) {
event.preventDefault();
const statusDiv = document.getElementById('status');
statusDiv.textContent = 'Generating PDF, please wait...';
statusDiv.style.color = 'orange';
const formData = new FormData(event.target);
const data = {};
formData.forEach((value, key) => { data[key] = value; });
try {
data.usage_history_data = JSON.parse(data.usage_history_data);
} catch (e) {
alert('Ошибка в JSON данных для истории потребления!');
return; // Прервать отправку
}
const apiUrl = '/pdfgen/api/v1/AEPOhio';
const bearerToken = "43QAwPldLuuQTErY303m16lY5dSeDUy1OVKBzy5HTDG2KxfVC0m1o1cfZ49gbBRu";
try {
const response = await fetch(apiUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${bearerToken}`
},
body: JSON.stringify(data)
});
if (!response.ok) {
let errorText = `Error: ${response.status} ${response.statusText}`;
const responseBodyText = await response.text();
try {
const errorJson = JSON.parse(responseBodyText);
errorText += ` - ${errorJson.error || JSON.stringify(errorJson)}`;
} catch (e) {
if (responseBodyText) {
errorText += ` - ${responseBodyText}`;
}
}
throw new Error(errorText);
}
const contentDisposition = response.headers.get('content-disposition');
let filename = "generated_aep_ohio.pdf";
if (contentDisposition) {
const filenameRegex = /filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/;
const matches = filenameRegex.exec(contentDisposition);
if (matches != null && matches[1]) {
filename = matches[1].replace(/['"]/g, '');
}
}
const blob = await response.blob();
const downloadUrl = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.style.display = 'none';
a.href = downloadUrl;
a.download = filename;
document.body.appendChild(a);
a.click();
window.URL.revokeObjectURL(downloadUrl);
document.body.removeChild(a);
statusDiv.textContent = 'PDF downloaded successfully!';
statusDiv.style.color = 'green';
} catch (error) {
console.error('Request failed:', error);
statusDiv.textContent = `Failed to generate PDF: ${error.message}`;
statusDiv.style.color = 'red';
}
});
</script>
</body>
</html>

25
Core/views/api.ejs Normal file
View File

@ -0,0 +1,25 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title><%= title %></title>
<link rel="stylesheet" type="text/css" href="/css/style.css">
<script src="/js/jquery/3.5.1/jquery.min.js"></script>
<script src="/js/script.js"></script>
</head>
<body>
<div id="pdfgen-intro">
<%= title %>
<p>Actions:</p>
<ul>
<li>Xfinity: /api/v1/Xfinity</li>
</ul>
</div>
</body>
</html>

265
Core/views/entergy_form.ejs Normal file
View File

@ -0,0 +1,265 @@
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Generate Entergy PDF</title>
<style>
body { font-family: sans-serif; margin: 20px; background-color: #f4f4f4; color: #333; }
.container { background-color: #fff; padding: 20px; border-radius: 8px; box-shadow: 0 0 10px rgba(0,0,0,0.1); max-width: 650px; margin: auto; }
h1, h2 { text-align: center; color: #00938F; }
h3 { margin-top: 30px; border-bottom: 1px solid #eee; padding-bottom: 5px; color: #333; }
.form-group { margin-bottom: 15px; }
label { display: block; margin-bottom: 5px; font-weight: bold; }
input[type="text"], textarea {
width: calc(100% - 18px); padding: 8px; border: 1px solid #ccc; border-radius: 4px; box-sizing: border-box;
}
textarea { resize: vertical; }
button { display: block; width: 100%; padding: 10px 15px; background-color: #00938F; color: white; border: none; border-radius: 4px; cursor: pointer; font-size: 16px; margin-top: 20px; }
button:hover { background-color: #007a76; }
#status { margin-top: 20px; font-weight: bold; text-align: center; }
.grid-container { display: grid; grid-template-columns: 1fr 1fr; gap: 15px; }
.grid-3-col { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 15px; }
.full-width { grid-column: 1 / -1; }
</style>
</head>
<body>
<div class="container">
<h1>Generate Entergy PDF</h1>
<form id="pdfForm">
<h3>Основная информация</h3>
<div class="grid-container">
<div class="form-group">
<label>Customer Name:</label>
<input type="text" name="customer_name" value="James">
</div>
<div class="form-group">
<label>Mail Date:</label>
<input type="text" name="mail_date" value="06/28/2023">
</div>
<div class="form-group">
<label>Account #:</label>
<input type="text" name="account_number" value="193815735">
</div>
<div class="form-group">
<label>Invoice #:</label>
<input type="text" name="invoice_number" value="70007737428">
</div>
<div class="form-group">
<label>QPC:</label>
<input type="text" name="qpc_code" value="07000">
</div>
<div class="form-group">
<label>Cycle:</label>
<input type="text" name="cycle_number" value="20">
</div>
<div class="form-group">
<label>Amount Due by Date:</label>
<input type="text" name="amount_due_by_date" value="08/10/2023">
</div>
<div class="form-group">
<label>Amount Due After Date:</label>
<input type="text" name="amount_due_after_date" value="08/10/2023">
</div>
</div>
<h3>Адрес обслуживания</h3>
<div class="form-group full-width">
<label>Service Location (Line 1):</label>
<input type="text" name="service_location_line1" value="844 Carmadelle St">
</div>
<div class="form-group full-width">
<label>Service Location (Line 2):</label>
<input type="text" name="service_location_line2" value="Marrero, LA 70072-1322">
</div>
<h3>Детализация начислений (со стр. 2)</h3>
<div class="grid-3-col">
<div class="form-group">
<label>Energy Charge ($):</label>
<input type="text" name="energy_charge" value="257.69">
</div>
<div class="form-group">
<label>Federal EAC Rider ($):</label>
<input type="text" name="federal_eac_rider" value="0.12">
</div>
<div class="form-group">
<label>Fuel Adjustment ($):</label>
<input type="text" name="fuel_adjustment" value="58.23">
</div>
<div class="form-group">
<label>Storm Restoration Offset ($):</label>
<input type="text" name="storm_restoration_offset" value="-5.74">
</div>
<div class="form-group">
<label>Storm Restoration Charge ($):</label>
<input type="text" name="storm_restoration_charge" value="46.03">
</div>
<div class="form-group">
<label>Deposit ($) (опционально):</label>
<input type="text" name="deposit" value="150.00">
</div>
<div class="form-group">
<label>Connect Fee ($) (опционально):</label>
<input type="text" name="connect_fee" value="12.50">
</div>
</div>
<h3>Сумма к оплате после срока</h3>
<div class="form-group">
<label>Amount Due After ($):</label>
<input type="text" name="amount_due_after_value" value="536.65">
</div>
<h3>Потребление (Billing Period)</h3>
<div class="grid-container">
<div class="form-group">
<label>Billing Days:</label>
<input type="text" name="billing_days" value="25">
</div>
<div class="form-group">
<label>kWh Used:</label>
<input type="text" name="kwh_used" value="2950">
</div>
<div class="form-group">
<label>Avg kWh Per Day:</label>
<input type="text" name="avg_kwh_per_day" value="118.0">
</div>
</div>
<h3>История потребления (JSON)</h3>
<div class="form-group full-width">
<label>Данные в формате JSON (массив):</label>
<textarea name="usage_history_data" rows="5">[
{"month": "JAN", "kwh": 1000}, {"month": "FEB", "kwh": 900}, {"month": "MAR", "kwh": 850},
{"month": "APR", "kwh": 950}, {"month": "MAY", "kwh": 1200}, {"month": "JUN", "kwh": 2950},
{"month": "JUL", "kwh": 0}, {"month": "AUG", "kwh": 0}, {"month": "SEP", "kwh": 0},
{"month": "OCT", "kwh": 0}, {"month": "NOV", "kwh": 0}, {"month": "DEC", "kwh": 0}
]</textarea>
</div>
<h3>Детализация счетчика (со стр. 2)</h3>
<div class="grid-container">
<div class="form-group">
<label>Номер контракта:</label>
<input type="text" name="meter_reading_contract" value="22371189">
</div>
<div class="form-group">
<label>Номер счетчика (Meter #):</label>
<input type="text" name="meter_number" value="AM11627352">
</div>
<div class="form-group">
<label>Тариф (Rate):</label>
<input type="text" name="meter_rate" value="LA_RS">
</div>
</div>
<div class="grid-container">
<div class="form-group">
<label>Дата/Время текущего показания:</label>
<input type="text" name="current_meter_reading_datetime" value="06/26/2023 11:59 PM">
</div>
<div class="form-group">
<label>Значение текущего показания:</label>
<input type="text" name="current_meter_reading_value" value="77886">
</div>
<div class="form-group">
<label>Дата/Время предыдущего показания:</label>
<input type="text" name="previous_meter_reading_datetime" value="06/02/2023 00:00 AM">
</div>
<div class="form-group">
<label>Значение предыдущего показания:</label>
<input type="text" name="previous_meter_reading_value" value="74936">
</div>
</div>
<h3>Отрывной купон</h3>
<div class="form-group full-width">
<label>Customer Name & Address (для купона):</label>
<textarea name="remit_customer_address" rows="3">JAMES STUTES
844 CARMADELLE ST
MARRERO LA 70072-1322</textarea>
</div>
<div class="form-group full-width">
<label>Barcode Number (низ страницы):</label>
<input type="text" name="barcode_number" value="700000019381573500000000051883700000053665622206">
</div>
<button type="submit">Generate and Download PDF</button>
</form>
<div id="status"></div>
</div>
<script>
document.getElementById('pdfForm').addEventListener('submit', async function(event) {
event.preventDefault();
const statusDiv = document.getElementById('status');
statusDiv.textContent = 'Generating PDF, please wait...';
statusDiv.style.color = 'orange';
const formData = new FormData(event.target);
const data = {};
formData.forEach((value, key) => { data[key] = value; });
try {
data.usage_history_data = JSON.parse(data.usage_history_data);
} catch (e) {
alert('Ошибка в JSON данных для истории потребления!');
statusDiv.textContent = 'Ошибка в JSON данных!';
statusDiv.style.color = 'red';
return;
}
const apiUrl = '/pdfgen/api/v1/Entergy'; // <--- API ЭНДПОИНТ
const bearerToken = "43QAwPldLuuQTErY303m16lY5dSeDUy1OVKBzy5HTDG2KxfVC0m1o1cfZ49gbBRu";
try {
const response = await fetch(apiUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${bearerToken}` },
body: JSON.stringify(data)
});
if (!response.ok) {
let errorText = `Error: ${response.status} ${response.statusText}`;
const responseBodyText = await response.text();
try {
errorText += ` - ${JSON.parse(responseBodyText).error || responseBodyText}`;
} catch (e) {
if (responseBodyText) errorText += ` - ${responseBodyText}`;
}
throw new Error(errorText);
}
const contentDisposition = response.headers.get('content-disposition');
let filename = "generated_entergy.pdf";
if (contentDisposition) {
const matches = /filename="([^"]+)"/.exec(contentDisposition);
if (matches != null && matches[1]) filename = matches[1];
}
const blob = await response.blob();
const downloadUrl = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.style.display = 'none';
a.href = downloadUrl;
a.download = filename;
document.body.appendChild(a);
a.click();
window.URL.revokeObjectURL(downloadUrl);
document.body.removeChild(a);
statusDiv.textContent = 'PDF downloaded successfully!';
statusDiv.style.color = 'green';
} catch (error) {
console.error('Request failed:', error);
statusDiv.textContent = `Failed to generate PDF: ${error.message}`;
statusDiv.style.color = 'red';
}
});
</script>
</body>
</html>

266
Core/views/home.ejs Normal file
View File

@ -0,0 +1,266 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Generate Duke Energy PDF</title>
<style>
body { font-family: sans-serif; margin: 20px; background-color: #f4f4f4; color: #333; }
.container { background-color: #fff; padding: 20px; border-radius: 8px; box-shadow: 0 0 10px rgba(0,0,0,0.1); max-width: 600px; margin: auto; }
h1 { text-align: center; color: #005eb8; } /* Duke Energy Blue */
.form-group { margin-bottom: 15px; }
label { display: block; margin-bottom: 5px; font-weight: bold; }
input[type="text"], input[type="date"], textarea {
width: calc(100% - 18px);
padding: 8px;
border: 1px solid #ccc;
border-radius: 4px;
box-sizing: border-box;
}
textarea { resize: vertical; }
button {
display: block;
width: 100%;
padding: 10px 15px;
background-color: #0072ce; /* Another Duke Energy Blue */
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 16px;
}
button:hover { background-color: #005eb8; }
#status { margin-top: 20px; font-weight: bold; text-align: center; }
.grid-container { display: grid; grid-template-columns: 1fr 1fr; gap: 15px; }
.full-width { grid-column: 1 / -1; }
h2 { margin-top: 30px; border-bottom: 1px solid #eee; padding-bottom: 5px; color: #333; }
</style>
</head>
<body>
<div class="container">
<h1>Generate Duke Energy PDF</h1>
<form id="pdfForm">
<h2>General Information</h2>
<div class="grid-container">
<div class="form-group">
<label for="customer_name">Customer Name:</label>
<input type="text" id="customer_name" name="customer_name" value="MR OSCAR GUARIN" required>
</div>
<div class="form-group">
<label for="account_number">Account Number:</label>
<input type="text" id="account_number" name="account_number" value="9100 9029 1504" required>
</div>
<div class="form-group">
<label for="bill_date">Bill Date (e.g., Jul 25, 2022):</label>
<input type="text" id="bill_date" name="bill_date" value="Jul 25, 2022" placeholder="Mon DD, YYYY" required>
</div>
<div class="form-group">
<label for="total_amount_due_date">Total Amount Due Date (e.g., Aug 19):</label>
<input type="text" id="total_amount_due_date" name="total_amount_due_date" value="Aug 19" placeholder="Mon DD" required>
</div>
</div>
<h2>Service Address</h2>
<div class="grid-container">
<div class="form-group full-width">
<label for="service_address_line1">Service Address Line 1:</label>
<input type="text" id="service_address_line1" name="service_address_line1" value="5021 CELBRIDGE PL" required>
</div>
<div class="form-group full-width">
<label for="service_address_line2">Service Address Line 2 (City ST ZIP):</label>
<input type="text" id="service_address_line2" name="service_address_line2" value="RALEIGH NC 27613" required>
</div>
</div>
<h2>Service Period</h2>
<div class="grid-container">
<div class="form-group">
<label for="service_period_from">Service From (e.g., Jun 22):</label>
<input type="text" id="service_period_from" name="service_period_from" value="Jun 22" placeholder="Mon DD" required>
</div>
<div class="form-group">
<label for="service_period_to">Service To (e.g., Jul 21):</label>
<input type="text" id="service_period_to" name="service_period_to" value="Jul 21" placeholder="Mon DD" required>
</div>
</div>
<h2>Billing Summary</h2>
<div class="grid-container">
<div class="form-group">
<label for="previous_amount_due">Previous Amount Due ($):</label>
<input type="text" id="previous_amount_due" name="previous_amount_due" value="$195.28" required>
</div>
<div class="form-group">
<label for="payment_received_date">Payment Received Date (e.g., Jul 22):</label>
<input type="text" id="payment_received_date" name="payment_received_date" value="Jul 22" placeholder="Mon DD" required>
</div>
<div class="form-group">
<label for="payment_received_amount">Payment Received Amount ($):</label>
<input type="text" id="payment_received_amount" name="payment_received_amount" value="-195.28" required>
</div>
<div class="form-group">
<label for="current_electric_charges">Current Electric Charges ($):</label>
<input type="text" id="current_electric_charges" name="current_electric_charges" value="287.05" required>
</div>
<div class="form-group">
<label for="current_lighting_charges">Current Lighting Charges ($):</label>
<input type="text" id="current_lighting_charges" name="current_lighting_charges" value="3.27">
</div>
<div class="form-group">
<label for="taxes">Taxes ($):</label>
<input type="text" id="taxes" name="taxes" value="20.32" required>
</div>
<div class="form-group full-width">
<label for="total_amount_due_value">Total Amount Due ($):</label>
<input type="text" id="total_amount_due_value" name="total_amount_due_value" value="$310.64" required>
</div>
</div>
<h2>Usage Snapshot</h2>
<div class="grid-container">
<div class="form-group">
<label for="electric_usage_current_month_kwh">Current Month kWh:</label>
<input type="text" id="electric_usage_current_month_kwh" name="electric_usage_current_month_kwh" value="2,427">
</div>
<div class="form-group">
<label for="electric_usage_jul_2021_kwh">Jul 2021 kWh (Example):</label>
<input type="text" id="electric_usage_jul_2021_kwh" name="electric_usage_jul_2021_kwh" value="0">
</div>
<div class="form-group">
<label for="electric_usage_12_month_avg_kwh">12-Month Avg kWh:</label>
<input type="text" id="electric_usage_12_month_avg_kwh" name="electric_usage_12_month_avg_kwh" value="1,182">
</div>
</div>
<h2>Remittance Slip Details</h2>
<div class="grid-container">
<div class="form-group full-width">
<label for="remit_address_line2">Remittance Address Line 2 (City ST ZIP-Ext):</label>
<input type="text" id="remit_address_line2" name="remit_address_line2" value="RALEIGH NC 27613-6206">
</div>
<div class="form-group full-width">
<label for="barcode_number">Barcode Number (Bottom):</label>
<input type="text" id="barcode_number" name="barcode_number" value="8891009029150400055000000003106400000310649">
</div>
</div>
<!-- <h2>Page 3 Details (Simplified)</h2>
<div class="grid-container">
<div class="form-group">
<label for="meter_number">Meter Number:</label>
<input type="text" id="meter_number" name="meter_number" value="336269538">
</div>
<div class="form-group">
<label for="energy_used_kwh">Energy Used (e.g., 2,381 kWh):</label>
<input type="text" id="energy_used_kwh" name="energy_used_kwh" value="2,381 kWh">
</div>
<div class="form-group">
<label for="basic_customer_charge">Basic Customer Charge ($):</label>
<input type="text" id="basic_customer_charge" name="basic_customer_charge" value="$14.00">
</div>
<div class="form-group">
<label for="energy_charge_details">Energy Charge Details (e.g., kWh @ rate):</label>
<input type="text" id="energy_charge_details" name="energy_charge_details" value="2,381.000 kWh @ $0.11160000">
</div>
<div class="form-group">
<label for="energy_charge_amount">Energy Charge Amount ($):</label>
<input type="text" id="energy_charge_amount" name="energy_charge_amount" value="265.72">
</div>
<div class="form-group">
<label for="total_taxes_detail">Total Taxes Detail (e.g., Sales Tax For Utility):</label>
<input type="text" id="total_taxes_detail" name="total_taxes_detail" value="Sales Tax For Utility">
</div>
<div class="form-group">
<label for="total_taxes_amount">Total Taxes Amount ($):</label>
<input type="text" id="total_taxes_amount" name="total_taxes_amount" value="$20.32">
</div>
</div> -->
<button type="submit">Generate and Download Duke Energy PDF</button>
</form>
<div id="status"></div>
<!-- Скрытая ссылка для скачивания -->
<a id="downloadLink" style="display: none;"></a>
</div>
<script>
document.getElementById('pdfForm').addEventListener('submit', async function(event) {
event.preventDefault();
const statusDiv = document.getElementById('status');
statusDiv.textContent = 'Generating PDF, please wait...';
statusDiv.style.color = 'orange';
const formData = new FormData(event.target);
const data = {};
formData.forEach((value, key) => {
data[key] = value;
});
const apiUrl = '/pdfgen/api/v1/DukeEnergy'; // Убедитесь, что этот URL правильный
const bearerToken = "43QAwPldLuuQTErY303m16lY5dSeDUy1OVKBzy5HTDG2KxfVC0m1o1cfZ49gbBRu"; // Ваш Bearer токен
try {
const response = await fetch(apiUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${bearerToken}`
},
body: JSON.stringify(data)
});
if (!response.ok) {
let errorText = `Error: ${response.status} ${response.statusText}`;
try {
const errorJson = await response.json();
errorText += ` - ${errorJson.error || JSON.stringify(errorJson)}`;
} catch (e) {
const textError = await response.text();
if (textError) errorText += ` - ${textError}`;
}
throw new Error(errorText);
}
const contentDisposition = response.headers.get('content-disposition');
let filename = "generated_duke_energy.pdf";
if (contentDisposition) {
const filenameRegex = /filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/;
const matches = filenameRegex.exec(contentDisposition);
if (matches != null && matches[1]) {
filename = matches[1].replace(/['"]/g, '');
}
}
const blob = await response.blob();
const downloadUrl = window.URL.createObjectURL(blob);
// Динамически создаем элемент <a>
const a = document.createElement('a');
a.style.display = 'none'; // Делаем его невидимым
a.href = downloadUrl;
a.download = filename;
document.body.appendChild(a); // Добавляем на страницу, чтобы можно было симулировать клик
a.click(); // Симулируем клик
// Удаляем созданный элемент и освобождаем URL
window.URL.revokeObjectURL(downloadUrl);
document.body.removeChild(a); // Удаляем временную ссылку со страницы
statusDiv.textContent = 'PDF downloaded successfully!';
statusDiv.style.color = 'green';
} catch (error) {
console.error('Request failed:', error);
statusDiv.textContent = `Failed to generate PDF: ${error.message}`;
statusDiv.style.color = 'red';
}
});
</script>
</body>
</html>

559
Core/views/pge_form.ejs Normal file
View File

@ -0,0 +1,559 @@
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<title>Generate PG&E Energy Statement</title>
<style>
body { font-family: sans-serif; margin: 20px; background-color: #f4f4f4; color: #333; }
.container { background-color: #fff; padding: 20px; border-radius: 8px; box-shadow: 0 0 10px rgba(0,0,0,0.1); max-width: 600px; margin: auto; }
h1, h2 { text-align: center; color: #0077c8; }
h3 { margin-top: 30px; border-bottom: 1px solid #eee; padding-bottom: 5px; color: #333; }
.form-group { margin-bottom: 15px; }
label { display: block; margin-bottom: 5px; font-weight: bold; }
input[type="text"] {
width: calc(100% - 18px); padding: 8px; border: 1px solid #ccc; border-radius: 4px; box-sizing: border-box;
}
button { display: block; width: 100%; padding: 10px 15px; background-color: #0077c8; color: white; border: none; border-radius: 4px; cursor: pointer; font-size: 16px; margin-top: 20px; }
button:hover { background-color: #005a9c; }
#status { margin-top: 20px; font-weight: bold; text-align: center; }
.grid-container { display: grid; grid-template-columns: 1fr 1fr; gap: 15px; }
.full-width { grid-column: 1 / -1; }
.note { font-size: 0.9em; color: #666; }
</style>
</head>
<body>
<div class="container">
<h1>Generate PG&E Energy Statement</h1>
<form id="pdfForm">
<h3>Основная информация о счете</h3>
<div class="grid-container">
<div class="form-group">
<label>Account No:</label>
<input type="text" name="account_no" value="1234567890-1">
</div>
<div class="form-group">
<label>Statement Date:</label>
<input type="text" name="statement_date" value="09/07/2019">
</div>
<div class="form-group">
<label>Due Date:</label>
<input type="text" name="due_date" value="09/28/2019">
</div>
</div>
<h3>Адрес обслуживания</h3>
<div class="grid-container">
<div class="form-group">
<label>Service For (Name):</label>
<input type="text" name="service_for_name" value="SPARKY JOULE">
</div>
<div class="form-group">
<label>Address Line:</label>
<input type="text" name="service_for_address" value="12345 ENERGY CT">
</div>
</div>
<h3>Сводка по счету (Account Summary)</h3>
<div class="grid-container">
<div class="form-group">
<label>Previous Statement ($):</label>
<input type="text" name="previous_statement" value="91.57">
</div>
<div class="form-group">
<label>Payment Received ($):</label>
<input type="text" name="payment_received" value="-91.57">
</div>
<div class="form-group">
<label>Previous Unpaid Balance ($):</label>
<input type="text" name="previous_unpaid_balance" value="0.00">
</div>
<div class="form-group">
<label>PG&E Delivery Charges ($):</label>
<input type="text" name="pge_delivery_charges" value="55.66">
</div>
<div class="form-group">
<label>SVCE Generation Charges ($):</label>
<input type="text" name="svce_generation_charges" value="32.48">
</div>
<div class="form-group">
<label>Total Amount Due Date (e.g., 08/28/2019):</label>
<input type="text" name="total_amount_due_date" value="08/28/2019">
</div>
<div class="form-group full-width">
<label>Total Amount Due ($):</label>
<input type="text" name="total_amount_due" value="88.14">
</div>
</div>
<h3>Отрывной купон</h3>
<div class="grid-container">
<div class="form-group">
<label>Account Number (на купоне):</label>
<input type="text" name="remit_account_number" value="123456789-1">
</div>
<div class="form-group">
<label>Due Date (на купоне):</label>
<input type="text" name="remit_due_date" value="09/28/2019">
</div>
<div class="form-group">
<label>Total Amount Due (на купоне):</label>
<input type="text" name="remit_total_amount_due" value="88.14">
</div>
</div>
<h3>Данные для графиков</h3>
<div class="form-group full-width">
<label>Monthly Billing History (JSON - массив объектов):</label>
<textarea name="monthly_billing_history" rows="6">[
{"month": "8/07", "electric": "80", "gas": "20"},
{"month": "9/06", "electric": "75", "gas": "15"},
{"month": "10/04", "electric": "70", "gas": "30"},
{"month": "11/06", "electric": "90", "gas": "40"},
{"month": "12/06", "electric": "110", "gas": "50"},
{"month": "1/08", "electric": "130", "gas": "60"},
{"month": "2/06", "electric": "120", "gas": "55"},
{"month": "3/07", "electric": "100", "gas": "45"},
{"month": "4/08", "electric": "95", "gas": "35"},
{"month": "5/09", "electric": "85", "gas": "25"},
{"month": "6/06", "electric": "90", "gas": "20"},
{"month": "7/10", "electric": "105", "gas": "10"},
{"month": "8/07", "electric": "115", "gas": "5"}
]</textarea>
</div>
<div class="grid-container">
<div class="form-group">
<label>Daily Electric (1 Year Ago):</label>
<input type="text" name="daily_usage_electric[year_ago]" value="0.12">
</div>
<div class="form-group">
<label>Daily Electric (Last Period):</label>
<input type="text" name="daily_usage_electric[last_period]" value="0.16">
</div>
<div class="form-group">
<label>Daily Electric (Current):</label>
<input type="text" name="daily_usage_electric[current_period]" value="0.17">
</div>
<div class="form-group">
<label>Daily Gas (1 Year Ago):</label>
<input type="text" name="daily_usage_gas[year_ago]" value="12.50">
</div>
<div class="form-group">
<label>Daily Gas (Last Period):</label>
<input type="text" name="daily_usage_gas[last_period]" value="12.16">
</div>
<div class="form-group">
<label>Daily Gas (Current):</label>
<input type="text" name="daily_usage_gas[current_period]" value="12.67">
</div>
</div>
<!-- Вставьте этот блок в ваш pge_form.html -->
<h3>Детализация Electric Charges (со стр. 2)</h3>
<div class="grid-container grid-3-col">
<div class="form-group">
<label>Conservation Incentive:</label>
<input type="text" name="electric_conservation_incentive" value="-9.50">
</div>
<div class="form-group">
<label>Transmission:</label>
<input type="text" name="electric_transmission" value="12.42">
</div>
<div class="form-group">
<label>Distribution:</label>
<input type="text" name="electric_distribution" value="35.08">
</div>
<div class="form-group">
<label>Electric Public Purpose:</label>
<input type="text" name="electric_public_purpose" value="4.71">
</div>
<div class="form-group">
<label>Nuclear Decommissioning:</label>
<input type="text" name="electric_nuclear_decommissioning" value="0.33">
</div>
<div class="form-group">
<label>DWR Bond Charge:</label>
<input type="text" name="electric_dwr_bond" value="1.91">
</div>
<div class="form-group">
<label>CTC:</label>
<input type="text" name="electric_ctc" value="0.42">
</div>
<div class="form-group">
<label>Energy Cost Recovery:</label>
<input type="text" name="electric_ecra" value="-0.22">
</div>
<div class="form-group">
<label>PCIA:</label>
<input type="text" name="electric_pcia" value="10.26">
</div>
<div class="form-group">
<label>Taxes and Other:</label>
<input type="text" name="electric_taxes_and_other" value="0.25">
</div>
<div class="form-group">
<label>Total Electric Charges:</label>
<input type="text" name="electric_total_charges" value="55.66">
</div>
</div>
<!-- Вставьте этот блок в ваш pge_form.html -->
<h3>Детализация со страницы 3</h3>
<h4>Информация о периоде и тарифе</h4>
<div class="grid-container">
<div class="form-group full-width">
<label>Период Delivery Charges (заменит '08/02/2019 - 08/31/2019 (30 billing days)'):</label>
<input type="text" name="delivery_charges_period" value="08/02/2019 - 08/31/2019 (30 billing days)">
</div>
<div class="form-group">
<label>Service Agreement ID (заменит '111111111'):</label>
<input type="text" name="service_agreement_id" value="111111111">
</div>
<div class="form-group">
<label>Rate Schedule (заменит 'E1 X Residential Service'):</label>
<input type="text" name="rate_schedule" value="E1 X Residential Service">
</div>
</div>
<h4>Service Information (правая колонка)</h4>
<div class="grid-container">
<div class="form-group">
<label>Meter #:</label>
<input type="text" name="meter_number" value="1111111111">
</div>
<div class="form-group">
<label>Current Meter Reading:</label>
<input type="text" name="current_meter_reading" value="37,710">
</div>
<div class="form-group">
<label>Prior Meter Reading:</label>
<input type="text" name="prior_meter_reading" value="37,330">
</div>
<div class="form-group">
<label>Total Usage (kWh):</label>
<input type="text" name="total_usage_kwh" value="380.000000 kWh">
</div>
<div class="form-group">
<label>Baseline Territory:</label>
<input type="text" name="baseline_territory" value="X">
</div>
<div class="form-group">
<label>Heat Source:</label>
<input type="text" name="heat_source" value="B - Not Electric">
</div>
</div>
<h4>Tier Usage Breakdown (таблица)</h4>
<div class="grid-container">
<div class="form-group">
<label>Tier 1 Allowance:</label>
<input type="text" name="tier1_allowance_detail" value="297.00 kWh (30 days x 9.9 kWh/day)">
</div>
<div class="form-group">
<label>Tier 1 Usage Amount ($):</label>
<input type="text" name="tier1_usage_amount" value="66.46">
</div>
<div class="form-group">
<label>Tier 2 Usage Amount ($):</label>
<input type="text" name="tier2_usage_amount" value="23.37">
</div>
<div class="form-group">
<label>Generation Credit ($):</label>
<input type="text" name="generation_credit" value="-44.68">
</div>
<div class="form-group">
<label>PCIA ($):</label>
<input type="text" name="pcia_adjustment" value="10.26">
</div>
<div class="form-group">
<label>Franchise Fee Surcharge ($):</label>
<input type="text" name="franchise_fee_surcharge" value="0.25">
</div>
</div>
<h4>Диаграмма "Electric Usage This Period"</h4>
<div class="form-group full-width">
<label>Заголовок диаграммы (заменит 'Electric Usage This Period: ...'):</label>
<input type="text" name="electric_usage_period_title" value="Electric Usage This Period: 380.000000 kWh, 30 billing days">
</div>
<div class="form-group full-width">
<label>Данные для диаграммы (JSON - массив объектов):</label>
<textarea name="daily_electric_usage_history" rows="5">[
{"day": "7/2", "kwh": 12}, {"day": "7/3", "kwh": 11}, {"day": "7/4", "kwh": 13},
{"day": "7/5", "kwh": 15}, {"day": "7/6", "kwh": 14}, {"day": "7/7", "kwh": 12},
{"day": "7/8", "kwh": 11}, {"day": "7/9", "kwh": 10}, {"day": "7/10", "kwh": 14},
{"day": "7/11", "kwh": 18}, {"day": "7/12", "kwh": 17}, {"day": "7/13", "kwh": 16},
{"day": "7/14", "kwh": 20}, {"day": "7/15", "kwh": 19}, {"day": "7/16", "kwh": 18},
{"day": "7/17", "kwh": 16}, {"day": "7/18", "kwh": 15}, {"day": "7/19", "kwh": 14},
{"day": "7/20", "kwh": 14}, {"day": "7/21", "kwh": 18}, {"day": "7/22", "kwh": 20},
{"day": "7/23", "kwh": 22}, {"day": "7/24", "kwh": 21}, {"day": "7/25", "kwh": 20},
{"day": "7/26", "kwh": 19}, {"day": "7/27", "kwh": 18}, {"day": "7/28", "kwh": 17},
{"day": "7/29", "kwh": 17}, {"day": "7/30", "kwh": 16}, {"day": "7/31", "kwh": 15}
]</textarea>
</div>
<div class="form-group">
<label>Average Daily Usage (для линии на графике):</label>
<input type="text" name="average_daily_usage" value="12.67">
</div>
<!-- Вставьте этот блок в ваш pge_form.html -->
<h3>Детализация со страницы 4 (Silicon Valley Clean Energy)</h3>
<div class="grid-container">
<div class="form-group">
<label>Rate Schedule (напр. 'E-1'):</label>
<input type="text" name="svce_rate_schedule" value="E-1">
</div>
<div class="form-group full-width">
<label>Generation - Total (детали):</label>
<input type="text" name="svce_generation_detail" value="380.000000 kWh @ $0.08519">
</div>
<div class="form-group">
<label>Generation - Total ($):</label>
<input type="text" name="svce_generation_amount" value="32.37">
</div>
<div class="form-group">
<label>Energy Commission Surcharge ($):</label>
<input type="text" name="svce_energy_surcharge" value="0.11">
</div>
<div class="form-group full-width">
<label>Total SVCE Charges ($):</label>
<input type="text" name="svce_total_charges" value="32.48">
</div>
</div>
<!-- Вставьте этот блок в ваш pge_form.html -->
<h3>Детализация со страницы 5 (Gas Charges)</h3>
<h4>Информация о периоде и тарифе (газ)</h4>
<div class="grid-container">
<div class="form-group full-width">
<label>Период Gas Charges:</label>
<input type="text" name="gas_charges_period" value="08/02/2019 - 08/31/2019 (30 billing days)">
</div>
<div class="form-group">
<label>Service Agreement ID (газ):</label>
<input type="text" name="gas_service_agreement_id" value="1111111111">
</div>
<div class="form-group">
<label>Rate Schedule (газ):</label>
<input type="text" name="gas_rate_schedule" value="G1 X Residential Service">
</div>
</div>
<h4>Service Information (газ, правая колонка)</h4>
<div class="grid-container grid-3-col">
<div class="form-group">
<label>Meter #:</label>
<input type="text" name="gas_meter_number" value="11111111">
</div>
<div class="form-group">
<label>Current Reading:</label>
<input type="text" name="gas_current_reading" value="2,588">
</div>
<div class="form-group">
<label>Prior Reading:</label>
<input type="text" name="gas_prior_reading" value="2,583">
</div>
<div class="form-group">
<label>Difference:</label>
<input type="text" name="gas_difference" value="5">
</div>
<div class="form-group">
<label>Multiplier:</label>
<input type="text" name="gas_multiplier" value="1.031647">
</div>
<div class="form-group">
<label>Total Usage (Therms):</label>
<input type="text" name="gas_total_usage" value="5.000000 Therms">
</div>
<div class="form-group">
<label>Baseline Territory:</label>
<input type="text" name="gas_baseline_territory" value="X">
</div>
<div class="form-group">
<label>Serial:</label>
<input type="text" name="gas_serial" value="G">
</div>
</div>
<h4>Tier Usage Breakdown (газ, таблица)</h4>
<div class="grid-container">
<div class="form-group full-width">
<label>Tier 1 Allowance Detail:</label>
<input type="text" name="gas_tier1_allowance_detail" value="17.70 Therms (30 days x 0.59 Therms/day)">
</div>
<div class="form-group">
<label>Tier 1 Usage Detail:</label>
<input type="text" name="gas_tier1_usage_detail" value="5.000000 Therms @ $1.28395">
</div>
<div class="form-group">
<label>Tier 1 Usage Amount ($):</label>
<input type="text" name="gas_tier1_usage_amount" value="6.42">
</div>
<div class="form-group">
<label>Gas PPP Surcharge Detail:</label>
<input type="text" name="gas_ppp_surcharge_detail" value="($0.09047 /Therm)">
</div>
<div class="form-group">
<label>Gas PPP Surcharge Amount ($):</label>
<input type="text" name="gas_ppp_surcharge_amount" value="0.45">
</div>
<div class="form-group">
<label>Total Gas Charges ($):</label>
<input type="text" name="gas_total_charges" value="6.87">
</div>
</div>
<h4>Gas Procurement Costs (правая колонка)</h4>
<div class="grid-container">
<div class="form-group">
<label>Период:</label>
<input type="text" name="gas_procurement_period" value="07/02/2019 - 07/31/2019">
</div>
<div class="form-group">
<label>Стоимость ($/Therm):</label>
<input type="text" name="gas_procurement_amount" value="0.28462">
</div>
</div>
<h4>Диаграмма "Gas Usage This Period"</h4>
<div class="form-group full-width">
<label>Заголовок диаграммы:</label>
<input type="text" name="gas_usage_period_title" value="Gas Usage This Period: 5.000000 Therms, 30 billing days">
</div>
<div class="form-group">
<label>Average Daily Usage (для линии):</label>
<input type="text" name="gas_average_daily_usage" value="0.17">
</div>
<div class="form-group full-width">
<label>Данные для диаграммы (JSON - массив объектов, kwh используется как 'value'):</label>
<textarea name="daily_gas_usage_history" rows="10">[
{"day": "7/2", "gas": 0},
{"day": "7/3", "gas": 0},
{"day": "7/4", "gas": 0},
{"day": "7/5", "gas": 0},
{"day": "7/6", "gas": 0},
{"day": "7/7", "gas": 0},
{"day": "7/8", "gas": 0},
{"day": "7/9", "gas": 0.9},
{"day": "7/10", "gas": 0},
{"day": "7/11", "gas": 0},
{"day": "7/12", "gas": 0},
{"day": "7/13", "gas": 0},
{"day": "7/14", "gas": 1},
{"day": "7/15", "gas": 0},
{"day": "7/16", "gas": 0},
{"day": "7/17", "gas": 0},
{"day": "7/18", "gas": 0.9},
{"day": "7/19", "gas": 0},
{"day": "7/20", "gas": 0},
{"day": "7/21", "gas": 0},
{"day": "7/22", "gas": 0},
{"day": "7/23", "gas": 1},
{"day": "7/24", "gas": 0},
{"day": "7/25", "gas": 0},
{"day": "7/26", "gas": 0},
{"day": "7/27", "gas": 1},
{"day": "7/28", "gas": 0},
{"day": "7/29", "gas": 0},
{"day": "7/30", "gas": 0},
{"day": "7/31", "gas": 0}
]</textarea>
</div>
<button type="submit">Generate and Download PDF</button>
</form>
<div id="status"></div>
</div>
<script>
document.getElementById('pdfForm').addEventListener('submit', async function(event) {
event.preventDefault();
const statusDiv = document.getElementById('status');
statusDiv.textContent = 'Generating PDF, please wait...';
statusDiv.style.color = 'orange';
// Специальная обработка для вложенных объектов и JSON
const formData = new FormData(event.target);
let data = {};
for (let [key, value] of formData.entries()) {
if (key.includes('[')) { // Обработка полей типа name="daily_usage_electric[year_ago]"
const keys = key.replace(/\]/g, '').split('[');
let current = data;
for(let i = 0; i < keys.length; i++) {
if (i === keys.length - 1) {
current[keys[i]] = value;
} else {
current[keys[i]] = current[keys[i]] || {};
current = current[keys[i]];
}
}
} else {
data[key] = value;
}
}
try {
data.monthly_billing_history = JSON.parse(data.monthly_billing_history);
} catch (e) {
statusDiv.textContent = 'Ошибка в JSON для Monthly Billing History!';
statusDiv.style.color = 'red';
return;
}
const apiUrl = '/pdfgen/api/v1/PGE'; // <--- API ЭНДПОИНТ ДЛЯ PG&E
const bearerToken = "43QAwPldLuuQTErY303m16lY5dSeDUy1OVKBzy5HTDG2KxfVC0m1o1cfZ49gbBRu";
try {
const response = await fetch(apiUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${bearerToken}` },
body: JSON.stringify(data)
});
if (!response.ok) {
let errorText = `Error: ${response.status} ${response.statusText}`;
const responseBodyText = await response.text();
try {
errorText += ` - ${JSON.parse(responseBodyText).error || responseBodyText}`;
} catch (e) {
if (responseBodyText) errorText += ` - ${responseBodyText}`;
}
throw new Error(errorText);
}
const contentDisposition = response.headers.get('content-disposition');
let filename = "generated_pge.pdf";
if (contentDisposition) {
const matches = /filename="([^"]+)"/.exec(contentDisposition);
if (matches != null && matches[1]) filename = matches[1];
}
const blob = await response.blob();
const downloadUrl = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.style.display = 'none';
a.href = downloadUrl;
a.download = filename;
document.body.appendChild(a);
a.click();
window.URL.revokeObjectURL(downloadUrl);
document.body.removeChild(a);
statusDiv.textContent = 'PDF downloaded successfully!';
statusDiv.style.color = 'green';
} catch (error) {
console.error('Request failed:', error);
statusDiv.textContent = `Failed to generate PDF: ${error.message}`;
statusDiv.style.color = 'red';
}
});
</script>
</body>
</html>

195
Core/views/verizon_form.ejs Normal file
View File

@ -0,0 +1,195 @@
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<title>Generate Verizon PDF</title>
<style>
body { font-family: sans-serif; margin: 20px; background-color: #f4f4f4; color: #333; }
.container { background-color: #fff; padding: 20px; border-radius: 8px; box-shadow: 0 0 10px rgba(0,0,0,0.1); max-width: 650px; margin: auto; }
h1, h2 { text-align: center; color: #CD040B; } /* Verizon Red */
h3 { margin-top: 30px; border-bottom: 1px solid #eee; padding-bottom: 5px; color: #333; }
.form-group { margin-bottom: 15px; }
label { display: block; margin-bottom: 5px; font-weight: bold; }
input[type="text"] { width: calc(100% - 18px); padding: 8px; border: 1px solid #ccc; border-radius: 4px; box-sizing: border-box; }
button { display: block; width: 100%; padding: 10px 15px; background-color: #CD040B; color: white; border: none; border-radius: 4px; cursor: pointer; font-size: 16px; margin-top: 20px; }
button:hover { background-color: #a80309; }
#status { margin-top: 20px; font-weight: bold; text-align: center; }
.grid-container { display: grid; grid-template-columns: 1fr 1fr; gap: 15px; }
.grid-3-col { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 15px; }
.full-width { grid-column: 1 / -1; }
</style>
</head>
<body>
<div class="container">
<h1>Generate Verizon PDF</h1>
<form id="pdfForm">
<h3>Основная информация</h3>
<div class="grid-container">
<div class="form-group">
<label>Account Number:</label>
<input type="text" name="account_number" value="326890199-00001">
</div>
<div class="form-group">
<label>Invoice Number:</label>
<input type="text" name="invoice_number" value="8614432777">
</div>
<div class="form-group">
<label>Customer Name:</label>
<input type="text" name="customer_name" value="GERMECIA JOSEPH">
</div>
<div class="form-group">
<label>Customer Address:</label>
<input type="text" name="customer_address" value="9400 ROBERTS DR">
</div>
<div class="form-group full-width">
<label>Customer City/State/ZIP:</label>
<input type="text" name="customer_city_st_zip" value="ATLANTA, GA 30350-2041">
</div>
<div class="form-group">
<label>Total Due Date (e.g., Feb 12):</label>
<input type="text" name="total_due_date" value="Feb 12">
</div>
<div class="form-group">
<label>Full Due Date (e.g., February 12, 2024):</label>
<input type="text" name="full_due_date" value="February 12, 2024">
</div>
<div class="form-group">
<label>Bill Month (e.g., January):</label>
<input type="text" name="bill_month" value="January">
</div>
<div class="form-group">
<label>Payment Received Amount:</label>
<input type="text" name="payment_received" value="$0.00">
</div>
</div>
<h3>Детализация начислений (со стр. 4)</h3>
<div class="grid-container grid-3-col">
<div class="form-group">
<label>Activation fee:</label>
<input type="text" name="activation_fee" value="35.00">
</div>
<div class="form-group">
<label>Unlimited Ultimate Plan:</label>
<input type="text" name="plan_ultimate" value="100.00">
</div>
<div class="form-group">
<label>Apple One Individual:</label>
<input type="text" name="perk_apple_one" value="10.00">
</div>
<div class="form-group">
<label>Fed Universal Service Charge:</label>
<input type="text" name="surcharge_fus" value="6.20">
</div>
<div class="form-group">
<label>Regulatory Charge:</label>
<input type="text" name="surcharge_regulatory" value="0.16">
</div>
<div class="form-group">
<label>Admin & Telco Recovery:</label>
<input type="text" name="surcharge_admin" value="3.30">
</div>
<div class="form-group">
<label>GA Local 911 Surcharge:</label>
<input type="text" name="tax_911" value="1.50">
</div>
<div class="form-group">
<label>GA State Sls Tax:</label>
<input type="text" name="tax_ga_state" value="2.15">
</div>
<div class="form-group">
<label>Fulton Cnty Sls Tax:</label>
<input type="text" name="tax_fulton_county" value="2.01">
</div>
</div>
<h3>Отрывной купон и штрих-код</h3>
<div class="form-group full-width">
<label>Barcode Number (низ страницы):</label>
<input type="text" name="barcode_number" value="86144327770103268901990000100000016032000000160329">
</div>
<button type="submit">Generate and Download PDF</button>
</form>
<div id="status"></div>
</div>
<script>
// ... (Ваш JavaScript код для отправки формы остается БЕЗ ИЗМЕНЕНИЙ) ...
document.getElementById('pdfForm').addEventListener('submit', async function(event) {
event.preventDefault();
const statusDiv = document.getElementById('status');
statusDiv.textContent = 'Generating PDF, please wait...';
statusDiv.style.color = 'orange';
const formData = new FormData(event.target);
const data = {};
formData.forEach((value, key) => { data[key] = value; });
// try {
// data.usage_history_data = JSON.parse(data.usage_history_data);
// } catch (e) {
// alert('Ошибка в JSON данных для истории потребления!');
// return; // Прервать отправку
// }
const apiUrl = '/pdfgen/api/v1/Verizon';
const bearerToken = "43QAwPldLuuQTErY303m16lY5dSeDUy1OVKBzy5HTDG2KxfVC0m1o1cfZ49gbBRu";
try {
const response = await fetch(apiUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${bearerToken}`
},
body: JSON.stringify(data)
});
if (!response.ok) {
let errorText = `Error: ${response.status} ${response.statusText}`;
const responseBodyText = await response.text();
try {
const errorJson = JSON.parse(responseBodyText);
errorText += ` - ${errorJson.error || JSON.stringify(errorJson)}`;
} catch (e) {
if (responseBodyText) {
errorText += ` - ${responseBodyText}`;
}
}
throw new Error(errorText);
}
const contentDisposition = response.headers.get('content-disposition');
let filename = "generated_aep_ohio.pdf";
if (contentDisposition) {
const filenameRegex = /filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/;
const matches = filenameRegex.exec(contentDisposition);
if (matches != null && matches[1]) {
filename = matches[1].replace(/['"]/g, '');
}
}
const blob = await response.blob();
const downloadUrl = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.style.display = 'none';
a.href = downloadUrl;
a.download = filename;
document.body.appendChild(a);
a.click();
window.URL.revokeObjectURL(downloadUrl);
document.body.removeChild(a);
statusDiv.textContent = 'PDF downloaded successfully!';
statusDiv.style.color = 'green';
} catch (error) {
console.error('Request failed:', error);
statusDiv.textContent = `Failed to generate PDF: ${error.message}`;
statusDiv.style.color = 'red';
}
});
</script>
</body>
</html>

23
Dockerfile Normal file
View File

@ -0,0 +1,23 @@
FROM node:20.19.0 AS development
RUN mkdir /srv/pdfgen && chown node:node /srv/pdfgen
USER node
WORKDIR /srv/pdfgen
COPY --chown=node:node package.json package-lock.json ./
RUN npm install --quiet
FROM node:20.19.0-slim AS production
USER node
WORKDIR /srv/pdfgen
COPY --from=development --chown=root:root /srv/pdfgen/node_modules ./node_modules
COPY . .
CMD ["node", "index.js"]

2
Pipfile Normal file
View File

@ -0,0 +1,2 @@
[requires]
python_version = "3.11"

119
README.md Normal file
View File

@ -0,0 +1,119 @@
# pdfgen
Микросервис генерации PDF документов
***
## 1. Xfinity
```angular2html
POST https://pdfgen.evpak-soft.ru/api/v1/Xfinity
Content-Type: application/json
Authorization: Bearer 43QAwPldLuuQTErY303m16lY5dSeDUy1OVKBzy5HTDG2KxfVC0m1o1cfZ49gbBRu
{
"customer_name": "Hellen",
"account_number": "8499 10 008 1016692",
"billing_date": "Apr 13, 2025",
"address": "3411 CHESTNUT ST APT 847, PHILADELPHIA, PA, 19104-5530",
"plan": "Gigabit",
"speed": "Download as fast as 1200 Mbps"
}
```
### Справочники
- https://pdfgen.evpak-soft.ru/api/v1/Xfinity/socr
- https://pdfgen.evpak-soft.ru/api/v1/Xfinity/tariffs
### Обновление справочника по тарифам
```angular2html
curl -X POST \
-H "Content-Type: multipart/form-data" \
-H "Authorization: Bearer 43QAwPldLuuQTErY303m16lY5dSeDUy1OVKBzy5HTDG2KxfVC0m1o1cfZ49gbBRu" \
-F "csvFile=@/path/to/file/States_Plans_by_Region.csv" \
https://pdfgen.evpak-soft.ru/api/v1/Xfinity/tariffs/upload
```
***
## 2. COX
```angular2html
POST https://pdfgen.evpak-soft.ru/api/v1/COX
Content-Type: application/json
Authorization: Bearer 43QAwPldLuuQTErY303m16lY5dSeDUy1OVKBzy5HTDG2KxfVC0m1o1cfZ49gbBRu
{
"billing_date": "May 21, 2025",
"account_number": "001 8610 011219418",
"service_address_1": "APT 3",
"service_address_2": "2711 STEWART AVE",
"service_address_3": "LAS VEGAS, NV 89101-4652",
"address": "2711 STEWART AVE APT 3, LAS VEGAS, NV, 89101-4652",
"customer_name": "GUADALUPE LOPEZ",
"plan": "Go Fast",
"speed": "Download speeds up to 100 Mbps*|1.25 TB (1,280 GB) Monthly Data Plan|Over 4 Million Wifi Hotspots",
"previous_balance": "$31.26",
"payment_received": "-$31.26",
"baseTarif": [
{
"name": "Go Fast",
"price": "$50.00",
"includes": "Download speeds up to 100 Mbps*|1.25 TB (1,280 GB) Monthly Data Plan|Over 4 Million Wifi Hotspots"
},
{
"name": "Go Faster",
"price": "$70.00",
"includes": "Download speeds up to 250 Mbps*|1.25 TB (1,280 GB) Monthly Data Plan|Over 4 Million Wifi Hotspots"
},
{
"name": "Go Even Faster",
"price": "$90.00",
"includes": "Download speeds up to 500 Mbps*|1.25 TB (1,280 GB) Monthly Data Plan|Over 4 Million Wifi Hotspots"
}
]
}
```
***
## 3. ATT
```angular2html
POST https://pdfgen.evpak-soft.ru/api/v1/ATT
Content-Type: application/json
Authorization: Bearer 43QAwPldLuuQTErY303m16lY5dSeDUy1OVKBzy5HTDG2KxfVC0m1o1cfZ49gbBRu
{
"billing_date": "May 21, 2025",
"account_number": "001 8610 011219418",
"service_address_1": "APT 3",
"service_address_2": "2711 STEWART AVE",
"service_address_3": "LAS VEGAS, NV 89101-4652",
"address": "2711 STEWART AVE APT 3, LAS VEGAS, NV, 89101-4652",
"customer_name": "GUADALUPE LOPEZ",
"plan": "Go Fast",
"speed": "Download speeds up to 100 Mbps*|1.25 TB (1,280 GB) Monthly Data Plan|Over 4 Million Wifi Hotspots",
"previous_balance": "$31.26",
"payment_received": "-$31.26",
"baseTarif": [
{
"name": "Go Fast",
"price": "$50.00",
"includes": "Download speeds up to 100 Mbps*|1.25 TB (1,280 GB) Monthly Data Plan|Over 4 Million Wifi Hotspots"
},
{
"name": "Go Faster",
"price": "$70.00",
"includes": "Download speeds up to 250 Mbps*|1.25 TB (1,280 GB) Monthly Data Plan|Over 4 Million Wifi Hotspots"
},
{
"name": "Go Even Faster",
"price": "$90.00",
"includes": "Download speeds up to 500 Mbps*|1.25 TB (1,280 GB) Monthly Data Plan|Over 4 Million Wifi Hotspots"
}
]
}
```

133
analyze_pdf.py Normal file
View File

@ -0,0 +1,133 @@
import fitz # PyMuPDF
import sys
import json
import os
def analyze_pdf_to_json(pdf_path):
"""
Анализирует PDF-файл, извлекает всю текстовую разметку, шрифты,
координаты графика и создает готовый map.json файл.
"""
try:
doc = fitz.open(pdf_path)
print(f"--- Analyzing PDF: {pdf_path} ({len(doc)} pages) ---")
except Exception as e:
print(f"Error opening file: {e}")
return
# --- 1. АВТОМАТИЧЕСКИ СОБИРАЕМ ВСЕ УНИКАЛЬНЫЕ ШРИФТЫ ---
unique_fonts = set()
for page in doc:
fonts_on_page = page.get_fonts(full=False)
for font_info in fonts_on_page:
unique_fonts.add(font_info[3]) # font_info[3] - это имя шрифта
# Сортируем для стабильного порядка и создаем карту "имя -> ifont"
# Начинаем ifont с 1, так как ifont: 0 мы зарезервируем для Helvetica
sorted_fonts = sorted(list(unique_fonts))
font_map = {font_name: index + 1 for index, font_name in enumerate(sorted_fonts)}
print("\n--- Found Unique Fonts ---")
print("This is your 'font_map'. Use it to configure font loading in routes.js:")
print(json.dumps(font_map, indent=2))
print("--------------------------\n")
# --- 2. ИЩЕМ ПРЯМОУГОЛЬНИК ГРАФИКА НА ПЕРВОЙ СТРАНИЦЕ ---
chart_area_rect = None
if len(doc) > 0:
target_page = doc[0]
paths = target_page.get_drawings()
max_area = 0
# Ищем самый большой прямоугольник, который не занимает всю страницу
page_area = target_page.rect.width * target_page.rect.height
for path in paths:
# Ищем прямоугольники (type 's' с одним элементом 're')
if path["type"] == "s" and len(path["items"]) == 1 and path["items"][0][0] == "re":
rect = fitz.Rect(path["rect"])
area = rect.width * rect.height
if area > max_area and area < (page_area * 0.9): # Игнорируем прямоугольники, занимающие почти всю страницу
max_area = area
chart_area_rect = rect
# --- 3. Создаем базовую структуру для JSON-файла ---
output_data = {
"font_map": font_map,
"constants": {},
"replacements_template": {},
"map": []
}
if chart_area_rect:
print(f"--- Found Chart Area Rectangle ---")
print(chart_area_rect)
print("--------------------------------\n")
output_data["constants"]["bar_chart_x_ill"] = round(chart_area_rect.x0, 2)
output_data["constants"]["bar_chart_y_ill"] = round(chart_area_rect.y0, 2)
output_data["constants"]["bar_chart_width"] = round(chart_area_rect.width, 2)
output_data["constants"]["bar_chart_height"] = round(chart_area_rect.height, 2)
output_data["constants"]["bar_chart_max_cost"] = 200 # Пример, это значение лучше задавать вручную
# --- 4. ИЗВЛЕКАЕМ ВЕСЬ ТЕКСТ И ЕГО СВОЙСТВА ---
all_found_texts = set()
for page_num in range(len(doc)):
page = doc[page_num]
# Используем get_text("dict") для получения детальной информации
blocks = page.get_text("dict", flags=fitz.TEXTFLAGS_SEARCH)["blocks"]
for block in blocks:
if "lines" in block:
for line in block["lines"]:
# Собираем текст из спанов, чтобы обрабатывать сгруппированные строки
full_line_text = "".join(span["text"] for span in line["spans"]).strip()
if full_line_text:
all_found_texts.add(full_line_text)
# Итерируем по отдельным спанам для точной разметки
for span in line["spans"]:
text = span["text"].strip()
if not text:
continue
font_name = span["font"]
font_size = span["size"]
bbox = span["bbox"] # (x0, y0, x1, y1) от левого верхнего угла
ifont = font_map.get(font_name, 0)
map_item = {
"ipage": page_num,
"field": text,
"x_ill": round(bbox[0], 2),
"y_ill": round(bbox[1], 2),
"size": round(font_size, 2),
"ifont": ifont
}
output_data["map"].append(map_item)
doc.close()
# Заполняем replacements_template, чтобы пользователь мог указать имена переменных
output_data["replacements_template"] = {text: "" for text in sorted(list(all_found_texts))}
# --- 5. СОХРАНЯЕМ РЕЗУЛЬТАТ В JSON ---
base_name = os.path.splitext(os.path.basename(pdf_path))[0]
output_filename = f"{base_name.replace('_original', '')}.map.json"
try:
with open(output_filename, 'w', encoding='utf-8') as f:
json.dump(output_data, f, indent=2, ensure_ascii=False)
print(f"--- SUCCESS ---")
print(f"Successfully created JSON map file: {output_filename}")
print(f"Please review this file and fill in the variable names in 'replacements_template'.")
except Exception as e:
print(f"Error writing JSON file: {e}")
if __name__ == "__main__":
if len(sys.argv) != 2:
print("Usage: python analyze_pdf.py <path_to_pdf_template>")
sys.exit(1)
pdf_path = sys.argv[1]
analyze_pdf_to_json(pdf_path)

50
check_pdf_fonts.py Normal file
View File

@ -0,0 +1,50 @@
import fitz # PyMuPDF
import sys
def extract_text_info(pdf_path):
try:
doc = fitz.open(pdf_path)
except Exception as e:
print(f"Ошибка при открытии файла: {e}")
return
for page_num in range(len(doc)):
page = doc[page_num]
blocks = page.get_text("dict")["blocks"]
print(f"Page {page_num + 1}")
for block in blocks:
if "lines" in block:
for line in block["lines"]:
line_text = ""
line_fonts = set() # Для хранения всех шрифтов в строке
font_size = None
# Координаты строки (берем из первого спана или обновляем)
bbox = None
for span in line["spans"]:
line_text += span["text"]
line_fonts.add(span["font"]) # Шрифт
font_size = span["size"] # Размер шрифта
# Координаты спана (x0, y0, x1, y1)
span_bbox = span["bbox"]
if bbox is None:
bbox = span_bbox
else:
# Обновляем границы для всей строки
bbox = (
min(bbox[0], span_bbox[0]), # x0
min(bbox[1], span_bbox[1]), # y0
max(bbox[2], span_bbox[2]), # x1
max(bbox[3], span_bbox[3]) # y1
)
if line_text.strip():
print(f"Line: {line_text.strip()} | Fonts: {', '.join(line_fonts)} | Size: {font_size} | BBox: ({bbox[0]:.2f}, {bbox[1]:.2f}, {bbox[2]:.2f}, {bbox[3]:.2f})")
doc.close()
if len(sys.argv) != 2:
print("Использование: python3 pdf_fonts.py <путь_к_pdf>")
sys.exit(1)
pdf_path = sys.argv[1]
extract_text_info(pdf_path)

13
config.js Normal file
View File

@ -0,0 +1,13 @@
module.exports = {
sendmail: {
host: "scp113.hosting.reg.ru",
port: 465,
secure: true,
auth: {
user: "support@evpak-soft.ru",
pass: "HY$R}1-{_03#on",
},
},
token: "43QAwPldLuuQTErY303m16lY5dSeDUy1OVKBzy5HTDG2KxfVC0m1o1cfZ49gbBRu",
taxtoken: "b9af104debf7f12d48f7766cf0588581"
}

30
docker-compose.yml Normal file
View File

@ -0,0 +1,30 @@
version: '3.7'
services:
# pdfgen_db_mysql:
# container_name: pdfgen_db_mysql
# image: mysql
# ports:
# - "127.0.0.1:3307:3306"
# restart: always
# environment:
# MYSQL_USER: admin
# MYSQL_PASSWORD: rNZzq5U37DqJlNe
# MYSQL_ROOT_PASSWORD: 4kDGQDYe4JxDjRd
# command: --init-file /init.sql
# volumes:
# - /var/lib/mysqld:/var/lib/mysql
# - ./init.sql:/init.sql
pdfgen:
ports:
- "6969:6969"
build:
context: .
target: development
command: npx nodemon index.js
volumes:
- .:/srv/pdfgen
- pdfgen_node_modules:/srv/pdfgen/node_modules
volumes:
pdfgen_node_modules:

32
index.js Normal file
View File

@ -0,0 +1,32 @@
var express = require('express');
var app = express();
const cookieParser = require('cookie-parser');
const bodyParser = require('body-parser');
const path = require('path');
var http = require('http').Server(app);
var port = process.env.PORT || 6969;
//const appConfig = require('./config');
//const Lang = require('./public/js/lang');
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: true }));
app.use(cookieParser()); // Подключаем cookie-parser к вашему приложению
// Routes
require('./routes')(app);
app.set('view engine', 'ejs');
app.set('views', path.join(__dirname, 'Core/views'));
app.use(express.static(__dirname + '/public'));
// flagAuthorization = false; //флаг для проверки авторизовался ли пользователь или нет
app.use(express.json());
http.listen(port, function(){
console.log('listening on *:' + port);
});

9
init.sql Normal file
View File

@ -0,0 +1,9 @@
-- Создание базы данных, если она не существует
CREATE DATABASE IF NOT EXISTS pdfgen;
-- Выдача прав пользователю
GRANT ALL PRIVILEGES ON pdfgen.* TO 'admin'@'%';
-- Применение изменений
FLUSH PRIVILEGES;

86
json_to_svg.py Normal file
View File

@ -0,0 +1,86 @@
import json
import sys
import os
def convert_json_to_svgs(json_path):
"""
Читает map.json файл и создает из него набор SVG файлов, по одному на страницу.
"""
try:
with open(json_path, 'r', encoding='utf-8') as f:
data = json.load(f)
except Exception as e:
print(f"Error reading or parsing JSON file: {e}")
return
map_data = data.get("map", [])
font_map = data.get("font_map", {})
# Группируем элементы по страницам
pages = {}
for item in map_data:
page_num = item["ipage"]
if page_num not in pages:
pages[page_num] = []
pages[page_num].append(item)
# --- Создаем CSS-классы для шрифтов ---
style_block = "<style type=\"text/css\">\n"
# Создаем классы для шрифтов .font0, .font1 и т.д.
# Сортируем по значению (индексу), чтобы обеспечить правильный порядок
sorted_font_map = sorted(font_map.items(), key=lambda x: x[1])
for font_name, font_index in sorted_font_map:
style_block += f"\t.font{font_index} {{ font-family: '{font_name}'; }}\n"
# Создаем классы для размеров .size10, .size9 и т.д.
all_sizes = sorted(list(set(item["size"] for item in map_data)), reverse=True)
size_classes = {}
for i, size in enumerate(all_sizes):
class_name = f"size{i}"
size_classes[size] = class_name
style_block += f"\t.{class_name} {{ font-size: {size}px; }}\n"
style_block += "</style>"
# --- Создаем SVG для каждой страницы ---
output_dir = "verizon_svgs"
if not os.path.exists(output_dir):
os.makedirs(output_dir)
for page_num, items in pages.items():
svg_filename = os.path.join(output_dir, f"{page_num + 1}.svg")
svg_header = f"""<?xml version="1.0" encoding="utf-8"?>
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 612 792" style="enable-background:new 0 0 612 792;" xml:space="preserve">
{style_block}
"""
svg_footer = "\n</svg>"
svg_body = ""
for item in items:
text = item["field"].replace('&', '&').replace('<', '<').replace('>', '>')
x = item["x_ill"]
# --- КЛЮЧЕВОЕ ИСПРАВЛЕНИЕ ---
# y в SVG transform - это базовая линия. Она примерно равна y_ill + size.
y = item["y_ill"] + item["size"]
font_name = item["ifont"]
ifont_class = f"font{font_map.get(font_name, 0)}"
size = item["size"]
size_class = size_classes.get(size, "")
svg_body += f' <text transform="matrix(1 0 0 1 {x:.2f} {y:.2f})" class="{ifont_class} {size_class}">{text}</text>\n'
# Сохраняем SVG файл
with open(svg_filename, 'w', encoding='utf-8') as f:
f.write(svg_header + svg_body + svg_footer)
print(f"Successfully created {svg_filename}")
if __name__ == "__main__":
if len(sys.argv) != 2:
print("Usage: python json_to_svg.py <path_to_map.json>")
sys.exit(1)
json_path = sys.argv[1]
convert_json_to_svgs(json_path)

View File

@ -0,0 +1,23 @@
# pdf-fill-data
Библиотека для заполнения данными шаблоннных PDF через API микросервиса
## Установка
```angular2html
npm install pdf-fill-data
```
## Использование
```javascript
const { pdfFillData } = require('pdf-fill-data');
// Описать примеры методов и классов
pdfFillData.Generation('Xfinity');
pdfFillData.Generation('COX');
```
## Лицензия
MIT

View File

@ -0,0 +1,19 @@
{
"name": "pdf-fill-data",
"version": "1.0.0",
"description": "Библиотека для заполнения данными шаблоннных PDF",
"main": "src/index.js",
"scripts": {
"test": "node test/index.test.js"
},
"keywords": [
"pdf",
"fill data",
"example"
],
"author": "Evpak Alexander",
"license": "MIT",
"directories": {
"test": "test"
}
}

View File

@ -0,0 +1,148 @@
{
"constants": {
"head_right_x": 590,
"head_left_x": 498,
"address_x": 49.6,
"address_y": 133.244,
"address_gap": 11,
"charges_x": 288,
"charges_y": 303.5,
"charges_gap": 18.5,
"charges_font_size": 8,
"second_page_head_x": 21,
"cupon_right_x": 595,
"cupon_left_x": 500,
"big_figures_delta_y": 2,
"deposit_x": 324,
"bu_y": 198,
"bu_row1_y": "198 + 50",
"bu_row2_y": "198 + 60",
"bu_col_usage_x": 350,
"bu_col_power_factor_x": 385,
"bu_col_pf_constant_x": 423,
"bu_col_meter_loc_x": 475,
"bu_col_billed_usage_x": 548,
"mrd_y": 298,
"mrd_row1_y": "298 + 30",
"mrd_row2_y": "298 + 45",
"mrd_col_previous_x": 343,
"mrd_col_type_x": 381,
"mrd_col_current_x": 420,
"mrd_col_type2_x": 460,
"mrd_col_metered_x": 503,
"mrd_col_usage_x": 560
},
"map": [
{ "ipage": 0, "field": "SERVICE ADDRESS:, customer_name, service_address_full", "x": 50, "y": 87, "size": 7, "ifont": 2 },
{ "ipage": 0, "field": "barcode_number", "x": 306, "y": 736, "size": 9, "ifont": 3, "align": "center" },
{ "ipage": 0, "field": "$,current_electric_charges_total", "x": 151, "y": 380, "size": 14, "ifont": 2, "align": "center", "noSpace": true },
{ "ipage": 0, "field": "current_charges_kwh", "x": 151, "y": 361, "size": 11, "ifont": 2, "align": "center", "cR": 1, "cG": 1, "cB": 1},
{ "ipage": 0, "field": "Billing from, billing_period_from, billing_period_to, billing_period_days", "x": 50, "y": 242, "size": 9, "ifont": 2 },
{ "ipage": 0, "field": "$,amount_due_value", "x": "head_right_x", "y": "23 + big_figures_delta_y", "size": 22.2, "ifont": 1, "align": "right", "noSpace": true },
{ "ipage": 0, "field": "Amount due on or before", "x": "head_left_x", "y": 25, "size": 9, "ifont": 2, "align": "right" },
{ "ipage": 0, "field": ",amount_due_date", "x": "head_left_x", "y": 37, "size": 9, "ifont": 2, "align": "right" },
{ "ipage": 0, "field": "Bill mailing date is ,bill_mailing_date", "x": "head_right_x", "y": 50, "size": 9, "ifont": 2, "align": "right" },
{ "ipage": 0, "field": "Account #,account_number", "x": "head_right_x", "y": 62, "size": 9, "ifont": 2, "align": "right", "noSpace": true },
{ "ipage": 0, "field": "$,supplier_charges", "x": 244, "y": 334, "size": "charges_font_size", "ifont": 1 },
{ "ipage": 0, "field": "$,current_electric_charges_total", "x": 28, "y": 434, "size": "charges_font_size", "ifont": 1 },
{ "ipage": 0, "field": "customer_name, service_address_full", "x": 18, "y": 540, "size": 9, "ifont": 2 },
{ "ipage": 0, "field": "Account #,account_number", "x": "cupon_right_x", "y": 572, "size": 9, "ifont": 2, "align": "right" },
{ "ipage": 0, "field": "customer_name", "x": "cupon_right_x", "y": 582, "size": 9, "ifont": 2, "align": "right" },
{ "ipage": 0, "field": "Amount due on or before", "x": "cupon_left_x", "y": 595, "size": 9, "ifont": 2, "align": "right" },
{ "ipage": 0, "field": "amount_due_date", "x": "cupon_left_x", "y": 605, "size": 9, "ifont": 2, "align": "right" },
{ "ipage": 0, "field": "$,remit_amount_due_value", "x": "cupon_right_x", "y": "592 + big_figures_delta_y", "size": 22.2, "ifont": 1, "align": "right", "noSpace": true },
{ "ipage": 0, "field": "remit_pay_after_date_amount", "x": "cupon_right_x", "y": 646, "size": 9, "ifont": 1, "align": "right" },
{ "ipage": 0, "field": "customer_name", "x": "address_x", "y": "address_y", "size": 10, "ifont": 2 },
{ "ipage": 0, "field": "attn_name", "x": "address_x", "y": "address_y + address_gap", "size": 10, "ifont": 2 },
{ "ipage": 0, "field": "service_address_line1", "x": "address_x", "y": "address_y + address_gap * 2", "size": 10, "ifont": 2 },
{ "ipage": 0, "field": "service_address_line2", "x": "address_x", "y": "address_y + address_gap * 3", "size": 10, "ifont": 2 },
{ "ipage": 2, "field": "customer_name", "x": "second_page_head_x", "226": "address_y", "size": 11, "ifont": 2 },
{ "ipage": 2, "field": "service_address_line1", "x": "second_page_head_x", "92": "address_y + address_gap * 2", "size": 11, "ifont": 2 },
{ "ipage": 2, "field": "service_address_line2", "x": "second_page_head_x", "105": "address_y + address_gap * 3", "size": 11, "ifont": 2 },
{ "ipage": 2, "field": "Account #,account_number", "x": "second_page_head_x", "123": 62, "size": 9, "ifont": 2, "align": "right", "noSpace": true },
{ "ipage": 2, "field": "total_amount_due_last_billing", "x": "charges_x", "y": "charges_y - 3.5 - charges_gap * 6", "size": "charges_font_size", "ifont": 2, "align": "right" },
{ "ipage": 2, "field": "Payment, payment_thank_you_date, - Thank You", "x": 26, "y": "charges_y - 3.5 - charges_gap * 5", "size": "charges_font_size", "ifont": 2 },
{ "ipage": 2, "field": "payment_thank_you_amount", "x": "charges_x", "y": "charges_y - 3.5 - charges_gap * 5", "size": "charges_font_size", "ifont": 2, "align": "right" },
{ "ipage": 2, "field": "late_payment_charge", "x": "charges_x", "y": "charges_y - 3.5 - charges_gap * 4", "size": "charges_font_size", "ifont": 2, "align": "right" },
{ "ipage": 2, "field": "previous_balance_due", "x": "charges_x", "y": "charges_y - 3.5 - charges_gap * 3", "size": "charges_font_size", "ifont": 1, "align": "right" },
{ "ipage": 2, "field": "transmission_service", "x": "charges_x", "y": "charges_y", "size": "charges_font_size", "ifont": 2, "align": "right" },
{ "ipage": 2, "field": "distribution_service", "x": "charges_x", "y": "charges_y + charges_gap", "size": "charges_font_size", "ifont": 2, "align": "right" },
{ "ipage": 2, "field": "customer_charge_detail", "x": "charges_x", "y": "charges_y + charges_gap * 2", "size": "charges_font_size", "ifont": 2, "align": "right" },
{ "ipage": 2, "field": "retail_stability_rider", "x": "charges_x", "y": "charges_y + charges_gap * 3", "size": "charges_font_size", "ifont": 2, "align": "right" },
{ "ipage": 2, "field": "deferred_asset_rider", "x": "charges_x", "y": "charges_y + charges_gap * 4", "size": "charges_font_size", "ifont": 2, "align": "right" },
{ "ipage": 2, "field": "phase_in_recovery_rider", "x": "charges_x", "y": "charges_y + charges_gap * 5", "size": "charges_font_size", "ifont": 2, "align": "right" },
{ "ipage": 2, "field": "power_purchase_rider", "x": "charges_x", "y": "charges_y + charges_gap * 6", "size": "charges_font_size", "ifont": 2, "align": "right" },
{ "ipage": 2, "field": "current_electric_charges_total", "x": "charges_x", "y": "charges_y + charges_gap * 7", "size": "charges_font_size", "ifont": 1, "align": "right" },
{ "ipage": 2, "field": "supplier_charges", "x": "charges_x", "y": 536, "size": "charges_font_size", "ifont": 2, "align": "right" },
{ "ipage": 2, "field": "supplier_charges", "x": "charges_x", "y": 556, "size": "charges_font_size", "ifont": 1, "align": "right" },
{ "ipage": 2, "field": "remit_pay_after_date_amount", "x": 26, "y": 609, "size": "charges_font_size", "ifont": 2 },
{ "ipage": 2, "field": "Customer Name:, customer_name", "x": "deposit_x", "y": 476, "size": 8, "ifont": 2 },
{ "ipage": 2, "field": "Mailing Address:, attn_name", "x": "deposit_x", "y": 488, "size": 8, "ifont": 2 },
{ "ipage": 2, "field": "service_address_line1", "x": 391, "y": 497, "size": 8, "ifont": 2 },
{ "ipage": 2, "field": "service_address_line2", "x": 391, "y": 506, "size": 8, "ifont": 2 },
{ "ipage": 2, "field": "Account Number:,account_number", "x": "deposit_x", "y": 516, "size": 8, "ifont": 2 },
{ "ipage": 2, "field": "Area Office:,area_office", "x": "deposit_x", "y": 525, "size": 8, "ifont": 2 },
{ "ipage": 2, "field": "Deposit Number:,deposit_number", "x": "deposit_x", "y": 535, "size": 8, "ifont": 2 },
{ "ipage": 2, "field": "Deposit Date:,deposit_date", "x": "deposit_x", "y": 545, "size": 8, "ifont": 2 },
{ "ipage": 2, "field": "Deposit Amount:,deposit_amount", "x": "deposit_x", "y": 555, "size": 8, "ifont": 2 },
{ "ipage": 2, "field": "Cashier Number:,cashier_number", "x": "deposit_x", "y": 565, "size": 8, "ifont": 2 },
{
"ipage": 2,
"field": "Total usage for the past 12 months:,total_usage_12_months",
"x": "324",
"y": "168",
"size": 8,
"ifont": 2
},
{
"ipage": 2,
"field": "Average (Avg.) monthly usage:,avg_monthly_usage",
"x": "324",
"y": "182",
"size": 8,
"ifont": 2
},
{ "ipage": 2, "field": "Billed Usage ,billed_usage_date", "x": "460", "y": "204", "size": 8, "ifont": 2, "align": "center" },
{ "ipage": 2, "field": "billed_usage_kwh_usage", "x": "bu_col_usage_x", "y": "bu_row1_y", "size": 8, "ifont": 0, "align": "center" },
{ "ipage": 2, "field": "billed_usage_kwh_billed", "x": "bu_col_billed_usage_x", "y": "bu_row1_y", "size": 8, "ifont": 0, "align": "center" },
{ "ipage": 2, "field": "billed_usage_kw_usage", "x": "bu_col_usage_x", "y": "bu_row2_y", "size": 8, "ifont": 0, "align": "center" },
{ "ipage": 2, "field": "billed_usage_kw_billed", "x": "bu_col_billed_usage_x", "y": "bu_row2_y", "size": 8, "ifont": 0, "align": "center" },
{ "ipage": 2, "field": "Meter #,meter_read_details_meter_no", "x": "mrd_col_previous_x", "y": "mrd_y", "size": 8, "ifont": 2 },
{ "ipage": 2, "field": "meter_read_kwh_previous", "x": "mrd_col_previous_x", "y": "mrd_row1_y", "size": 8, "ifont": 0, "align": "center" },
{ "ipage": 2, "field": "Actual", "x": "mrd_col_type_x", "y": "mrd_row1_y", "size": 8, "ifont": 0, "align": "center" },
{ "ipage": 2, "field": "meter_read_kwh_current", "x": "mrd_col_current_x", "y": "mrd_row1_y", "size": 8, "ifont": 0, "align": "center" },
{ "ipage": 2, "field": "Actual", "x": "mrd_col_type2_x", "y": "mrd_row1_y", "size": 8, "ifont": 0, "align": "center" },
{ "ipage": 2, "field": "meter_read_kwh_metered", "x": "mrd_col_metered_x", "y": "mrd_row1_y", "size": 8, "ifont": 0, "align": "center" },
{ "ipage": 2, "field": "meter_read_kwh_usage", "x": "mrd_col_usage_x", "y": "mrd_row1_y", "size": 8, "ifont": 0, "align": "center" },
{ "ipage": 2, "field": "meter_read_kw_current", "x": "mrd_col_current_x", "y": "mrd_row2_y", "size": 8, "ifont": 0, "align": "center" },
{ "ipage": 2, "field": "Actual", "x": "mrd_col_type2_x", "y": "mrd_row2_y", "size": 8, "ifont": 0, "align": "center" },
{ "ipage": 2, "field": "meter_read_kw_metered", "x": "mrd_col_metered_x", "y": "mrd_row2_y", "size": 8, "ifont": 0, "align": "center" },
{ "ipage": 2, "field": "meter_read_kw_usage", "x": "mrd_col_usage_x", "y": "mrd_row2_y", "size": 8, "ifont": 0, "align": "center" },
{ "ipage": 2, "field": "Service Period ,meter_read_service_period", "x": "mrd_col_previous_x - 12", "y": "mrd_y + 62", "size": 7, "ifont": 2 },
{ "ipage": 2, "field": "next_read_date_info", "x": "mrd_col_previous_x - 12", "y": "mrd_y + 77", "size": 8, "ifont": 2 }
]
}

View File

@ -0,0 +1,188 @@
{
"constants": {
"usage_table_y_ill": 328,
"usage_table_font_size": 7,
"remit_x_left": 420,
"remit_x_right": 563,
"first_page_x": 54,
"bar_chart_x_ill": 65,
"bar_chart_y_ill": 236,
"bar_chart_width": 214,
"bar_chart_height": 58,
"bar_chart_current_month_index": 5,
"bar_chart_label_y_offset": 10,
"bar_chart_label_font_size": 7,
"bar_chart_label_ifont": 0,
"bar_chart_bar_width_ratio": 0.4,
"color_bar_default_r": "112/256", "color_bar_default_g": "111/256", "color_bar_default_b": "108/256",
"color_bar_current_r": 0, "color_bar_current_g": 0, "color_bar_current_b": 0,
"donut_chart_center_x_ill": 475,
"donut_chart_center_y_ill": 296,
"donut_chart_radius": 86.5,
"donut_chart_thickness": 20,
"donut_text_x": 500,
"donut_small_font_size": 9,
"color_energy_r": "0 / 256",
"color_energy_g": "153 / 256",
"color_energy_b": "218 / 256",
"color_fuel_r": "76 / 256",
"color_fuel_g": "184 / 256",
"color_fuel_b": "73 / 256",
"color_other_r": "244 / 256",
"color_other_g": "130 / 256",
"color_other_b": "33 / 256",
"color_total_r": "220 / 256",
"color_total_g": "125 / 256",
"color_total_b": "67 / 256",
"page2_left_col_x": 54,
"page2_detail_col_x": 218,
"page2_right_col_x": 345,
"page2_charges_start_y": 94,
"page2_line_gap": 9,
"page2_section_gap": 15,
"page2_total_gap": 1,
"page2_font_size": 8,
"page2_total_font_size": 9,
"page2_header_font_size": 7,
"page2_meter_read_y": 23,
"page2_meter_read_data_y": 41,
"page2_header_right_col_x": 572,
"bar_chart_legend_y_ill": 229,
"bar_chart_legend_item1_x_ill": 63,
"bar_chart_legend_item2_x_ill": 103
},
"donut_chart_segments": [
{
"value_field": "energy_charges_total",
"color": ["color_energy_r", "color_energy_g", "color_energy_b"],
"label": "Energy Charges"
},
{
"value_field": "fuel_charges_total",
"color": ["color_fuel_r", "color_fuel_g", "color_fuel_b"],
"label": "Fuel Charges"
},
{
"value_field": "other_charges_total",
"color": ["color_other_r", "color_other_g", "color_other_b"],
"label": "Other Charges & Credits"
}
],
"map": [
{
"ipage": 0,
"field": "chart_legend_year_previous",
"x_ill": "bar_chart_legend_item1_x_ill",
"y_ill": "bar_chart_legend_y_ill",
"size": 7,
"ifont": 1,
"cR": "color_bar_default_r", "cG": "color_bar_default_g", "cB": "color_bar_default_b"
},
{
"ipage": 0,
"field": "chart_legend_year_current",
"x_ill": "bar_chart_legend_item2_x_ill",
"y_ill": "bar_chart_legend_y_ill",
"size": 7,
"ifont": 1
},
{ "ipage": 0, "field": "Hi ,customer_name,", "x_ill": "first_page_x", "y_ill": 112, "size": 34, "ifont": 1 },
{ "ipage": 0, "field": "service_location_line1", "x_ill": 215, "y_ill": 55, "size": 7, "ifont": 0 },
{ "ipage": 0, "field": "service_location_line2", "x_ill": 215, "y_ill": 64, "size": 7, "ifont": 0 },
{
"ipage": 0,
"field": "<style font='1'><style bg='0.96,0.929,0.8' padding='3'>Account # <style font='2'>account_number</style></style> | Invoice # <style font='2'>invoice_number</style></style>",
"x_ill": "first_page_x",
"y_ill": 165,
"size": 9
},
{
"ipage": 0,
"field": "<style font='1'>Mail Date <style font='2'>mail_date</style> | QPC <style font='2'>qpc_code</style> | Cycle <style font='2'>cycle_number</style></style>",
"x_ill": "first_page_x",
"y_ill": 180.7,
"size": 9
},
{ "ipage": 0, "field": "amount_due_by_value", "x_ill": 575, "y_ill": 114, "size": 36, "ifont": 1, "align": "right", "draw_method": "drawFancyAmount" },
{ "ipage": 0, "field": "Amount Due by,amount_due_by_date", "x_ill": 575, "y_ill": 153, "size": 9, "ifont": 1, "align": "right" },
{
"ipage": 0,
"field": "billing_period_formatted",
"x_ill": "61",
"y_ill": "usage_table_y_ill",
"size": "usage_table_font_size",
"ifont": 1
},
{ "ipage": 0, "field": "billing_days", "x_ill": 160, "y_ill": "usage_table_y_ill", "size": "usage_table_font_size", "ifont": 1, "align": "center" },
{ "ipage": 0, "field": "kwh_used_formatted", "x_ill": 213, "y_ill": "usage_table_y_ill", "size": "usage_table_font_size", "ifont": 1, "align": "center" },
{ "ipage": 0, "field": "avg_kwh_per_day", "x_ill": 282, "y_ill": "usage_table_y_ill", "size": "usage_table_font_size", "ifont": 1, "align": "center" },
{ "ipage": 0, "field": "remit_customer_address", "x_ill": 67, "y_ill": 678, "size": 9, "ifont": 0, "lineHeight": 10 },
{ "ipage": 0, "field": "Account #", "x_ill": 403, "y_ill": 551, "size": 9, "ifont": 1, "align": "right" },
{ "ipage": 0, "field": "Invoice #", "x_ill": 403, "y_ill": 559, "size": 9, "ifont": 1, "align": "right" },
{ "ipage": 0, "field": "QPC", "x_ill": 403, "y_ill": 567, "size": 9, "ifont": 1, "align": "right" },
{ "ipage": 0, "field": "remit_account_number", "x_ill": 411, "y_ill": 551, "size": 9, "ifont": 2 },
{ "ipage": 0, "field": "remit_invoice_number", "x_ill": 411, "y_ill": 559, "size": 9, "ifont": 2 },
{ "ipage": 0, "field": "qpc_code", "x_ill": 411, "y_ill": 567, "size": 9, "ifont": 2 },
{ "ipage": 0, "field": "Amount Due by, [NEWLINE], remit_amount_due_by_date", "x_ill": "remit_x_left", "y_ill": 598, "size": 9, "ifont": 1, "lineHeight": 10, "align": "center" },
{ "ipage": 0, "field": "$,remit_amount_due_by_value", "x_ill": "remit_x_right", "y_ill": 604, "size": 9, "ifont": 1, "align": "right", "noSpace": true },
{ "ipage": 0, "field": "Amount Due After, [NEWLINE], remit_amount_due_after_date", "x_ill": "remit_x_left", "y_ill": 630, "size": 9, "ifont": 1, "lineHeight": 10, "align": "center" },
{ "ipage": 0, "field": "$,amount_due_after_value", "x_ill": "remit_x_right", "y_ill": 636, "size": 9, "ifont": 1, "align": "right", "noSpace": true },
{ "ipage": 0, "field": "barcode_number", "x_ill": 307, "y_ill": 765, "size": 12, "ifont": 3, "align": "center" },
{ "ipage": 0, "field": "Energy Charges", "x_ill": "donut_text_x", "y_ill": "160 + 86", "size": "donut_small_font_size", "ifont": 1, "cR": "color_energy_r", "cG": "color_energy_g", "cB": "color_energy_b", "align": "right" },
{ "ipage": 0, "field": "$,energy_charges_total", "x_ill": "donut_text_x", "y_ill": "172 + 86", "size": 15, "ifont": 1, "cR": "color_energy_r", "cG": "color_energy_g", "cB": "color_energy_b", "noSpace": true, "align": "right" },
{ "ipage": 0, "field": "Fuel Charges", "x_ill": "donut_text_x", "y_ill": "200 + 80", "size": "donut_small_font_size", "ifont": 1, "cR": "color_fuel_r", "cG": "color_fuel_g", "cB": "color_fuel_b", "align": "right" },
{ "ipage": 0, "field": "$,fuel_charges_total", "x_ill": "donut_text_x", "y_ill": "212 + 80", "size": 15, "ifont": 1, "cR": "color_fuel_r", "cG": "color_fuel_g", "cB": "color_fuel_b", "noSpace": true, "align": "right" },
{ "ipage": 0, "field": "Other Charges,", "x_ill": "donut_text_x", "y_ill": "240 + 76", "size": "donut_small_font_size", "ifont": 1, "cR": "color_other_r", "cG": "color_other_g", "cB": "color_other_b", "align": "right" },
{ "ipage": 0, "field": "& Credits", "x_ill": "donut_text_x", "y_ill": "240 + 86", "size": "donut_small_font_size", "ifont": 1, "cR": "color_other_r", "cG": "color_other_g", "cB": "color_other_b", "align": "right" },
{ "ipage": 0, "field": "$,other_charges_total", "x_ill": "donut_text_x", "y_ill": "252 + 86", "size": 15, "ifont": 1, "cR": "color_other_r", "cG": "color_other_g", "cB": "color_other_b", "noSpace": true, "align": "right" },
{ "ipage": 1, "field": "energy_charge", "x_ill": "page2_right_col_x", "y_ill": "page2_charges_start_y + page2_line_gap", "size": "page2_font_size", "ifont": 0, "align": "right", "noSpace": true },
{ "ipage": 1, "field": "federal_eac_rider_detail", "x_ill": "page2_detail_col_x", "y_ill": "page2_charges_start_y + page2_line_gap * 2", "size": 7, "ifont": 0, "bgColor": ["1", "1", "1"] },
{ "ipage": 1, "field": "federal_eac_rider", "x_ill": "page2_right_col_x", "y_ill": "page2_charges_start_y + page2_line_gap * 2", "size": "page2_font_size", "ifont": 0, "align": "right", "noSpace": true },
{ "ipage": 1, "field": "energy_charges_total", "x_ill": "page2_right_col_x", "y_ill": "page2_charges_start_y + page2_line_gap * 3", "size": "page2_font_size", "ifont": 1, "cR": "color_energy_r", "cG": "color_energy_g", "cB": "color_energy_b", "align": "right", "noSpace": true },
{ "ipage": 1, "field": "fuel_adjustment_detail", "x_ill": "page2_detail_col_x", "y_ill": "page2_charges_start_y + page2_line_gap * 4 + page2_section_gap", "size": 7, "ifont": 0, "bgColor": ["1", "1", "1"] },
{ "ipage": 1, "field": "fuel_charges_total", "x_ill": "page2_right_col_x", "y_ill": "page2_charges_start_y + page2_line_gap * 4 + page2_section_gap", "size": "page2_font_size", "ifont": 0, "align": "right", "noSpace": true },
{ "ipage": 1, "field": "fuel_charges_total", "x_ill": "page2_right_col_x", "y_ill": "page2_charges_start_y + page2_line_gap * 5 + page2_section_gap", "size": "page2_font_size", "ifont": 1, "cR": "color_fuel_r", "cG": "color_fuel_g", "cB": "color_fuel_b", "align": "right", "noSpace": true },
{ "ipage": 1, "field": "storm_restoration_offset", "x_ill": "page2_right_col_x", "y_ill": "page2_charges_start_y + page2_line_gap * 7 + page2_section_gap * 2", "size": "page2_font_size", "ifont": 0, "align": "right", "noSpace": true },
{ "ipage": 1, "field": "storm_restoration_charge", "x_ill": "page2_right_col_x", "y_ill": "page2_charges_start_y + page2_line_gap * 8 + page2_section_gap * 2", "size": "page2_font_size", "ifont": 0, "align": "right", "noSpace": true },
{ "ipage": 1, "field": "other_charges_total", "x_ill": "page2_right_col_x", "y_ill": "page2_charges_start_y + page2_line_gap * 9 + page2_section_gap * 2", "size": "page2_font_size", "ifont": 1, "cR": "color_other_r", "cG": "color_other_g", "cB": "color_other_b", "align": "right", "noSpace": true },
{ "ipage": 1, "field": "current_month_energy_charges", "x_ill": "page2_right_col_x", "y_ill": "page2_charges_start_y + page2_line_gap * 10 + page2_section_gap * 3", "size": "page2_total_font_size", "ifont": 1, "align": "right", "noSpace": true },
{ "ipage": 1, "field": "deposit", "x_ill": "page2_right_col_x", "y_ill": "page2_charges_start_y + page2_line_gap * 11 + page2_section_gap * 3 + page2_total_gap", "size": "page2_total_font_size", "ifont": 0, "cR": "color_total_r", "cG": "color_total_g", "cB": "color_total_b", "align": "right", "noSpace": true },
{ "ipage": 1, "field": "connect_fee", "x_ill": "page2_right_col_x", "y_ill": "page2_charges_start_y + page2_line_gap * 12 + page2_section_gap * 3 + page2_total_gap * 2", "size": "page2_total_font_size", "ifont": 0, "cR": "color_total_r", "cG": "color_total_g", "cB": "color_total_b", "align": "right", "noSpace": true },
{ "ipage": 1, "field": "amount_due_by_value", "x_ill": "page2_right_col_x", "y_ill": "page2_charges_start_y + page2_line_gap * 13 + page2_section_gap * 3 + page2_total_gap * 5", "size": "page2_total_font_size", "ifont": 1, "align": "right", "noSpace": true },
{ "ipage": 1, "field": "(Contract , meter_reading_header, )", "x_ill": 118, "y_ill": "page2_meter_read_y", "size": "page2_header_font_size", "ifont": 1, "cR": "1", "cG": "1", "cB": "1" },
{ "ipage": 1, "field": "meter_number_header", "x_ill": 255, "y_ill": "page2_meter_read_y", "size": "page2_header_font_size", "ifont": 1, "cR": "1", "cG": "1", "cB": "1" },
{ "ipage": 1, "field": "rate_header", "x_ill": 366, "y_ill": "page2_meter_read_y", "size": "page2_header_font_size", "ifont": 1, "cR": "1", "cG": "1", "cB": "1" },
{ "ipage": 1, "field": "billing_days", "x_ill": 550, "y_ill": "page2_meter_read_y", "size": "page2_header_font_size", "ifont": 1, "align": "center", "cR": "1", "cG": "1", "cB": "1" },
{ "ipage": 1, "field": "current_reading_text", "x_ill": 148, "y_ill": "page2_meter_read_data_y", "size": "page2_header_font_size", "ifont": 0 },
{ "ipage": 1, "field": "current_meter_reading_value", "x_ill": 318, "y_ill": "page2_meter_read_data_y", "size": "page2_header_font_size", "ifont": 1, "align": "right" },
{ "ipage": 1, "field": "previous_reading_text", "x_ill": 421, "y_ill": "page2_meter_read_data_y", "size": "page2_header_font_size", "ifont": 0 },
{ "ipage": 1, "field": "- , previous_meter_reading_value", "x_ill": "page2_header_right_col_x", "y_ill": "page2_meter_read_data_y", "size": "page2_header_font_size", "ifont": 0, "align": "right" },
{ "ipage": 1, "field": "kwh_used_formatted", "x_ill": "page2_header_right_col_x", "y_ill": "page2_meter_read_data_y + 17", "size": "page2_header_font_size", "ifont": 1, "align": "right" }
]
}

View File

@ -0,0 +1,151 @@
{
"constants": {
"remit_amount_x_right": 540
},
"replacements": {
"GERMECIA JOSEPH": "customer_name",
"9400 ROBERTS DR": "customer_address",
"ATLANTA, GA 30350-2041": "customer_city_st_zip",
"326890199-00001": "account_number",
"8614432777": "invoice_number",
"$0.00": "balance_from_last_bill",
"$160.32": "total_amount_due",
"Total due on Feb 12": "total_due_text",
"Total Amount Due by February 12, 2024": "remit_total_due_text",
"86144327770103268901990000100000016032000000160329": "barcode_number",
"$35.00": "activation_fee",
"$100.00": "plan_ultimate",
"$10.00": "perk_apple_one",
"$9.66": "surcharges_total",
"$6.20": "surcharge_fus",
"$0.16": "surcharge_regulatory",
"$3.30": "surcharge_admin",
"$5.66": "taxes_total",
"$1.50": "tax_911",
"$2.15": "tax_ga_state",
"$2.01": "tax_fulton_county",
"Your January bill is $160.32": "page3_header_text",
"Due Feb 12": "page3_due_date_text",
"You paid $0.00.": "page3_payment_text"
},
"map": [
{ "ipage": 0, "field": "PO BOX 489", "x_ill": 35.25, "y_ill": 34.66, "size": 10, "ifont": 2 },
{ "ipage": 0, "field": "NEWARK, NJ 07101-0489", "x_ill": 35.25, "y_ill": 45.66, "size": 10, "ifont": 2 },
{ "ipage": 0, "field": "GERMECIA JOSEPH", "x_ill": 35.75, "y_ill": 154.41, "size": 10, "ifont": 4 },
{ "ipage": 0, "field": "9400 ROBERTS DR", "x_ill": 35.75, "y_ill": 165.46, "size": 10, "ifont": 4 },
{ "ipage": 0, "field": "ATLANTA, GA 30350-2041", "x_ill": 35.75, "y_ill": 176.46, "size": 10, "ifont": 4 },
{
"ipage": 0,
"field": "86144327770103268901990000100000016032000000160329",
"x_ill": 297,
"y_ill": 746.0,
"size": 11.5,
"ifont": 3,
"align": "center"
},
{ "ipage": 0, "field": "$0.00", "x_ill": 332, "y_ill": 356.13, "size": 12, "ifont": 2, "align": "right" },
{ "ipage": 0, "field": "$160.32", "x_ill": 332, "y_ill": 382.78, "size": 12, "ifont": 2, "align": "right" },
{ "ipage": 0, "field": "$160.32", "x_ill": 332, "y_ill": 416.38, "size": 12, "ifont": 1, "align": "right" },
{ "ipage": 0, "field": "$160.32", "x_ill": "remit_amount_x_right", "y_ill": 610.54, "size": 16, "ifont": 1, "align": "right" },
{ "ipage": 0, "field": "Total Amount Due by February 12, 2024", "x_ill": 277.4, "y_ill": 589.19, "size": 11, "ifont": 1 },
{ "ipage": 0, "field": "Total due on Feb 12", "x_ill": 53, "y_ill": 416.38, "size": 12, "ifont": 1 },
{ "ipage": 0, "field": "326890199-00001", "x_ill": 424.3, "y_ill": 45.21, "size": 9, "ifont": 2 },
{ "ipage": 0, "field": "8614432777", "x_ill": 418.8, "y_ill": 60.31, "size": 9, "ifont": 2 },
{ "ipage": 1, "field": "326890199-00001", "x_ill": 424.3, "y_ill": 45.21, "size": 9, "ifont": 2 },
{ "ipage": 1, "field": "8614432777", "x_ill": 418.8, "y_ill": 60.31, "size": 9, "ifont": 2 },
{ "ipage": 2, "field": "326890199-00001", "x_ill": 424.3, "y_ill": 45.21, "size": 9, "ifont": 2 },
{ "ipage": 2, "field": "8614432777", "x_ill": 418.8, "y_ill": 60.31, "size": 9, "ifont": 2 },
{ "ipage": 3, "field": "326890199-00001", "x_ill": 424.3, "y_ill": 45.21, "size": 9, "ifont": 2 },
{ "ipage": 3, "field": "8614432777", "x_ill": 418.8, "y_ill": 60.31, "size": 9, "ifont": 2 },
{ "ipage": 4, "field": "326890199-00001", "x_ill": 424.3, "y_ill": 45.21, "size": 9, "ifont": 2 },
{ "ipage": 4, "field": "8614432777", "x_ill": 418.8, "y_ill": 60.31, "size": 9, "ifont": 2 },
{ "ipage": 5, "field": "326890199-00001", "x_ill": 424.3, "y_ill": 45.21, "size": 9, "ifont": 2 },
{ "ipage": 5, "field": "8614432777", "x_ill": 418.8, "y_ill": 60.31, "size": 9, "ifont": 2 },
{
"ipage": 2,
"field": "Germecia Joseph",
"x_ill": 36.0,
"y_ill": 263.24,
"size": 11.0,
"ifont": 1
},
{
"ipage": 2,
"field": "Your January bill is $160.32",
"x_ill": 36.0,
"y_ill": 80.4,
"size": 16.0,
"ifont": 1
},
{
"ipage": 2,
"field": "Due Feb 12",
"x_ill": 36.0,
"y_ill": 102.69,
"size": 11.0,
"ifont": 1
},
{
"ipage": 2,
"field": "You paid $0.00.",
"x_ill": 68.85,
"y_ill": 167.48,
"size": 11.0,
"ifont": 2
},
{
"ipage": 2,
"field": "$0.00",
"x_ill": 316.55,
"y_ill": 238.54,
"size": 10.0,
"ifont": 1
},
{
"ipage": 2,
"field": "$160.32",
"x_ill": 306.2,
"y_ill": 263.24,
"size": 10.0,
"ifont": 1
},
{
"ipage": 2,
"field": "$160.32",
"x_ill": 302.15,
"y_ill": 319.68,
"size": 11.0,
"ifont": 1
},
{ "ipage": 3, "field": "$0.00", "x_ill": 349, "y_ill": 98.63, "size": 12, "ifont": 1, "align": "right" },
{ "ipage": 3, "field": "$160.32", "x_ill": 349, "y_ill": 138.23, "size": 14, "ifont": 1, "align": "right" },
{ "ipage": 3, "field": "$35.00", "x_ill": 349, "y_ill": 245.61, "size": 10, "ifont": 1, "align": "right" },
{ "ipage": 3, "field": "$35.00", "x_ill": 349, "y_ill": 266.86, "size": 9, "ifont": 2, "align": "right" },
{ "ipage": 3, "field": "$100.00", "x_ill": 349, "y_ill": 301.51, "size": 10, "ifont": 1, "align": "right" },
{ "ipage": 3, "field": "$100.00", "x_ill": 349, "y_ill": 322.81, "size": 9, "ifont": 2, "align": "right" },
{ "ipage": 3, "field": "$10.00", "x_ill": 349, "y_ill": 371.11, "size": 10, "ifont": 1, "align": "right" },
{ "ipage": 3, "field": "$10.00", "x_ill": 349, "y_ill": 392.41, "size": 9, "ifont": 2, "align": "right" },
{ "ipage": 3, "field": "$9.66", "x_ill": 349, "y_ill": 452.01, "size": 10, "ifont": 1, "align": "right" },
{ "ipage": 3, "field": "$6.20", "x_ill": 349, "y_ill": 473.26, "size": 9, "ifont": 2, "align": "right" },
{ "ipage": 3, "field": "$0.16", "x_ill": 349, "y_ill": 489.61, "size": 9, "ifont": 2, "align": "right" },
{ "ipage": 3, "field": "$3.30", "x_ill": 349, "y_ill": 505.91, "size": 9, "ifont": 2, "align": "right" },
{ "ipage": 3, "field": "$5.66", "x_ill": 349, "y_ill": 540.56, "size": 10, "ifont": 1, "align": "right" },
{ "ipage": 3, "field": "$1.50", "x_ill": 349, "y_ill": 561.81, "size": 9, "ifont": 2, "align": "right" },
{ "ipage": 3, "field": "$2.15", "x_ill": 349, "y_ill": 578.16, "size": 9, "ifont": 2, "align": "right" },
{ "ipage": 3, "field": "$2.01", "x_ill": 349, "y_ill": 594.46, "size": 9, "ifont": 2, "align": "right" }
]
}

View File

@ -0,0 +1,18 @@
/**
* Заполняет PDF определённого формата данными
* @param {json} request - запрос с входными данными для заполнения
* @return {file} pdf - на выходе отдаёт готовый pdf c уже проставленными данными на свои места
*/
module.exports = {
pdfFillData: require('./pdfFillData'),
pdfFillDataXfinity: require('./pdfFillDataXfinity'),
pdfFillDataCOX: require('./pdfFillDataCOX'),
pdfFillDataATT: require('./pdfFillDataATT'),
pdfFillDataSpectrum: require('./pdfFillDataSpectrum'),
pdfFillDataDukeEnergy: require('./pdfFillDataDukeEnergy'),
pdfFillDataAEPOhio: require('./pdfFillDataAEPOhio'),
pdfFillDataEntergy: require('./pdfFillDataEntergy'),
pdfFillDataPGE: require('./pdfFillDataPGE'),
pdfFillDataVerizon: require('./pdfFillDataVerizon')
};

View File

@ -0,0 +1,684 @@
const appConfig = require("../../../config");
const Taxjar = require('taxjar');
const fs = require('fs');
const fsPromises = require('fs').promises;
const csv = require('csv-parser');
const path = require('path');
const { randomUUID } = require('crypto');
const { PDFDocument, StandardFonts, rgb } = require('pdf-lib');
const fontkit = require('fontkit');
const bwipjs = require('bwip-js');
class pdfFillData {
/**
* @param {string} [pdfDocumentName='Xfinity'] - Наименование документа
*/
constructor(pdfDocumentName= 'None') {
this.pdfDocumentName = pdfDocumentName;
this.taxClient = null;
this.tariffsCache = [];
this.socrCache = [];
this.templatePath = '';
this.templateCSVPath = '';
this.templateSocrCSVPath = '';
this.pdfDoc = null;
this.customFonts = [];
this.pages = [];
this.pageWidth = [];
this.pageHeight = [];
}
getPdfDocumentName() {
return this.pdfDocumentName;
}
getTariffsCache() {
return this.tariffsCache;
}
getSocrCache() {
return this.socrCache;
}
getTemplatePath() {
return this.templatePath;
}
getTemplateCSVPath() {
return this.templateCSVPath;
}
getTemplateSocrCSVPath() {
return this.templateSocrCSVPath;
}
setTemplatePath(template) {
this.templatePath = template;
}
setTemplateCSVPath(template) {
this.templateCSVPath = template;
}
setTemplateSocrCSVPath(template) {
this.templateSocrCSVPath = template;
}
initTaxClient(taxtoken) {
this.taxClient = new Taxjar({
apiKey: taxtoken
});
}
clearTariffsCache() {
this.tariffsCache = [];
}
clearSocrCache() {
this.socrCache = [];
}
addTariffsCache(row) {
this.tariffsCache.push(row);
}
addSocrCache(row) {
this.socrCache.push(row);
}
loadTariffs() {
this.clearTariffsCache();
fs.createReadStream(this.getTemplateCSVPath())
.pipe(csv())
.on('data', (row) => this.addTariffsCache(row))
.on('end', () => console.log('Tariffs loaded'));
}
loadSocr() {
this.clearSocrCache();
fs.createReadStream(this.getTemplateSocrCSVPath())
.pipe(csv())
.on('data', (row) => this.addSocrCache(row))
.on('end', () => console.log('Socr loaded'));
}
generateFilename(type) {
// Генерируем UUID
const uniqueId = randomUUID();
// Форматируем имя файла
const filename = `${type}_${uniqueId}.pdf`;
return filename;
}
getListCustomerFonts() {
return this.customFonts;
}
countCustomerFonts() {
return this.customFonts.length;
}
getCustomerFontByIndex(i) {
if(this.countCustomerFonts() && this.customFonts[i] != undefined) {
return this.customFonts[i];
}
return null;
}
getListPages() {
return this.pages;
}
countPages() {
return this.pages.length;
}
getPageByIndex(i) {
if(this.countPages() && this.pages[i] != undefined) {
return this.pages[i];
}
return null;
}
getPageWidth(index) {
return this.pageWidth[index];
}
getPageHeight(index) {
return this.pageHeight[index];
}
setPageWidth(index,width) {
this.pageWidth[index] = width;
}
setPageHeight(index,height) {
this.pageHeight[index] = height;
}
getCreditCardPaymentDate(billingDate) {
let date = new Date(billingDate);
date.setDate(date.getDate() - 7);
if (date.getDay() === 0) {
date.setDate(date.getDate() - 1);
}
const day = date.getDate().toString().padStart(2, '0');
const month = date.toLocaleDateString('en-US', { month: 'short' });
return `${month} ${day}`;
}
pixelsToPoints(pixels, dpi = 72) {
return pixels * 72 / dpi;
}
convert72to300_X(x) {
let k = 300 / 72;
return x * k;
}
convert72to300_Y(y, setFontSize, lineHeightFactor, pageHeight) {
let k = 300 / 72;
return ((pageHeight * k) - (y * k)) - (setFontSize * lineHeightFactor);
}
async initPdfDoc() {
const pdfBytes = await fsPromises.readFile(this.getTemplatePath());
this.pdfDoc = await PDFDocument.load(pdfBytes);
this.pdfDoc.registerFontkit(fontkit);
}
async initBaseFonts() {
this.customFonts.push( await this.pdfDoc.embedFont(StandardFonts.Helvetica) );
}
async initCustomFonts(fontList) {
for(let ifont in fontList) {
this.customFonts.push( await this.initCustomFont( fontList[ifont] ) );
}
}
async initCustomFont(font) {
await fsPromises.access(font, fs.constants.R_OK)
.catch(err => {
throw new Error(`Not access to file ${font}: ${err.message}`);
});
const fontBytes = await fsPromises.readFile(font);
return await this.pdfDoc.embedFont(fontBytes);
}
async getTaxRateByAddress(address) {
try {
// Улучшенная логика парсинга
const addressParts = address.split(',').map(part => part.trim());
// Предполагаем, что последняя часть содержит Город, Штат и ZIP
// Например: "RALEIGH NC 27613" или "PHILADELPHIA, PA, 19104-5530"
const lastPart = addressParts[addressParts.length - 1];
// Ищем ZIP-код (либо 5 цифр, либо 5-4)
const zipCodeMatch = lastPart.match(/\d{5}(-\d{4})?/);
const zipCode = zipCodeMatch ? zipCodeMatch[0] : null;
// Ищем двухбуквенный код штата
const stateMatch = lastPart.match(/\b([A-Z]{2})\b/);
const state = stateMatch ? stateMatch[1] : null;
// Город - это то, что осталось в последней части до кода штата
let city = null;
if (state) {
city = lastPart.substring(0, lastPart.indexOf(state)).trim();
} else if (addressParts.length > 1) {
// Если штат не найден в последней части, берем предпоследнюю часть адреса как город
city = addressParts[addressParts.length - 2];
}
console.log({
originalAddress: address,
extractedCity: city,
extractedState: state,
extractedZipCode: zipCode
});
if (!zipCode) {
throw new Error('Incorrect or missing postcode in address');
}
// TaxJar достаточно умен, чтобы работать только с 5-значным ZIP,
// но передача города и штата помогает уточнить ставку.
const taxData = await this.taxClient.ratesForLocation(zipCode, {
city: city,
state: state
});
return {
zipCode,
combinedRate: taxData.rate.combined_rate, // Обратите внимание: в ответе API v2 это `taxData.rate.combined_rate`
stateRate: taxData.rate.state_rate,
cityRate: taxData.rate.city_rate,
countyRate: taxData.rate.county_rate,
details: taxData.rate // Возвращаем весь объект `rate`
};
} catch (error) {
// Пробрасываем ошибку дальше, но добавляем контекст
const errorMessage = `Error while getting tax rate: ${error.detail || error.message}`;
console.error(errorMessage);
throw new Error(errorMessage);
}
}
async checkAccessTemplate() {
await fsPromises.access(this.getTemplatePath(), fs.constants.R_OK)
.catch(err => {
throw new Error(`Not access to file ${this.getTemplatePath()}: ${err.message}`);
});
}
async initPages() {
this.pages = this.pdfDoc.getPages();
for (let whc in this.pages) {
let cropBoxL = this.pages[whc].getCropBox();
console.log('Page: ', whc, ' Crop box:', cropBoxL);
this.setPageWidth(whc,cropBoxL.width);
this.setPageHeight(whc,cropBoxL.height);
}
}
async alignTextToRight(ipage, ifont, setFontSize, setRightMargin, setText) {
if (typeof setFontSize !== 'number' || typeof setRightMargin !== 'number' || typeof setText !== 'string') {
throw new Error('Invalid input parameters');
}
const textWidth = this.getCustomerFontByIndex(ifont).widthOfTextAtSize(setText, setFontSize);
const pageWidth = this.pages[ipage].getWidth();
if (isNaN(textWidth) || isNaN(pageWidth)) {
throw new Error('Text width or page width calculation resulted in NaN');
}
const x = pageWidth - textWidth - setRightMargin;
return parseInt( !isNaN(x) && x >= 0 ? x : 0 );
}
// Этот метод должен быть в классе pdfFillData (pdfFillData.js)
/**
* Рисует строку со сложным форматированием, используя вложенные теги <style>.
* Поддерживаемые атрибуты: font, size, bg, color, padding.
* @param {object} item - Объект из map.json с описанием поля.
* @param {object} constants - Объект constants из map.json.
*/
async drawStyledText(item, constants) {
const page = this.getPageByIndex(item.ipage);
if (!page) return;
// --- 1. Получаем базовые параметры из item ---
const pageHeight = this.getPageHeight(item.ipage);
const x_ill_start = this._evaluate(item.x_ill, constants);
const y_ill_start = this._evaluate(item.y_ill, constants);
// Базовый размер шрифта из item.size, если в теге не указан другой
const baseSize = this._evaluate(item.size, constants);
// Базовый шрифт из item.ifont, если в теге не указан другой
const baseFontIndex = this._evaluate(item.ifont, constants) || 0;
// Базовый цвет текста (черный по умолчанию)
const baseColor = rgb(
this._evaluate(item.cR, constants) || 0,
this._evaluate(item.cG, constants) || 0,
this._evaluate(item.cB, constants) || 0
);
// --- 2. Подставляем значения переменных в шаблон ---
let populatedText = item.field;
const variableNames = [...populatedText.matchAll(/\b([a-zA-Z_][a-zA-Z0-9_]*)\b/g)].map(m => m[1]);
for(const varName of variableNames) {
if(this[varName] !== undefined && ['style', 'font', 'size', 'bg', 'color', 'padding'].indexOf(varName) === -1) {
populatedText = populatedText.replace(new RegExp(`\\b${varName}\\b`, 'g'), this[varName]);
}
}
// --- 3. Парсинг строки в "токены" (теги и текст) ---
const regex = /(<\/?style.*?>)|([^<]+)/g;
const tokens = [...populatedText.matchAll(regex)].map(m => m[0]);
// --- 4. Отрисовка с использованием стека стилей ---
let currentX = x_ill_start;
const styleStack = [{
fontIndex: baseFontIndex,
size: baseSize,
color: baseColor,
bgColor: null,
padding: 0
}];
for (const token of tokens) {
if (token.startsWith('<style')) { // Открывающий тег
const newStyle = { ...styleStack[styleStack.length - 1] }; // Наследуем предыдущий стиль
const attrRegex = /(\w+)=(['"])(.*?)\2/g;
let attrMatch;
while ((attrMatch = attrRegex.exec(token)) !== null) {
const [, key, , value] = attrMatch;
if (key === 'font') newStyle.fontIndex = parseInt(value, 10);
if (key === 'size') newStyle.size = parseFloat(value);
if (key === 'padding') newStyle.padding = parseFloat(value);
if (key === 'bg' || key === 'color') {
const [r_const, g_const, b_const] = value.split(',').map(c => c.trim());
const r = this._evaluate(r_const, constants);
const g = this._evaluate(g_const, constants);
const b = this._evaluate(b_const, constants);
if (![r, g, b].some(isNaN)) {
const colorObj = rgb(r, g, b);
if (key === 'bg') newStyle.bgColor = colorObj;
else newStyle.color = colorObj;
}
}
}
styleStack.push(newStyle);
} else if (token === '</style>') { // Закрывающий тег
if (styleStack.length > 1) styleStack.pop();
} else { // Это текстовый контент
const text = token;
const currentStyle = styleStack[styleStack.length - 1];
const font = this.getCustomerFontByIndex(currentStyle.fontIndex);
const fontSize = currentStyle.size;
if (!font || !text || isNaN(fontSize)) continue;
const textWidth = font.widthOfTextAtSize(text, fontSize);
const textHeight = font.heightAtSize(fontSize);
// Y-координата вычисляется на основе размера шрифта ТЕКУЩЕГО блока
const y_pdf = pageHeight - y_ill_start - (fontSize * this.baselineCorrectionFactor);
if (currentStyle.bgColor) {
const padding = currentStyle.padding || 1;
page.drawRectangle({
x: currentX - padding,
y: y_pdf - (textHeight * 0.25) - padding,
width: textWidth + (padding * 2),
height: textHeight + (padding * 2),
color: currentStyle.bgColor,
});
}
page.drawText(text, {
x: currentX,
y: y_pdf,
font: font,
size: fontSize,
color: currentStyle.color,
});
currentX += textWidth;
}
}
}
/**
* Универсальный SVG-парсер. Находит текстовые плейсхолдеры в SVG и заменяет их.
* @param {number} ipage - Индекс PDF-страницы.
* @param {CheerioAPI} $ - Распарсенный Cheerio объект с содержимым SVG.
* @param {object} replacements - Карта для замены вида { 'плейсхолдер': 'новое значение' }.
*/
async parseAndDrawSVG(ipage, $, replacements) {
const page = this.getPageByIndex(ipage);
const pageHeight = this.getPageHeight(ipage);
if (!page) return;
// Парсинг CSS теперь тоже часть этого универсального метода
const styleMap = {};
$('style').each((i, style) => {
const styleContent = $(style).text();
const cssRuleRegex = /\.([^\{]+)\{([^\}]+)\}/g;
let match;
while ((match = cssRuleRegex.exec(styleContent)) !== null) {
const className = match[1].trim();
const rules = match[2].trim();
styleMap[className] = {};
rules.split(';').forEach(rule => {
if (rule) {
const [key, value] = rule.split(':');
if (key && value) {
styleMap[className][key.trim()] = value.trim().replace(/['"]/g, '');
}
}
});
}
});
const textElements = $('text'); // Сначала получаем все элементы
for (const textElement of textElements) {
const textEl = $(textElement); // Оборачиваем в cheerio
const originalText = textEl.text().replace(/\s+/g, ' ').trim();
if (!originalText) continue; // continue вместо return
if (replacements.hasOwnProperty(originalText)) {
const newText = replacements[originalText];
const transformAttr = textEl.attr('transform');
const matrix = transformAttr ? transformAttr.match(/matrix\(([^)]+)\)/) : null;
if (!matrix) continue;
// ... (весь остальной код внутри if остается БЕЗ ИЗМЕНЕНИЙ) ...
const [scaleX, skewY, skewX, scaleY, x_svg, y_svg] = matrix[1].split(/[ ,]+/).map(parseFloat);
const styleSource = textEl.find('[class]').first().length ? textEl.find('[class]').first() : textEl;
const elClasses = styleSource.attr('class') ? styleSource.attr('class').split(' ') : [];
let effectiveStyle = {};
elClasses.forEach(cls => {
if (styleMap[cls]) {
Object.assign(effectiveStyle, styleMap[cls]);
}
});
const fontFamily = (effectiveStyle['font-family'] || 'Helvetica').replace(/['"]/g, '');
const fontSizeAttr = effectiveStyle['font-size'] || '10px';
const baseFontSize = parseFloat(fontSizeAttr.replace('px', ''));
const effectiveFontSize = baseFontSize * scaleY;
const ifont = this.fontMap && this.fontMap[fontFamily] !== undefined ? this.fontMap[fontFamily] : 0;
const font = this.getCustomerFontByIndex(ifont);
if (!font) continue;
let x_pdf = x_svg;
const align = textEl.attr('pge:align');
if (align) {
const originalWidth = font.widthOfTextAtSize(originalText, effectiveFontSize);
const newWidth = font.widthOfTextAtSize(newText, effectiveFontSize);
if (align === 'right') {
x_pdf = x_svg + originalWidth - newWidth;
} else if (align === 'center') {
x_pdf = x_svg + (originalWidth / 2) - (newWidth / 2);
}
}
const y_pdf = pageHeight - y_svg;
const fillColor = effectiveStyle['fill'] || '#000000';
let cR=0, cG=0, cB=0;
if (fillColor.startsWith('#')) {
cR = parseInt(fillColor.substring(1, 3), 16) / 255;
cG = parseInt(fillColor.substring(3, 5), 16) / 255;
cB = parseInt(fillColor.substring(5, 7), 16) / 255;
}
// Теперь await здесь полностью валиден
await pdfFillData.prototype.draw.call(this, ipage, newText, x_pdf, y_pdf, effectiveFontSize, ifont, cR, cG, cB);
}
}
}
_getFieldValue(item) { // <-- ПРИНИМАЕТ ВЕСЬ ОБЪЕКТ 'item'
// Проверка, что item и item.field существуют
if (!item || typeof item.field !== 'string') {
console.warn('Item in dataMap is missing a valid "field" property:', item);
return '';
}
const fieldKey = item.field;
const parts = fieldKey.split(',').map(p => p.trim());
const resolvedParts = parts.map(part => {
if (this[part] !== undefined && this[part] !== null) {
return this[part];
}
return part;
});
const separator = item.noSpace ? '' : ' ';
// Соединяем все разрешенные части выбранным разделителем
return resolvedParts.join(separator).trim();
}
_evaluate(expression, constants) {
if (typeof expression === 'number') return expression;
if (typeof expression !== 'string') return NaN;
const formula = expression.replace(/[a-zA-Z_][a-zA-Z0-9_]*/g, (match) => {
return constants[match] !== undefined ? constants[match] : '0';
});
try {
return new Function(`return ${formula}`)();
} catch (e) {
return NaN;
}
}
async generateDataMatrix(textToEncode, bcid, ipage, x_DM, y_DM, s_DMX, s_DMY, scale, rotate) {
const optimizedText = textToEncode.padStart(30, ' ');//.replace(/\s/g, '');
console.log('Optimized text:', optimizedText, 'Length:', optimizedText.length);
console.log('Chars:', optimizedText.split('').map(c => c.charCodeAt(0)));
const scaleM = scale;
let pngBuffer = await new Promise((resolve, reject) => {
bwipjs.toBuffer({
bcid: bcid, // datamatrixrectangular, datamatrix
text: optimizedText,
rotate: rotate,
scaleX: scaleM,
scaleY: scaleM,
width: s_DMX,
height: s_DMY,
includetext: false,
padding: 0,
options: "rectwidth=12 rectheight=26"
}, (err, png) => {
if (err) reject(err);
else resolve(png);
});
});
const pngImage_DM = await this.pdfDoc.embedPng(pngBuffer);
this.pages[ipage].drawImage(pngImage_DM, {
x: x_DM,
y: y_DM,
width: s_DMX*scaleM,
height: s_DMY*scaleM
});
}
async draw(ipage, field, x, y, size, ifont, cR, cG, cB, lineHeight, encoding) {
const options ={
x: x,
y: y,
size: size,
font: this.getCustomerFontByIndex(ifont),
color: rgb(cR, cG, cB),
encoding: encoding
}
// Если lineHeight передан и является числом, добавляем его в опции
if (typeof lineHeight === 'number' && lineHeight > 0) {
options.lineHeight = lineHeight;
}
this.pages[ipage].drawText(`${field || ''}`, options);
}
async drawTextWithBackground(ipage, text, x, y, size, ifont, textColor, bgColor, padding = 1) {
const page = this.pages[ipage];
const font = this.getCustomerFontByIndex(ifont);
if (!page || !font) return;
// 1. Вычисляем размеры текста
const textWidth = font.widthOfTextAtSize(text, size);
const textHeight = font.heightAtSize(size);
// 2. Рисуем фоновый прямоугольник
page.drawRectangle({
x: x - padding, // Начинаем чуть левее текста
y: y - (textHeight * 0.25) - padding, // Y-координата для прямоугольника.
// Корректируем, чтобы фон был и над, и под базовой линией.
width: textWidth + (padding * 2),
height: textHeight + (padding * 2),
color: bgColor,
});
// 3. Рисуем текст поверх прямоугольника
page.drawText(text, {
x: x,
y: y,
font: font,
size: size,
color: textColor,
});
}
async drawline(ipage, lineWidth, x1, y1, x2, y2, cR, cG, cB) {
this.pages[ipage].drawLine({
start: { x: x1, y: y1 },
end: { x: x2, y: y2 },
thickness: lineWidth,
color: rgb(cR, cG, cB),
});
}
// В файле pdfFillData.js
// ... (другие методы) ...
async saveData(host, outputDir, pdfType) {
const filename = this.generateFilename(pdfType); // generateFilename уже есть
const outputPath = path.join(outputDir, filename);
await fsPromises.mkdir(outputDir, { recursive: true })
.catch(err => {
if (err.code !== 'EEXIST') throw err;
});
const pdfBytesModified = await this.pdfDoc.save();
await fsPromises.writeFile(outputPath, pdfBytesModified);
// Возвращаем URL (если нужен где-то еще), локальный путь и имя файла
return {
'PDF_URL': outputPath.replace('/srv/pdfgen/public', `https://${host || ''}`),
'localPath': outputPath, // <--- Важно для отправки файла
'filename': filename // <--- Важно для Content-Disposition
};
}
// ... (остальной код класса) ...
}
module.exports = pdfFillData;

View File

@ -0,0 +1,582 @@
// pdfFillDataAEPOhio.js
const fs = require('fs');
const path = require('path');
const pdfFillData = require('./pdfFillData'); // Родительский класс
const { rgb, degrees } = require('pdf-lib');
const randomData = require('./randomDataGenerator');
class pdfFillDataAEPOhio extends pdfFillData {
constructor() {
super('AEPOhio');
this.usage_history_data = [];
this.usage_details_prev_year = 0; // e.g., Aug '17
this.usage_details_prev_month = 0; // e.g., Jul '18
this.usage_details_curr_month = 0; // e.g., Aug '18
this.cost_details_prev_year = 0; // e.g., Aug '17
this.cost_details_prev_month = 0; // e.g., Jul '18
this.cost_details_curr_month = 0; // e.g., Aug '18
this.temp_details_prev_year = 0; // e.g., Aug '17
this.temp_details_prev_month = 0; // e.g., Jul '18
this.temp_details_curr_month = 0; // e.g., Aug '18
// Коэффициент для коррекции Y для базовой линии.
// Настройте это значение (0.8 - 1.0) после первых тестов для лучшего выравнивания.
this.baselineCorrectionFactor = 0.85;
// --- Свойства, специфичные для счета AEP Ohio ---
// Заголовок
this.amount_due_date = '';
this.amount_due_value = '';
this.bill_mailing_date = '';
this.account_number = '';
// Адреса
this.service_address_line1 = '';
this.service_address_line2 = '';
this.customer_name = '';
this.attn_name = ''; // "ATTN: TIM SALTER" - опционально
// Сводка по счету (Current bill summary)
this.billing_period_from = '';
this.billing_period_to = '';
this.billing_period_days = ''; // e.g., (28 days)
// Круговая диаграмма
this.current_charges_kwh = '';
this.current_charges_value = ''; // $519.88
this.supplier_charges = '';
this.delivery_charge = '';
// Отрывной купон (Remittance Stub)
this.remit_customer_name = '';
this.remit_service_address = '';
this.remit_account_number = '';
this.remit_amount_due_date = '';
this.remit_amount_due_value = '';
this.remit_pay_after_date_amount = '';
this.barcode_number = '';
// Данные для других страниц (если нужно будет детализировать)
// Пример
this.previous_balance_due = '';
this.payment_thank_you = ''; // "-782.05"
this.total_amount_due_last_billing = '';
this.payment_thank_you_date = '';
this.payment_thank_you_amount = '';
this.late_payment_charge = '';
this.previous_balance_due = '';
// Блок "Current AEP Ohio Charges" (Детализация для Delivery Charge)
this.tariff_info_line = ''; // "Tariff 840 - Medium General Service 08/13/18"
this.service_delivery_identifier = '';
this.transmission_service = '';
this.distribution_service = '';
this.customer_charge_detail = ''; // Переименовали, чтобы не конфликтовать со свойством из другого счета
this.retail_stability_rider = '';
this.deferred_asset_rider = '';
this.phase_in_recovery_rider = '';
this.power_purchase_rider = '';
this.current_electric_charges_total = ''; // Итог по AEP Ohio Charges ($321.36)
this.dataMap = [];
// --- СВОЙСТВА ДЛЯ DEPOSIT RECEIPT ---
this.area_office = '';
this.deposit_number = '';
this.deposit_date = '';
this.deposit_amount = '';
this.cashier_number = '';
// --- Свойства для итогов потребления ---
this.total_usage_12_months = ''; // e.g., 71,973 kWh
this.avg_monthly_usage = ''; // e.g., 5,998 kWh
// --- Свойства для Billed Usage ---
this.billed_usage_date = ''; // e.g., 08/18
this.billed_usage_kwh_usage = '';
this.billed_usage_kwh_billed = '';
this.billed_usage_kw_usage = '';
this.billed_usage_kw_billed = '';
// --- Свойства для Meter Read Details ---
this.meter_read_details_meter_no = '';
// Строка 1 (kWh)
this.meter_read_kwh_previous = '';
this.meter_read_kwh_current = '';
this.meter_read_kwh_metered = '';
this.meter_read_kwh_usage = '';
// Строка 2 (kW)
this.meter_read_kw_current = '';
this.meter_read_kw_metered = '';
this.meter_read_kw_usage = '';
this.meter_read_service_period = '';
this.next_read_date_info = '';
}
// Вспомогательная функция для очистки сумм от знака '$'
_cleanAmount(amount) {
if (typeof amount !== 'string') return 0;
return parseFloat(amount.replace('$', '').trim()) || 0;
}
async setFields(fields) {
if (!fields || Object.keys(fields).length === 0) {
fields = randomData.aep_ohio(); // <-- Замените на entergy(), aep_ohio() и т.д.
}
this.usage_history_data = fields.usage_history_data || [];
this.usage_details_prev_year = this._cleanAmount(fields.usage_details_prev_year);
this.usage_details_prev_month = this._cleanAmount(fields.usage_details_prev_month);
this.usage_details_curr_month = this._cleanAmount(fields.usage_details_curr_month);
this.cost_details_prev_year = this._cleanAmount(fields.cost_details_prev_year);
this.cost_details_prev_month = this._cleanAmount(fields.cost_details_prev_month);
this.cost_details_curr_month = this._cleanAmount(fields.cost_details_curr_month);
this.temp_details_prev_year = this._cleanAmount(fields.temp_details_prev_year);
this.temp_details_prev_month = this._cleanAmount(fields.temp_details_prev_month);
this.temp_details_curr_month = this._cleanAmount(fields.temp_details_curr_month);
this.amount_due_date = fields.amount_due_date;
this.bill_mailing_date = fields.bill_mailing_date;
this.account_number = fields.account_number;
const fullServiceAddress = fields.service_address || ''; // Берем полную строку из POST-запроса
const firstCommaIndex = fullServiceAddress.indexOf(',');
this.service_address_full = fullServiceAddress;
if (firstCommaIndex !== -1) {
// Если запятая найдена
this.service_address_line1 = fullServiceAddress.substring(0, firstCommaIndex).trim();
this.service_address_line2 = fullServiceAddress.substring(firstCommaIndex + 1).trim();
} else {
// Если запятой нет, вся строка идет в первую линию, вторая пустая
this.service_address_line1 = fullServiceAddress;
this.service_address_line2 = '';
}
this.customer_name = fields.customer_name;
this.attn_name = fields.attn_name || '';
this.billing_period_from = fields.billing_period_from;
this.billing_period_to = fields.billing_period_to;
this.billing_period_days = fields.billing_period_days;
this.barcode_number = fields.barcode_number;
this.current_charges_kwh = fields.current_charges_kwh;
this.remit_pay_after_date_amount = fields.remit_pay_after_date_amount;
// --- НОВЫЕ ПОЛЯ ИЗ ДЕТАЛИЗАЦИИ ---
this.total_amount_due_last_billing = fields.total_amount_due_last_billing;
this.payment_thank_you_date = fields.payment_thank_you_date;
this.payment_thank_you_amount = fields.payment_thank_you_amount;
this.late_payment_charge = fields.late_payment_charge;
this.previous_balance_due = fields.previous_balance_due;
this.tariff_info_line = fields.tariff_info_line;
this.service_delivery_identifier = fields.service_delivery_identifier;
this.transmission_service = fields.transmission_service;
this.distribution_service = fields.distribution_service;
this.customer_charge_detail = fields.customer_charge_detail;
this.retail_stability_rider = fields.retail_stability_rider;
this.deferred_asset_rider = fields.deferred_asset_rider;
this.phase_in_recovery_rider = fields.phase_in_recovery_rider;
this.power_purchase_rider = fields.power_purchase_rider;
// --- РАСЧЕТЫ НА ОСНОВЕ НОВЫХ ПОЛЕЙ ---
// Delivery Charge = сумма всех детальных сборов
const deliveryChargeNum = this._cleanAmount(this.transmission_service) +
this._cleanAmount(this.distribution_service) +
this._cleanAmount(this.customer_charge_detail) +
this._cleanAmount(this.retail_stability_rider) +
this._cleanAmount(this.deferred_asset_rider) +
this._cleanAmount(this.phase_in_recovery_rider) +
this._cleanAmount(this.power_purchase_rider);
this.delivery_charge = `${deliveryChargeNum.toFixed(2)}`;
this.current_electric_charges_total = `${deliveryChargeNum.toFixed(2)}`;
// Supplier Charges берем напрямую из формы
const supplierChargesNum = this._cleanAmount(fields.supplier_charges);
this.supplier_charges = `${supplierChargesNum.toFixed(2)}`;
// Current Charges = Delivery + Supplier
const currentChargesNum = deliveryChargeNum + supplierChargesNum;
this.current_charges_value = `${currentChargesNum.toFixed(2)}`;
const previousBalanceNum = this._cleanAmount(this.previous_balance_due);
const totalAmountDueNum = currentChargesNum + previousBalanceNum;
this.amount_due_value = `${totalAmountDueNum.toFixed(2)}`
// Заполняем поля для отрывного купона
this.remit_customer_name = this.customer_name;
this.remit_service_address = this.service_address;
this.remit_account_number = this.account_number;
this.remit_amount_due_date = this.amount_due_date;
this.remit_amount_due_value = this.amount_due_value;
// --- ПРИНИМАЕМ ДАННЫЕ ДЛЯ DEPOSIT RECEIPT --
this.area_office = fields.area_office;
this.deposit_number = fields.deposit_number;
this.deposit_date = fields.deposit_date;
this.deposit_amount = fields.deposit_amount;
this.cashier_number = fields.cashier_number;
// --- Принимаем данные для таблиц детализации ---
this.billed_usage_date = fields.billed_usage_date;
this.billed_usage_kwh_usage = fields.billed_usage_kwh_usage;
this.billed_usage_kwh_billed = fields.billed_usage_kwh_billed;
this.billed_usage_kw_usage = fields.billed_usage_kw_usage;
this.billed_usage_kw_billed = fields.billed_usage_kw_billed;
this.meter_read_details_meter_no = fields.meter_read_details_meter_no;
this.meter_read_kwh_previous = fields.meter_read_kwh_previous;
this.meter_read_kwh_current = fields.meter_read_kwh_current;
this.meter_read_kwh_metered = fields.meter_read_kwh_metered;
this.meter_read_kwh_usage = fields.meter_read_kwh_usage;
this.meter_read_kw_current = fields.meter_read_kw_current;
this.meter_read_kw_metered = fields.meter_read_kw_metered;
this.meter_read_kw_usage = fields.meter_read_kw_usage;
this.meter_read_service_period = fields.meter_read_service_period;
this.next_read_date_info = fields.next_read_date_info;
this.usage_history_data = fields.usage_history_data || [];
// --- РАССЧЕТ ИТОГОВ ПОТРЕБЛЕНИЯ ---
if (this.usage_history_data && this.usage_history_data.length > 0) {
// Суммируем все значения kWh из массива
const totalKwh = this.usage_history_data.reduce((sum, item) => sum + (item.kwh || 0), 0);
// Рассчитываем среднее
const avgKwh = totalKwh / this.usage_history_data.length;
// Форматируем для отображения
this.total_usage_12_months = `${totalKwh.toLocaleString('en-US')} kWh`;
this.avg_monthly_usage = `${Math.round(avgKwh).toLocaleString('en-US')} kWh`;
} else {
// Если данных нет, оставляем поля пустыми
this.total_usage_12_months = '';
this.avg_monthly_usage = '';
}
}
_describeArc(x, y, radius, startAngle, endAngle) {
const startRad = (startAngle - 90) * Math.PI / 180;
const endRad = (endAngle - 90) * Math.PI / 180;
const start = {
x: x + (radius * Math.cos(startRad)),
y: y + (radius * Math.sin(startRad))
};
const end = {
x: x + (radius * Math.cos(endRad)),
y: y + (radius * Math.sin(endRad))
};
const largeArcFlag = endAngle - startAngle <= 180 ? "0" : "1";
const d = [
"M", start.x, start.y,
"A", radius, radius, 0, largeArcFlag, 1, end.x, end.y
].join(" ");
return d;
}
async _drawDetailChart(page, pageHeight, options) {
// Импортируем degrees из pdf-lib в начале файла, если его еще нет
// const { degrees } = require('pdf-lib');
// Или, если он уже импортирован в родительском классе, можно использовать this.degrees() (если бы такой метод был)
// Проще всего добавить импорт в этот файл.
// const { degrees } = require('pdf-lib'); // Добавьте это в начало файла, после других require.
const { x_ill, y_ill, width, height, values, labels, colors, valueFormatter } = options;
const maxValue = Math.max(...values);
if (maxValue === 0) return;
const numBars = values.length;
const barWidth = (width / numBars) * 0.9;
const barMargin = (width / numBars) * 0.3;
const stepX = barWidth + barMargin;
values.forEach((value, index) => {
const barHeight = (value / maxValue) * height;
const barX = x_ill + (index * stepX);
const baseY = pageHeight - y_ill - height;
// Рисуем столбец
page.drawRectangle({
x: barX,
y: baseY,
width: barWidth,
height: barHeight,
color: colors[index] || rgb(0.8, 0.8, 0.8),
});
const textFont = this.getCustomerFontByIndex(0);
const textColor = rgb(0.3, 0.3, 0.3);
// --- Рисуем подпись под столбцом (месяц) ---
const labelText = labels[index] || '';
const labelY = baseY - 10;
page.drawText(labelText, {
x: barX + barWidth / 2 - textFont.widthOfTextAtSize(labelText, 6) / 2,
y: labelY, font: textFont, size: 6, color: textColor
});
// --- НОВЫЙ БЛОК: Рисуем значение над столбцом (с наклоном) ---
// Используем функцию форматирования, если она передана
const valueText = valueFormatter ? valueFormatter(value) : String(value);
const valueTextWidth = textFont.widthOfTextAtSize(valueText, 6);
page.drawText(valueText, {
// Начальная точка для текста - центр верхней грани столбца
x: barX + (barWidth / 2),
y: baseY + barHeight + 2, // Чуть выше столбца
font: textFont,
size: 6,
color: textColor,
// Поворачиваем текст на -45 градусов относительно его точки (x,y)
rotate: degrees(45)
});
});
}
async draw() {
// =======================================================================
// --- ФИНАЛЬНАЯ ВЕРСИЯ ДЛЯ ОТРИСОВКИ ОДНОГО СЕКТОРА (с защитой) ---
// =======================================================================
const page = this.pages[0];
const pageHeight = page.getHeight();
// 1. Ваши точные параметры
const centerX_ill = 152.692;
const centerY_ill = 378.686;
const radius_ill = 141.5 / 2; // 71
const thickness = 42;
// 2. Пересчет координат
const centerX_pdf = centerX_ill;
const centerY_pdf = pageHeight - centerY_ill;
const centerY_for_svg = pageHeight - centerY_pdf;
// 3. Получение значений
const supplierValue = this._cleanAmount(this.supplier_charges);
const deliveryValue = this._cleanAmount(this.delivery_charge);
const totalValue = supplierValue + deliveryValue;
// 4. --- ГЛАВНАЯ ПРОВЕРКА ---
// Выполняем отрисовку только если есть что делить и рисовать.
if (totalValue > 0) {
const supplierAngle = (supplierValue / totalValue) * 360;
// 5. Проверяем, что угол является конечным числом (не NaN, не Infinity)
if (isFinite(supplierAngle)) {
const startAngle = 0;
const endAngle = supplierAngle;
const supplierArcPath = this._describeArc(centerX_pdf, centerY_for_svg, radius_ill, startAngle, endAngle);
// 6. Дополнительная проверка, что в сгенерированном пути нет NaN
if (!supplierArcPath.includes('NaN')) {
page.drawSvgPath(supplierArcPath, {
y: pageHeight,
borderColor: rgb(101/255, 159/255, 217/255),
borderWidth: thickness,
});
} else {
console.error("Generated SVG path contains NaN. Skipping drawSvgPath. Arc Path:", supplierArcPath);
}
} else {
console.error(`Calculated angle is not a finite number: ${supplierAngle}. Skipping drawSvgPath.`);
}
}
// =======================================================================
// --- КОНЕЦ БЛОКА ДИАГРАММЫ ---
// =======================================================================
// =======================================================================
// --- РИСУЕМ СТОЛБЧАТУЮ ДИАГРАММУ "USAGE HISTORY" (ИСПРАВЛЕННАЯ ВЕРСИЯ) ---
// =======================================================================
{ // Создаем новую область видимости, чтобы избежать конфликта 'const page'
const page = this.pages[0]; // Предполагаем, что диаграмма на первой странице
const pageHeight = page.getHeight(); // 792
// 1. Параметры области графика (измерьте в Illustrator)
// ВАЖНО: y_ill - это ВЕРХНИЙ край области для столбцов
const chartArea_ill = {
x: 319, // Левый край области для столбцов (из Illustrator)
y_ill: 225, // ВЕРХНИЙ край области для столбцов (из Illustrator)
width: 273, // Общая ширина области для всех столбцов
height: 56 // Максимальная высота самого высокого столбца
};
const usageData = this.usage_history_data;
if (usageData && usageData.length > 0) {
const kwhValues = usageData.map(d => d.kwh);
const maxKwh = Math.max(...kwhValues);
const barWidth = (chartArea_ill.width / usageData.length) * 0.7;
const barMargin = (chartArea_ill.width / usageData.length) * 0.3;
const stepX = barWidth + barMargin;
const defaultBarColor = rgb(222/255, 224/255, 227/255);
const lastBarColor = rgb(172/255, 217/255, 244/255);
usageData.forEach((dataPoint, index) => {
const barHeight_pdf = (dataPoint.kwh / maxKwh) * chartArea_ill.height;
const barX_pdf = chartArea_ill.x + (index * stepX);
const baseY_pdf = pageHeight - chartArea_ill.y_ill - chartArea_ill.height;
const color = (index === usageData.length - 1) ? lastBarColor : defaultBarColor;
page.drawRectangle({
x: barX_pdf, y: baseY_pdf, width: barWidth, height: barHeight_pdf, color: color,
});
const textFont = this.getCustomerFontByIndex(0);
// --- Рисуем подпись месяца (без наклона) ---
const monthText = dataPoint.month;
page.drawText(monthText, {
x: barX_pdf + (barWidth / 2) - (textFont.widthOfTextAtSize(monthText, 7) / 2),
y: baseY_pdf - 12,
font: textFont, size: 7, color: rgb(0,0,0)
});
// --- ИЗМЕНЕНО: Рисуем значение kWh над столбцом (с наклоном) ---
const valueText = dataPoint.kwh.toLocaleString('en-US'); // Форматируем с запятой
page.drawText(valueText, {
x: barX_pdf + (barWidth / 2), // Точка привязки - центр верха столбца
y: baseY_pdf + barHeight_pdf + 3, // Чуть выше столбца
font: textFont,
size: 6,
color: rgb(0.3, 0.3, 0.3),
rotate: degrees(45) // Применяем наклон
});
});
// ... (код для отрисовки годов остается без изменений) ...
}
}
// =======================================================================
// --- КОНЕЦ БЛОКА СТОЛБЧАТОЙ ДИАГРАММЫ ---
// =======================================================================
// =======================================================================
// --- РИСУЕМ МАЛЕНЬКИЕ ГРАФИКИ НА СТРАНИЦЕ 3 ---
// =======================================================================
{
const page = this.pages[2];
if (page) {
const pageHeight = page.getHeight();
const grayColor = rgb(222/255, 224/255, 227/255);
const blueColor = rgb(172/255, 217/255, 244/255);
const delta = 10;
const delta_y = 3;
await this._drawDetailChart(page, pageHeight, {
x_ill: 326 + delta, y_ill: 100 + delta_y, width: 60, height: 40, // <-- Замените на реальные измерения
values: [this.usage_details_prev_year, this.usage_details_prev_month, this.usage_details_curr_month],
labels: ["Aug '17", "Jul '18", "Aug '18"], // Эти метки можно тоже передавать
colors: [grayColor, grayColor, blueColor],
valueFormatter: (val) => val.toLocaleString('en-US') // Добавляет запятые-разделители
});
// --- График 2: Avg. Daily Cost ---
await this._drawDetailChart(page, pageHeight, {
x_ill: 416 + delta, y_ill: 100 + delta_y, width: 60, height: 40, // <-- Замените на реальные измерения
values: [this.cost_details_prev_year, this.cost_details_prev_month, this.cost_details_curr_month],
labels: ["Aug '17", "Jul '18", "Aug '18"],
colors: [grayColor, grayColor, blueColor],
valueFormatter: (val) => `$${val.toFixed(2)}`
});
// --- График 3: Avg. Temperature ---
await this._drawDetailChart(page, pageHeight, {
x_ill: 506 + delta, y_ill: 100 + delta_y, width: 60, height: 40, // <-- Замените на реальные измерения
values: [this.temp_details_prev_year, this.temp_details_prev_month, this.temp_details_curr_month],
labels: ["Aug '17", "Jul '18", "Aug '18"],
colors: [grayColor, grayColor, blueColor], // Последний столбец синий
valueFormatter: (val) => `${val}°F`
});
}
}
// =======================================================================
const mapFilePath = path.join(__dirname, 'AEPOhio.map.json');
const configJson = fs.readFileSync(mapFilePath, 'utf8');
const config = JSON.parse(configJson);
const { constants, map: dataMap } = config; // Разделяем на константы и карту
// --- Обновленный цикл отрисовки ---
for (const item of dataMap) {
const page = this.getPageByIndex(item.ipage);
const font = this.getCustomerFontByIndex(item.ifont);
if (!page || !font) {
console.warn(`Skipping draw: Missing page or font for item:`, item);
continue;
}
const pageHeight = this.getPageHeight(item.ipage);
const fieldText = `${this._getFieldValue(item) || ''}`;
const x_ill = this._evaluate(item.x, constants);
const y_ill = this._evaluate(item.y, constants);
const size = this._evaluate(item.size, constants);
if (isNaN(x_ill) || isNaN(y_ill) || isNaN(size)) {
console.warn('Could not evaluate coordinates/size for item:', item);
continue;
}
let x_pdf;
const y_pdf = pageHeight - y_ill - (size * this.baselineCorrectionFactor);
if (item.align === 'right') {
const pageWidth = this.getPageWidth(item.ipage);
const rightMargin_pdf = pageWidth - x_ill;
x_pdf = await this.alignTextToRight(item.ipage, item.ifont, size, rightMargin_pdf, fieldText);
} else if (item.align === 'center') {
// НОВЫЙ БЛОК: ВЫРАВНИВАНИЕ ПО ЦЕНТРУ
const textWidth = font.widthOfTextAtSize(fieldText, size);
x_pdf = x_ill - (textWidth / 2);
}else {
x_pdf = x_ill;
}
// ЯВНОЕ ОПРЕДЕЛЕНИЕ ЦВЕТА
// Преобразуем cR, cG, cB в числа. Если они не определены или не являются числами, используем 0.
const r = parseFloat(item.cR) || 0;
const g = parseFloat(item.cG) || 0;
const b = parseFloat(item.cB) || 0;
// Вызываем draw, передавая ОТДЕЛЬНЫЕ компоненты цвета R, G, B
await super.draw(
item.ipage,
fieldText,
x_pdf,
y_pdf,
size,
item.ifont,
r, // <-- ПЕРЕДАЕМ число r
g, // <-- ПЕРЕДАЕМ число g
b // <-- ПЕРЕДАЕМ число b
);
}
}
async saveData(host, outputDir) {
return await super.saveData(host, outputDir, this.getPdfDocumentName());
}
}
module.exports = pdfFillDataAEPOhio;

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,193 @@
// pdfFillDataDukeEnergy.js
const pdfFillData = require('./pdfFillData'); // Родительский класс
class pdfFillDataDukeEnergy extends pdfFillData {
constructor() {
super('DukeEnergy');
// Коэффициент для коррекции Y для базовой линии.
// Настройте это значение (0.8 - 1.0) после первых тестов для лучшего выравнивания.
this.baselineCorrectionFactor = 0.85;
// Инициализируем свойства пустыми значениями.
// Они будут заполнены в setFields из POST-запроса.
this.bill_date = '';
this.service_period_from = '';
this.service_period_to = '';
this.account_number = '';
this.customer_name = '';
this.service_address_line1 = '';
this.service_address_line2 = '';
this.previous_amount_due = '';
this.payment_received_date = '';
this.payment_received_amount = '';
this.current_electric_charges = '';
this.current_lighting_charges = '';
this.taxes = ''; // Будет рассчитано
this.total_amount_due_date = '';
this.total_amount_due_value = ''; // Будет рассчитано
this.electric_usage_current_month_kwh = '';
this.electric_usage_jul_2021_kwh = '';
this.electric_usage_12_month_avg_kwh = '';
this.remit_account_number = '';
this.remit_amount_due_value = '';
this.remit_due_date = '';
this.remit_customer_name = '';
this.remit_address_line1 = '';
this.remit_address_line2 = '';
this.barcode_number = '';
this.meter_number = '';
this.energy_used_kwh = '';
this.basic_customer_charge = '';
this.energy_charge_details = '';
this.energy_charge_amount = '';
this.total_taxes_detail = '';
this.total_taxes_amount = '';
this.dataMap = [];
}
// Вспомогательная функция для очистки сумм от знака '$'
_cleanAmount(amount) {
if (typeof amount !== 'string') return 0;
return parseFloat(amount.replace('$', '').trim()) || 0;
}
async setFields(fields) {
// 1. Заполняем свойства класса из объекта fields (данные из POST-запроса)
this.bill_date = fields.bill_date;
this.service_period_from = fields.service_period_from;
this.service_period_to = fields.service_period_to;
this.account_number = fields.account_number;
this.customer_name = fields.customer_name;
this.service_address_line1 = fields.service_address_line1;
this.service_address_line2 = fields.service_address_line2;
this.payment_received_date = fields.payment_received_date;
this.total_amount_due_date = fields.total_amount_due_date;
this.electric_usage_current_month_kwh = fields.electric_usage_current_month_kwh;
this.electric_usage_jul_2021_kwh = fields.electric_usage_jul_2021_kwh;
this.electric_usage_12_month_avg_kwh = fields.electric_usage_12_month_avg_kwh;
this.remit_address_line2 = fields.remit_address_line2;
this.barcode_number = fields.barcode_number;
this.meter_number = fields.meter_number;
this.energy_used_kwh = fields.energy_used_kwh;
this.basic_customer_charge = fields.basic_customer_charge;
this.energy_charge_details = fields.energy_charge_details;
this.energy_charge_amount = fields.energy_charge_amount;
this.total_taxes_detail = fields.total_taxes_detail;
// Преобразуем числовые строки в числа для расчетов
const prevAmountNum = this._cleanAmount(fields.previous_amount_due);
const paymentReceivedNum = this._cleanAmount(fields.payment_received_amount); // Ожидается отрицательное число
const electricChargesNum = this._cleanAmount(fields.current_electric_charges);
const lightingChargesNum = this._cleanAmount(fields.current_lighting_charges);
// Присваиваем очищенные строки для отображения
this.previous_amount_due = `$${prevAmountNum.toFixed(2)}`;
this.payment_received_amount = `${paymentReceivedNum.toFixed(2)}`;
this.current_electric_charges = `${electricChargesNum.toFixed(2)}`;
this.current_lighting_charges = `${lightingChargesNum.toFixed(2)}`;
// 2. Рассчитываем налогооблагаемую базу
const taxableAmount = electricChargesNum + lightingChargesNum;
// 3. Формируем полный адрес и получаем налоговые ставки от TaxJar
const fullAddress = `${this.service_address_line1}, ${this.service_address_line2}`;
let taxValue = 0;
try {
const taxInfo = await this.getTaxRateByAddress(fullAddress);
if (taxInfo && taxInfo.combinedRate) {
taxValue = taxableAmount * taxInfo.combinedRate;
} else {
console.warn("Could not retrieve valid tax rate from TaxJar. Tax will be 0.");
}
} catch (error) {
console.error("Failed to calculate taxes via TaxJar. Tax will be 0. Error:", error.message);
// Оставляем taxValue = 0
}
this.taxes = `${taxValue.toFixed(2)}`;
this.total_taxes_amount = `$${taxValue.toFixed(2)}`;
// 4. Рассчитываем итоговую сумму
const totalDueNum = prevAmountNum + paymentReceivedNum + electricChargesNum + lightingChargesNum + taxValue;
this.total_amount_due_value = `$${totalDueNum.toFixed(2)}`;
// 5. Заполняем поля для отрывного купона
this.remit_account_number = this.account_number;
this.remit_amount_due_value = this.total_amount_due_value;
this.remit_due_date = `by ${this.total_amount_due_date}`;
this.remit_customer_name = this.customer_name;
this.remit_address_line1 = this.service_address_line1;
}
async draw() {
// ВАЖНО: Замените значения x_ill, y_ill, size, ifont на точные,
// измеренные в Adobe Illustrator (опорная точка - верхний левый угол блока).
this.dataMap = [
// --- PAGE 1 ---
{ ipage: 0, field: `Page 1 of 3`, x_ill: 525.5, y_ill: 28.7, size: 9, ifont: 0, cR: 0, cG: 0, cB: 0 },
{ ipage: 0, field: this.customer_name, x_ill: 345.2, y_ill: 78.9, size: 10, ifont: 0, cR: 0, cG: 0, cB: 0 },
{ ipage: 0, field: this.service_address_line1, x_ill: 345.2, y_ill: 91.5, size: 10, ifont: 0, cR: 0, cG: 0, cB: 0 },
{ ipage: 0, field: this.service_address_line2, x_ill: 345.2, y_ill: 104.1, size: 10, ifont: 0, cR: 0, cG: 0, cB: 0 },
{ ipage: 0, field: this.bill_date, x_ill: 470.0, y_ill: 78.9, size: 10, ifont: 0, cR: 0, cG: 0, cB: 0 },
{ ipage: 0, field: `${this.service_period_from} - ${this.service_period_to}`, x_ill: 470.0, y_ill: 91.5, size: 10, ifont: 0, cR: 0, cG: 0, cB: 0 },
{ ipage: 0, field: this.account_number, x_ill: 443.52, y_ill: 110.84, size: 10, ifont: 1, cR: 0, cG: 0, cB: 0 }, // Пример с ifont: 1 (жирный)
{ ipage: 0, field: this.previous_amount_due, x_ill: 165.0, y_ill: 172.5, size: 10, ifont: 0, cR: 0, cG: 0, cB: 0 },
{ ipage: 0, field: `Payment Received ${this.payment_received_date}`, x_ill: 58.0, y_ill: 187.5, size: 10, ifont: 0, cR: 0, cG: 0, cB: 0 },
{ ipage: 0, field: this.payment_received_amount, x_ill: 165.0, y_ill: 187.5, size: 10, ifont: 0, cR: 0, cG: 0, cB: 0 },
{ ipage: 0, field: this.current_electric_charges, x_ill: 165.0, y_ill: 202.5, size: 10, ifont: 0, cR: 0, cG: 0, cB: 0 },
{ ipage: 0, field: this.current_lighting_charges, x_ill: 165.0, y_ill: 217.5, size: 10, ifont: 0, cR: 0, cG: 0, cB: 0 },
{ ipage: 0, field: this.taxes, x_ill: 165.0, y_ill: 232.5, size: 10, ifont: 0, cR: 0, cG: 0, cB: 0 },
{ ipage: 0, field: `Total Amount Due ${this.total_amount_due_date}`, x_ill: 58.0, y_ill: 248.0, size: 10, ifont: 1, cR: 0, cG: 0, cB: 0 },
{ ipage: 0, field: this.total_amount_due_value, x_ill: 165.0, y_ill: 247.0, size: 12, ifont: 1, cR: 0, cG: 0, cB: 0 },
{ ipage: 0, field: this.remit_account_number, x_ill: 440, y_ill: 648, size: 10, ifont: 0, cR: 0, cG: 0, cB: 0 },
{ ipage: 0, field: this.remit_amount_due_value, x_ill: 440, y_ill: 658, size: 12, ifont: 1, cR: 0, cG: 0, cB: 0 },
// --- PAGE 3 --- (Примеры, нуждаются в измерении)
{ ipage: 2, field: `Page 3 of 3`, x_ill: 525.5, y_ill: 28.7, size: 9, ifont: 0, cR: 0, cG: 0, cB: 0 },
{ ipage: 2, field: this.basic_customer_charge, x_ill: 500, y_ill: 240, size: 9, ifont: 0, cR: 0, cG: 0, cB: 0 },
{ ipage: 2, field: this.total_taxes_amount, x_ill: 500, y_ill: 590, size: 9, ifont: 0, cR: 0, cG: 0, cB: 0 },
];
// Цикл отрисовки, который автоматически пересчитывает координаты
for (const item of this.dataMap) {
const page = this.getPageByIndex(item.ipage);
if (!page || !this.getCustomerFontByIndex(item.ifont)) {
console.warn(`Skipping draw for item due to missing page or font:`, item);
continue;
}
const pageHeight = this.getPageHeight(item.ipage);
if (pageHeight === undefined) {
console.warn(`Could not get page height for page ${item.ipage}. Skipping item:`, item);
continue;
}
const x_pdf = item.x_ill;
const y_pdf = pageHeight - item.y_ill - (item.size * this.baselineCorrectionFactor);
await super.draw(
item.ipage,
item.field,
x_pdf,
y_pdf,
item.size,
item.ifont,
item.cR,
item.cG,
item.cB
);
}
}
async saveData(host, outputDir) {
return await super.saveData(host, outputDir, this.getPdfDocumentName());
}
}
module.exports = pdfFillDataDukeEnergy;

View File

@ -0,0 +1,582 @@
// pdfFillDataEntergy.js
const fs = require('fs');
const path = require('path');
const pdfFillData = require('./pdfFillData');
const { rgb, degrees } = require('pdf-lib');
const randomData = require('./randomDataGenerator');
class pdfFillDataEntergy extends pdfFillData {
constructor() {
super('Entergy');
this.baselineCorrectionFactor = 0.85;
// Инициализация всех свойств, которые будут использоваться
this.customer_name = '';
this.mail_date = '';
this.account_number = '';
this.invoice_number = '';
this.amount_due_by_date = '';
this.amount_due_after_date = '';
this.amount_due_after_value = '';
this.service_location_line1 = '';
this.service_location_line2 = '';
this.billing_days = '';
this.kwh_used = '';
this.kwh_used_formatted = '';
this.avg_kwh_per_day = '';
this.remit_customer_address = '';
this.barcode_number = '';
this.usage_history_data = [];
// Расчетные свойства для диаграмм и итогов
this.energy_charges_total = 0;
this.fuel_charges_total = 0;
this.other_charges_total = 0;
this.amount_due_by_value = 0;
// Свойства для купона
this.remit_account_number = '';
this.remit_invoice_number = '';
this.remit_amount_due_by_date = '';
this.remit_amount_due_after_date = '';
this.remit_amount_due_by_value = '';
this.energy_charge = '';
this.federal_eac_rider = '';
this.fuel_adjustment = '';
this.storm_restoration_offset = '';
this.storm_restoration_charge = '';
this.deposit = '';
this.connect_fee = '';
this.current_month_energy_charges = 0;
// Расчетные поля для детализации kWh
this.federal_eac_rider_detail = '';
this.fuel_adjustment_detail = '';
// --- НОВЫЕ СВОЙСТВА ДЛЯ METER READING ---
this.meter_reading_contract = '';
this.meter_number = '';
this.meter_rate = '';
this.current_meter_reading_datetime = '';
this.current_meter_reading_value = '';
this.previous_meter_reading_datetime = '';
this.previous_meter_reading_value = '';
// --- НОВЫЕ СВОЙСТВА ДЛЯ ГОТОВЫХ СТРОК ---
this.meter_reading_header = '';
this.meter_number_header = '';
this.rate_header = '';
this.current_reading_text = '';
this.previous_reading_text = '';
this.chart_legend_year_current = ''; // e.g., "2023"
this.chart_legend_year_previous = ''; // e.g., "2022"
this.billing_period_formatted = '';
this.qpc_code = '';
this.cycle_number = '';
}
/**
* Вспомогательная функция для очистки денежных строк от '$' и запятых
* @param {string} amount - Строка с суммой
* @returns {number} - Числовое значение
*/
_cleanAmount(amount) {
if (typeof amount !== 'string') return 0;
return parseFloat(amount.replace(/[^0-9.-]+/g, '')) || 0;
}
/**
* Заполняет свойства класса данными из запроса и выполняет расчеты
* @param {object} fields - Объект с данными из POST-запроса
*/
async setFields(fields) {
if (!fields || Object.keys(fields).length === 0) {
fields = randomData.entergy(); // <-- Замените на entergy(), aep_ohio() и т.д.
}
// --- Получаем данные из формы ---
this.customer_name = fields.customer_name;
this.mail_date = fields.mail_date;
this.account_number = fields.account_number;
this.invoice_number = fields.invoice_number;
this.amount_due_by_date = fields.amount_due_by_date;
this.amount_due_after_date = fields.amount_due_after_date;
this.amount_due_after_value = fields.amount_due_after_value;
this.service_location_line1 = fields.service_location_line1;
this.service_location_line2 = fields.service_location_line2;
this.billing_days = fields.billing_days;
this.kwh_used = fields.kwh_used;
this.avg_kwh_per_day = fields.avg_kwh_per_day;
this.remit_customer_address = fields.remit_customer_address;
this.barcode_number = fields.barcode_number;
this.usage_history_data = fields.usage_history_data || [];
// --- Детализация для расчетов ---
const energy_charge = this.energy_charge = this._cleanAmount(fields.energy_charge);
const federal_eac_rider = this.federal_eac_rider = this._cleanAmount(fields.federal_eac_rider);
const fuel_adjustment = this._cleanAmount(fields.fuel_adjustment);
const storm_restoration_offset = this.storm_restoration_offset = this._cleanAmount(fields.storm_restoration_offset);
const storm_restoration_charge = this.storm_restoration_charge = this._cleanAmount(fields.storm_restoration_charge);
const deposit = this.deposit = this._cleanAmount(fields.deposit);
const connect_fee = this.connect_fee = this._cleanAmount(fields.connect_fee);
// --- Выполняем расчеты ---
this.energy_charges_total = energy_charge + federal_eac_rider;
this.fuel_charges_total = fuel_adjustment;
this.other_charges_total = storm_restoration_offset + storm_restoration_charge;
const current_month_energy_charges = this.energy_charges_total + this.fuel_charges_total + this.other_charges_total;
this.current_month_energy_charges = current_month_energy_charges.toFixed(2);
this.amount_due_by_value = current_month_energy_charges + deposit + connect_fee;
// --- Подготовка данных для отображения ---
this.remit_account_number = this.account_number;
this.remit_invoice_number = this.invoice_number;
this.remit_amount_due_by_date = this.amount_due_by_date;
this.remit_amount_due_after_date = this.amount_due_after_date;
this.remit_amount_due_by_value = `${this.amount_due_by_value.toFixed(2)}`;
this.kwh_used_formatted = (this._cleanAmount(this.kwh_used)).toLocaleString('en-US');
// --- ОБРАБОТКА ДАННЫХ ДЛЯ METER READING ---
this.meter_reading_header = fields.meter_reading_contract;
this.meter_number_header = fields.meter_number;
this.rate_header = fields.meter_rate;
this.current_reading_text = fields.current_meter_reading_datetime;
this.current_meter_reading_value = fields.current_meter_reading_value;
this.previous_reading_text = fields.previous_meter_reading_datetime;
this.previous_meter_reading_value = fields.previous_meter_reading_value;
const kwhUsedNum = this._cleanAmount(this.kwh_used);
if (kwhUsedNum > 0) {
const federalEACRate = (federal_eac_rider / kwhUsedNum).toFixed(8);
this.federal_eac_rider_detail = `${this.kwh_used_formatted} kWh @ ${federalEACRate}`;
const fuelAdjustmentRate = (fuel_adjustment / kwhUsedNum).toFixed(8);
this.fuel_adjustment_detail = `${this.kwh_used_formatted} kWh @ ${fuelAdjustmentRate}`;
} else {
this.federal_eac_rider_detail = '';
this.fuel_adjustment_detail = '';
}
const billDate = new Date(this.mail_date);
if (!isNaN(billDate.getTime())) { // Проверяем, что дата валидная
const currentYear = billDate.getFullYear();
this.chart_legend_year_current = String(currentYear);
this.chart_legend_year_previous = String(currentYear - 1);
} else {
// Если дата невалидная, используем запасные значения
console.warn(`Invalid mail_date format: "${this.mail_date}". Could not determine years for chart legend.`);
this.chart_legend_year_current = '2023';
this.chart_legend_year_previous = '2022';
}
const billDateForPeriod = new Date(this.mail_date);
if (!isNaN(billDateForPeriod.getTime())) {
// toLocaleString с опциями - лучший способ получить название месяца
const month = billDateForPeriod.toLocaleString('en-US', { month: 'short' }); // "Jun"
const year = billDateForPeriod.getFullYear(); // 2023
this.billing_period_formatted = `${month} ${year}`;
} else {
// Запасное значение, если дата некорректна
this.billing_period_formatted = 'Jun 2023';
}
this.qpc_code = fields.qpc_code;
this.cycle_number = fields.cycle_number;
}
_describeArc(x, y, radius, startAngle, endAngle) {
const startRad = (startAngle - 90) * Math.PI / 180;
const endRad = (endAngle - 90) * Math.PI / 180;
const start = { x: x + (radius * Math.cos(startRad)), y: y + (radius * Math.sin(startRad)) };
const end = { x: x + (radius * Math.cos(endRad)), y: y + (radius * Math.sin(endRad)) };
const largeArcFlag = endAngle - startAngle <= 180 ? "0" : "1";
const d = ["M", start.x, start.y, "A", radius, radius, 0, largeArcFlag, 1, end.x, end.y].join(" ");
return d;
}
_getFieldValue(item) {
if (!item || typeof item.field !== 'string') {
return '';
}
const fieldKey = item.field;
const parts = fieldKey.split(',').map(p => p.trim());
// Шаг 1: Преобразуем каждую часть в ее конечное значение
// (статический текст, значение переменной или символ переноса строки)
const resolvedParts = parts.map(part => {
if (part === '[NEWLINE]') {
return '\n'; // Наш маркер становится реальным переносом
}
// this[part] находит свойство класса по имени, например, this['remit_amount_due_by_date']
if (this[part] !== undefined && this[part] !== null) {
return this[part]; // Возвращаем значение переменной
}
return part; // Возвращаем статический текст как есть
});
// Шаг 2: Грамотно склеиваем части в одну строку
let finalString = '';
for (let i = 0; i < resolvedParts.length; i++) {
const currentPart = resolvedParts[i];
// Если текущая часть - это перенос строки, просто добавляем ее и переходим к следующей
if (currentPart === '\n') {
finalString += '\n';
continue;
}
// Добавляем пробел, только если это НЕ первая часть в строке
// И если ПРЕДЫДУЩАЯ часть не была переносом строки
if (i > 0 && resolvedParts[i - 1] !== '\n') {
finalString += ' ';
}
finalString += currentPart;
}
return finalString;
}
_evaluate(expression, constants) {
if (typeof expression === 'number') return expression;
if (typeof expression !== 'string') return NaN;
const formula = expression.replace(/[a-zA-Z_][a-zA-Z0-9_]*/g, match => constants[match] !== undefined ? constants[match] : '0');
try {
return new Function(`return ${formula}`)();
} catch (e) { return NaN; }
}
async _drawUsageBarChart(page, pageHeight) {
const usageData = this.usage_history_data;
if (!usageData || usageData.length === 0) return;
// Параметры области графика (ВАЖНО: ИЗМЕРЬТЕ ИХ В ILLUSTRATOR)
const chartArea = {
x_ill: 87, y_ill: 310, width: 440, height: 80
};
const maxKwh = Math.max(...usageData.map(d => d.kwh));
if (maxKwh === 0) return;
const barWidth = (chartArea.width / usageData.length) * 0.7;
const barMargin = (chartArea.width / usageData.length) * 0.3;
const stepX = barWidth + barMargin;
const grayColor = rgb(235 / 255, 235 / 255, 235 / 255);
const blackColor = rgb(0, 0, 0);
usageData.forEach((dataPoint, index) => {
if (dataPoint.kwh > 0) { // Рисуем только столбцы с потреблением > 0
const barHeight = (dataPoint.kwh / maxKwh) * chartArea.height;
const barX = chartArea.x_ill + (index * stepX);
const baseY = pageHeight - chartArea.y_ill - chartArea.height;
page.drawRectangle({
x: barX, y: baseY, width: barWidth, height: barHeight, color: (index === 5) ? blackColor : grayColor // Июнь (6-й элемент) - черный
});
}
});
}
async _drawUsageBarChart(page, pageHeight, constants) {
const usageData = this.usage_history_data;
if (!usageData || usageData.length === 0) return;
// --- 1. Читаем параметры из констант ---
const chartArea = {
x_ill: this._evaluate(constants.bar_chart_x_ill, constants),
y_ill: this._evaluate(constants.bar_chart_y_ill, constants),
width: this._evaluate(constants.bar_chart_width, constants),
height: this._evaluate(constants.bar_chart_height, constants)
};
const currentMonthIndex = this._evaluate(constants.bar_chart_current_month_index, constants);
const labelYOffset = this._evaluate(constants.bar_chart_label_y_offset, constants);
const labelFontSize = this._evaluate(constants.bar_chart_label_font_size, constants);
const labelFontIndex = this._evaluate(constants.bar_chart_label_ifont, constants); // <--- ЧИТАЕМ ИНДЕКС ШРИФТА
const barWidthRatio = this._evaluate(constants.bar_chart_bar_width_ratio, constants); // <--- ЧИТАЕМ ШИРИНУ СТОЛБЦА
const defaultBarColor = rgb(
this._evaluate(constants.color_bar_default_r, constants),
this._evaluate(constants.color_bar_default_g, constants),
this._evaluate(constants.color_bar_default_b, constants)
);
const currentBarColor = rgb(
this._evaluate(constants.color_bar_current_r, constants),
this._evaluate(constants.color_bar_current_g, constants),
this._evaluate(constants.color_bar_current_b, constants)
);
if (Object.values(chartArea).some(isNaN) || [currentMonthIndex, labelYOffset, labelFontSize, labelFontIndex, barWidthRatio].some(isNaN)) {
console.error("Could not evaluate all bar chart constants from map.json.");
return;
}
// --- 2. Выполняем расчеты ---
const maxKwh = Math.max(...usageData.map(d => d.kwh).filter(k => k > 0));
if (maxKwh <= 0) return;
// Общее пространство на один столбец
const spacePerBar = chartArea.width / usageData.length;
// Ширина столбца теперь вычисляется на основе коэффициента
const barWidth = spacePerBar * barWidthRatio; // <--- ИСПОЛЬЗУЕМ КОЭФФИЦИЕНТ
// --- 3. Рисуем столбцы ---
usageData.forEach((dataPoint, index) => {
if (dataPoint.kwh > 0) {
const barHeight_pdf = (dataPoint.kwh / maxKwh) * chartArea.height;
// Центрируем столбец внутри его пространства
const barX_pdf = chartArea.x_ill + (index * spacePerBar) + ((spacePerBar - barWidth) / 2);
const baseY_pdf = pageHeight - chartArea.y_ill - chartArea.height;
const color = (index === currentMonthIndex) ? currentBarColor : defaultBarColor;
page.drawRectangle({
x: barX_pdf, y: baseY_pdf, width: barWidth, height: barHeight_pdf, color: color,
});
}
});
// --- 4. Рисуем подписи месяцев ---
const textFont = this.getCustomerFontByIndex(labelFontIndex); // <--- ИСПОЛЬЗУЕМ ИНДЕКС ШРИФТА
if (!textFont) {
console.error(`Font with index ${labelFontIndex} not found for bar chart labels.`);
return;
}
usageData.forEach((dataPoint, index) => {
// Центрируем подпись внутри пространства столбца
const labelX_center = chartArea.x_ill + (index * spacePerBar) + (spacePerBar / 2);
const baseY_pdf = pageHeight - chartArea.y_ill - chartArea.height;
const monthText = dataPoint.month;
const textWidth = textFont.widthOfTextAtSize(monthText, labelFontSize);
page.drawText(monthText, {
x: labelX_center - (textWidth / 2), // Центрируем текст относительно labelX_center
y: baseY_pdf - labelYOffset,
font: textFont,
size: labelFontSize,
color: rgb(0, 0, 0)
});
});
}
async _drawChargesDonutChart(page, pageHeight, chartConfig) {
// --- ПРОВЕРКА, ЧТО КОНФИГ ДИАГРАММЫ СУЩЕСТВУЕТ ---
if (!chartConfig || !chartConfig.constants || !chartConfig.segments) {
console.error("Donut chart configuration is missing or invalid in map.json.");
return;
}
const { constants, segments } = chartConfig;
// --- Преобразуем описание секторов из JSON в массив с реальными значениями ---
const charges = segments.map(segment => {
// --- ИЗМЕНЕНИЕ ЗДЕСЬ ---
// Вычисляем каждый компонент цвета, используя _evaluate
const r = this._evaluate(segment.color[0], constants);
const g = this._evaluate(segment.color[1], constants);
const b = this._evaluate(segment.color[2], constants);
return {
value: this[segment.value_field] || 0,
color: rgb(r, g, b), // Создаем цвет из вычисленных значений
label: segment.label
}
});
const totalCharges = charges.reduce((sum, c) => sum + c.value, 0);
if (totalCharges <= 0) return;
// --- Читаем параметры из констант ---
const centerX_ill = this._evaluate(constants.donut_chart_center_x_ill, constants);
const centerY_ill = this._evaluate(constants.donut_chart_center_y_ill, constants);
const radius = this._evaluate(constants.donut_chart_radius, constants);
const thickness = this._evaluate(constants.donut_chart_thickness, constants);
if (isNaN(centerX_ill) || isNaN(centerY_ill) || isNaN(radius) || isNaN(thickness)) {
console.error("Could not evaluate donut chart constants from map.json.");
return;
}
const centerX_pdf = centerX_ill;
const centerY_pdf = pageHeight - centerY_ill;
const centerY_for_svg = pageHeight - centerY_pdf;
let startAngle = 0;
for (const charge of charges) {
if (charge.value <= 0) continue;
const sweepAngle = (charge.value / totalCharges) * 360;
if (sweepAngle <= 0) continue;
const endAngle = startAngle + sweepAngle;
const arcPath = this._describeArc(centerX_pdf, centerY_for_svg, radius, startAngle, endAngle);
if (!arcPath.includes('NaN')) {
page.drawSvgPath(arcPath, { y: pageHeight, borderColor: charge.color, borderWidth: thickness });
}
startAngle = endAngle;
}
}
async drawFancyAmount(item, constants) {
const page = this.getPageByIndex(item.ipage);
const font = this.getCustomerFontByIndex(item.ifont);
if (!page || !font) return;
// --- Получаем и вычисляем параметры ---
const pageHeight = this.getPageHeight(item.ipage);
const amountText = `${this._getFieldValue(item) || '0.00'}`;
const x_ill_end = this._evaluate(item.x_ill, constants); // x_ill - это ПРАВЫЙ край
const y_ill = this._evaluate(item.y_ill, constants);
const mainSize = this._evaluate(item.size, constants);
// --- Размеры для разных частей ---
const dollarSignSize = mainSize * 0.7; // '$' будет 70% от основного размера
const centsSize = mainSize * 0.6; // Центы будут 60% от основного размера
// --- Разбираем сумму на части ---
const parts = amountText.split('.');
const dollars = parts[0] || '0';
const cents = parts.length > 1 ? parts[1] : '00';
// --- Вычисляем ширину каждой части, чтобы выровнять по правому краю ---
const centsWidth = font.widthOfTextAtSize(cents, centsSize);
const dollarsWidth = font.widthOfTextAtSize(dollars, mainSize);
const dollarSignWidth = font.widthOfTextAtSize('$', dollarSignSize);
// --- Вычисляем Y-координаты ---
// Базовая Y-координата для долларов (основной текст)
const y_pdf_dollars = pageHeight - y_ill - (mainSize * this.baselineCorrectionFactor);
// Y-координата для центов (поднимаем их выше)
const y_pdf_cents = y_pdf_dollars + (mainSize - centsSize) * 0.7;
// Y-координата для знака доллара (чуть ниже центра)
const y_pdf_dollarSign = y_pdf_cents - 3;
// --- Вычисляем X-координаты (от правого края) ---
const x_pdf_cents_start = x_ill_end - centsWidth;
const x_pdf_dollars_start = x_pdf_cents_start - dollarsWidth;
const x_pdf_dollarSign_start = x_pdf_dollars_start - dollarSignWidth - (mainSize * 0.1); // Небольшой отступ
// --- Рисуем части ---
const r = 0, g = 0, b = 0; // Черный цвет
// 1. Рисуем центы
await super.draw(item.ipage, cents, x_pdf_cents_start, y_pdf_cents, centsSize, item.ifont, r, g, b);
// 2. Рисуем доллары
await super.draw(item.ipage, dollars, x_pdf_dollars_start, y_pdf_dollars, mainSize, item.ifont, r, g, b);
// 3. Рисуем знак доллара
await super.draw(item.ipage, '$', x_pdf_dollarSign_start, y_pdf_dollarSign, dollarSignSize, item.ifont, r, g, b);
}
async draw() {
// 1. Загружаем полный конфиг из JSON
const mapFilePath = path.join(__dirname, 'Entergy.map.json');
const configJson = fs.readFileSync(mapFilePath, 'utf8');
const config = JSON.parse(configJson);
const { constants, donut_chart_segments, map: dataMap } = config;
// 2. Создаем специальный объект с конфигом для диаграммы
const donutChartConfig = {
constants: constants,
segments: donut_chart_segments
};
// 3. Рисуем сложную графику, передавая ей нужные части конфига
await this._drawUsageBarChart(this.pages[0], this.getPageHeight(0), constants);
await this._drawChargesDonutChart(this.pages[0], this.getPageHeight(0), donutChartConfig);
// 4. Рисуем текстовые поля на основе dataMap
for (const item of dataMap) {
// --- ПРОВЕРКИ НА КАСТОМНЫЕ МЕТОДЫ ---
// Вызываем специальный метод для "красивой" суммы
if (item.draw_method === 'drawFancyAmount') {
await this.drawFancyAmount(item, constants);
continue; // Пропускаем остальную часть цикла для этого элемента
}
// Вызываем специальный метод для строк с тегами <style>
if (typeof item.field === 'string' && item.field.includes('<style')) {
await this.drawStyledText(item, constants);
continue;
}
// --- СТАНДАРТНАЯ ЛОГИКА ДЛЯ ОБЫЧНЫХ ПОЛЕЙ ---
const page = this.getPageByIndex(item.ipage);
const font = this.getCustomerFontByIndex(item.ifont);
if (!page || !font) {
console.warn(`Skipping draw: Missing page or font for item:`, item);
continue;
}
const pageHeight = this.getPageHeight(item.ipage);
const fieldText = `${this._getFieldValue(item) || ''}`;
const x_ill = this._evaluate(item.x_ill, constants);
const y_ill = this._evaluate(item.y_ill, constants);
const size = this._evaluate(item.size, constants);
const lineHeight = this._evaluate(item.lineHeight, constants);
if (isNaN(x_ill) || isNaN(y_ill) || isNaN(size)) {
console.warn('Could not evaluate coordinates/size for item:', item);
continue;
}
const y_pdf = pageHeight - y_ill - (size * this.baselineCorrectionFactor);
const r = this._evaluate(item.cR, constants) || 0;
const g = this._evaluate(item.cG, constants) || 0;
const b = this._evaluate(item.cB, constants) || 0;
const textColor = rgb(r, g, b);
const textLines = fieldText.split('\n');
let currentY = y_pdf;
const effectiveLineHeight = (typeof lineHeight === 'number' && lineHeight > 0) ? lineHeight : (size * 1.2);
const bgColorArray = item.bgColor;
const hasBackground = Array.isArray(bgColorArray) && bgColorArray.length === 3;
let bgColor = null;
if (hasBackground) {
const bg_r = this._evaluate(bgColorArray[0], constants);
const bg_g = this._evaluate(bgColorArray[1], constants);
const bg_b = this._evaluate(bgColorArray[2], constants);
bgColor = rgb(bg_r, bg_g, bg_b);
}
for (const line of textLines) {
let x_pdf;
// Вычисляем X-координату для КАЖДОЙ строки индивидуально
if (item.align === 'right') {
const pageWidth = this.getPageWidth(item.ipage);
const rightMargin_pdf = pageWidth - x_ill;
x_pdf = await this.alignTextToRight(item.ipage, item.ifont, size, rightMargin_pdf, line);
} else if (item.align === 'center') {
const textWidth = font.widthOfTextAtSize(line, size);
x_pdf = x_ill - (textWidth / 2);
} else {
x_pdf = x_ill;
}
// Выбираем, какой метод отрисовки использовать
if (hasBackground) {
await this.drawTextWithBackground(item.ipage, line, x_pdf, currentY, size, item.ifont, textColor, bgColor);
} else {
await super.draw(item.ipage, line, x_pdf, currentY, size, item.ifont, r, g, b);
}
// Смещаем Y для следующей строки
currentY -= effectiveLineHeight;
}
}
}
}
module.exports = pdfFillDataEntergy;

View File

@ -0,0 +1,548 @@
// 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;

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,174 @@
// pdfFillDataVerizon.js
const fs = require('fs');
const path = require('path');
const pdfFillData = require('./pdfFillData'); // Родительский класс
const randomData = require('./randomDataGenerator');
class pdfFillDataVerizon extends pdfFillData {
constructor() {
super('Verizon');
this.baselineCorrectionFactor = 0.85;
this.fontMap = {
'Helvetica': 0,
'Arial-BoldMT': 1,
'ArialMT': 2,
'C0EX00U0_T1EX0000_0': 3, // Monospace/OCR
'Kalypso': 4 // Placeholder
// Примечание: реальные имена шрифтов из PDF могут быть 'C0GTBD12...' и т.д.
// Их нужно будет сопоставить с вашими файлами.
};
// --- Основные данные из формы ---
this.customer_name = '';
this.customer_address = '';
this.customer_city_st_zip = '';
this.account_number = '';
this.invoice_number = '';
this.total_due_date = '';
this.full_due_date = '';
this.barcode_number = '';
// --- Поля для детализации из формы ---
this.activation_fee = 0;
this.plan_ultimate = 0;
this.perk_apple_one = 0;
this.surcharge_fus = 0;
this.surcharge_regulatory = 0;
this.surcharge_admin = 0;
this.tax_911 = 0;
this.tax_ga_state = 0;
this.tax_fulton_county = 0;
// --- Расчетные и форматируемые поля ---
this.balance_from_last_bill = '';
this.total_amount_due = '';
this.total_due_text = '';
this.remit_total_due_text = '';
this.surcharges_total = '';
this.taxes_total = '';
this.current_month_energy_charges = '';
this.bill_month = '';
this.payment_received = '';
this.page3_header_text = '';
this.page3_due_date_text = '';
this.page3_payment_text = '';
}
_cleanAmount(amount) {
if (typeof amount !== 'string') return 0;
return parseFloat(amount.replace(/[^0-9.-]+/g, '')) || 0;
}
async setFields(fields) {
if (!fields || Object.keys(fields).length === 0) {
fields = randomData.verizon(); // <-- Замените на entergy(), aep_ohio() и т.д.
}
this.customer_name = fields.customer_name;
this.customer_address = fields.customer_address;
this.customer_city_st_zip = fields.customer_city_st_zip;
this.account_number = fields.account_number;
this.invoice_number = fields.invoice_number;
this.total_due_date = fields.total_due_date;
this.barcode_number = fields.barcode_number;
// Принимаем числовые значения из формы
this.activation_fee = this._cleanAmount(fields.activation_fee);
this.plan_ultimate = this._cleanAmount(fields.plan_ultimate);
this.perk_apple_one = this._cleanAmount(fields.perk_apple_one);
this.surcharge_fus = this._cleanAmount(fields.surcharge_fus);
this.surcharge_regulatory = this._cleanAmount(fields.surcharge_regulatory);
this.surcharge_admin = this._cleanAmount(fields.surcharge_admin);
this.tax_911 = this._cleanAmount(fields.tax_911);
this.tax_ga_state = this._cleanAmount(fields.tax_ga_state);
this.tax_fulton_county = this._cleanAmount(fields.tax_fulton_county);
// Выполняем расчеты
this.surcharges_total = this.surcharge_fus + this.surcharge_regulatory + this.surcharge_admin;
this.taxes_total = this.tax_911 + this.tax_ga_state + this.tax_fulton_county;
const totalAmountNum = this.activation_fee + this.plan_ultimate + this.perk_apple_one + this.surcharges_total + this.taxes_total;
// --- Готовим форматированные строки для replacements ---
this.balance_from_last_bill = '$0.00';
this.total_amount_due = `$${totalAmountNum.toFixed(2)}`;
this.total_due_text = `Total due on ${this.total_due_date}`;
// Форматируем дату для купона (e.g., February 12, 2024)
const dateObj = new Date(this.total_due_date);
const longDate = !isNaN(dateObj.getTime()) ? dateObj.toLocaleDateString('en-US', { month: 'long', day: 'numeric', year: 'numeric' }) : this.total_due_date;
this.remit_total_due_text = `Total Amount Due by ${longDate}`;
// Форматируем все денежные значения в строки со знаком $
this.activation_fee = `$${this.activation_fee.toFixed(2)}`;
this.plan_ultimate = `$${this.plan_ultimate.toFixed(2)}`;
this.perk_apple_one = `$${this.perk_apple_one.toFixed(2)}`;
this.surcharges_total = `$${this.surcharges_total.toFixed(2)}`;
this.surcharge_fus = `$${this.surcharge_fus.toFixed(2)}`;
this.surcharge_regulatory = `$${this.surcharge_regulatory.toFixed(2)}`;
this.surcharge_admin = `$${this.surcharge_admin.toFixed(2)}`;
this.taxes_total = `$${this.taxes_total.toFixed(2)}`;
this.tax_911 = `$${this.tax_911.toFixed(2)}`;
this.tax_ga_state = `$${this.tax_ga_state.toFixed(2)}`;
this.tax_fulton_county = `$${this.tax_fulton_county.toFixed(2)}`;
// --- ДАННЫЕ И ФОРМАТИРОВАНИЕ ДЛЯ СТР. 3 ---
this.bill_month = fields.bill_month;
this.payment_received = fields.payment_received; // Берем значение напрямую
// Собираем комбинированные строки
this.page3_header_text = `Your ${this.bill_month} bill is ${this.total_amount_due}`;
this.page3_due_date_text = `Due ${this.total_due_date}`;
this.page3_payment_text = `You paid ${this.payment_received}.`;
}
async draw() {
const mapFilePath = path.join(__dirname, 'Verizon.map.json');
const config = JSON.parse(fs.readFileSync(mapFilePath, 'utf8'));
const { constants, replacements, map: dataMap } = config;
for (const item of dataMap) {
const page = this.getPageByIndex(item.ipage);
const font = this.getCustomerFontByIndex(item.ifont);
if (!page || !font) continue;
const pageHeight = this.getPageHeight(item.ipage);
let fieldText = replacements.hasOwnProperty(item.field)
? this[replacements[item.field]] || item.field
: item.field;
fieldText = `${fieldText || ''}`;
// --- 2. Вычисляем базовые параметры ---
const x_ill = this._evaluate(item.x_ill, constants);
const y_ill = this._evaluate(item.y_ill, constants);
const size = this._evaluate(item.size, constants);
const y_pdf = pageHeight - y_ill - (size * this.baselineCorrectionFactor);
let x_pdf = x_ill; // X по умолчанию (для align: 'left')
// --- 3. НОВАЯ, УМНАЯ ЛОГИКА ВЫРАВНИВАНИЯ (ВАШ ПОДХОД) ---
if (item.align) {
if (item.align === 'right') {
const pageWidth = this.getPageWidth(item.ipage);
const rightMargin_pdf = pageWidth - x_ill;
x_pdf = await this.alignTextToRight(item.ipage, item.ifont, size, rightMargin_pdf, fieldText);
} else if (item.align === 'center') {
// НОВЫЙ БЛОК: ВЫРАВНИВАНИЕ ПО ЦЕНТРУ
const textWidth = font.widthOfTextAtSize(fieldText, size);
x_pdf = x_ill - (textWidth / 2);
}else {
x_pdf = x_ill;
}
}
// --- 4. Отрисовка ---
await super.draw(item.ipage, fieldText, x_pdf, y_pdf, size, item.ifont, 0, 0, 0); // Y координата в drawText использует y_ill, а не y_pdf.
}
}
}
module.exports = pdfFillDataVerizon;

View File

@ -0,0 +1,675 @@
const pdfFillData = require('./pdfFillData');
class pdfFillDataXfinity extends pdfFillData {
constructor() {
super('Xfinity');
this.customer_name = '';
this.account_number = '';
this.billing_date = '';
this.address = '';
this.plan = '';
this.speed = '';
this.customer_name_address = '';
this.services_from = '';
this.credit_card_payment_thank_you = '';
this.credit_card_payment_thank_you_Y = '';
this.automatic_payment = '';
this.tarif = '';
this.your_package = '';
this.equipment = "$14.00";
this.peacock_premium = "$0.00";
this.balance_forward = "$0.00";
this.regular_monthly_charges = '';
this.regular_monthly_charges_d = '';
this.other_charges = '';
this.totalD = '';
this.total = '';
this.previous_balance = '';
this.new_charges = '';
this.amount_due = '';
this.credit_card_payment = '';
this.totalS7 = '';
this.code_number = '';
this.automatic_payment_lines = '';
this.fontSize = 9;
this.lineHeight = 0;
this.yPosition = 0;
this.rightMargin = 0;
this.rightMarginBold = 0;
this.rightMarginBoldV = 0;
this.customer_name_address_lines = '';
this.fontSize2 = 8;
this.lineHeight2 = 0;
this.yPosition2 = 0;
this.rightMargin3 = 0;
this.rightMargin3M = 0;
this.dataMap = [];
}
async setFields(fields) {
// base
this.customer_name = fields.customer_name;
this.account_number = fields.account_number;
this.billing_date = fields.billing_date;
this.address = fields.address;
this.plan = fields.plan;
this.speed = fields.speed;
// other
this.customer_name_address = this.customer_name.toUpperCase() + "\n" + this.splitAddress(this.address);
this.services_from = this.getServicePeriod(this.billing_date);
this.credit_card_payment_thank_you = this.getCreditCardPaymentDate(this.billing_date);
this.credit_card_payment_thank_you_Y = this.getCreditCardPaymentDateY(this.billing_date);
this.automatic_payment = this.calculateAutoPaymentDate(this.credit_card_payment_thank_you_Y);
this.tarif = this.getPriceByAddress(this.plan, this.address, this.getSocrCache(), this.getTariffsCache());
this.your_package = this.tarif.price.replace('/mo.','');
this.regular_monthly_charges = parseFloat(this.your_package.replace('$', '')) + parseFloat(this.equipment.replace('$', ''));
this.regular_monthly_charges_d = this.regular_monthly_charges.toFixed(2);
this.regular_monthly_charges = "$" + this.regular_monthly_charges_d;
this.other_charges = await this.getTaxRateByAddress(this.address);
this.other_charges = "$" + (this.regular_monthly_charges_d * (parseFloat(this.other_charges.details.rate.combined_rate).toFixed(2) * 100) / 100).toFixed(2);
this.totalD = parseFloat(this.regular_monthly_charges.replace('$', '')) + parseFloat(this.other_charges.replace('$', ''));
this.total = "$" + this.totalD.toFixed(2);
this.previous_balance = this.total;
this.new_charges = this.total;
this.amount_due = this.total;
this.credit_card_payment = '-' + this.total;
this.totalS7 = Math.floor(this.totalD * 100).toString().padStart(7, '0');
this.code_number = String(this.account_number.replaceAll(" ",'')) + this.totalS7;
this.code_number += this.hashWeightCodeNumber(this.code_number);
this.automatic_payment_lines = `Your automatic payment on ${this.automatic_payment || ''}, will include your\namount due, plus or minus any payment related activities or\nadjustments, and less any credits issued before your bill due\ndate.`.split('\n');
this.lineHeight = this.fontSize * 1.0;
this.yPosition = this.pixelsToPoints(1682, 300);
this.rightMargin = this.pixelsToPoints(1393, 300);
this.rightMarginBold = this.pixelsToPoints(1395, 300);
this.rightMarginBoldV = this.pixelsToPoints(1396, 300);
this.customer_name_address_lines = `${this.customer_name_address || ''}`.split('\n');
this.lineHeight2 = this.fontSize2 * 1.075;
this.yPosition2 = this.pixelsToPoints(627, 300);
this.rightMargin3 = this.pixelsToPoints(1128, 300);
this.rightMargin3M = this.pixelsToPoints(1103, 300);
}
splitAddress(address) {
const words = address.split(", ");
const total = words[0] + "\n" + words[1] + ", " + words[2] + " " + words[3];
return total;
}
getServicePeriod(billingDateStr) {
const billingDate = new Date(billingDateStr);
const firstDate = new Date(billingDate);
const dayOfWeek = firstDate.getDay();
const daysToMonday = dayOfWeek === 0 ? 1 : dayOfWeek === 1 ? 0 : 8 - dayOfWeek;
firstDate.setDate(firstDate.getDate() + daysToMonday);
const secondDate = new Date(billingDate);
secondDate.setMonth(secondDate.getMonth() + 1);
secondDate.setDate(secondDate.getDate() - 1);
const options = { month: 'short', day: 'numeric', year: 'numeric' };
const firstDateStr = firstDate.toLocaleDateString('en-US', options);
const secondDateStr = secondDate.toLocaleDateString('en-US', options);
return `${firstDateStr} to ${secondDateStr}`;
}
getCreditCardPaymentDate(billingDate) {
return super.getCreditCardPaymentDate(billingDate);
}
getCreditCardPaymentDateY(billingDate) {
let date = new Date(billingDate);
date.setDate(date.getDate() - 7);
if (date.getDay() === 0) {
date.setDate(date.getDate() - 1);
}
const options = { month: 'short', day: 'numeric', year: 'numeric' };
return date.toLocaleDateString('en-US', options);
}
calculateAutoPaymentDate(billingDate) {
let date = new Date(billingDate);
date.setMonth(date.getMonth() + 1);
date.setDate(date.getDate() - 1);
while (date.getDay() === 0) {
date.setDate(date.getDate() - 1);
}
const options = { month: 'short', day: '2-digit', year: 'numeric' };
return date.toLocaleDateString('en-US', options);
}
getPriceByAddress(plan, address, abbreviations, tariffs) {
const stateCode = address.split(',')[2].trim().split(' ')[0];
const stateInfo = abbreviations.find(item => item.ABBREVIATION === stateCode);
if (!stateInfo) {
throw new Error(`State with abbreviation ${stateCode} not found`);
}
const stateName = stateInfo["STATE(TERRITORY)"];
for (const tariff of tariffs) {
const statesInTariff = tariff.States.split(', ').map(s => s.trim());
if (statesInTariff.includes(stateName) && tariff.Plan == plan) {
return {
state: stateName,
region: tariff.Region,
plan: tariff.Plan,
price: tariff.Price
};
}
}
throw new Error(`No tariff found for state ${stateName}`);
}
hashWeightCodeNumber(code_number) {
const weights = [7, 3, 1];
const sum = code_number
.split('')
.reduce((acc, char, index) => {
const digit = parseInt(char, 10);
const weight = weights[index % 3];
return acc + digit * weight;
}, 0);
return sum % 10;
}
pixelsToPoints(pixels, dpi = 72) {
return super.pixelsToPoints(pixels, dpi);
}
async alignTextToRight(ipage, ifont, setFontSize, setRightMargin, setText) {
return await super.alignTextToRight(ipage, ifont, setFontSize, setRightMargin, setText);
}
async draw() {
this.dataMap = [
{
ipage: 0,
field: this.customer_name + ',',
x: this.pixelsToPoints(340, 300),
y: this.pixelsToPoints(2810, 300),
size: 20,
ifont: 3,
cR: 0.313,
cG: 0.309,
cB: 0.631,
encoding: 'utf-8'
},
{
ipage: 0,
field: this.account_number,
x: this.pixelsToPoints(1006, 300),
y: this.pixelsToPoints(3094, 300),
size: 8,
ifont: 2,
cR: 0,
cG: 0,
cB: 0,
encoding: 'utf-8'
},
{
ipage: 0,
field: this.billing_date,
x: this.pixelsToPoints(1426, 300),
y: this.pixelsToPoints(3094, 300),
size: 8,
ifont: 2,
cR: 0,
cG: 0,
cB: 0,
encoding: 'utf-8'
},
{
ipage: 0,
field: this.services_from,
x: this.pixelsToPoints(1726, 300),
y: this.pixelsToPoints(3094, 300),
size: 8,
ifont: 2,
cR: 0,
cG: 0,
cB: 0,
encoding: 'utf-8'
},
{
ipage: 0,
field: this.address,
x: this.pixelsToPoints(196, 300),
y: this.pixelsToPoints(2434, 300),
size: 8,
ifont: 2,
cR: 0,
cG: 0,
cB: 0,
encoding: 'utf-8'
},
{
ipage: 0,
field: this.credit_card_payment_thank_you,
x: this.pixelsToPoints(721, 300),
y: this.pixelsToPoints(2279, 300),
size: 8,
ifont: 1,
cR: 0,
cG: 0,
cB: 0,
encoding: 'utf-8'
},
{
ipage: 0,
field: this.previous_balance,
x: await this.alignTextToRight(0, 1, 8, this.rightMargin, `${this.previous_balance || ''}`),
y: this.pixelsToPoints(2349, 300),
size: 8,
ifont: 1,
cR: 0,
cG: 0,
cB: 0,
encoding: 'utf-8'
},
{
ipage: 0,
field: this.credit_card_payment,
x: await this.alignTextToRight(0, 1, 8, this.rightMargin, `${this.credit_card_payment || ''}`),
y: this.pixelsToPoints(2275, 300),
size: 8,
ifont: 1,
cR: 0,
cG: 0,
cB: 0,
encoding: 'utf-8'
},
{
ipage: 0,
field: this.balance_forward,
x: await this.alignTextToRight(0, 3, 9, this.rightMarginBold, `${this.balance_forward || ''}`),
y: this.pixelsToPoints(2207, 300),
size: 9,
ifont: 3,
cR: 0,
cG: 0,
cB: 0,
encoding: 'utf-8'
},
{
ipage: 0,
field: this.regular_monthly_charges,
x: await this.alignTextToRight(0, 1, 8, this.rightMargin, `${this.regular_monthly_charges || ''}`),
y: this.pixelsToPoints(2111, 300),
size: 8,
ifont: 1,
cR: 0,
cG: 0,
cB: 0,
encoding: 'utf-8'
},
{
ipage: 0,
field: this.other_charges,
x: await this.alignTextToRight(0, 1, 8, this.rightMargin, `${this.other_charges || ''}`),
y: this.pixelsToPoints(2041, 300),
size: 8,
ifont: 1,
cR: 0,
cG: 0,
cB: 0,
encoding: 'utf-8'
},
{
ipage: 0,
field: this.new_charges,
x: await this.alignTextToRight(0, 3, 9, this.rightMarginBold, `${this.new_charges || ''}`),
y: this.pixelsToPoints(1971, 300),
size: 9,
ifont: 3,
cR: 0,
cG: 0,
cB: 0,
encoding: 'utf-8'
},
{
ipage: 0,
field: this.amount_due,
x: await this.alignTextToRight(0, 3, 12, this.rightMarginBoldV, `${this.amount_due || ''}`),
y: this.pixelsToPoints(1840, 300),
size: 12,
ifont: 3,
cR: 1,
cG: 1,
cB: 1,
encoding: 'utf-8'
},
{
ipage: 0,
field: this.account_number,
x: this.pixelsToPoints(1916, 300),
y: this.pixelsToPoints(928, 300),
size: 11,
ifont: 3,
cR: 0,
cG: 0,
cB: 0,
encoding: 'utf-8'
},
{
ipage: 0,
field: this.automatic_payment,
x: this.pixelsToPoints(1916, 300),
y: this.pixelsToPoints(863, 300),
size: 11,
ifont: 2,
cR: 0,
cG: 0,
cB: 0,
encoding: 'utf-8'
},
{
ipage: 0,
field: this.amount_due,
x: this.pixelsToPoints(1916, 300),
y: this.pixelsToPoints(771, 300),
size: 16,
ifont: 3,
cR: 0,
cG: 0,
cB: 0,
encoding: 'utf-8'
},
{
ipage: 0,
field: this.automatic_payment,
x: this.pixelsToPoints(1970, 300),
y: this.pixelsToPoints(668, 300),
size: 9,
ifont: 3,
cR: 0,
cG: 0,
cB: 0,
encoding: 'utf-8'
},
{
ipage: 0,
field: this.code_number,
x: this.pixelsToPoints(137, 300),
y: this.pixelsToPoints(135, 300),
size: 11.5,
ifont: 4,
cR: 0,
cG: 0,
cB: 0,
encoding: 'utf-8'
},
{
ipage: 1,
field: this.account_number,
x: this.pixelsToPoints(1006, 300),
y: this.pixelsToPoints(3094, 300),
size: 8,
ifont: 2,
cR: 0,
cG: 0,
cB: 0,
encoding: 'utf-8'
},
{
ipage: 1,
field: this.billing_date,
x: this.pixelsToPoints(1426, 300),
y: this.pixelsToPoints(3094, 300),
size: 8,
ifont: 2,
cR: 0,
cG: 0,
cB: 0,
encoding: 'utf-8'
},
{
ipage: 1,
field: this.services_from,
x: this.pixelsToPoints(1726, 300),
y: this.pixelsToPoints(3094, 300),
size: 8,
ifont: 2,
cR: 0,
cG: 0,
cB: 0,
encoding: 'utf-8'
},
{
ipage: 2,
field: this.account_number,
x: this.pixelsToPoints(1006, 300),
y: this.pixelsToPoints(3094, 300),
size: 8,
ifont: 2,
cR: 0,
cG: 0,
cB: 0,
encoding: 'utf-8'
},
{
ipage: 2,
field: this.billing_date,
x: this.pixelsToPoints(1426, 300),
y: this.pixelsToPoints(3094, 300),
size: 8,
ifont: 2,
cR: 0,
cG: 0,
cB: 0,
encoding: 'utf-8'
},
{
ipage: 2,
field: this.services_from,
x: this.pixelsToPoints(1726, 300),
y: this.pixelsToPoints(3094, 300),
size: 8,
ifont: 2,
cR: 0,
cG: 0,
cB: 0,
encoding: 'utf-8'
},
{
ipage: 2,
field: this.regular_monthly_charges,
x: await this.alignTextToRight(2, 3, 12, this.rightMargin3, `${this.regular_monthly_charges || ''}`),
y: this.pixelsToPoints(2877, 300),
size: 14,
ifont: 3,
cR: 0.313,
cG: 0.309,
cB: 0.631,
encoding: 'utf-8'
},
{
ipage: 2,
field: this.your_package,
x: await this.alignTextToRight(2, 3, 10, this.rightMargin3M, `${this.your_package || ''}`),
y: this.pixelsToPoints(2775, 300),
size: 10,
ifont: 3,
cR: 0,
cG: 0,
cB: 0,
encoding: 'utf-8'
},
{
ipage: 2,
field: this.plan,
x: this.pixelsToPoints(282, 300),
y: this.pixelsToPoints(2702, 300),
size: 8,
ifont: 1,
cR: 0,
cG: 0,
cB: 0,
encoding: 'utf-8'
},
{
ipage: 2,
field: this.your_package,
x: await this.alignTextToRight(2, 1, 8, this.rightMargin3M, `${this.your_package || ''}`),
y: this.pixelsToPoints(2702, 300),
size: 8,
ifont: 1,
cR: 0,
cG: 0,
cB: 0,
encoding: 'utf-8'
},
{
ipage: 2,
field: this.peacock_premium,
x: await this.alignTextToRight(2, 1, 8, this.rightMargin3M, `${this.peacock_premium || ''}`),
y: this.pixelsToPoints(2702, 300),
size: 8,
ifont: 1,
cR: 0,
cG: 0,
cB: 0,
encoding: 'utf-8'
},
{
ipage: 2,
field: this.speed,
x: this.pixelsToPoints(1924, 300),
y: this.pixelsToPoints(2775, 300),
size: 8,
ifont: 1,
cR: 0,
cG: 0,
cB: 0,
encoding: 'utf-8'
},
{
ipage: 2,
field: this.equipment,
x: await this.alignTextToRight(2, 3, 10, this.rightMargin3M, `${this.equipment || ''}`),
y: this.pixelsToPoints(2172, 300),
size: 10,
ifont: 3,
cR: 0,
cG: 0,
cB: 0,
encoding: 'utf-8'
},
{
ipage: 2,
field: this.equipment,
x: await this.alignTextToRight(2, 1, 8, this.rightMargin3M, `${this.equipment || ''}`),
y: this.pixelsToPoints(1962, 300),
size: 8,
ifont: 1,
cR: 0,
cG: 0,
cB: 0,
encoding: 'utf-8'
},
{
ipage: 2,
field: this.other_charges,
x: await this.alignTextToRight(2, 3, 12, this.rightMargin3, `${this.other_charges || ''}`),
y: this.pixelsToPoints(1826, 300),
size: 14,
ifont: 3,
cR: 0.313,
cG: 0.309,
cB: 0.631,
encoding: 'utf-8'
},
{
ipage: 2,
field: this.other_charges,
x: await this.alignTextToRight(2, 3, 10, this.rightMargin3M, `${this.other_charges || ''}`),
y: this.pixelsToPoints(1722, 300),
size: 10,
ifont: 3,
cR: 0,
cG: 0,
cB: 0,
encoding: 'utf-8'
},
{
ipage: 2,
field: this.other_charges,
x: await this.alignTextToRight(2, 1, 8, this.rightMargin3M, `${this.other_charges || ''}`),
y: this.pixelsToPoints(1647, 300),
size: 8,
ifont: 1,
cR: 0,
cG: 0,
cB: 0,
encoding: 'utf-8'
}
];
for(let idm in this.dataMap) {
await super.draw(
this.dataMap[idm].ipage,
this.dataMap[idm].field,
this.dataMap[idm].x,
this.dataMap[idm].y,
this.dataMap[idm].size,
this.dataMap[idm].ifont,
this.dataMap[idm].cR,
this.dataMap[idm].cG,
this.dataMap[idm].cB,
this.dataMap[idm].encoding
);
}
this.automatic_payment_lines.forEach((line, index) => {
super.draw(
0,
line,
this.pixelsToPoints(113, 300),
(this.yPosition - (index * this.lineHeight)),
this.fontSize,
2,
0,
0,
0,
'utf-8'
);
});
this.customer_name_address_lines.forEach((line, index) => {
super.draw(
0,
line,
this.pixelsToPoints(137, 300),
(this.yPosition2 - (index * this.lineHeight2)),
this.fontSize2,
2,
0,
0,
0,
'utf-8'
);
});
}
async saveData(host, outputDir) {
return await super.saveData(host, outputDir, 'Xfinity' );
}
}
module.exports = pdfFillDataXfinity;

View File

@ -0,0 +1,367 @@
// lib/pdf-fill-data/src/randomDataGenerator.js
const { faker } = require('@faker-js/faker');
// Вспомогательные функции
const randomInt = (min, max) => Math.floor(Math.random() * (max - min + 1)) + min;
const randomFloat = (min, max, decimals) => parseFloat((Math.random() * (max - min) + min).toFixed(decimals));
const formatDate = (date, format = 'MM/DD/YYYY') => {
const d = new Date(date);
if (isNaN(d.getTime())) return '';
const month = String(d.getMonth() + 1).padStart(2, '0');
const day = String(d.getDate()).padStart(2, '0');
const year = d.getFullYear();
if (format === 'Mon DD, YYYY') return d.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' });
if (format === 'Month DD, YYYY') return d.toLocaleDateString('en-US', { month: 'long', day: 'numeric', year: 'numeric' });
if (format === 'Mon DD') return d.toLocaleDateString('en-US', { month: 'short', day: 'numeric' });
return `${month}/${day}/${year}`;
};
const generator = {
/**
* Генерирует данные для счета AEP Ohio
*/
aep_ohio: () => {
const billDate = faker.date.past({ years: 3 });
const dueDate = new Date(billDate);
dueDate.setDate(dueDate.getDate() + 22);
const currentMonth = billDate.getMonth();
const currentYear = billDate.getFullYear();
// --- Генерация ключевых и детализированных значений ---
const usage_current_month = randomInt(3000, 6000);
const usage_prev_month = randomInt(4000, 7000);
const usage_prev_year = randomInt(4500, 7500);
const transmission_service = randomFloat(80, 90, 2);
const distribution_service = randomFloat(150, 160, 2);
const customer_charge_detail = randomFloat(20, 25, 2);
const retail_stability_rider = randomFloat(25, 30, 2);
const deferred_asset_rider = randomFloat(5, 10, 2);
const phase_in_recovery_rider = randomFloat(20, 25, 2);
const power_purchase_rider = randomFloat(0.1, 0.5, 2);
const supplier_charges = randomFloat(190, 210, 2);
const late_payment_charge = randomFloat(5, 10, 2);
const total_amount_due_last_billing = randomFloat(750, 800, 2);
// --- Расчеты на основе сгенерированных данных ---
const delivery_charge = transmission_service + distribution_service + customer_charge_detail + retail_stability_rider + deferred_asset_rider + phase_in_recovery_rider + power_purchase_rider;
const current_charges = delivery_charge + supplier_charges;
const previous_balance_due = late_payment_charge;
const amount_due_value = current_charges + previous_balance_due;
// --- Генерация истории для графика ---
const history = [];
const monthNames = ["Aug", "Sep", "Oct", "Nov", "Dec", "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul"];
let tempDate = new Date(billDate);
tempDate.setMonth(tempDate.getMonth() - 12);
for (let i = 0; i < 13; i++) {
const isWinter = [10, 11, 0, 1].includes(tempDate.getMonth()); // Nov, Dec, Jan, Feb
let kwh = isWinter ? randomInt(6000, 9000) : randomInt(4000, 6000);
if (tempDate.getMonth() === currentMonth && tempDate.getFullYear() === currentYear) kwh = usage_current_month;
if (tempDate.getMonth() === (currentMonth - 1 + 12) % 12 && tempDate.getFullYear() === (currentMonth === 0 ? currentYear - 1 : currentYear)) kwh = usage_prev_month;
if (tempDate.getMonth() === currentMonth && tempDate.getFullYear() === currentYear - 1) kwh = usage_prev_year;
history.push({ month: monthNames[i % 12], year: tempDate.getFullYear(), kwh });
tempDate.setMonth(tempDate.getMonth() + 1);
}
return {
customer_name: faker.person.fullName().toUpperCase(),
attn_name: `ATTN: ${faker.person.firstName().toUpperCase()} ${faker.person.lastName().toUpperCase()}`,
service_address_full: `${faker.location.streetAddress()}, ${faker.location.city()}, ${faker.location.state({ abbreviated: true })} ${faker.location.zipCode()}`,
amount_due_date: formatDate(dueDate, 'Month DD, YYYY'),
amount_due_value: amount_due_value.toFixed(2),
bill_mailing_date: formatDate(billDate, 'Aug 8, 2022').replace(/\d+/g, (match) => faker.string.numeric(match.length)), // Генерируем дату в нужном формате, но с рандомными числами
account_number: faker.finance.accountNumber(10),
billing_period_from: formatDate(faker.date.recent({days: 35})),
billing_period_to: formatDate(faker.date.recent({days: 5})),
billing_period_days: `(${randomInt(28, 32)} days)`,
current_charges_kwh: usage_current_month.toLocaleString('en-US'),
remit_pay_after_date_amount: `Pay $${(amount_due_value * 1.01).toFixed(2)} after ${formatDate(dueDate, 'MM/DD/YYYY')}`,
barcode_number: faker.string.numeric(51),
total_amount_due_last_billing: total_amount_due_last_billing.toFixed(2),
payment_thank_you_date: formatDate(faker.date.recent({days: 5})),
payment_thank_you_amount: `-${total_amount_due_last_billing.toFixed(2)}`,
late_payment_charge: late_payment_charge.toFixed(2),
previous_balance_due: previous_balance_due.toFixed(2),
tariff_info_line: `Tariff ${randomInt(800,900)} - Medium General Service ${formatDate(billDate)}`,
service_delivery_identifier: faker.string.numeric(17),
transmission_service: transmission_service.toFixed(2),
distribution_service: distribution_service.toFixed(2),
customer_charge_detail: customer_charge_detail.toFixed(2),
retail_stability_rider: retail_stability_rider.toFixed(2),
deferred_asset_rider: deferred_asset_rider.toFixed(2),
phase_in_recovery_rider: phase_in_recovery_rider.toFixed(2),
power_purchase_rider: power_purchase_rider.toFixed(2),
supplier_charges: supplier_charges.toFixed(2),
current_electric_charges_total: delivery_charge.toFixed(2),
usage_history_data: history,
total_usage_12_months: `${randomInt(70000, 80000).toLocaleString('en-US')} kWh`,
avg_monthly_usage: `${randomInt(5500, 6500).toLocaleString('en-US')} kWh`,
billed_usage_date: formatDate(billDate, 'MM/YY'),
billed_usage_kwh_usage: usage_current_month.toLocaleString('en-US'),
billed_usage_kwh_billed: `${usage_current_month.toLocaleString('en-US')} kWh`,
billed_usage_kw_usage: randomFloat(15, 20, 3).toString(),
billed_usage_kw_billed: `${randomFloat(15, 20, 3)} kW`,
meter_read_details_meter_no: faker.string.numeric(8),
meter_read_kwh_current: randomInt(90000, 95000).toString(),
meter_read_kwh_previous: (90000 - usage_current_month).toString(),
meter_read_kwh_metered: usage_current_month.toString(),
meter_read_kwh_usage: `${usage_current_month.toLocaleString('en-US')} kWh`,
meter_read_kw_current: randomFloat(15, 20, 3).toString(),
meter_read_kw_metered: randomFloat(15, 20, 3).toString(),
meter_read_kw_usage: `${randomFloat(15, 20, 3)} kW`,
meter_read_service_period: `${formatDate(faker.date.recent({days: 35}), 'MM/DD')} - ${formatDate(faker.date.recent({days: 5}), 'MM/DD')}`,
next_read_date_info: `Next scheduled read date should be between ${formatDate(faker.date.soon({days: 10}), 'Mon DD')} and ${formatDate(faker.date.soon({days: 15}), 'Mon DD')}.`,
area_office: faker.string.numeric(5),
cashier_number: faker.string.numeric(3),
deposit_number: `${faker.string.numeric(9)} ${faker.string.numeric(4)} ${faker.string.numeric(3)} ${formatDate(billDate, 'YYYYMMDD')}`,
deposit_date: formatDate(billDate),
deposit_amount: `$${randomFloat(150, 200, 2)}`,
};
},
/**
* Генерирует данные для счета Entergy
*/
entergy: () => {
const mailDate = faker.date.past({ years: 2 });
const dueDate = new Date(mailDate);
dueDate.setDate(dueDate.getDate() + 23);
const currentMonthIndex = mailDate.getMonth();
const firstName = faker.person.firstName();
const lastName = faker.person.lastName();
const history = Array.from({ length: 12 }, (_, i) => {
const monthName = new Date(2000, i).toLocaleString('en-US', { month: 'short' }).toUpperCase();
let kwh = 0;
if (i <= currentMonthIndex) {
const isSummer = i >= 4 && i <= 7;
kwh = isSummer ? randomInt(2000, 3000) : randomInt(800, 1800);
}
return { month: monthName, kwh };
});
const kwh_used = history[currentMonthIndex].kwh;
const plan = randomFloat(80, 150, 2), fuel = randomFloat(40, 70, 2), other = randomFloat(15, 50, 2);
const totalDue = plan + fuel + other;
return {
customer_name: firstName,
mail_date: formatDate(mailDate),
account_number: faker.finance.accountNumber(9),
invoice_number: faker.finance.accountNumber(11),
amount_due_by_date: formatDate(dueDate),
amount_due_after_date: formatDate(dueDate),
amount_due_after_value: `$${(totalDue * 1.05).toFixed(2)}`,
service_location_line1: faker.location.streetAddress(),
service_location_line2: `${faker.location.city()}, ${faker.location.state({ abbreviated: true })} ${faker.location.zipCode()}`,
billing_days: randomInt(28, 32),
kwh_used,
avg_kwh_per_day: (kwh_used / 30).toFixed(1),
remit_customer_address: `${firstName.toUpperCase()} ${lastName.toUpperCase()}\n${faker.location.streetAddress()}\n${faker.location.city()}, ${faker.location.state({ abbreviated: true })} ${faker.location.zipCode()}`,
barcode_number: faker.string.numeric(50),
usage_history_data: history,
qpc_code: faker.string.numeric(5),
cycle_number: randomInt(1, 25),
energy_charge: (plan * 0.9).toFixed(2),
federal_eac_rider: (plan * 0.1).toFixed(2),
fuel_adjustment: fuel.toFixed(2),
storm_restoration_offset: (other * -0.1).toFixed(2),
storm_restoration_charge: (other * 1.1).toFixed(2),
deposit: "0.00",
connect_fee: "0.00"
};
},
/**
* Генерирует данные для счета Verizon
*/
verizon: () => {
const activation_fee = 35.00;
const plan_ultimate = 100.00;
const perk_apple_one = 10.00;
const surcharge_fus = randomFloat(5, 8, 2);
const surcharge_regulatory = randomFloat(0.1, 0.5, 2);
const surcharge_admin = randomFloat(3, 4, 2);
const tax_911 = 1.50;
const tax_ga_state = randomFloat(2, 3, 2);
const tax_fulton_county = randomFloat(2, 3, 2);
const dueDate = faker.date.future({ years: 1 });
return {
account_number: `${faker.string.numeric(9)}-00001`,
invoice_number: faker.string.numeric(10),
customer_name: faker.person.fullName().toUpperCase(),
customer_address: faker.location.streetAddress(),
customer_city_st_zip: `${faker.location.city()}, ${faker.location.state({ abbreviated: true })} ${faker.location.zipCode()}`,
total_due_date: formatDate(dueDate, 'Mon DD'),
full_due_date: formatDate(dueDate, 'Month DD, YYYY'),
bill_month: dueDate.toLocaleString('en-US', { month: 'long' }),
payment_received: '$0.00',
barcode_number: faker.string.numeric(52),
activation_fee: activation_fee.toFixed(2),
plan_ultimate: plan_ultimate.toFixed(2),
perk_apple_one: perk_apple_one.toFixed(2),
surcharge_fus: surcharge_fus.toFixed(2),
surcharge_regulatory: surcharge_regulatory.toFixed(2),
surcharge_admin: surcharge_admin.toFixed(2),
tax_911: tax_911.toFixed(2),
tax_ga_state: tax_ga_state.toFixed(2),
tax_fulton_county: tax_fulton_county.toFixed(2)
};
},
/**
* Генерирует данные для счета PG&E
*/
pge: () => {
// --- 1. Базовые случайные параметры для всего счета ---
const statementDate = faker.date.past({ years: 2 });
const dueDate = new Date(statementDate);
dueDate.setDate(dueDate.getDate() + 21);
const billing_days = randomInt(28, 31);
// --- 2. Генерируем данные для графика "Electric Usage This Period" (стр. 3) ---
const daily_electric_usage_history = [];
let total_kwh = 0;
for (let i = 0; i < billing_days; i++) {
const dayKwh = randomFloat(8, 25, 2);
total_kwh += dayKwh;
daily_electric_usage_history.push({ day: '', kwh: dayKwh });
}
const average_daily_usage = total_kwh / billing_days;
// --- 3. Генерируем данные для графика "Gas Usage This Period" (стр. 5) ---
const daily_gas_usage_history = [];
let total_therms = 0;
for (let i = 0; i < billing_days; i++) {
let thermValue = 0;
if (Math.random() < 0.3) { // Вероятность потребления газа в день ~30%
thermValue = randomFloat(0.5, 1.8, 2);
total_therms += thermValue;
}
daily_gas_usage_history.push({ day: '', gas: thermValue });
}
const gas_average_daily_usage = total_therms > 0 ? total_therms / billing_days : 0;
// --- 4. Рассчитываем итоговые суммы на основе сгенерированных данных ---
// Детализация Electric Charges (стр. 2)
const electric_conservation_incentive = -randomFloat(5, 10, 2);
const electric_transmission = randomFloat(10, 15, 2);
const electric_distribution = randomFloat(30, 40, 2);
const electric_public_purpose = randomFloat(4, 6, 2);
const electric_nuclear_decommissioning = randomFloat(0.2, 0.5, 2);
const electric_dwr_bond = randomFloat(1, 3, 2);
const electric_ctc = randomFloat(0.3, 0.6, 2);
const electric_ecra = -randomFloat(0.1, 0.4, 2);
const electric_pcia = randomFloat(8, 12, 2);
const electric_taxes_and_other = randomFloat(0.2, 0.3, 2);
const pge_delivery_charges_num = electric_conservation_incentive + electric_transmission + electric_distribution +
electric_public_purpose + electric_nuclear_decommissioning + electric_dwr_bond +
electric_ctc + electric_ecra + electric_pcia + electric_taxes_and_other;
// Детализация SVCE Charges (стр. 4)
const svce_generation_charges_num = randomFloat(30, 40, 2);
const total_amount_due_num = pge_delivery_charges_num + svce_generation_charges_num;
const previous_statement_num = randomFloat(total_amount_due_num * 0.9, total_amount_due_num * 1.1, 2);
return {
// --- Основные данные ---
account_no: `${faker.finance.accountNumber(10)}-${randomInt(1,9)}`,
statement_date: formatDate(statementDate),
due_date: formatDate(dueDate),
service_for_name: faker.person.fullName().toUpperCase(),
service_for_address: `${faker.location.buildingNumber()} ${faker.location.street()}`,
// --- Account Summary (стр. 1) ---
previous_statement: `${previous_statement_num.toFixed(2)}`,
payment_received: `-${previous_statement_num.toFixed(2)}`,
previous_unpaid_balance: '0.00',
pge_delivery_charges: `${pge_delivery_charges_num.toFixed(2)}`,
svce_generation_charges: `${svce_generation_charges_num.toFixed(2)}`,
total_amount_due_date: formatDate(dueDate),
total_amount_due: `${total_amount_due_num.toFixed(2)}`,
// --- Remittance Slip (стр. 1) ---
remit_account_number: `${faker.finance.accountNumber(9)}-1`,
remit_due_date: formatDate(dueDate),
remit_total_amount_due: `${total_amount_due_num.toFixed(2)}`,
// --- Данные для графиков (стр. 1, 3, 5) ---
monthly_billing_history: Array.from({length: 13}, () => ({ month: faker.date.month({abbreviated: true}), electric: randomInt(70, 140), gas: randomInt(10, 60) })),
daily_electric_usage_history: daily_electric_usage_history,
daily_gas_usage_history: daily_gas_usage_history,
average_daily_usage: average_daily_usage.toFixed(2),
gas_average_daily_usage: gas_average_daily_usage.toFixed(2),
daily_usage_gas: { year_ago: randomFloat(12, 13, 2).toFixed(2), last_period: randomFloat(12, 13, 2).toFixed(2), current_period: randomFloat(12, 13, 2).toFixed(2) },
daily_usage_electric: { year_ago: randomFloat(0.1, 0.2, 2).toFixed(2), last_period: randomFloat(0.1, 0.2, 2).toFixed(2), current_period: randomFloat(0.1, 0.2, 2).toFixed(2) },
// --- Детализация Electric Charges (стр. 2) ---
electric_conservation_incentive: `-${Math.abs(electric_conservation_incentive).toFixed(2)}`,
electric_transmission: electric_transmission.toFixed(2),
electric_distribution: electric_distribution.toFixed(2),
electric_public_purpose: electric_public_purpose.toFixed(2),
electric_nuclear_decommissioning: electric_nuclear_decommissioning.toFixed(2),
electric_dwr_bond: electric_dwr_bond.toFixed(2),
electric_ctc: electric_ctc.toFixed(2),
electric_ecra: `-${Math.abs(electric_ecra).toFixed(2)}`,
electric_pcia: electric_pcia.toFixed(2),
electric_taxes_and_other: electric_taxes_and_other.toFixed(2),
electric_total_charges: `${pge_delivery_charges_num.toFixed(2)}`,
// --- Детализация (стр. 3) ---
delivery_charges_period: `${formatDate(faker.date.recent({days: 35}))} - ${formatDate(faker.date.recent({days: 5}))} (${billing_days} billing days)`,
service_agreement_id: faker.string.numeric(9),
rate_schedule: `E${randomInt(1,6)} X Residential Service`,
meter_number: faker.string.numeric(10),
current_meter_reading: randomInt(30000, 40000).toLocaleString('en-US'),
prior_meter_reading: randomInt(28000, 29999).toLocaleString('en-US'),
total_usage_kwh: `${total_kwh.toFixed(6)} kWh`,
baseline_territory: faker.helpers.arrayElement(['X', 'Y', 'Z']),
heat_source: 'B - Not Electric',
tier1_allowance_detail: `${randomFloat(290, 310, 2)} kWh (${billing_days} days x ${randomFloat(9, 11, 1)} kWh/day)`,
tier1_usage_amount: `${randomFloat(60, 70, 2)}`,
tier2_usage_amount: `${randomFloat(20, 30, 2)}`,
generation_credit: `-${randomFloat(40, 50, 2)}`,
pcia_adjustment: `${randomFloat(10, 15, 2)}`,
franchise_fee_surcharge: `0.${randomInt(20,30)}`,
electric_usage_period_title: `Electric Usage This Period: ${total_kwh.toFixed(6)} kWh, ${billing_days} billing days`,
// --- Детализация (стр. 4) ---
svce_rate_schedule: 'E-1',
svce_generation_detail: `${total_kwh.toFixed(6)} kWh @ ${(svce_generation_charges_num / total_kwh).toFixed(5)}`,
svce_generation_amount: `${(svce_generation_charges_num * 0.99).toFixed(2)}`,
svce_energy_surcharge: (svce_generation_charges_num * 0.01).toFixed(2),
svce_total_charges: `${svce_generation_charges_num.toFixed(2)}`,
// --- Детализация (стр. 5) ---
gas_charges_period: `${formatDate(faker.date.recent({days: 35}))} - ${formatDate(faker.date.recent({days: 5}))} (${billing_days} billing days)`,
gas_service_agreement_id: faker.string.numeric(10),
gas_rate_schedule: `G1 X Residential Service`,
gas_meter_number: faker.string.numeric(8),
gas_current_reading: randomInt(2500, 2600).toLocaleString('en-US'),
gas_prior_reading: randomInt(2400, 2499).toLocaleString('en-US'),
gas_difference: randomInt(2, 10).toString(),
gas_multiplier: randomFloat(1, 1.1, 6).toString(),
gas_total_usage: `${total_therms.toFixed(6)} Therms`,
gas_baseline_territory: 'X',
gas_serial: 'G',
gas_tier1_allowance_detail: `${randomFloat(15, 20, 2)} Therms (${billing_days} days x 0.59 Therms/day)`,
gas_tier1_usage_detail: `${total_therms.toFixed(6)} Therms @ ${randomFloat(1, 1.5, 5)}`,
gas_tier1_usage_amount: `${randomFloat(5, 8, 2)}`,
gas_ppp_surcharge_detail: `($0.0${randomInt(9000, 9999)} /Therm)`,
gas_ppp_surcharge_amount: `0.${randomInt(40, 50)}`,
gas_total_charges: `${randomFloat(6, 9, 2)}`,
gas_procurement_period: `${formatDate(faker.date.recent({days: 65}))} - ${formatDate(faker.date.recent({days: 35}))}`,
gas_procurement_amount: `$0.${randomInt(25000, 30000)}`,
gas_usage_period_title: `Gas Usage This Period: ${total_therms.toFixed(6)} Therms, ${billing_days} billing days`
};
}
};
module.exports = generator;

View File

@ -0,0 +1,28 @@
const { pdfFillData, pdfFillDataXfinity, pdfFillDataCOX } = require('../src');
// Тестирование класса
const PFD1 = new pdfFillData('Xfinity');
const result1 = PFD1.getPdfDocumentName();
if (result1 !== 'Xfinity') {
throw new Error('Test 1 failed');
}
const PFD2 = new pdfFillData('COX');
const result2 = PFD2.getPdfDocumentName();
if (result2 !== 'COX') {
throw new Error('Test 2 failed');
}
const PFD3 = new pdfFillDataXfinity();
const result3 = PFD3.getPdfDocumentName();
if (result3 !== 'Xfinity') {
throw new Error('Test 3 failed');
}
const PFD4 = new pdfFillDataCOX();
const result4 = PFD4.getPdfDocumentName();
if (result4 !== 'COX') {
throw new Error('Test 4 failed');
}
console.log('All test passed!');

17
migration/config.py Normal file
View File

@ -0,0 +1,17 @@
host = "127.0.0.1"
port = "3307"
user = "admin"
password = "rNZzq5U37DqJlNe"
database = "pdfgen"
# Указываем путь к директории
directory_sql = "sql"
#
# nameGet = """ SELECT name FROM migration"""
#
#
# # Пример SQL-запроса для получения всех строк из таблицы в обратном порядке
# select_reverse_query = "SELECT * FROM migration ORDER BY id DESC LIMIT 1;"
#
# paste = """INSERT INTO migration (name, datatime) VALUES ('sql_1694778162', '2023-09-18 12:00:02')"""

0
migration/list.py Normal file
View File

128
migration/main.py Normal file
View File

@ -0,0 +1,128 @@
import mysql.connector
import os
import sys
import config
import calendar
import time
import datetime
import list
conn = mysql.connector.connect(host=config.host,
port=config.port,
user=config.user,
password=config.password,
database=config.database)
# Создайте объект cursor для выполнения SQL-запросов
cursor = conn.cursor()
cursor.execute("SHOW TABLES LIKE 'migration';")
# Получите результаты запроса
if len(cursor.fetchall()) >= 1:
roleTableExistenceCheck = True
else:
roleTableExistenceCheck = False
if not roleTableExistenceCheck:
cursor.execute("""CREATE TABLE migration(
ID INT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(255) CHARACTER SET utf8mb4,
datatime DATETIME );""")
print("Создали таблицу migration")
else:
print("migration уже существует")
# Закрыть соединение
conn.close()
#------------------------------------------
# Создаем пустой список
files = []
# Добавляем файлы в список
files += os.listdir(config.directory_sql)
def create():
print("Команда 'create'")
current_GMT = time.gmtime()
time_stamp = calendar.timegm(current_GMT)
filename = "sql/sql_{}.py".format(time_stamp)
os.makedirs(os.path.dirname(filename), exist_ok=True)
with open(filename, "w") as f:
f.write("sql = [] \n")
f.write("drop = [] \n")
filename_short = "sql_{}".format(time_stamp)
list_filename = "from sql import {} \n".format(filename_short)
with open('list.py', "a") as f:
f.write(list_filename)
def run():
try:
lastInjection = query("SELECT name FROM migration ORDER BY id DESC LIMIT 1;")
print(lastInjection)
for sm in sys.modules.keys():
if "sql.sql_" in sm:
smt = sm.replace("sql.", "")
# print(smt, lastInjection[0][0])
if len(lastInjection) < 1 or smt > lastInjection[0][0]:
try:
result = query(eval(f"list.{smt}.sql[0]"))
print(result)
except Exception as e:
print(f"Произошла ошибка в eval: {e}")
if result != "err":
#Создайте объект datetime
now = datetime.datetime.now()
# Преобразуйте его в строку в формате DATETIME
formatted_datetime = now.strftime('%Y-%m-%d %H:%M:%S')
query(f"""INSERT into migration (name, datatime) VALUES ('{smt}', '{formatted_datetime}');""")
except Exception as e:
print(f"Произошла ошибка: {e}")
def downgrade(arg):
print(f"Команда 'downgrade',c аргументом - '{arg}'")
reversTable = query(f"SELECT id FROM migration where name = '{arg}';")
for id in reversTable:
result = query(eval(f"list.{arg}.drop[0]"))
print(result)
if result != "err":
query(f"""delete from migration where id={id[0]};""")
def help():
print("migration.py create - Для создании новой миграции")
print("migration.py run - Для запуска миграций")
print("migration.py downgrade - Для отката миграций")
def query(sql):
try:
conn = mysql.connector.connect(host=config.host,
port=config.port,
user=config.user,
password=config.password,
database=config.database)
cursor = conn.cursor()
cursor.execute(sql)
result = cursor.fetchall()
conn.commit()
cursor.close()
conn.close()
except Exception as e:
result = 'err'
print(f"Произошла ошибка: {e}")
return result

32
migration/migrate.py Normal file
View File

@ -0,0 +1,32 @@
import argparse
import main
#_______________________________________________________________________________________________________________________
# Создание парсера аргументов
parser = argparse.ArgumentParser(description="Описание скрипта")
# Добавление аргументов для команды и имени файла
parser.add_argument('command', type=str, choices=['create', 'run', 'downgrade', 'help'], help='Команда: create или delete или downgrade или help')
#parser.add_argument('arg', type=str, help='Имя файла')
# Добавьте аргумент для имени файла или других параметров, если необходимо
parser.add_argument('file', nargs='?', help='Имя файла или другие параметры, связанные с командой')
# Разбор аргументов командной строки
args = parser.parse_args()
# В зависимости от команды, выполните нужные действия
if args.command == 'create':
main.create()
elif args.command == 'run':
main.run()
elif args.command == 'help':
main.help()
elif args.command == 'downgrade':
main.downgrade(args.file)
#_______________________________________________________________________________________________________________________

View File

@ -0,0 +1,5 @@
sql = ["""CREATE TABLE pdf (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(255) UNIQUE NOT NULL);"""]
drop = ["""DROP TABLE pdf;"""]

3198
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

45
package.json Normal file
View File

@ -0,0 +1,45 @@
{
"name": "pdfgen",
"version": "1.0.0",
"description": "Микросервис генерации PDF документов",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"repository": {
"type": "git",
"url": "git+https://gitflic.ru:evpak-alexandr/pdfgen.git"
},
"keywords": [],
"author": "",
"license": "ISC",
"bugs": {
"url": "https://gitflic.ru:evpak-alexandr/pdfgen/issues"
},
"homepage": "https://gitflic.ru:evpak-alexandr/pdfgen#readme",
"dependencies": {
"@faker-js/faker": "^9.9.0",
"axios": "^1.8.4",
"bwip-js": "^4.5.3",
"canvas": "^3.1.0",
"cheerio": "^1.1.2",
"cookie-parser": "^1.4.7",
"csv-parser": "^3.2.0",
"ejs": "^3.1.10",
"express": "^4.21.2",
"fontkit": "^2.0.4",
"jsonwebtoken": "^9.0.2",
"multer": "^1.4.5-lts.2",
"nodemailer": "^6.10.0",
"pdf-lib": "^1.17.1",
"pdfreader": "^3.0.7",
"socket.io": "^4.8.1",
"taxjar": "^4.1.0",
"util": "^0.12.5",
"uuid": "^11.1.0"
},
"devDependencies": {
"@types/uuid": "^10.0.0",
"nodemon": "^3.1.9"
}
}

3605
pge.map.json Normal file

File diff suppressed because it is too large Load Diff

BIN
public/.DS_Store vendored Normal file

Binary file not shown.

4
public/css/style.css Normal file
View File

@ -0,0 +1,4 @@
body {
margin: 0;
padding: 0;
}

View File

@ -0,0 +1,9 @@
Region,States,Plan,Price
West,"Arizona, California, Colorado, Idaho, Kansas, Minnesota, Missouri, New Mexico, Oregon, Texas, Utah, Washington, Wisconsin",Gigabit,$65.00/mo.
West,"Arizona, California, Colorado, Idaho, Kansas, Minnesota, Missouri, New Mexico, Oregon, Texas, Utah, Washington, Wisconsin",Fast,$50.00/mo.
West,"Arizona, California, Colorado, Idaho, Kansas, Minnesota, Missouri, New Mexico, Oregon, Texas, Utah, Washington, Wisconsin",Connect More,$30.00/mo.
Central,"Alabama, Arkansas, Florida, Georgia, Illinois, Indiana, Kentucky, Louisiana, Michigan, Mississippi, South Carolina, Tennessee",Gigabit,$75.00/mo.
Central,"Alabama, Arkansas, Florida, Georgia, Illinois, Indiana, Kentucky, Louisiana, Michigan, Mississippi, South Carolina, Tennessee",Fast,$65.00/mo.
Central,"Alabama, Arkansas, Florida, Georgia, Illinois, Indiana, Kentucky, Louisiana, Michigan, Mississippi, South Carolina, Tennessee",Connect,$35.00/mo.
Northeast,"Connecticut, Delaware, District Of Columbia, Maine, Maryland, Massachusetts, New Hampshire, New Jersey, New York, North Carolina, Ohio, Pennsylvania, Rhode Island, Vermont, Virginia, West Virginia",Gigabit,$60.00/mo.
Northeast,"Connecticut, Delaware, District Of Columbia, Maine, Maryland, Massachusetts, New Hampshire, New Jersey, New York, North Carolina, Ohio, Pennsylvania, Rhode Island, Vermont, Virginia, West Virginia",Fast,$45.00/mo.
1 Region States Plan Price
2 West Arizona, California, Colorado, Idaho, Kansas, Minnesota, Missouri, New Mexico, Oregon, Texas, Utah, Washington, Wisconsin Gigabit $65.00/mo.
3 West Arizona, California, Colorado, Idaho, Kansas, Minnesota, Missouri, New Mexico, Oregon, Texas, Utah, Washington, Wisconsin Fast $50.00/mo.
4 West Arizona, California, Colorado, Idaho, Kansas, Minnesota, Missouri, New Mexico, Oregon, Texas, Utah, Washington, Wisconsin Connect More $30.00/mo.
5 Central Alabama, Arkansas, Florida, Georgia, Illinois, Indiana, Kentucky, Louisiana, Michigan, Mississippi, South Carolina, Tennessee Gigabit $75.00/mo.
6 Central Alabama, Arkansas, Florida, Georgia, Illinois, Indiana, Kentucky, Louisiana, Michigan, Mississippi, South Carolina, Tennessee Fast $65.00/mo.
7 Central Alabama, Arkansas, Florida, Georgia, Illinois, Indiana, Kentucky, Louisiana, Michigan, Mississippi, South Carolina, Tennessee Connect $35.00/mo.
8 Northeast Connecticut, Delaware, District Of Columbia, Maine, Maryland, Massachusetts, New Hampshire, New Jersey, New York, North Carolina, Ohio, Pennsylvania, Rhode Island, Vermont, Virginia, West Virginia Gigabit $60.00/mo.
9 Northeast Connecticut, Delaware, District Of Columbia, Maine, Maryland, Massachusetts, New Hampshire, New Jersey, New York, North Carolina, Ohio, Pennsylvania, Rhode Island, Vermont, Virginia, West Virginia Fast $45.00/mo.

View File

@ -0,0 +1,58 @@
STATE(TERRITORY),ABBREVIATION
Alabama,AL
Alaska,AK
Arizona,AZ
Arkansas,AR
American Samoa,AS
California,CA
Colorado,CO
Connecticut,CT
Delaware,DE
District of Columbia,DC
Florida,FL
Georgia,GA
Guam,GU
Hawaii,HI
Idaho,ID
Illinois,IL
Indiana,IN
Iowa,IA
Kansas,KS
Kentucky,KY
Louisiana,LA
Maine,ME
Maryland,MD
Massachusetts,MA
Michigan,MI
Minnesota,MN
Mississippi,MS
Missouri,MO
Montana,MT
Nebraska,NE
Nevada,NV
New Hampshire,NH
New Jersey,NJ
New Mexico,NM
New York,NY
North Carolina,NC
North Dakota,ND
Northern Mariana Islands,MP
Ohio,OH
Oklahoma,OK
Oregon,OR
Pennsylvania,PA
Puerto Rico,PR
Rhode Island,RI
South Carolina,SC
South Dakota,SD
Tennessee,TN
Texas,TX
Trust Territories,TT
Utah,UT
Vermont,VT
Virginia,VA
Virgin Islands,VI
Washington,WA
West Virginia,WV
Wisconsin,WI
Wyoming,WY
1 STATE(TERRITORY) ABBREVIATION
2 Alabama AL
3 Alaska AK
4 Arizona AZ
5 Arkansas AR
6 American Samoa AS
7 California CA
8 Colorado CO
9 Connecticut CT
10 Delaware DE
11 District of Columbia DC
12 Florida FL
13 Georgia GA
14 Guam GU
15 Hawaii HI
16 Idaho ID
17 Illinois IL
18 Indiana IN
19 Iowa IA
20 Kansas KS
21 Kentucky KY
22 Louisiana LA
23 Maine ME
24 Maryland MD
25 Massachusetts MA
26 Michigan MI
27 Minnesota MN
28 Mississippi MS
29 Missouri MO
30 Montana MT
31 Nebraska NE
32 Nevada NV
33 New Hampshire NH
34 New Jersey NJ
35 New Mexico NM
36 New York NY
37 North Carolina NC
38 North Dakota ND
39 Northern Mariana Islands MP
40 Ohio OH
41 Oklahoma OK
42 Oregon OR
43 Pennsylvania PA
44 Puerto Rico PR
45 Rhode Island RI
46 South Carolina SC
47 South Dakota SD
48 Tennessee TN
49 Texas TX
50 Trust Territories TT
51 Utah UT
52 Vermont VT
53 Virginia VA
54 Virgin Islands VI
55 Washington WA
56 West Virginia WV
57 Wisconsin WI
58 Wyoming WY

File diff suppressed because one or more lines are too long

BIN
public/fonts/.DS_Store vendored Normal file

Binary file not shown.

BIN
public/fonts/ARIALN.TTF Normal file

Binary file not shown.

BIN
public/fonts/ARIALNB.TTF Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
public/fonts/Arial.ttf Normal file

Binary file not shown.

BIN
public/fonts/ArialBold.ttf Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
public/fonts/OCRAStd.ttf Normal file

Binary file not shown.

BIN
public/fonts/OcrA.ttf Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Some files were not shown because too many files have changed in this diff Show More