Files
itflow/agent/reports/tax_summary.php
johnnyq 359851c876 Security sweep of the 26.11 work: API contact lookup by phone or mobile kept to the key's clients; API credentials refuse a wrong decrypt password instead of saving unreadable usernames and passwords, link credential_vendor_id and credential_software_id, and drop another client's contact or asset; Products Receive window lists expenses only with Financial access and only for clients the agent can open; account ledger, reconcile page and ledger export show lines for clients the agent can't open as Another client with only date, kind and amount, with search and the client filter kept to reachable clients; Tax Collected folds unreachable exempt clients into one line; client-limited agents no longer see other clients' names in the export window client filters, Transfer to Client, company vendor expense lists, My Activity or the New Client duplicate check; Stripe switched off stops guest pay links and saving cards, and the Stripe return never books a payment twice; Admin > Cron job window is admins only; records per page needs the CSRF token and takes only the listed sizes; uploads/canned_responses closed to direct access; Admin > Debug no longer stops at the footer; report export audit log no longer double-escapes names; bulk interface network refuses a deleted network; API assets/update keeps the primary interface IP, MAC and network, tickets/resolve reports success, software/read by name works, clients/create takes the company currency, certificates/create returns its error, clients/archive logs as Client, and the 405 response no longer echoes the method; changelog database step count corrected to sixty
2026-10-23 12:09:07 -04:00

