mirror of
https://github.com/itflow-org/itflow
synced 2026-08-31 11:55:11 +00:00
Add Account Statment in Client Portal with option to download as a PDF
This commit is contained in:
@@ -64,13 +64,14 @@ header("X-Frame-Options: DENY"); // Legacy
|
||||
|
||||
<?php if (contactCan('accounting') && $config_module_enable_accounting == 1) { ?>
|
||||
<li class="nav-item dropdown">
|
||||
<a class="nav-link dropdown-toggle <?= in_array(basename($_SERVER['PHP_SELF']), ['invoices.php', 'quotes.php', 'autopay.php']) ? 'active' : '' ?>" href="#" id="navbarDropdown1" role="button" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
<a class="nav-link dropdown-toggle <?= in_array(basename($_SERVER['PHP_SELF']), ['invoices.php', 'quotes.php', 'autopay.php', 'statement.php']) ? 'active' : '' ?>" href="#" id="navbarDropdown1" role="button" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
Finance
|
||||
</a>
|
||||
<div class="dropdown-menu" aria-labelledby="navbarDropdown1">
|
||||
<a class="dropdown-item" href="/client/invoices.php">Invoices</a>
|
||||
<a class="dropdown-item" href="/client/recurring_invoices.php">Recurring Invoices</a>
|
||||
<a class="dropdown-item" href="/client/quotes.php">Quotes</a>
|
||||
<a class="dropdown-item" href="/client/statement.php">Account Statement</a>
|
||||
<a class="dropdown-item" href="/client/saved_payment_methods.php">Saved Payments</a>
|
||||
</div>
|
||||
</li>
|
||||
|
||||
159
client/post.php
159
client/post.php
@@ -348,6 +348,165 @@ if (isset($_GET['close_ticket'])) {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (isset($_GET['export_statement_pdf'])) {
|
||||
|
||||
validateCSRFToken();
|
||||
|
||||
enforceContactCan('accounting');
|
||||
|
||||
/*
|
||||
* SCOPING: the client is taken from the session, never from the request.
|
||||
* There is no client_id parameter on this handler by design - a contact
|
||||
* cannot ask for another company's statement because there is nothing to
|
||||
* ask with. enforceContactCan('accounting') above limits it to primary and
|
||||
* billing contacts, matching client/statement.php and client/invoices.php.
|
||||
*/
|
||||
$client_id = $session_client_id;
|
||||
|
||||
$sql = mysqli_query($mysqli, "SELECT client_name FROM clients WHERE client_id = $client_id LIMIT 1");
|
||||
$row = mysqli_fetch_assoc($sql);
|
||||
$client_name = escapeHtml($row['client_name']);
|
||||
|
||||
$sql = mysqli_query($mysqli, "SELECT company_address, company_city, company_country, company_logo, company_name,
|
||||
company_phone, company_phone_country_code, company_state, company_website, company_zip
|
||||
FROM companies WHERE company_id = 1");
|
||||
$row = mysqli_fetch_assoc($sql);
|
||||
|
||||
$company_name = escapeHtml($row['company_name']);
|
||||
$company_country = escapeHtml($row['company_country']);
|
||||
$company_address = escapeHtml($row['company_address']);
|
||||
$company_city = escapeHtml($row['company_city']);
|
||||
$company_state = escapeHtml($row['company_state']);
|
||||
$company_zip = escapeHtml($row['company_zip']);
|
||||
$company_phone = escapeHtml(formatPhoneNumber($row['company_phone'], $row['company_phone_country_code']));
|
||||
$company_website = escapeHtml($row['company_website']);
|
||||
$company_logo = escapeHtml($row['company_logo']);
|
||||
|
||||
// Same statement query as client/statement.php - payments summed in a
|
||||
// derived table so a twice-paid invoice is not counted twice, and the
|
||||
// balance test drops anything fully paid
|
||||
$statement_sql = mysqli_query(
|
||||
$mysqli,
|
||||
"SELECT invoice_amount, invoice_date, invoice_due, invoice_id, invoice_number, invoice_prefix,
|
||||
invoice_scope, IFNULL(amount_paid, 0) AS amount_paid
|
||||
FROM invoices
|
||||
LEFT JOIN (
|
||||
SELECT payment_invoice_id, SUM(payment_amount) AS amount_paid FROM payments
|
||||
WHERE payment_archived_at IS NULL
|
||||
GROUP BY payment_invoice_id
|
||||
) AS invoice_payments ON payment_invoice_id = invoice_id
|
||||
WHERE invoice_client_id = $client_id
|
||||
AND invoice_status NOT IN ('Draft', 'Cancelled', 'Non-Billable')
|
||||
AND invoice_amount - IFNULL(invoice_payments.amount_paid, 0) > 0
|
||||
ORDER BY invoice_date ASC, invoice_number ASC"
|
||||
);
|
||||
|
||||
if (mysqli_num_rows($statement_sql) == 0) {
|
||||
flashAlert("There is nothing outstanding to put on a statement", 'error');
|
||||
redirect("statement.php");
|
||||
}
|
||||
|
||||
require_once("../libs/TCPDF/tcpdf.php");
|
||||
|
||||
// Start TCPDF
|
||||
$pdf = new TCPDF('P', 'mm', 'A4', true, 'UTF-8', false);
|
||||
$pdf->SetMargins(10, 10, 10);
|
||||
$pdf->setPrintHeader(false);
|
||||
$pdf->setPrintFooter(false);
|
||||
$pdf->AddPage();
|
||||
$pdf->SetFont('helvetica', '', 10);
|
||||
|
||||
// Logo + title
|
||||
$html = '<table width="100%" cellspacing="0" cellpadding="3">
|
||||
<tr>
|
||||
<td width="40%">';
|
||||
if (!empty($company_logo) && file_exists("../uploads/settings/$company_logo")) {
|
||||
$html .= '<img src="/uploads/settings/' . $company_logo . '" width="120">';
|
||||
}
|
||||
$html .= '</td>
|
||||
<td width="60%" align="right">
|
||||
<span style="font-size:18pt; font-weight:bold;">Account Statement</span><br>
|
||||
<span style="font-size:11pt;">As of ' . date("Y-m-d") . '</span>
|
||||
</td>
|
||||
</tr>
|
||||
</table><br>';
|
||||
|
||||
$html .= '<table width="100%" cellspacing="0" cellpadding="2">
|
||||
<tr>
|
||||
<td width="50%" style="font-size:14pt; font-weight:bold;">' . $company_name . '</td>
|
||||
<td width="50%" align="right" style="font-size:14pt; font-weight:bold;">' . $client_name . '</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="font-size:10pt; line-height:1.4;">' . nl2br(formatAddress($company_address, $company_city, $company_state, $company_zip, $company_country) . "\n$company_phone\n$company_website") . '</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
</table><br>';
|
||||
|
||||
// Statement lines
|
||||
$html .= '<table border="0" cellpadding="4" cellspacing="0" width="100%">
|
||||
<tr style="background-color:#343a40; color:#ffffff; font-weight:bold;">
|
||||
<td width="14%">Invoice</td>
|
||||
<td width="30%">Scope</td>
|
||||
<td width="13%">Date</td>
|
||||
<td width="13%">Due</td>
|
||||
<td width="10%" align="right">Amount</td>
|
||||
<td width="10%" align="right">Paid</td>
|
||||
<td width="10%" align="right">Balance</td>
|
||||
</tr>';
|
||||
|
||||
$statement_total = 0;
|
||||
$statement_row_shade = false;
|
||||
|
||||
while ($row = mysqli_fetch_assoc($statement_sql)) {
|
||||
$invoice_prefix = escapeHtml($row['invoice_prefix']);
|
||||
$invoice_number = intval($row['invoice_number']);
|
||||
$invoice_scope = escapeHtml($row['invoice_scope']);
|
||||
$invoice_date = escapeHtml($row['invoice_date']);
|
||||
$invoice_due = escapeHtml($row['invoice_due']);
|
||||
$invoice_amount = floatval($row['invoice_amount']);
|
||||
$amount_paid = floatval($row['amount_paid']);
|
||||
$invoice_balance = $invoice_amount - $amount_paid;
|
||||
|
||||
$statement_total = $statement_total + $invoice_balance;
|
||||
|
||||
// Same one-day grace as client/statement.php and client/invoices.php
|
||||
if (strtotime($invoice_due) + 86400 < time()) {
|
||||
$due_style = ' style="color:#dc3545;"';
|
||||
} else {
|
||||
$due_style = '';
|
||||
}
|
||||
|
||||
$row_background = $statement_row_shade ? ' bgcolor="#f2f2f2"' : '';
|
||||
$statement_row_shade = !$statement_row_shade;
|
||||
|
||||
$html .= '<tr' . $row_background . '>
|
||||
<td style="font-size:9pt;">' . $invoice_prefix . $invoice_number . '</td>
|
||||
<td style="font-size:9pt;">' . $invoice_scope . '</td>
|
||||
<td style="font-size:9pt;">' . $invoice_date . '</td>
|
||||
<td style="font-size:9pt;"' . $due_style . '>' . $invoice_due . '</td>
|
||||
<td style="font-size:9pt;" align="right">' . numfmt_format_currency($currency_format, $invoice_amount, $session_company_currency) . '</td>
|
||||
<td style="font-size:9pt;" align="right">' . numfmt_format_currency($currency_format, $amount_paid, $session_company_currency) . '</td>
|
||||
<td style="font-size:9pt;" align="right">' . numfmt_format_currency($currency_format, $invoice_balance, $session_company_currency) . '</td>
|
||||
</tr>';
|
||||
}
|
||||
|
||||
$html .= '<tr>
|
||||
<td colspan="6" align="right" style="font-weight:bold;">Total Balance Due</td>
|
||||
<td align="right" style="font-weight:bold;">' . numfmt_format_currency($currency_format, $statement_total, $session_company_currency) . '</td>
|
||||
</tr>
|
||||
</table>';
|
||||
|
||||
$pdf->writeHTML($html, true, false, true, false, '');
|
||||
|
||||
$filename = toAlphanumeric($client_name) . "-Account_Statement-" . date("Y-m-d");
|
||||
|
||||
$pdf->Output("$filename.pdf", 'D');
|
||||
|
||||
exit();
|
||||
|
||||
}
|
||||
|
||||
if (isset($_GET['logout'])) {
|
||||
|
||||
setcookie("PHPSESSID", '', time() - 3600, "/");
|
||||
|
||||
152
client/statement.php
Normal file
152
client/statement.php
Normal file
@@ -0,0 +1,152 @@
|
||||
<?php
|
||||
/*
|
||||
* Client Portal
|
||||
* Account Statement
|
||||
*
|
||||
* SCOPING: every query on this page is keyed to $session_client_id, which
|
||||
* check_login.php sets from the session. Nothing here reads a client id from
|
||||
* the request, so there is no id for a contact to tamper with - and
|
||||
* enforceContactCan('accounting') keeps it to primary and billing contacts,
|
||||
* matching invoices.php and quotes.php.
|
||||
*/
|
||||
|
||||
header("Content-Security-Policy: default-src 'self'");
|
||||
|
||||
require_once "includes/inc_all.php";
|
||||
|
||||
|
||||
enforceContactCan('accounting');
|
||||
|
||||
/*
|
||||
* Payments are summed in a derived table rather than joined directly, or an
|
||||
* invoice with two payments against it would be counted twice.
|
||||
*
|
||||
* Draft / Cancelled / Non-Billable are not money owed, and the balance test
|
||||
* drops anything fully paid, so Paid invoices fall out without naming them.
|
||||
*/
|
||||
$statement_sql = mysqli_query(
|
||||
$mysqli,
|
||||
"SELECT invoice_amount, invoice_date, invoice_due, invoice_id, invoice_number, invoice_prefix,
|
||||
invoice_scope, invoice_url_key, IFNULL(amount_paid, 0) AS amount_paid
|
||||
FROM invoices
|
||||
LEFT JOIN (
|
||||
SELECT payment_invoice_id, SUM(payment_amount) AS amount_paid FROM payments
|
||||
WHERE payment_archived_at IS NULL
|
||||
GROUP BY payment_invoice_id
|
||||
) AS invoice_payments ON payment_invoice_id = invoice_id
|
||||
WHERE invoice_client_id = $session_client_id
|
||||
AND invoice_status NOT IN ('Draft', 'Cancelled', 'Non-Billable')
|
||||
AND invoice_amount - IFNULL(invoice_payments.amount_paid, 0) > 0
|
||||
ORDER BY invoice_date ASC, invoice_number ASC"
|
||||
);
|
||||
|
||||
$statement_count = mysqli_num_rows($statement_sql);
|
||||
$statement_total = 0;
|
||||
|
||||
?>
|
||||
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<h3 class="mb-0">Account Statement</h3>
|
||||
<?php if ($statement_count > 0) { ?>
|
||||
<a class="btn btn-primary" href="post.php?export_statement_pdf=1&csrf_token=<?= $_SESSION['csrf_token'] ?>">
|
||||
<i class="fa fa-fw fa-download me-2"></i>Download PDF
|
||||
</a>
|
||||
<?php } ?>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
|
||||
<div class="col-md-10">
|
||||
|
||||
<?php if ($statement_count == 0) { ?>
|
||||
|
||||
<div class="alert alert-success">
|
||||
<i class="fa fa-fw fa-check me-2"></i>There is nothing outstanding on this account.
|
||||
</div>
|
||||
|
||||
<?php } else { ?>
|
||||
|
||||
<table class="table table-bordered border border-dark">
|
||||
<thead class="table-dark">
|
||||
<tr>
|
||||
<th>#</th>
|
||||
<th>Scope</th>
|
||||
<th>Date</th>
|
||||
<th>Due</th>
|
||||
<th class="text-end">Amount</th>
|
||||
<th class="text-end">Paid</th>
|
||||
<th class="text-end">Balance</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
|
||||
<?php
|
||||
|
||||
while ($row = mysqli_fetch_assoc($statement_sql)) {
|
||||
$invoice_id = intval($row['invoice_id']);
|
||||
$invoice_prefix = escapeHtml($row['invoice_prefix']);
|
||||
$invoice_number = intval($row['invoice_number']);
|
||||
$invoice_scope = escapeHtml($row['invoice_scope']);
|
||||
$invoice_date = escapeHtml($row['invoice_date']);
|
||||
$invoice_due = escapeHtml($row['invoice_due']);
|
||||
$invoice_url_key = escapeHtml($row['invoice_url_key']);
|
||||
$invoice_amount = floatval($row['invoice_amount']);
|
||||
$amount_paid = floatval($row['amount_paid']);
|
||||
$invoice_balance = $invoice_amount - $amount_paid;
|
||||
|
||||
$statement_total = $statement_total + $invoice_balance;
|
||||
|
||||
if (empty($invoice_scope)) {
|
||||
$invoice_scope_display = "-";
|
||||
} else {
|
||||
$invoice_scope_display = $invoice_scope;
|
||||
}
|
||||
|
||||
// Same one-day grace as invoices.php, so the two pages agree
|
||||
// on what counts as late
|
||||
if (strtotime($invoice_due) + 86400 < time()) {
|
||||
$overdue_color = "text-danger fw-bold";
|
||||
} else {
|
||||
$overdue_color = "";
|
||||
}
|
||||
|
||||
?>
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
<a target="_blank" href="//<?= $config_base_url ?>/guest/guest_view_invoice.php?invoice_id=<?= "$invoice_id&url_key=$invoice_url_key" ?>">
|
||||
<?= "$invoice_prefix$invoice_number" ?>
|
||||
</a>
|
||||
</td>
|
||||
<td><?= $invoice_scope_display ?></td>
|
||||
<td><?= $invoice_date ?></td>
|
||||
<td class="<?= $overdue_color ?>"><?= $invoice_due ?></td>
|
||||
<td class="text-end"><?= numfmt_format_currency($currency_format, $invoice_amount, $session_company_currency) ?></td>
|
||||
<td class="text-end"><?= numfmt_format_currency($currency_format, $amount_paid, $session_company_currency) ?></td>
|
||||
<td class="text-end fw-bold"><?= numfmt_format_currency($currency_format, $invoice_balance, $session_company_currency) ?></td>
|
||||
</tr>
|
||||
|
||||
<?php
|
||||
|
||||
}
|
||||
|
||||
?>
|
||||
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr>
|
||||
<th colspan="6" class="text-end">Total Balance Due</th>
|
||||
<th class="text-end"><?= numfmt_format_currency($currency_format, $statement_total, $session_company_currency) ?></th>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
|
||||
<?php } ?>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
<?php
|
||||
require_once "includes/footer.php";
|
||||
Reference in New Issue
Block a user