diff --git a/client/includes/header.php b/client/includes/header.php index 120326d23..ea8b9e14f 100644 --- a/client/includes/header.php +++ b/client/includes/header.php @@ -64,13 +64,14 @@ header("X-Frame-Options: DENY"); // Legacy diff --git a/client/post.php b/client/post.php index c7a1f4f33..81ef1953b 100644 --- a/client/post.php +++ b/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 = ' + + + + +
'; + if (!empty($company_logo) && file_exists("../uploads/settings/$company_logo")) { + $html .= ''; + } + $html .= ' + Account Statement
+ As of ' . date("Y-m-d") . ' +

'; + + $html .= ' + + + + + + + + +
' . $company_name . '' . $client_name . '
' . nl2br(formatAddress($company_address, $company_city, $company_state, $company_zip, $company_country) . "\n$company_phone\n$company_website") . '

'; + + // Statement lines + $html .= ' + + + + + + + + + '; + + $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 .= ' + + + + + + + + '; + } + + $html .= ' + + + +
InvoiceScopeDateDueAmountPaidBalance
' . $invoice_prefix . $invoice_number . '' . $invoice_scope . '' . $invoice_date . '' . $invoice_due . '' . numfmt_format_currency($currency_format, $invoice_amount, $session_company_currency) . '' . numfmt_format_currency($currency_format, $amount_paid, $session_company_currency) . '' . numfmt_format_currency($currency_format, $invoice_balance, $session_company_currency) . '
Total Balance Due' . numfmt_format_currency($currency_format, $statement_total, $session_company_currency) . '
'; + + $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, "/"); diff --git a/client/statement.php b/client/statement.php new file mode 100644 index 000000000..338485c8a --- /dev/null +++ b/client/statement.php @@ -0,0 +1,152 @@ + 0 + ORDER BY invoice_date ASC, invoice_number ASC" +); + +$statement_count = mysqli_num_rows($statement_sql); +$statement_total = 0; + +?> + +
+

Account Statement

+ 0) { ?> + + Download PDF + + +
+ +
+ +
+ + + +
+ There is nothing outstanding on this account. +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
#ScopeDateDueAmountPaidBalance
+ "> + + +
Total Balance Due
+ + + +
+ +
+ + +