Files
itflow/agent/invoice.php
johnnyq 1e5bffff3c Rates, net terms and SLAs come from the client's contract, else the instance defaults: database 3.5.0 adds a default after-hours rate (Settings > Defaults, now Rates and terms), gives each client whose own rate, net terms or SLAs differ from the defaults an active Standard terms contract setting just those (12-month term renewing itself, starting the day the client was added or the day before its earliest contract, archived with an archived client), deletes per-client SLA assignments and drops clients.client_rate and client_net_terms; time bills at the contract's rate, its after-hours rate or the default after-hours rate, else the default rate, new invoices are due on the contract's net terms else the default, and new tickets get the contract's SLA else the default; a client's own terms still apply until the update runs; the client add and edit windows lose the rate, terms and SLA fields for a note saying what the client is billed on and where it comes from, the Clients list loses Set Hourly Rate and Set Net Terms and shows the contract's rate, the overview's Terms shows the after-hours rate and whether terms are a contract's or the defaults, a contract's page and document show what it leaves empty from another contract in force or the default so signed documents stay unchanged, contract windows show the defaults as placeholders, contract names take a proper possessive; the client import and API create make a Standard terms contract for a rate or terms that aren't the defaults (returning contract_id), API update reports them in not_kept; the client export gives the rates, terms and SLAs as billed; demo data puts each demo client's terms on a contract; changelog updated
2026-10-27 22:22:34 -04:00

609 lines
42 KiB
PHP