265 lines
15 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
/*
* Tax Collected - the tax on sales by part of each rate (a state's and a
* county's apart), with the sales it was charged on and the taxable, exempt
* and non-taxable sales beside it, by month or quarter - what a sales tax,
* GST or VAT return asks for. Then the tax paid on purchases and what is
* left to pay, and the exempt sales by client with their certificates.
*
* Invoiced counts each invoice sent in the month of its date; Paid counts
* each payment, carrying its share of its invoice (functions/tax.php:
* taxReportSales()). Sales are net of discounts, before tax.
*/
require_once "includes/inc_report.php";
reportEnforce('tax_summary');
$basis = isset(TAX_REPORT_BASES[$_GET['basis'] ?? '']) ? $_GET['basis'] : 'paid';
[$year, $years] = reportYear($basis === 'invoiced'
? "SELECT YEAR(invoice_date) AS y FROM invoices WHERE invoice_status IN ('Sent', 'Viewed', 'Partial', 'Paid') UNION SELECT YEAR(expense_date) FROM expenses WHERE expense_tax_amount != 0"
: "SELECT YEAR(payment_date) AS y FROM payments UNION SELECT YEAR(expense_date) FROM expenses WHERE expense_tax_amount != 0");
$view = ($_GET['view'] ?? 'quarter') === 'month' ? 'month' : 'quarter';
$current_year = $year === intval(date('Y'));
$columns = reportYearColumns($view);
$to = $current_year ? date('Y-m-d') : "$year-12-31";
$sales = taxReportSales("$year-01-01", $to, $basis);
// The tax figures are the company's, but the clients behind them are only named to agents who can open them
$hidden_exempt = 0.0;
foreach ($sales['exempt_clients'] as $exempt_client_id => $exempt_client) {
if (!hasClientAccess($exempt_client_id)) {
$hidden_exempt += $exempt_client['amount'];
unset($sales['exempt_clients'][$exempt_client_id]);
}
}
if ($hidden_exempt != 0) {
$sales['exempt_clients'][-1] = ['name' => 'Clients you cannot open', 'number' => '', 'expires' => '', 'expired' => false, 'amount' => $hidden_exempt];
}
$purchases = taxReportPurchases("$year-01-01", $to);
$prev = taxReportSales(($year - 1) . '-01-01', $current_year ? dashboardShiftMonths(date('Y-m-d'), -12) : ($year - 1) . '-12-31', $basis);
$compare = $current_year ? 'the same time last year' : (string) ($year - 1);
$sum = function (array $months) {
return array_sum($months);
};
$tax_months = taxReportMonths();
foreach ($sales['parts'] as $part) {
foreach ($part['months'] as $m => $amount) {
$tax_months[$m] += $amount;
}
}
$paid_months = taxReportMonths();
foreach ($purchases['parts'] as $part) {
foreach ($part['months'] as $m => $amount) {
$paid_months[$m] += $amount;
}
}
$net_months = array_map(function ($collected, $paid) {
return $collected - $paid;
}, $tax_months, $paid_months);
$net_months = array_combine(range(1, 12), $net_months);
$tax_total = $sum($tax_months);
$paid_total = $sum($paid_months);
$prev_tax_total = array_sum(array_column($prev['parts'], 'total'));
$has_purchases = (bool) $purchases['parts'];
// A table row for each figure: [group, name, sub, months, emphasis]
$grid = [];
foreach ($sales['parts'] as $label => $part) {
$grid[] = ['Tax collected', $label, '', $part['months'], false];
}
$grid[] = ['Tax collected', 'Total tax collected', '', $tax_months, true];
$grid[] = ['Sales, before tax and net of discounts', 'Total sales', '', $sales['sales'], true];
$grid[] = ['Sales, before tax and net of discounts', 'Taxable', 'Lines with tax on them', $sales['taxable'], false];
$grid[] = ['Sales, before tax and net of discounts', 'Exempt', 'No tax, for clients who are tax exempt', $sales['exempt'], false];
$grid[] = ['Sales, before tax and net of discounts', 'Non-taxable', 'No tax, for clients who are not exempt', $sales['nontaxable'], false];
if ($has_purchases) {
foreach ($purchases['parts'] as $label => $part) {
$grid[] = ['Tax paid on purchases', $label, '', $part['months'], false];
}
$grid[] = ['Tax paid on purchases', 'Total tax paid', '', $paid_months, true];
$grid[] = ['Tax paid on purchases', 'Purchases it was paid on', 'Before tax', $purchases['purchases'], false];
$grid[] = ['Collected less paid', 'Net tax', 'For GST, HST and VAT returns that take off tax paid', $net_months, true];
}
if (reportExporting() && ($_GET['table'] ?? 'tax') === 'tax') {
$csv_columns = ['group' => ['label' => 'Section'], 'name' => ['label' => 'Figure'], 'registration' => ['label' => 'Registration']];
foreach ($columns as $i => [$label]) {
$csv_columns["c$i"] = ['label' => $label, 'type' => 'money'];
}
$csv_columns['total'] = ['label' => (string) $year, 'type' => 'money'];
$rows = [];
foreach ($grid as [$group, $name, $sub, $months]) {
$row = ['group' => $group, 'name' => $name, 'registration' => $group === 'Tax collected' ? ($sales['numbers'][$name] ?? '') : '', 'total' => round(array_sum($months), 2)];
foreach (reportSumColumns($months, $columns) as $i => $value) {
$row["c$i"] = round($value, 2);
}
$rows[] = $row;
}
reportCsv('Tax Collected ' . $year . ' ' . TAX_REPORT_BASES[$basis], $csv_columns, $rows);
}
if (reportExporting('exempt')) {
$rows = [];
foreach ($sales['exempt_clients'] as $client) {
$rows[] = ['client' => $client['name'], 'number' => $client['number'], 'expires' => $client['expires'], 'amount' => round($client['amount'], 2)];
}
reportCsv("Exempt Sales $year", [
'client' => ['label' => 'Client'], 'number' => ['label' => 'Certificate'], 'expires' => ['label' => 'Expires'], 'amount' => ['label' => 'Exempt sales', 'type' => 'money'],
], $rows);
}
require_once "includes/inc_report_layout.php";
$span = count($columns) + 2;
$basis_note = $basis === 'invoiced'
? 'Counted on each invoice\'s date once it is sent, paid or not. Drafts, cancelled and non-billable invoices are left out.'
: 'Counted as payments come in: each payment carries its share of its invoice\'s tax and sales, so a part-paid invoice counts only the part paid.';
$stats = [
['label' => 'Tax collected', 'value' => reportMoney($tax_total), 'sub' => reportChangeHtml($tax_total, $prev_tax_total, 'up', $compare)],
['label' => 'Taxable sales', 'value' => reportMoney($sum($sales['taxable'])), 'sub' => '<span class="text-body-tertiary">Of ' . escapeHtml(reportMoney($sum($sales['sales']))) . ' in sales</span>'],
['label' => 'Exempt sales', 'value' => reportMoney($sum($sales['exempt'])), 'sub' => '<span class="text-body-tertiary">' . count($sales['exempt_clients']) . ' exempt client' . (count($sales['exempt_clients']) === 1 ? '' : 's') . '</span>'],
['label' => 'Non-taxable sales', 'value' => reportMoney($sum($sales['nontaxable'])), 'sub' => '<span class="text-body-tertiary">No tax on the line</span>'],
];
if ($has_purchases) {
$stats[] = ['label' => 'Tax paid on purchases', 'value' => reportMoney($paid_total), 'sub' => '<span class="text-body-tertiary">On ' . $purchases['expenses'] . ' expense' . ($purchases['expenses'] === 1 ? '' : 's') . '</span>'];
$stats[] = ['label' => 'Net tax', 'value' => reportMoney($tax_total - $paid_total), 'sub' => '<span class="text-body-tertiary">Collected less paid</span>'];
}
?>
<div class="itflow-report">
<?php reportHeader('tax_summary', [
'year' => [$year, $years],
'controls' => reportSegmentControl('basis', $basis, TAX_REPORT_BASES) . reportSegmentControl('view', $view, ['quarter' => 'Quarterly', 'month' => 'Monthly']),
'subtitle' => ($current_year ? "$year to date" : (string) $year) . ' · ' . TAX_REPORT_BASES[$basis],
'export' => $sales['exempt_clients'] ? ['tax' => 'Tax by ' . $view, 'exempt' => 'Exempt sales by client'] : ['tax' => 'Tax by ' . $view],
'note' => escapeHtml($basis_note) . ' Sales are before tax and net of discounts.',
'print_meta' => TAX_REPORT_BASES[$basis] . ' basis',
]); ?>
<?= reportStats($stats) ?>
<?php if (!$sales['invoices'] && !$has_purchases) { ?>
<div class="card"><div class="card-body"><?= reportEmpty($basis === 'invoiced' ? "No invoices were sent in $year." : "No payments came in during $year.", 'fa-percent', 'secondary') ?></div></div>
<?php } else { ?>
<?php if ($tax_total != 0) { ?>
<?= reportSectionOpen('Tax collected by month', escapeHtml((string) $year)) ?>
<div class="card-body">
<?php
$months_shown = $current_year ? intval(date('n')) : 12;
$series = [];
$i = 0;
foreach ($sales['parts'] as $label => $part) {
if (abs($part['total']) < 0.005) {
continue;
}
$series[] = ['label' => $label, 'data' => array_map(function ($m) use ($part, $months_shown) {
return $m <= $months_shown ? round($part['months'][$m], 2) : null;
}, range(1, 12)), 'tone' => reportPalette($i++), 'stack' => 'tax'];
}
echo reportChart([
'type' => 'bar',
'labels' => array_map(function ($m) { return date('M', mktime(0, 0, 0, $m, 1)); }, range(1, 12)),
'series' => $series,
'stacked' => true,
'format' => 'money',
], 'sm', 'Tax collected per month, by part');
?>
</div>
</div>
<?php } ?>
<?= reportSectionOpen('By ' . ($view === 'quarter' ? 'quarter' : 'month'), $view === 'quarter' ? 'Calendar quarters' : '') ?>
<div class="table-responsive">
<table class="table table-sm table-hover align-middle mb-0 itflow-report-table itflow-report-grid">
<thead class="table-light">
<tr>
<th><?= TAX_REPORT_BASES[$basis] ?> basis</th>
<?php foreach ($columns as [$label, $first, $last]) { ?>
<th class="text-end"<?php if ($view === 'quarter') { ?> title="<?= date('M', mktime(0, 0, 0, $first, 1)) ?> – <?= date('M', mktime(0, 0, 0, $last, 1)) ?>"<?php } ?>><?= $label ?></th>
<?php } ?>
<th class="text-end"><?= $year ?></th>
</tr>
</thead>
<tbody>
<?php
$group_shown = '';
foreach ($grid as [$group, $name, $sub, $months, $strong]) {
if ($group !== $group_shown) {
$group_shown = $group; ?>
<tr class="itflow-report-group-row"><td colspan="<?= $span ?>"><?= escapeHtml($group) ?></td></tr>
<?php }
$cells = reportSumColumns($months, $columns);
$registration = $group === 'Tax collected' ? ($sales['numbers'][$name] ?? '') : ''; ?>
<tr<?= $strong ? ' class="fw-semibold"' : '' ?>>
<td class="<?= $strong ? '' : 'itflow-report-sub' ?>">
<?= escapeHtml($name) ?>
<?php if ($sub !== '') { ?><div class="small text-body-secondary fw-normal"><?= escapeHtml($sub) ?></div><?php } ?>
<?php if ($registration !== '') { ?><div class="small text-body-secondary fw-normal">No. <?= escapeHtml($registration) ?></div><?php } ?>
</td>
<?php foreach ($cells as $value) { ?>
<td class="text-end<?= $strong ? '' : ' text-body-secondary' ?>"><?= abs($value) < 0.005 ? '<span class="itflow-report-zero">–</span>' : reportMoney($value) ?></td>
<?php } ?>
<td class="text-end fw-semibold"><?= reportMoney(array_sum($months)) ?></td>
</tr>
<?php } ?>
</tbody>
</table>
</div>
<div class="card-footer small text-body-secondary">
Tax shows by part, so a rate made of a state's and a county's part counts each where it is due. A rate that changed shows its old and new percents apart.
Exempt sales are untaxed lines for clients marked tax exempt now. Check the figures with your accountant before filing.
</div>
</div>
<?php if ($sales['exempt_clients']) { ?>
<?= reportSectionOpen('Exempt sales by client', count($sales['exempt_clients']) . ' client' . (count($sales['exempt_clients']) === 1 ? '' : 's')) ?>
<div class="table-responsive">
<table class="table table-sm table-hover align-middle mb-0 itflow-report-table">
<thead class="table-light">
<tr>
<th>Client</th>
<th>Certificate</th>
<th>Expires</th>
<th class="text-end">Exempt sales</th>
</tr>
</thead>
<tbody>
<?php foreach ($sales['exempt_clients'] as $client_id => $client) { ?>
<tr>
<?php if ($client_id < 0) { ?>
<td><span class="text-body-secondary"><?= escapeHtml($client['name']) ?></span></td>
<td><span class="text-body-secondary">–</span></td>
<?php } else { ?>
<td><?= reportClientLink($client_id, $client['name']) ?></td>
<td><?= $client['number'] !== '' ? escapeHtml($client['number']) : '<span class="text-warning-emphasis">None on file</span>' ?></td>
<?php } ?>
<td>
<?php if ($client['expires'] === '') { ?>
<span class="text-body-secondary">–</span>
<?php } else { ?>
<?= escapeHtml(date('M j, Y', strtotime($client['expires']))) ?>
<?php if ($client['expired']) { ?><span class="badge rounded-pill bg-warning-subtle text-warning-emphasis fw-normal ms-1">Expired</span><?php } ?>
<?php } ?>
</td>
<td class="text-end"><?= reportMoney($client['amount']) ?></td>
</tr>
<?php } ?>
</tbody>
</table>
</div>
</div>
<?php } ?>
<?php } ?>
</div>
<?php require_once "../../includes/footer.php"; ?>
<script src="/js/reports.js"></script>