mirror of
https://github.com/itflow-org/itflow
synced 2026-11-11 15:07:24 +00:00
301 lines
20 KiB
PHP
301 lines
20 KiB
PHP
<?php
|
||
|
||
/*
|
||
* An expense's quick look - the receipt (shown right in the window when it
|
||
* is a photo or a PDF, or attached here when there isn't one), who it was
|
||
* paid to and for what, the account and how it was paid (or the agent who
|
||
* paid it personally, and whether they are paid back), the client, who
|
||
* entered it, the refund it is or the refunds against it, billing it on to
|
||
* the client (the invoice it is on, with the margin, or what it will bill
|
||
* at), and the stock deliveries it paid for. Opened from the expenses list, the vendor's page,
|
||
* Transactions and search (expenses.php?open=).
|
||
*/
|
||
|
||
require_once '../../../includes/modal_header.php';
|
||
|
||
enforceUserPermission('module_financial');
|
||
|
||
$row = expenseRow(intval($_GET['id'] ?? 0));
|
||
if (!$row) {
|
||
echo json_encode(['error' => 'That expense no longer exists.']);
|
||
exit;
|
||
}
|
||
|
||
$expense_id = intval($row['expense_id']);
|
||
$cleared = reconcileLock('expense', $expense_id);
|
||
$client_id = intval($row['expense_client_id']);
|
||
$can_edit = lookupUserPermission('module_financial') >= 2;
|
||
$can_delete = lookupUserPermission('module_financial') >= 3;
|
||
$is_transfer = !empty($row['transfer_id']) || !empty($row['reimbursement_out_id']);
|
||
$is_payback = !empty($row['reimbursement_out_id']);
|
||
$paid_by = intval($row['expense_paid_by']);
|
||
$to_reimburse = $paid_by && !$row['expense_reimbursement_id'] && floatval($row['expense_amount']) > 0;
|
||
$amount = floatval($row['expense_amount']);
|
||
$currency = (string) $row['expense_currency_code'];
|
||
$receipt_kind = $row['expense_receipt'] && expenseReceiptPath($row['expense_receipt']) !== '' ? expenseReceiptKind($row['expense_receipt']) : '';
|
||
$receipt_missing = $row['expense_receipt'] && $receipt_kind === '' && expenseReceiptPath($row['expense_receipt']) === '';
|
||
$receipt_url = "expense_receipt.php?expense_id=$expense_id";
|
||
// Mileage - the trips in their own vehicle this pays for (functions/trips.php)
|
||
[$mileage_trips, $mileage_miles] = tripTotalsFor('trip_expense_id', $expense_id);
|
||
|
||
// Billing it on to the client
|
||
$can_sales = lookupUserPermission('module_sales');
|
||
$billed_on = intval($row['expense_invoice_id']);
|
||
$cost = round($amount - floatval($row['refunded_amount']), 2);
|
||
$billable = !$billed_on && intval($row['expense_billable']) && $client_id && $amount > 0 && !$is_transfer;
|
||
$bill_price = expenseBillPrice($cost, $row['expense_markup']);
|
||
$can_unbill = $billed_on && $can_edit && expenseCanUnbill($row['invoice_status']);
|
||
|
||
// Refunds against it, and the stock it paid for
|
||
$refunds = [];
|
||
$sql = mysqli_query($mysqli, "SELECT expense_id, expense_date, expense_amount, expense_description FROM expenses WHERE expense_refund_of_id = $expense_id ORDER BY expense_date ASC, expense_id ASC");
|
||
while ($refund = mysqli_fetch_assoc($sql)) {
|
||
$refunds[] = $refund;
|
||
}
|
||
$stock = [];
|
||
$sql = mysqli_query($mysqli, "SELECT stock_qty, stock_unit_cost, stock_created_at, product_id, product_name FROM product_stock
|
||
JOIN products ON product_id = stock_product_id
|
||
WHERE stock_expense_id = $expense_id ORDER BY stock_id ASC");
|
||
while ($move = mysqli_fetch_assoc($sql)) {
|
||
$stock[] = $move;
|
||
}
|
||
|
||
ob_start();
|
||
|
||
?>
|
||
|
||
<div class="modal-header bg-dark" data-bs-theme="dark">
|
||
<div class="d-flex align-items-center gap-3 min-w-0 flex-grow-1">
|
||
<?= vendorTile($row['vendor_name'] ?: 'Transfer', 'lg') ?>
|
||
<div class="min-w-0 flex-grow-1">
|
||
<h5 class="modal-title d-flex flex-wrap align-items-center column-gap-2">
|
||
<span class="fw-semibold text-break"><?= escapeHtml($is_payback ? 'Paid back to ' . ($row['reimbursed_to_name'] ?: 'an agent') : ($is_transfer ? 'Transfer out' : ($row['vendor_name'] ?: 'No vendor'))) ?></span>
|
||
<?php if ($amount < 0) { ?><span class="badge rounded-pill text-bg-success fw-normal fs-6">Refund</span><?php } ?>
|
||
</h5>
|
||
<div class="small text-body-secondary mt-1"><?= escapeHtml(implode(' · ', array_filter([date('M j, Y', strtotime($row['expense_date'])), $row['category_name']]))) ?></div>
|
||
</div>
|
||
<div class="text-end">
|
||
<div class="fs-4 fw-semibold itflow-money <?= $amount < 0 ? 'text-success' : 'text-white' ?>"><?= escapeHtml(billingMoney($amount, $currency)) ?></div>
|
||
<?php if (floatval($row['expense_tax_amount']) != 0) { ?>
|
||
<div class="small text-body-secondary">incl. <?= escapeHtml(billingMoney(abs(floatval($row['expense_tax_amount'])), $currency)) ?> tax</div>
|
||
<?php } ?>
|
||
</div>
|
||
</div>
|
||
<button type="button" class="btn-close btn-close-white ms-3" data-bs-dismiss="modal" aria-label="Close"></button>
|
||
</div>
|
||
|
||
<div class="modal-body">
|
||
|
||
<?php if ($is_payback) { ?>
|
||
<div class="alert alert-info small mb-3"><i class="fas fa-hand-holding-usd me-2"></i>This is money paid back to <?= escapeHtml($row['reimbursed_to_name'] ?: 'an agent') ?> for what they paid personally -
|
||
<a href="#" class="ajax-modal" data-modal-size="lg" data-modal-url="modals/expense/expense_reimbursement.php?id=<?= intval($row['reimbursement_out_id']) ?>">open the reimbursement</a> to see or undo it.</div>
|
||
<?php } elseif ($is_transfer) { ?>
|
||
<div class="alert alert-info small mb-3"><i class="fas fa-exchange-alt me-2"></i>This is the out side of a transfer between accounts - <a href="#" class="ajax-modal" data-modal-size="lg" data-modal-url="modals/transfer/transfer.php?id=<?= intval($row['transfer_id']) ?>">open the transfer</a> to change or delete it.</div>
|
||
<?php } ?>
|
||
|
||
<!-- The receipt -->
|
||
<?php if ($receipt_kind) { ?>
|
||
<div class="itflow-file-preview mb-3">
|
||
<?php if ($receipt_kind === 'image') { ?>
|
||
<a href="<?= $receipt_url ?>&action=view" target="_blank" title="Open full size"><img src="<?= $receipt_url ?>&action=view" alt="Receipt"></a>
|
||
<?php } else { ?>
|
||
<iframe src="<?= $receipt_url ?>&action=view#view=FitH" title="Receipt"></iframe>
|
||
<?php } ?>
|
||
</div>
|
||
<?php } elseif (!$is_transfer) { ?>
|
||
<div class="itflow-expense-receipt-empty mb-3">
|
||
<i class="fas fa-receipt fa-2x opacity-50 mb-2"></i>
|
||
<div class="mb-2"><?= $receipt_missing ? "The receipt's file is missing from uploads/expenses/." : 'No receipt yet.' ?></div>
|
||
<?php if ($can_edit) { ?>
|
||
<form action="post.php" method="post" enctype="multipart/form-data" class="d-flex flex-wrap justify-content-center gap-2" autocomplete="off">
|
||
<input type="hidden" name="csrf_token" value="<?= $_SESSION['csrf_token'] ?>">
|
||
<input type="hidden" name="expense_id" value="<?= $expense_id ?>">
|
||
<div class="input-group" style="max-width: 26rem;">
|
||
<span class="input-group-text"><i class="fa fa-fw fa-paperclip"></i></span>
|
||
<input type="file" class="form-control" name="file" accept="image/*,application/pdf" required>
|
||
<button type="submit" name="expense_receipt_save" class="btn btn-primary"><i class="fas fa-upload me-1"></i>Attach</button>
|
||
</div>
|
||
</form>
|
||
<?php } ?>
|
||
</div>
|
||
<?php } ?>
|
||
|
||
<!-- At a glance -->
|
||
<div class="row g-2 itflow-asset-glance">
|
||
<div class="col-sm-6">
|
||
<?php if (!$is_transfer && intval($row['expense_vendor_id'])) { ?>
|
||
<div class="itflow-glance-item" title="Vendor">
|
||
<i class="fas fa-fw fa-building"></i>
|
||
<a class="text-truncate" href="vendor.php?vendor_id=<?= intval($row['expense_vendor_id']) ?>"><?= escapeHtml($row['vendor_name']) ?></a>
|
||
</div>
|
||
<?php } ?>
|
||
<div class="itflow-glance-item" title="Category">
|
||
<i class="fas fa-fw fa-list"></i>
|
||
<span><?= expenseCategoryDot($row['category_color'], $row['category_icon'] ?? '') ?><?= escapeHtml($row['category_name'] ?: 'No category') ?></span>
|
||
</div>
|
||
<?php if (floatval($row['expense_tax_amount']) != 0) { ?>
|
||
<div class="itflow-glance-item" title="Tax included">
|
||
<i class="fas fa-fw fa-percent"></i>
|
||
<span><?= escapeHtml(billingMoney(abs(floatval($row['expense_tax_amount'])), $currency)) ?> tax<?= $row['tax_name'] !== null ? ' at ' . escapeHtml(taxLabel($row)) : '' ?>, on <?= escapeHtml(billingMoney(abs($amount) - abs(floatval($row['expense_tax_amount'])), $currency)) ?></span>
|
||
</div>
|
||
<?php } ?>
|
||
<?php if ($mileage_trips) { ?>
|
||
<div class="itflow-glance-item" title="Mileage">
|
||
<i class="fas fa-fw fa-car-side"></i>
|
||
<a class="text-truncate" href="trips.php?expense_id=<?= $expense_id ?>">Mileage for <?= $mileage_trips ?> trip<?= $mileage_trips === 1 ? '' : 's' ?>, <?= escapeHtml(tripMilesText($mileage_miles)) ?></a>
|
||
</div>
|
||
<?php } ?>
|
||
<?php if ($paid_by) { ?>
|
||
<div class="itflow-glance-item" title="Paid personally">
|
||
<i class="fas fa-fw fa-hand-holding-usd"></i>
|
||
<span>
|
||
Paid personally by <?= escapeHtml($row['paid_by_name'] ?: 'an agent since deleted') ?><?= $row['expense_payment_method'] ? ' <span class="text-body-secondary">· ' . escapeHtml($row['expense_payment_method']) . '</span>' : '' ?>
|
||
<?php if ($row['expense_reimbursement_id']) { ?>
|
||
<span class="d-block small"><a href="#" class="ajax-modal text-success-emphasis" data-modal-size="lg" data-modal-url="modals/expense/expense_reimbursement.php?id=<?= intval($row['expense_reimbursement_id']) ?>"><i class="fas fa-check-circle me-1"></i>Paid back <?= escapeHtml(date('M j, Y', strtotime($row['reimbursement_date']))) ?><?= $row['reimbursed_account_name'] ? ' from ' . escapeHtml($row['reimbursed_account_name']) : '' ?></a></span>
|
||
<?php } elseif ($to_reimburse) { ?>
|
||
<span class="d-block small text-warning-emphasis fw-semibold">To reimburse</span>
|
||
<?php } ?>
|
||
</span>
|
||
</div>
|
||
<?php } else { ?>
|
||
<div class="itflow-glance-item" title="Paid from">
|
||
<i class="fas fa-fw fa-piggy-bank"></i>
|
||
<span><?= escapeHtml($row['account_name'] ?: 'No account') ?><?= $row['expense_payment_method'] ? ' <span class="text-body-secondary">· ' . escapeHtml($row['expense_payment_method']) . '</span>' : '' ?></span>
|
||
</div>
|
||
<?php } ?>
|
||
<?php if ($row['expense_reference']) { ?>
|
||
<div class="itflow-glance-item" title="Reference">
|
||
<i class="fas fa-fw fa-hashtag"></i>
|
||
<span class="font-monospace text-break"><?= escapeHtml($row['expense_reference']) ?></span>
|
||
</div>
|
||
<?php } ?>
|
||
</div>
|
||
<div class="col-sm-6">
|
||
<div class="itflow-glance-item" title="Client">
|
||
<i class="fas fa-fw fa-user"></i>
|
||
<?php if ($client_id) { ?>
|
||
<a class="text-truncate" href="client_overview.php?client_id=<?= $client_id ?>"><?= escapeHtml($row['client_name']) ?></a>
|
||
<?php } else { ?>
|
||
<span class="text-body-secondary">Not for a client</span>
|
||
<?php } ?>
|
||
</div>
|
||
<div class="itflow-glance-item" title="Entered">
|
||
<i class="fas fa-fw fa-clock"></i>
|
||
<span><span title="<?= escapeHtml($row['expense_created_at']) ?>"><?= timeAgo($row['expense_created_at']) ?></span><?php if ($row['created_by_name']) { ?> <span class="text-body-secondary">by <?= escapeHtml($row['created_by_name']) ?></span><?php } ?></span>
|
||
</div>
|
||
<?= reconcileGlanceHtml($cleared) ?>
|
||
<?php if ($row['expense_recurring_expense_id']) { ?>
|
||
<div class="itflow-glance-item" title="Recorded by">
|
||
<i class="fas fa-fw fa-redo-alt"></i>
|
||
<span>Recorded by <?= $row['recurring_expense_frequency'] !== null
|
||
? '<a href="#" class="ajax-modal" data-modal-size="lg" data-modal-url="modals/recurring_expense/recurring_expense.php?id=' . intval($row['expense_recurring_expense_id']) . '">the ' . escapeHtml(strtolower(recurringInvoiceFrequencies()[$row['recurring_expense_frequency']] ?? 'monthly') . ' recurring expense') . '</a>'
|
||
: 'a recurring expense since deleted' ?></span>
|
||
</div>
|
||
<?php } ?>
|
||
<?php if ($billed_on) { ?>
|
||
<div class="itflow-glance-item" title="Billed">
|
||
<i class="fas fa-fw fa-file-invoice-dollar"></i>
|
||
<span>
|
||
<?php if ($row['invoice_status'] === null) { ?>
|
||
Billed on an invoice since deleted
|
||
<?php } else { ?>
|
||
Billed on <?= $can_sales ? '<a href="invoice.php?invoice_id=' . $billed_on . '">' . escapeHtml($row['invoice_prefix'] . $row['invoice_number']) . '</a>' : escapeHtml($row['invoice_prefix'] . $row['invoice_number']) ?><?= $row['invoice_status'] === 'Cancelled' ? ' <span class="text-danger-emphasis">(cancelled)</span>' : '' ?>
|
||
<?php if ($row['billed_subtotal'] !== null) { ?>
|
||
at <?= escapeHtml(billingMoney($row['billed_subtotal'], $currency)) ?>
|
||
<span class="text-body-secondary">· <?= escapeHtml(expenseMarginText($row['billed_subtotal'], $cost)) ?></span>
|
||
<?php } ?>
|
||
<?php } ?>
|
||
</span>
|
||
</div>
|
||
<?php } elseif ($billable) { ?>
|
||
<div class="itflow-glance-item" title="To bill">
|
||
<i class="fas fa-fw fa-file-invoice-dollar"></i>
|
||
<span>
|
||
<?php if ($cost <= 0) { ?>
|
||
Billable, but refunded in full
|
||
<?php } else { ?>
|
||
<span class="text-warning-emphasis fw-semibold">To bill</span> at <?= escapeHtml(billingMoney($bill_price, $currency)) ?>
|
||
<span class="text-body-secondary">· <?= escapeHtml(rtrim(rtrim(number_format(floatval($row['expense_markup']), 2, '.', ''), '0'), '.')) ?>% markup</span>
|
||
<?php if ($can_sales) { ?>
|
||
<span class="text-body-secondary">·</span> <a href="unbilled_time.php?client_id=<?= $client_id ?>">Unbilled Time</a>
|
||
<?php } ?>
|
||
<?php } ?>
|
||
</span>
|
||
</div>
|
||
<?php } ?>
|
||
<?php if ($row['expense_refund_of_id']) { ?>
|
||
<div class="itflow-glance-item" title="Refund of">
|
||
<i class="fas fa-fw fa-undo-alt"></i>
|
||
<span>Money back on <a href="#" class="ajax-modal" data-modal-size="lg" data-modal-url="modals/expense/expense.php?id=<?= intval($row['expense_refund_of_id']) ?>"><?= $row['refund_of_date'] !== null ? escapeHtml(billingMoney($row['refund_of_amount'], $currency) . ' from ' . $row['refund_of_date']) : 'an expense since deleted' ?></a></span>
|
||
</div>
|
||
<?php } ?>
|
||
<?php if ($refunds) { ?>
|
||
<div class="itflow-glance-item" title="Refunds">
|
||
<i class="fas fa-fw fa-undo-alt"></i>
|
||
<span>
|
||
<span class="text-success-emphasis"><?= escapeHtml(billingMoney($row['refunded_amount'], $currency)) ?> back</span>
|
||
<span class="text-body-secondary">·
|
||
<?php foreach ($refunds as $i => $refund) { ?><?= $i ? ', ' : '' ?><a href="#" class="ajax-modal" data-modal-size="lg" data-modal-url="modals/expense/expense.php?id=<?= intval($refund['expense_id']) ?>"><?= escapeHtml($refund['expense_date']) ?></a><?php } ?>
|
||
</span>
|
||
</span>
|
||
</div>
|
||
<?php } ?>
|
||
</div>
|
||
</div>
|
||
|
||
<?php if (trim((string) $row['expense_description']) !== '') { ?>
|
||
<div class="mt-3">
|
||
<div class="small fw-semibold text-uppercase text-body-secondary mb-1">Description</div>
|
||
<div class="text-break" style="white-space: pre-line;"><?= escapeHtml($row['expense_description']) ?></div>
|
||
</div>
|
||
<?php } ?>
|
||
|
||
<?php if ($stock) { ?>
|
||
<div class="mt-3">
|
||
<div class="small fw-semibold text-uppercase text-body-secondary mb-1">Stock it paid for</div>
|
||
<?php foreach ($stock as $move) { ?>
|
||
<div class="d-flex justify-content-between gap-2 small py-1 border-bottom">
|
||
<a class="text-truncate" href="product.php?product_id=<?= intval($move['product_id']) ?>"><i class="fas fa-fw fa-box-open text-secondary me-1"></i><?= escapeHtml(rtrim(rtrim(number_format(floatval($move['stock_qty']), 2, '.', ''), '0'), '.') . ' × ' . $move['product_name']) ?></a>
|
||
<span class="text-body-secondary text-nowrap"><?= $move['stock_unit_cost'] !== null ? escapeHtml(billingMoney($move['stock_unit_cost'], $currency)) . ' each' : '' ?></span>
|
||
</div>
|
||
<?php } ?>
|
||
</div>
|
||
<?php } ?>
|
||
|
||
<!-- Its history, from the audit log -->
|
||
<?= auditLogHistorySection(auditLogHistory('Expense', $expense_id, 10), ['client_id' => $client_id]) ?>
|
||
|
||
</div>
|
||
|
||
<div class="modal-footer">
|
||
<?php if ($receipt_kind) { ?>
|
||
<a class="btn btn-outline-secondary" href="<?= $receipt_url ?>"><i class="fas fa-download me-2"></i>Receipt</a>
|
||
<?php } ?>
|
||
<?php if ($can_edit && !$is_transfer) { ?>
|
||
<button type="button" class="btn btn-primary text-bold ajax-modal" data-modal-size="lg" data-modal-url="modals/expense/expense_edit.php?id=<?= $expense_id ?>"><i class="fas fa-edit me-2"></i>Edit</button>
|
||
<div class="btn-group">
|
||
<button type="button" class="btn btn-outline-secondary ajax-modal" data-modal-size="lg" data-modal-url="modals/expense/expense_copy.php?id=<?= $expense_id ?>"><i class="fas fa-copy me-2"></i>Copy</button>
|
||
<?php if ($amount > 0) { ?>
|
||
<button type="button" class="btn btn-outline-secondary ajax-modal" data-modal-size="lg" data-modal-url="modals/expense/expense_refund.php?id=<?= $expense_id ?>"><i class="fas fa-undo-alt me-2"></i>Refund</button>
|
||
<?php } ?>
|
||
</div>
|
||
<?php } ?>
|
||
<?php if ($to_reimburse && $can_edit) { ?>
|
||
<button type="button" class="btn btn-outline-success ajax-modal" data-modal-size="lg" data-modal-url="modals/expense/expense_reimburse.php?expense_ids[]=<?= $expense_id ?>"><i class="fas fa-hand-holding-usd"></i><span class="d-none d-sm-inline ms-2">Reimburse</span></button>
|
||
<?php } ?>
|
||
<?php if ($can_unbill) { ?>
|
||
<a class="btn btn-outline-secondary confirm-link" href="post.php?expense_unbill=<?= $expense_id ?>&csrf_token=<?= $_SESSION['csrf_token'] ?>"
|
||
data-confirm-text="<?= $row['invoice_status'] === 'Cancelled' ? escapeHtml($row['invoice_prefix'] . $row['invoice_number']) . ' is cancelled, so its line stays on it' : 'Its line comes off ' . escapeHtml($row['invoice_prefix'] . $row['invoice_number']) ?> and it waits to be billed again."><i class="fas fa-unlink"></i><span class="d-none d-sm-inline ms-2">Take off Invoice</span></a>
|
||
<?php } ?>
|
||
<?php if ($can_delete && !$is_transfer && $cleared) { ?>
|
||
<?= reconcileLockedButtonHtml($cleared) ?>
|
||
<?php } elseif ($can_delete && !$is_transfer) { ?>
|
||
<a class="btn btn-outline-danger confirm-link" href="post.php?delete_expense=<?= $expense_id ?>&csrf_token=<?= $_SESSION['csrf_token'] ?>"
|
||
data-confirm-text="<?= escapeHtml(expenseLabel($row)) ?> goes for good<?= $row['expense_receipt'] ? ', with its receipt' : '' ?>.<?= $billed_on && $row['invoice_status'] !== null ? ' Its line stays on ' . escapeHtml($row['invoice_prefix'] . $row['invoice_number']) . '.' : '' ?><?= $mileage_trips ? ' Its ' . ($mileage_trips === 1 ? 'trip is' : $mileage_trips . ' trips are') . ' to pay back for again.' : '' ?>"><i class="fas fa-trash"></i><span class="d-none d-sm-inline ms-2">Delete</span></a>
|
||
<?php } ?>
|
||
<button type="button" class="btn btn-light" data-bs-dismiss="modal"><i class="fa fa-times me-2"></i>Close</button>
|
||
</div>
|
||
|
||
<?php
|
||
require_once '../../../includes/modal_footer.php';
|