<?php
/*
* Invoice - one invoice, the way an MSP works it.
*
* The header says where it stands - its status, whether it is due or late
* and by how long - and has the next thing to do with it: send a draft,
* take a payment on one that is owed. The invoice itself is on the left,
* as the client sees it, with its lines edited in place while it is open.
* On the right: what has been paid and what is left, the payments, the
* hardware sold on it and whether it is on the client's assets yet, the
* tickets and expenses billed on it (the expenses with what they cost us and
* the margin) and its history.
*/
// If client_id is in URI then show client Side Bar and client header
if (isset($_GET['client_id'])) {
require_once "includes/inc_all_client.php";
$client_url = "client_id=$client_id&";
} else {
require_once "includes/inc_all.php";
$client_url = '';
}
// Perms
enforceUserPermission('module_sales');
$invoice_id = intval($_GET['invoice_id'] ?? 0);
$invoice = billingRow('invoice', $invoice_id);
if (!$invoice || ($client_url && intval($invoice['client_id']) !== intval($client_id))) {
billingNotFound('invoice', $client_url);
require_once "../includes/footer.php";
exit;
}
$client_id = intval($invoice['client_id']);
enforceClientAccess();
$number = billingNumber('invoice', $invoice);
$status = (string) $invoice['invoice_status'];
$currency = (string) $invoice['invoice_currency_code'];
$money = function ($amount) use ($currency) {
return escapeHtml(billingMoney($amount, $currency));
};
$items = billingItems('invoice', $invoice_id);
$totals = billingTotals('invoice', $invoice, $items);
$paid = invoicePaid($invoice_id);
$state = invoiceState($invoice, $paid);
$payments = invoicePayments($invoice_id);
$tickets = invoiceTickets($invoice_id);
$history = billingHistory('invoice', $invoice_id);
$can_edit = lookupUserPermission('module_sales') >= 2;
$can_delete = lookupUserPermission('module_sales') >= 3;
// The money on it is accounting's too - Financial, with Sales at Read (billingMoneyCan())
$can_take_payment = billingMoneyCan('take') && invoiceCanPay($invoice, $state);
$can_edit_payments = billingMoneyCan('edit');
$can_delete_payments = billingMoneyCan('delete');
$can_remind = billingMoneyCan('remind');
$can_edit_lines = billingCanEditItems('invoice', $invoice);
$mail_ready = !empty($config_smtp_provider) && billingEmailableContacts('invoice', $client_id) > 0;
$saved_card = $can_take_payment && in_array($status, ['Sent', 'Viewed'], true) && invoiceHasSavedCard($client_id);
$unbilled_tickets = $can_edit && $can_edit_lines && $config_module_enable_ticketing ? count(billingUnbilledTickets($client_id)) : 0;
// Expenses billed on it, against what they cost us (functions/expenses.php)
$can_see_expenses = lookupUserPermission('module_financial') >= 1;
$can_bill_expenses = $can_edit && $can_edit_lines && lookupUserPermission('module_financial') >= 2;
$can_unbill_expenses = $can_edit && lookupUserPermission('module_financial') >= 2 && expenseCanUnbill($status);
$billed_expenses = $can_see_expenses ? expenseOnInvoice($invoice_id) : [];
$unbilled_expenses = $can_bill_expenses ? count(expenseUnbilled($client_id)) : 0;
// Trips billed on it (functions/trips.php) - the driving out to the client
$billed_trips = $can_see_expenses ? tripsOnInvoice($invoice_id) : [];
$unbilled_trips = $can_bill_expenses ? count(tripUnbilled($client_id)) : 0;
$can_unbill_trips = $can_edit && tripCanUnbill($status);
$guest_url = billingGuestUrl('invoice', $invoice);
// Hardware sold on it, and how much of it is on the client's assets (functions/products.php)
$can_see_assets = lookupUserPermission('module_support') >= 1;
$hardware = $can_see_assets ? productInvoiceHardware($invoice_id) : [];
$hardware_units = array_sum(array_column($hardware, 'units'));
$hardware_to_add = $status === 'Cancelled' ? 0 : array_sum(array_column($hardware, 'to_add'));
$can_add_assets = $hardware_to_add > 0 && lookupUserPermission('module_support') >= 2;
// Payment reminders - how many have gone, and when the next one goes (functions/billing_email.php)
$owed = in_array($state['state'], ['open', 'overdue'], true) && $status !== 'Draft';
$reminder_rules = billingReminderRules();
$reminders_sent = $owed ? mysqli_fetch_assoc(mysqli_query($mysqli, "SELECT COUNT(*) AS sent, MAX(history_created_at) AS last FROM history
WHERE history_invoice_id = $invoice_id AND (history_description LIKE 'Reminder emailed%' OR history_description LIKE 'Cron Emailed Overdue Invoice%')")) : null;
$client_reminders = intval(getFieldById('clients', $client_id, 'client_reminders') ?? 1);
$reminder_next = $owed && $client_reminders ? billingReminderNext($invoice, $reminder_rules, billingReminderAutopay($invoice)) : null;
$csrf = $_SESSION['csrf_token'];
// Made by a recurring invoice?
$recurring = intval($invoice['invoice_recurring_invoice_id'])
? mysqli_fetch_assoc(mysqli_query($mysqli, "SELECT recurring_invoice_id, recurring_invoice_prefix, recurring_invoice_number FROM recurring_invoices WHERE recurring_invoice_id = " . intval($invoice['invoice_recurring_invoice_id'])))
: null;
// Made from a quote?
$from_quote = intval($invoice['invoice_quote_id'] ?? 0)
? mysqli_fetch_assoc(mysqli_query($mysqli, "SELECT quote_id, quote_prefix, quote_number FROM quotes WHERE quote_id = " . intval($invoice['invoice_quote_id'])))
: null;
$progress = $state['paid'] > 0 && $totals['total'] > 0 ? min(100, round($state['paid'] / $totals['total'] * 100)) : 0;
// Override Tab Title // No Sanitizing needed as this var will only be used in the tab title
$tab_title = $invoice['client_name'];
$page_title = $number;
$paper = [
'type' => 'invoice',
'row' => $invoice,
'items' => $items,
'totals' => $totals,
'paid' => $state['paid'],
'balance' => $state['balance'],
'edit' => $can_edit_lines,
'ai' => $can_edit_lines && aiReady('Billing'),
'notes_edit' => $can_edit,
'taxes' => $can_edit_lines ? billingTaxes($invoice['invoice_created_at']) : [],
'logo_url' => billingCompany()['logo_url'],
'csrf' => $csrf,
];
?>
<ol class="breadcrumb d-print-none">
<li class="breadcrumb-item"><a href="client_overview.php?client_id=<?= $client_id ?>"><?= escapeHtml($invoice['client_name']) ?></a></li>
<li class="breadcrumb-item"><a href="invoices.php?client_id=<?= $client_id ?>">Invoices</a></li>
<li class="breadcrumb-item active"><?= escapeHtml($number) ?></li>
</ol>
<!-- What it is, where it stands, and the next thing to do -->
<div class="card mb-3 d-print-none">
<div class="card-body d-flex flex-wrap align-items-center gap-3">
<?= billingTile('invoice', 'lg') ?>
<div class="flex-grow-1 min-w-0">
<h1 class="h4 fw-semibold mb-1 d-flex flex-wrap align-items-center gap-2">
<span>Invoice <?= escapeHtml($number) ?></span>
<span class="fs-6"><?= invoiceStatusBadge($status) ?></span>
<span class="fs-6"><?= invoiceDuePill($state) ?></span>
</h1>
<div class="text-body-secondary small d-flex flex-wrap column-gap-3 row-gap-1">
<span><i class="fas fa-fw fa-building me-1"></i><a class="text-reset" href="invoices.php?client_id=<?= $client_id ?>"><?= escapeHtml($invoice['client_name']) ?></a></span>
<?php if (trim((string) $invoice['invoice_scope']) !== '') { ?><span><i class="fas fa-fw fa-comment me-1"></i><?= escapeHtml($invoice['invoice_scope']) ?></span><?php } ?>
<?php if ($invoice['category_name']) { ?><span><i class="fas fa-fw fa-tag me-1"></i><?= escapeHtml($invoice['category_name']) ?></span><?php } ?>
<?php if ($recurring) { ?>
<span><i class="fas fa-fw fa-redo-alt me-1"></i>From <a class="text-reset" href="recurring_invoice.php?recurring_invoice_id=<?= intval($recurring['recurring_invoice_id']) ?>"><?= escapeHtml($recurring['recurring_invoice_prefix'] . $recurring['recurring_invoice_number']) ?></a></span>
<?php } ?>
<?php if ($from_quote) { ?>
<span><i class="fas fa-fw fa-comment-dollar me-1"></i>From <a class="text-reset" href="quote.php?quote_id=<?= intval($from_quote['quote_id']) ?>"><?= escapeHtml($from_quote['quote_prefix'] . $from_quote['quote_number']) ?></a></span>
<?php } ?>
<span title="<?= escapeHtml($invoice['invoice_created_at']) ?>"><i class="far fa-fw fa-clock me-1"></i>Created <?= escapeHtml(timeAgo($invoice['invoice_created_at'])) ?></span>
</div>
</div>
<div class="d-flex flex-wrap gap-2">
<?php if ($can_edit && $status === 'Draft') { ?>
<div class="btn-group">
<?php if ($mail_ready) { ?>
<button type="button" class="btn btn-primary ajax-modal" data-modal-size="lg" data-modal-url="modals/invoice/invoice_email.php?invoice_id=<?= $invoice_id ?>">
<i class="fas fa-paper-plane me-2"></i>Send
</button>
<button type="button" class="btn btn-primary dropdown-toggle dropdown-toggle-split" data-bs-toggle="dropdown" aria-label="More ways to send"></button>
<div class="dropdown-menu dropdown-menu-end">
<button type="submit" class="dropdown-item confirm-link" form="quickSendInvoice" name="invoice_id" value="<?= $invoice_id ?>"
data-confirm-title="Send <?= escapeHtml($number) ?> now?" data-confirm-text="It goes to the client's primary and billing contacts." data-confirm-button="Send">
<i class="fas fa-fw fa-bolt text-secondary me-2"></i>Quick Send
</button>
<a class="dropdown-item ajax-modal" href="#" data-modal-url="modals/invoice/invoice_mark_sent.php?invoice_id=<?= $invoice_id ?>">
<i class="fas fa-fw fa-check text-secondary me-2"></i>Mark Sent<span class="text-body-secondary">...</span>
</a>
</div>
<?php } else { ?>
<button type="button" class="btn btn-primary ajax-modal" data-modal-url="modals/invoice/invoice_mark_sent.php?invoice_id=<?= $invoice_id ?>">
<i class="fas fa-check me-2"></i>Mark Sent
</button>
<?php } ?>
</div>
<?php } ?>
<?php if ($can_take_payment) { ?>
<div class="btn-group">
<button type="button" class="btn btn-success ajax-modal" data-modal-url="modals/payment/payment_add.php?id=<?= $invoice_id ?>">
<i class="fas fa-credit-card me-2"></i>Add Payment
</button>
<?php if ($saved_card) { ?>
<button type="button" class="btn btn-success dropdown-toggle dropdown-toggle-split" data-bs-toggle="dropdown" aria-label="More ways to pay"></button>
<div class="dropdown-menu dropdown-menu-end">
<a class="dropdown-item ajax-modal" href="#" data-modal-url="modals/payment/payment_saved_method_add.php?id=<?= $invoice_id ?>">
<i class="fas fa-fw fa-wallet text-secondary me-2"></i>Charge Saved Card
</a>
</div>
<?php } ?>
</div>
<?php } ?>
<?php if ($can_edit && $status !== 'Draft' && $mail_ready && in_array($status, invoiceOpenStatuses(), true)) { ?>
<button type="button" class="btn btn-outline-primary ajax-modal" data-modal-size="lg" data-modal-url="modals/invoice/invoice_email.php?invoice_id=<?= $invoice_id ?>">
<i class="fas fa-paper-plane me-2"></i>Send Again
</button>
<?php } ?>
<?php if ($can_edit && in_array($status, ['Sent', 'Viewed'], true) && floatval($invoice['invoice_amount']) == 0) { ?>
<a class="btn btn-outline-dark confirm-link" href="post.php?mark_invoice_non-billable=<?= $invoice_id ?>&csrf_token=<?= $csrf ?>"
data-confirm-text="Nothing is owed on it - it is closed off as non-billable.">
<i class="fas fa-ban me-2"></i>Non-Billable
</a>
<?php } ?>
<?php if ($can_edit) { ?>
<button type="button" class="btn btn-outline-secondary ajax-modal" data-modal-url="modals/invoice/invoice_edit.php?id=<?= $invoice_id ?>">
<i class="fas fa-edit me-2"></i>Edit
</button>
<?php } ?>
<div class="dropdown dropstart">
<button class="btn btn-outline-secondary" title="More actions" type="button" data-bs-toggle="dropdown">
<i class="fas fa-fw fa-ellipsis-v"></i>
</button>
<div class="dropdown-menu">
<a class="dropdown-item" href="post.php?export_invoice_pdf=<?= $invoice_id ?>&csrf_token=<?= $csrf ?>" target="_blank">
<i class="fas fa-fw fa-file-pdf text-secondary me-2"></i>Download PDF
</a>
<a class="dropdown-item" href="#" onclick="window.print(); return false;">
<i class="fas fa-fw fa-print text-secondary me-2"></i>Print
</a>
<a class="dropdown-item" href="post.php?export_invoice_packing_slip=<?= $invoice_id ?>&csrf_token=<?= $csrf ?>" target="_blank">
<i class="fas fa-fw fa-box-open text-secondary me-2"></i>Packing Slip
</a>
<div class="dropdown-divider"></div>
<a class="dropdown-item clipboardjs" href="#" data-clipboard-text="<?= escapeHtml($guest_url) ?>">
<i class="fas fa-fw fa-link text-secondary me-2"></i>Copy Client Link
</a>
<a class="dropdown-item" href="<?= escapeHtml($guest_url) ?>" target="_blank" rel="noopener">
<i class="fas fa-fw fa-external-link-alt text-secondary me-2"></i>View as Client
</a>
<?php if ($can_remind && $mail_ready && $owed && $state['balance'] > 0) { ?>
<a class="dropdown-item ajax-modal" href="#" data-modal-size="lg" data-modal-url="modals/invoice/invoice_email.php?invoice_id=<?= $invoice_id ?>&amp;kind=reminder">
<i class="fas fa-fw fa-bell text-secondary me-2"></i>Send Reminder<span class="text-body-secondary">...</span>
</a>
<?php } ?>
<?php if ($can_edit) { ?>
<?php if ($status !== 'Draft' && $mail_ready && !in_array($status, invoiceOpenStatuses(), true)) { ?>
<a class="dropdown-item ajax-modal" href="#" data-modal-size="lg" data-modal-url="modals/invoice/invoice_email.php?invoice_id=<?= $invoice_id ?>">
<i class="fas fa-fw fa-paper-plane text-secondary me-2"></i>Send Again<span class="text-body-secondary">...</span>
</a>
<?php } ?>
<div class="dropdown-divider"></div>
<a class="dropdown-item ajax-modal" href="#" data-modal-url="modals/invoice/invoice_copy.php?id=<?= $invoice_id ?>">
<i class="fas fa-fw fa-copy text-secondary me-2"></i>Copy
</a>
<a class="dropdown-item ajax-modal" href="#" data-modal-url="modals/invoice/invoice_recurring_add.php?invoice_id=<?= $invoice_id ?>">
<i class="fas fa-fw fa-redo-alt text-secondary me-2"></i>Make Recurring
</a>
<?php if (!in_array($status, ['Cancelled', 'Paid', 'Non-Billable'], true)) { ?>
<div class="dropdown-divider"></div>
<a class="dropdown-item text-danger confirm-link" href="post.php?cancel_invoice=<?= $invoice_id ?>&csrf_token=<?= $csrf ?>"
data-confirm-text="<?= escapeHtml($number) ?> is cancelled - nothing more is owed on it. It stays on record.">
<i class="fas fa-fw fa-times me-2"></i>Cancel Invoice
</a>
<?php } ?>
<?php } ?>
<?php if ($can_delete) { ?>
<a class="dropdown-item text-danger text-bold confirm-link" href="post.php?delete_invoice=<?= $invoice_id ?>&csrf_token=<?= $csrf ?>"
data-confirm-text="<?= escapeHtml($number) ?> goes for good, with its lines, payments and history. Its tickets become unbilled again, and its expenses and trips are to bill again.">
<i class="fas fa-fw fa-trash me-2"></i>Delete
</a>
<?php } ?>
</div>
</div>
</div>
</div>
</div>
<?= taxDocWarning('invoice', $invoice_id, $client_id, $items, billingLinesOpen('invoice', $status), $can_edit) ?>
<div class="row g-3">
<!-- The invoice, as the client sees it -->
<div class="col-xl-8 itflow-paper-col">
<?php require "../includes/billing_paper.php"; ?>
</div>
<!-- Money, tickets and what has happened -->
<div class="col-xl-4 d-print-none">
<div class="card mb-3">
<div class="card-header">
<h3 class="card-title mb-0"><i class="fas fa-fw fa-coins text-secondary me-2"></i>Payment</h3>
</div>
<div class="card-body">
<div class="d-flex justify-content-between align-items-end mb-2">
<div>
<div class="itflow-billing-stat-label"><?= $state['state'] === 'paid' ? 'Paid in full' : ($state['state'] === 'overdue' ? 'Overdue' : 'Balance') ?></div>
<div class="itflow-billing-stat-value <?= $state['state'] === 'overdue' ? 'text-danger-emphasis' : '' ?>"><?= $money($state['state'] === 'paid' ? $state['paid'] : $state['balance']) ?></div>
</div>
<div class="text-end small text-body-secondary">of <?= $money($totals['total']) ?></div>
</div>
<div class="progress itflow-billing-progress mb-3" role="progressbar" aria-label="Paid" aria-valuenow="<?= $progress ?>" aria-valuemin="0" aria-valuemax="100">
<div class="progress-bar bg-success" style="width: <?= $progress ?>%"></div>
</div>
<dl class="itflow-asset-dl mb-0">
<dt>Paid</dt><dd><?= $money($state['paid']) ?><?= $payments ? ' <span class="text-body-secondary">in ' . count($payments) . ' payment' . (count($payments) === 1 ? '' : 's') . '</span>' : '' ?></dd>
<dt>Due</dt>
<dd>
<?= escapeHtml($invoice['invoice_due']) ?>
<?php if (in_array($state['state'], ['open', 'overdue'], true)) { ?>
<div class="small <?= $state['days'] < 0 ? 'text-danger-emphasis' : 'text-body-secondary' ?>">
<?= $state['days'] < 0 ? (-$state['days']) . ' day' . (-$state['days'] === 1 ? '' : 's') . ' late' : ($state['days'] === 0 ? 'Today' : 'In ' . $state['days'] . ' day' . ($state['days'] === 1 ? '' : 's')) ?>
</div>
<?php } ?>
</dd>
<dt>Terms</dt><dd><?php $invoice_terms = contractNetTerms(intval($invoice['client_id']), (string) $invoice['invoice_date']); echo $invoice_terms ? "$invoice_terms days" : 'Due on receipt'; ?></dd>
<?php if ($owed) { ?>
<dt>Reminders</dt>
<dd>
<?= intval($reminders_sent['sent']) ? intval($reminders_sent['sent']) . ' sent, the last ' . escapeHtml(billingEmailDate($reminders_sent['last'])) : 'None sent yet' ?>
<div class="small text-body-secondary">
<?php if (!$reminder_rules['enabled']) { ?>
Off - set in Invoice Settings
<?php } elseif (!$client_reminders) { ?>
Off for <?= escapeHtml($invoice['client_name']) ?>
<?php } elseif ($reminder_next) { ?>
Next <?= escapeHtml(billingEmailDate($reminder_next['date'])) ?> - <?= escapeHtml(billingReminderWhen($reminder_next['days'])) ?>
<?php } else { ?>
No more on the schedule
<?php } ?>
</div>
</dd>
<?php } ?>
<?php $margin_html = billingMarginHtml('invoice', $items, $currency); if ($margin_html) { ?>
<dt>Margin</dt><dd><?= $margin_html ?></dd>
<?php } ?>
<?php $reprice_doc = $can_edit && $status === 'Draft' ? productRepriceCount(['kind' => 'invoice', 'doc_id' => $invoice_id]) : null; if ($reprice_doc && $reprice_doc['lines']) { ?>
<dt>Prices</dt>
<dd><a class="text-warning-emphasis" href="products_reprice.php?kind=invoice&amp;doc_id=<?= $invoice_id ?>"><i class="fas fa-tags me-1"></i><?= intval($reprice_doc['lines']) ?> line<?= $reprice_doc['lines'] == 1 ? '' : 's' ?> at an old price</a></dd>
<?php } ?>
<?php if ($status === 'Draft') { ?>
<dt>Client</dt><dd class="text-body-secondary">Not sent yet - the client can't see it</dd>
<?php } ?>
</dl>
</div>
</div>
<div class="card mb-3">
<div class="card-header d-flex align-items-center">
<h3 class="card-title mb-0 me-auto"><i class="fas fa-fw fa-credit-card text-secondary me-2"></i>Payments <span class="badge rounded-pill bg-secondary-subtle text-secondary-emphasis fw-normal ms-1"><?= count($payments) ?></span></h3>
<?php if ($can_take_payment) { ?>
<button type="button" class="btn btn-sm btn-outline-success ajax-modal" data-modal-url="modals/payment/payment_add.php?id=<?= $invoice_id ?>"><i class="fas fa-plus me-1"></i>Add</button>
<?php } ?>
</div>
<?php if ($payments) { ?>
<div class="list-group list-group-flush">
<?php foreach ($payments as $payment) {
$payment_id = intval($payment['payment_id']); ?>
<div class="list-group-item d-flex align-items-center gap-2">
<div class="flex-grow-1 min-w-0">
<div class="fw-semibold itflow-money"><?= escapeHtml(billingMoney($payment['payment_amount'], $payment['payment_currency_code'])) ?></div>
<div class="small text-body-secondary text-truncate">
<?= escapeHtml(implode(' · ', array_filter([$payment['payment_date'], $payment['payment_method'], $payment['payment_reference'], $payment['account_name']]))) ?>
</div>
</div>
<?php if ($can_edit_payments || $can_delete_payments) { ?>
<div class="dropdown dropstart">
<button class="btn btn-sm btn-link text-body-secondary" type="button" data-bs-toggle="dropdown" title="Payment actions"><i class="fas fa-fw fa-ellipsis-v"></i></button>
<div class="dropdown-menu">
<?php if ($can_edit_payments) { ?>
<a class="dropdown-item ajax-modal" href="#" data-modal-url="modals/payment/payment_edit.php?id=<?= $payment_id ?>">
<i class="fas fa-fw fa-edit text-secondary me-2"></i>Edit
</a>
<?php } ?>
<?php if ($can_delete_payments) { ?>
<?php if ($payment['payment_method'] === 'Stripe') { ?>
<a class="dropdown-item text-warning confirm-link" href="post.php?refund_payment_stripe=<?= $payment_id ?>&csrf_token=<?= $csrf ?>"
data-confirm-text="The payment is refunded to the client's card through Stripe.">
<i class="fas fa-fw fa-undo me-2"></i>Refund through Stripe
</a>
<?php } ?>
<?php if ($can_edit_payments) { ?>
<div class="dropdown-divider"></div>
<?php } ?>
<a class="dropdown-item text-danger confirm-link" href="post.php?delete_payment=<?= $payment_id ?>&csrf_token=<?= $csrf ?>"
data-confirm-text="The payment comes off the invoice and its account. No money moves.">
<i class="fas fa-fw fa-trash me-2"></i>Delete
</a>
<?php } ?>
</div>
</div>
<?php } ?>
</div>
<?php } ?>
</div>
<?php } else { ?>
<div class="card-body small text-body-secondary">No payments yet.</div>
<?php } ?>
</div>
<?php if ($hardware) { ?>
<div class="card mb-3">
<div class="card-header d-flex align-items-center">
<h3 class="card-title mb-0 me-auto"><i class="fas fa-fw fa-laptop text-secondary me-2"></i>Hardware <span class="badge rounded-pill bg-secondary-subtle text-secondary-emphasis fw-normal ms-1"><?= $hardware_units ?></span></h3>
<?php if ($can_add_assets) { ?>
<button type="button" class="btn btn-sm btn-outline-primary ajax-modal" data-modal-size="xl" data-modal-url="modals/invoice/invoice_assets.php?invoice_id=<?= $invoice_id ?>">
<i class="fas fa-plus me-1"></i>Add to Assets <span class="badge rounded-pill text-bg-primary ms-1" title="Not on their assets yet"><?= $hardware_to_add ?></span>
</button>
<?php } ?>
</div>
<div class="list-group list-group-flush">
<?php foreach ($hardware as $line) {
$line_done = count($line['assets']); ?>
<div class="list-group-item">
<div class="d-flex align-items-center gap-2">
<?= productAssetIcon($line['product_asset_type'], 'sm') ?>
<div class="flex-grow-1 min-w-0">
<div class="fw-semibold text-truncate"><?= escapeHtml($line['item_name']) ?></div>
<div class="small text-body-secondary"><?= escapeHtml($line['product_asset_type']) ?> &middot; <?= escapeHtml(billingQuantity($line['item_quantity'])) ?> sold</div>
</div>
<?php if ($line['units'] && $line_done >= $line['units']) { ?>
<span class="badge rounded-pill bg-success-subtle text-success-emphasis fw-normal text-nowrap"><i class="fas fa-check me-1"></i>On assets</span>
<?php } elseif ($status !== 'Cancelled' && $line['units']) { ?>
<span class="badge rounded-pill bg-warning-subtle text-warning-emphasis fw-normal text-nowrap" title="On the client's assets"><?= $line_done ?> of <?= $line['units'] ?></span>
<?php } ?>
</div>
<?php if ($line['assets']) { ?>
<ul class="list-unstyled small mb-0 mt-1 itflow-hw-assets">
<?php foreach ($line['assets'] as $line_asset) {
$line_asset_name = escapeHtml($line_asset['asset_name']); ?>
<li class="text-truncate">
<?php if (hasClientAccess($line_asset['asset_client_id'])) { ?>
<a href="asset.php?client_id=<?= intval($line_asset['asset_client_id']) ?>&asset_id=<?= intval($line_asset['asset_id']) ?>"><?= $line_asset_name ?></a>
<?php } else { ?>
<?= $line_asset_name ?>
<?php } ?>
<?php if ($line_asset['asset_serial']) { ?><span class="font-monospace text-body-secondary ms-1"><?= escapeHtml($line_asset['asset_serial']) ?></span><?php } ?>
<?php if ($line_asset['asset_archived_at']) { ?><span class="badge rounded-pill text-bg-secondary fw-normal ms-1">Archived</span><?php } ?>
<?php if (intval($line_asset['asset_client_id']) !== $client_id) { ?><span class="text-body-secondary ms-1">(moved to another client)</span><?php } ?>
</li>
<?php } ?>
</ul>
<?php } ?>
</div>
<?php } ?>
</div>
<?php if ($status === 'Cancelled') { ?>
<div class="card-footer small text-body-secondary">Cancelled - nothing on it was sold.</div>
<?php } elseif ($hardware_to_add && !$can_add_assets) { ?>
<div class="card-footer small text-body-secondary"><?= $hardware_to_add ?> not on <?= escapeHtml($invoice['client_name']) ?>'s assets yet.</div>
<?php } ?>
</div>
<?php } ?>
<?php if ($tickets || $unbilled_tickets) { ?>
<div class="card mb-3">
<div class="card-header d-flex align-items-center">
<h3 class="card-title mb-0 me-auto"><i class="fas fa-fw fa-life-ring text-secondary me-2"></i>Tickets <span class="badge rounded-pill bg-secondary-subtle text-secondary-emphasis fw-normal ms-1"><?= count($tickets) ?></span></h3>
<?php if ($unbilled_tickets) { ?>
<button type="button" class="btn btn-sm btn-outline-primary ajax-modal" data-modal-url="modals/invoice/invoice_add_ticket.php?invoice_id=<?= $invoice_id ?>">
<i class="fas fa-plus me-1"></i>Add <span class="badge rounded-pill text-bg-primary ms-1" title="Unbilled"><?= $unbilled_tickets ?></span>
</button>
<?php } ?>
</div>
<?php if ($tickets) { ?>
<div class="list-group list-group-flush">
<?php foreach ($tickets as $ticket) {
$ticket_id = intval($ticket['ticket_id']); ?>
<div class="list-group-item d-flex align-items-center gap-2">
<div class="flex-grow-1 min-w-0">
<a class="text-truncate d-block" href="ticket.php?ticket_id=<?= $ticket_id ?>"><span class="font-monospace small me-1"><?= escapeHtml($ticket['ticket_prefix'] . $ticket['ticket_number']) ?></span><?= escapeHtml($ticket['ticket_subject']) ?></a>
<div class="small text-body-secondary"><i class="far fa-clock me-1"></i><?= billingDuration($ticket['seconds']) ?> worked</div>
</div>
<?php if ($can_edit) { ?>
<a class="btn btn-sm btn-link text-body-secondary confirm-link" title="Take off the invoice" href="post.php?remove_ticket_from_invoice&invoice_id=<?= $invoice_id ?>&ticket_id=<?= $ticket_id ?>&csrf_token=<?= $csrf ?>"
data-confirm-text="The ticket becomes unbilled again. The invoice's lines don't change.">
<i class="fas fa-fw fa-unlink"></i>
</a>
<?php } ?>
</div>
<?php } ?>
</div>
<?php } else { ?>
<div class="card-body small text-body-secondary"><?= $unbilled_tickets ?> unbilled ticket<?= $unbilled_tickets === 1 ? '' : 's' ?> for <?= escapeHtml($invoice['client_name']) ?> could go on this invoice.</div>
<?php } ?>
</div>
<?php } ?>
<?php if ($billed_expenses || $unbilled_expenses) {
$expenses_cost = array_sum(array_column($billed_expenses, 'cost'));
$expenses_billed = array_sum(array_map(function ($e) { return floatval($e['billed']); }, $billed_expenses)); ?>
<div class="card mb-3">
<div class="card-header d-flex align-items-center">
<h3 class="card-title mb-0 me-auto"><i class="fas fa-fw fa-shopping-cart text-secondary me-2"></i>Expenses <span class="badge rounded-pill bg-secondary-subtle text-secondary-emphasis fw-normal ms-1"><?= count($billed_expenses) ?></span></h3>
<?php if ($unbilled_expenses) { ?>
<button type="button" class="btn btn-sm btn-outline-primary ajax-modal" data-modal-size="lg" data-modal-url="modals/invoice/invoice_add_expenses.php?invoice_id=<?= $invoice_id ?>">
<i class="fas fa-plus me-1"></i>Add <span class="badge rounded-pill text-bg-primary ms-1" title="To bill"><?= $unbilled_expenses ?></span>
</button>
<?php } ?>
</div>
<?php if ($billed_expenses) { ?>
<div class="list-group list-group-flush">
<?php foreach ($billed_expenses as $expense) {
$expense_id = intval($expense['expense_id']);
[$line_name] = expenseBillLine($expense); ?>
<div class="list-group-item">
<div class="d-flex align-items-start gap-2">
<div class="flex-grow-1 min-w-0">
<a class="text-truncate d-block" href="expenses.php?open=<?= $expense_id ?>" title="<?= escapeHtml($line_name) ?>"><?= escapeHtml($line_name) ?></a>
<div class="small text-body-secondary text-truncate"><?= escapeHtml(implode(' · ', array_filter([$expense['vendor_name'], $expense['expense_date']]))) ?></div>
</div>
<?php if ($can_unbill_expenses) { ?>
<a class="btn btn-sm btn-link text-body-secondary confirm-link p-0" title="Take off the invoice" href="post.php?expense_unbill=<?= $expense_id ?>&csrf_token=<?= $csrf ?>"
data-confirm-text="<?= $status === 'Cancelled' ? 'The invoice is cancelled, so its line stays on it' : 'Its line comes off the invoice' ?> and the expense waits to be billed again.">
<i class="fas fa-fw fa-unlink"></i>
</a>
<?php } ?>
</div>
<div class="d-flex justify-content-between gap-2 small mt-1">
<span class="itflow-money text-nowrap"><span class="text-body-secondary">Cost <?= $money($expense['cost']) ?> &rarr;</span> <?= $expense['billed'] !== null ? $money($expense['billed']) : '<span class="text-body-secondary">line gone</span>' ?></span>
<?php if ($expense['billed'] !== null) { ?><span class="text-nowrap <?= $expense['billed'] < $expense['cost'] ? 'text-danger-emphasis' : 'text-body-secondary' ?>"><?= escapeHtml(expenseMarginText($expense['billed'], $expense['cost'])) ?></span><?php } ?>
</div>
</div>
<?php } ?>
</div>
<div class="card-footer small d-flex justify-content-between gap-2">
<span class="text-body-secondary">Cost <?= $money($expenses_cost) ?> &middot; billed <?= $money($expenses_billed) ?></span>
<span class="fw-semibold <?= $expenses_billed < $expenses_cost ? 'text-danger-emphasis' : '' ?>"><?= escapeHtml(expenseMarginText($expenses_billed, $expenses_cost) ?: 'No charge') ?></span>
</div>
<?php } else { ?>
<div class="card-body small text-body-secondary"><?= $unbilled_expenses ?> expense<?= $unbilled_expenses === 1 ? '' : 's' ?> to bill to <?= escapeHtml($invoice['client_name']) ?> could go on this invoice.</div>
<?php } ?>
</div>
<?php } ?>
<?php if ($billed_trips || $unbilled_trips) {
$trips_billed = array_sum(array_map(function ($t) { return floatval($t['billed']); }, $billed_trips));
$trips_miles = array_sum(array_map(function ($t) { return floatval($t['trip_miles']); }, $billed_trips)); ?>
<div class="card mb-3">
<div class="card-header d-flex align-items-center">
<h3 class="card-title mb-0 me-auto"><i class="fas fa-fw fa-route text-secondary me-2"></i>Travel <span class="badge rounded-pill bg-secondary-subtle text-secondary-emphasis fw-normal ms-1"><?= count($billed_trips) ?></span></h3>
<?php if ($unbilled_trips) { ?>
<button type="button" class="btn btn-sm btn-outline-primary ajax-modal" data-modal-size="lg" data-modal-url="modals/invoice/invoice_add_trips.php?invoice_id=<?= $invoice_id ?>">
<i class="fas fa-plus me-1"></i>Add <span class="badge rounded-pill text-bg-primary ms-1" title="To bill"><?= $unbilled_trips ?></span>
</button>
<?php } ?>
</div>
<?php if ($billed_trips) { ?>
<div class="list-group list-group-flush">
<?php foreach ($billed_trips as $trip) {
$trip_id = intval($trip['trip_id']); ?>
<div class="list-group-item">
<div class="d-flex align-items-start gap-2">
<div class="flex-grow-1 min-w-0">
<a class="text-truncate d-block ajax-modal" href="#" data-modal-size="lg" data-modal-url="modals/trip/trip.php?id=<?= $trip_id ?>" title="<?= escapeHtml($trip['trip_purpose']) ?>"><?= escapeHtml($trip['trip_purpose']) ?></a>
<div class="small text-body-secondary text-truncate"><?= escapeHtml(implode(' · ', array_filter([$trip['trip_date'], $trip['driver_name'], tripMilesText($trip['trip_miles'])]))) ?></div>
</div>
<?php if ($can_unbill_trips) { ?>
<a class="btn btn-sm btn-link text-body-secondary confirm-link p-0" title="Take off the invoice" href="post.php?trip_unbill=<?= $trip_id ?>&csrf_token=<?= $csrf ?>"
data-confirm-text="<?= $status === 'Cancelled' ? 'The invoice is cancelled, so its line stays on it' : 'Its line comes off the invoice' ?> and the trip waits to be billed again.">
<i class="fas fa-fw fa-unlink"></i>
</a>
<?php } ?>
</div>
<div class="small mt-1 text-end itflow-money"><?= $trip['billed'] !== null ? $money($trip['billed']) : '<span class="text-body-secondary">line gone</span>' ?></div>
</div>
<?php } ?>
</div>
<div class="card-footer small d-flex justify-content-between gap-2">
<span class="text-body-secondary"><?= escapeHtml(tripMilesText($trips_miles)) ?> driven</span>
<span class="fw-semibold"><?= $money($trips_billed) ?></span>
</div>
<?php } else { ?>
<div class="card-body small text-body-secondary"><?= $unbilled_trips ?> trip<?= $unbilled_trips === 1 ? '' : 's' ?> to bill to <?= escapeHtml($invoice['client_name']) ?> could go on this invoice.</div>
<?php } ?>
</div>
<?php } ?>
<div class="card mb-3">
<div class="card-header">
<h3 class="card-title mb-0"><i class="fas fa-fw fa-history text-secondary me-2"></i>History</h3>
</div>
<?= billingHistoryHtml($history) ?>
</div>
</div>
</div>
<?php if ($can_edit && $mail_ready) { ?>
<!-- Quick Send buttons post this, carrying the invoice id as their own name/value -->
<form id="quickSendInvoice" action="post.php" method="post" class="d-none">
<input type="hidden" name="csrf_token" value="<?= $csrf ?>">
<input type="hidden" name="email_invoice" value="1">
<input type="hidden" name="quick_send" value="1">
</form>
<?php } ?>
<script src="/js/inline_notes.js"></script>
<script src="/libs/SortableJS/Sortable.min.js"></script>
<script src="/js/billing_items.js"></script>
<script>
document.addEventListener('DOMContentLoaded', function () {
itflowBillingItems({
products: <?= $can_edit_lines ? getProductsForAutocomplete($mysqli, intval($invoice['client_id'])) : '[]' ?>,
csrf: <?= json_encode($csrf) ?>,
currency: <?= json_encode($currency) ?>,
locale: <?= json_encode(str_replace('_', '-', (string) $session_company_locale)) ?>
});
});
</script>
<?php require_once "../includes/footer.php";