first commit
This commit is contained in:
commit
48bb7355f9
BIN
Core/.DS_Store
vendored
Normal file
BIN
Core/.DS_Store
vendored
Normal file
Binary file not shown.
21
Core/js/getcookies.js
Normal file
21
Core/js/getcookies.js
Normal 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
29
Core/js/sendmail.js
Normal 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;
|
433
Core/views/aep_ohio_form.ejs
Normal file
433
Core/views/aep_ohio_form.ejs
Normal 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
25
Core/views/api.ejs
Normal 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
265
Core/views/entergy_form.ejs
Normal 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
266
Core/views/home.ejs
Normal 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
559
Core/views/pge_form.ejs
Normal 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
195
Core/views/verizon_form.ejs
Normal 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
23
Dockerfile
Normal 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"]
|
119
README.md
Normal file
119
README.md
Normal 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
133
analyze_pdf.py
Normal 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
50
check_pdf_fonts.py
Normal 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
13
config.js
Normal 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
30
docker-compose.yml
Normal 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:
|
Binary file not shown.
BIN
ilide.info-spectrum-bill-pr_40c465c13b0b153cd82ab3b430364ab3.pdf
Normal file
BIN
ilide.info-spectrum-bill-pr_40c465c13b0b153cd82ab3b430364ab3.pdf
Normal file
Binary file not shown.
32
index.js
Normal file
32
index.js
Normal 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
9
init.sql
Normal 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
86
json_to_svg.py
Normal 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)
|
23
lib/pdf-fill-data/README.md
Normal file
23
lib/pdf-fill-data/README.md
Normal 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
|
19
lib/pdf-fill-data/package.json
Normal file
19
lib/pdf-fill-data/package.json
Normal 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"
|
||||
}
|
||||
}
|
148
lib/pdf-fill-data/src/AEPOhio.map.json
Normal file
148
lib/pdf-fill-data/src/AEPOhio.map.json
Normal 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 }
|
||||
]
|
||||
}
|
188
lib/pdf-fill-data/src/Entergy.map.json
Normal file
188
lib/pdf-fill-data/src/Entergy.map.json
Normal 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" }
|
||||
]
|
||||
}
|
151
lib/pdf-fill-data/src/Verizon.map.json
Normal file
151
lib/pdf-fill-data/src/Verizon.map.json
Normal 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" }
|
||||
]
|
||||
}
|
18
lib/pdf-fill-data/src/index.js
Normal file
18
lib/pdf-fill-data/src/index.js
Normal 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')
|
||||
};
|
684
lib/pdf-fill-data/src/pdfFillData.js
Normal file
684
lib/pdf-fill-data/src/pdfFillData.js
Normal 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;
|
582
lib/pdf-fill-data/src/pdfFillDataAEPOhio.js
Normal file
582
lib/pdf-fill-data/src/pdfFillDataAEPOhio.js
Normal 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;
|
1453
lib/pdf-fill-data/src/pdfFillDataATT.js
Normal file
1453
lib/pdf-fill-data/src/pdfFillDataATT.js
Normal file
File diff suppressed because it is too large
Load Diff
1021
lib/pdf-fill-data/src/pdfFillDataCOX.js
Normal file
1021
lib/pdf-fill-data/src/pdfFillDataCOX.js
Normal file
File diff suppressed because it is too large
Load Diff
193
lib/pdf-fill-data/src/pdfFillDataDukeEnergy.js
Normal file
193
lib/pdf-fill-data/src/pdfFillDataDukeEnergy.js
Normal 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;
|
582
lib/pdf-fill-data/src/pdfFillDataEntergy.js
Normal file
582
lib/pdf-fill-data/src/pdfFillDataEntergy.js
Normal 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;
|
548
lib/pdf-fill-data/src/pdfFillDataPGE.js
Normal file
548
lib/pdf-fill-data/src/pdfFillDataPGE.js
Normal 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;
|
1453
lib/pdf-fill-data/src/pdfFillDataSpectrum.js
Normal file
1453
lib/pdf-fill-data/src/pdfFillDataSpectrum.js
Normal file
File diff suppressed because it is too large
Load Diff
174
lib/pdf-fill-data/src/pdfFillDataVerizon.js
Normal file
174
lib/pdf-fill-data/src/pdfFillDataVerizon.js
Normal 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;
|
675
lib/pdf-fill-data/src/pdfFillDataXfinity.js
Normal file
675
lib/pdf-fill-data/src/pdfFillDataXfinity.js
Normal 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;
|
367
lib/pdf-fill-data/src/randomDataGenerator.js
Normal file
367
lib/pdf-fill-data/src/randomDataGenerator.js
Normal 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;
|
28
lib/pdf-fill-data/test/index.test.js
Normal file
28
lib/pdf-fill-data/test/index.test.js
Normal 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
17
migration/config.py
Normal 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
0
migration/list.py
Normal file
128
migration/main.py
Normal file
128
migration/main.py
Normal 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
32
migration/migrate.py
Normal 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)
|
||||
|
||||
|
||||
|
||||
#_______________________________________________________________________________________________________________________
|
||||
|
5
migration/sql/sql_1695129841.py
Normal file
5
migration/sql/sql_1695129841.py
Normal 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
3198
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
45
package.json
Normal file
45
package.json
Normal 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
3605
pge.map.json
Normal file
File diff suppressed because it is too large
Load Diff
BIN
public/.DS_Store
vendored
Normal file
BIN
public/.DS_Store
vendored
Normal file
Binary file not shown.
4
public/css/style.css
Normal file
4
public/css/style.css
Normal file
@ -0,0 +1,4 @@
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
9
public/csv/Xfinity_States_Plans_by_Region.csv
Normal file
9
public/csv/Xfinity_States_Plans_by_Region.csv
Normal 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.
|
|
58
public/csv/Xfinity_socr.csv
Normal file
58
public/csv/Xfinity_socr.csv
Normal 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
|
|
1336
public/download/DukeEnergy_bb492448-6fee-4ef5-bda1-c308b70fa21b.pdf
Normal file
1336
public/download/DukeEnergy_bb492448-6fee-4ef5-bda1-c308b70fa21b.pdf
Normal file
File diff suppressed because one or more lines are too long
BIN
public/fonts/.DS_Store
vendored
Normal file
BIN
public/fonts/.DS_Store
vendored
Normal file
Binary file not shown.
BIN
public/fonts/ARIALN.TTF
Normal file
BIN
public/fonts/ARIALN.TTF
Normal file
Binary file not shown.
BIN
public/fonts/ARIALNB.TTF
Normal file
BIN
public/fonts/ARIALNB.TTF
Normal file
Binary file not shown.
BIN
public/fonts/ATT Aleck Sans Black Italic.ttf
Normal file
BIN
public/fonts/ATT Aleck Sans Black Italic.ttf
Normal file
Binary file not shown.
BIN
public/fonts/ATT Aleck Sans Black Regular.ttf
Normal file
BIN
public/fonts/ATT Aleck Sans Black Regular.ttf
Normal file
Binary file not shown.
BIN
public/fonts/ATT Aleck Sans Italic.ttf
Normal file
BIN
public/fonts/ATT Aleck Sans Italic.ttf
Normal file
Binary file not shown.
BIN
public/fonts/ATT Aleck Sans Medium Regular.ttf
Normal file
BIN
public/fonts/ATT Aleck Sans Medium Regular.ttf
Normal file
Binary file not shown.
BIN
public/fonts/ATT Aleck Sans Regular.ttf
Normal file
BIN
public/fonts/ATT Aleck Sans Regular.ttf
Normal file
Binary file not shown.
BIN
public/fonts/Arial.ttf
Normal file
BIN
public/fonts/Arial.ttf
Normal file
Binary file not shown.
BIN
public/fonts/ArialBold.ttf
Normal file
BIN
public/fonts/ArialBold.ttf
Normal file
Binary file not shown.
BIN
public/fonts/ArialItalic.ttf
Normal file
BIN
public/fonts/ArialItalic.ttf
Normal file
Binary file not shown.
BIN
public/fonts/Gotham-Bold.otf
Normal file
BIN
public/fonts/Gotham-Bold.otf
Normal file
Binary file not shown.
BIN
public/fonts/Gotham-Book.otf
Normal file
BIN
public/fonts/Gotham-Book.otf
Normal file
Binary file not shown.
BIN
public/fonts/OCRAStd.ttf
Normal file
BIN
public/fonts/OCRAStd.ttf
Normal file
Binary file not shown.
BIN
public/fonts/OcrA.ttf
Normal file
BIN
public/fonts/OcrA.ttf
Normal file
Binary file not shown.
BIN
public/fonts/OpenSans-Bold.ttf
Normal file
BIN
public/fonts/OpenSans-Bold.ttf
Normal file
Binary file not shown.
BIN
public/fonts/OpenSans-BoldItalic.ttf
Normal file
BIN
public/fonts/OpenSans-BoldItalic.ttf
Normal file
Binary file not shown.
BIN
public/fonts/OpenSans-ExtraBold.ttf
Normal file
BIN
public/fonts/OpenSans-ExtraBold.ttf
Normal file
Binary file not shown.
BIN
public/fonts/OpenSans-ExtraBoldItalic.ttf
Normal file
BIN
public/fonts/OpenSans-ExtraBoldItalic.ttf
Normal file
Binary file not shown.
BIN
public/fonts/OpenSans-Italic-VariableFont_wdth,wght.ttf
Normal file
BIN
public/fonts/OpenSans-Italic-VariableFont_wdth,wght.ttf
Normal file
Binary file not shown.
BIN
public/fonts/OpenSans-Italic.ttf
Normal file
BIN
public/fonts/OpenSans-Italic.ttf
Normal file
Binary file not shown.
BIN
public/fonts/OpenSans-Light.ttf
Normal file
BIN
public/fonts/OpenSans-Light.ttf
Normal file
Binary file not shown.
BIN
public/fonts/OpenSans-LightItalic.ttf
Normal file
BIN
public/fonts/OpenSans-LightItalic.ttf
Normal file
Binary file not shown.
BIN
public/fonts/OpenSans-Medium.ttf
Normal file
BIN
public/fonts/OpenSans-Medium.ttf
Normal file
Binary file not shown.
BIN
public/fonts/OpenSans-MediumItalic.ttf
Normal file
BIN
public/fonts/OpenSans-MediumItalic.ttf
Normal file
Binary file not shown.
BIN
public/fonts/OpenSans-Regular.ttf
Normal file
BIN
public/fonts/OpenSans-Regular.ttf
Normal file
Binary file not shown.
BIN
public/fonts/OpenSans-SemiBold.ttf
Normal file
BIN
public/fonts/OpenSans-SemiBold.ttf
Normal file
Binary file not shown.
BIN
public/fonts/OpenSans-SemiBoldItalic.ttf
Normal file
BIN
public/fonts/OpenSans-SemiBoldItalic.ttf
Normal file
Binary file not shown.
BIN
public/fonts/OpenSans-VariableFont_wdth,wght.ttf
Normal file
BIN
public/fonts/OpenSans-VariableFont_wdth,wght.ttf
Normal file
Binary file not shown.
BIN
public/fonts/OpenSans_Condensed-Bold.ttf
Normal file
BIN
public/fonts/OpenSans_Condensed-Bold.ttf
Normal file
Binary file not shown.
BIN
public/fonts/OpenSans_Condensed-BoldItalic.ttf
Normal file
BIN
public/fonts/OpenSans_Condensed-BoldItalic.ttf
Normal file
Binary file not shown.
BIN
public/fonts/OpenSans_Condensed-ExtraBold.ttf
Normal file
BIN
public/fonts/OpenSans_Condensed-ExtraBold.ttf
Normal file
Binary file not shown.
BIN
public/fonts/OpenSans_Condensed-ExtraBoldItalic.ttf
Normal file
BIN
public/fonts/OpenSans_Condensed-ExtraBoldItalic.ttf
Normal file
Binary file not shown.
BIN
public/fonts/OpenSans_Condensed-Italic.ttf
Normal file
BIN
public/fonts/OpenSans_Condensed-Italic.ttf
Normal file
Binary file not shown.
BIN
public/fonts/OpenSans_Condensed-Light.ttf
Normal file
BIN
public/fonts/OpenSans_Condensed-Light.ttf
Normal file
Binary file not shown.
BIN
public/fonts/OpenSans_Condensed-LightItalic.ttf
Normal file
BIN
public/fonts/OpenSans_Condensed-LightItalic.ttf
Normal file
Binary file not shown.
BIN
public/fonts/OpenSans_Condensed-Medium.ttf
Normal file
BIN
public/fonts/OpenSans_Condensed-Medium.ttf
Normal file
Binary file not shown.
BIN
public/fonts/OpenSans_Condensed-MediumItalic.ttf
Normal file
BIN
public/fonts/OpenSans_Condensed-MediumItalic.ttf
Normal file
Binary file not shown.
BIN
public/fonts/OpenSans_Condensed-Regular.ttf
Normal file
BIN
public/fonts/OpenSans_Condensed-Regular.ttf
Normal file
Binary file not shown.
BIN
public/fonts/OpenSans_Condensed-SemiBold.ttf
Normal file
BIN
public/fonts/OpenSans_Condensed-SemiBold.ttf
Normal file
Binary file not shown.
BIN
public/fonts/OpenSans_Condensed-SemiBoldItalic.ttf
Normal file
BIN
public/fonts/OpenSans_Condensed-SemiBoldItalic.ttf
Normal file
Binary file not shown.
BIN
public/fonts/OpenSans_SemiCondensed-Bold.ttf
Normal file
BIN
public/fonts/OpenSans_SemiCondensed-Bold.ttf
Normal file
Binary file not shown.
BIN
public/fonts/OpenSans_SemiCondensed-BoldItalic.ttf
Normal file
BIN
public/fonts/OpenSans_SemiCondensed-BoldItalic.ttf
Normal file
Binary file not shown.
BIN
public/fonts/OpenSans_SemiCondensed-ExtraBold.ttf
Normal file
BIN
public/fonts/OpenSans_SemiCondensed-ExtraBold.ttf
Normal file
Binary file not shown.
BIN
public/fonts/OpenSans_SemiCondensed-ExtraBoldItalic.ttf
Normal file
BIN
public/fonts/OpenSans_SemiCondensed-ExtraBoldItalic.ttf
Normal file
Binary file not shown.
BIN
public/fonts/OpenSans_SemiCondensed-Italic.ttf
Normal file
BIN
public/fonts/OpenSans_SemiCondensed-Italic.ttf
Normal file
Binary file not shown.
BIN
public/fonts/OpenSans_SemiCondensed-Light.ttf
Normal file
BIN
public/fonts/OpenSans_SemiCondensed-Light.ttf
Normal file
Binary file not shown.
BIN
public/fonts/OpenSans_SemiCondensed-LightItalic.ttf
Normal file
BIN
public/fonts/OpenSans_SemiCondensed-LightItalic.ttf
Normal file
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user