Files
itflow/agent/modals/expense/expense.php
johnnyq c93cb20f14 Expenses stage 1: rebuilt expenses list with this month against the same days last month, year so far with monthly average, recurring cost a month and expenses without a receipt in the last 90 days along the top, pills for client, no receipt and refunds with counts, vendor, category, client, date, account and payment method filters and a total of what the filters leave, an expense quick look showing the receipt inline with attach from the window, who entered it, refunds against it and the stock it paid for, one shared form for New, Edit, Copy and Refund with icons on every field, payment method saved from the default in Settings, vendor picks filling in the category, account and method of the last expense with them, refunds linked to the expense they give back and starting at what has not come back, receipts replaced or removed in Edit and served through agent/expense_receipt.php after the Financial and client checks with uploads/expenses closed to the web, bulk set category, account, payment method or client in one window and a single bulk delete, export with the list filters plus paid by, receipt and entered by columns, expenses in global search with open on load, the vendor page, Transactions and product stock history opening the quick look and Client Profitability linking direct costs to the client's expenses, fixes for the dead bulk delete button, bulk set client ignoring client restrictions, the New Expense window opening without write access, unlink warnings on empty receipts and photo receipts downloading as pdf, functions/expenses.php shared by the list, windows, export and search, database 2.9.9 with expense_created_by backfilled from the audit log and expense_refund_of_id, CHANGELOG updated
2026-10-16 17:43:11 -04:00

201 lines
12 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
/*
* 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, the client, who
* entered it, the refund it is or the refunds against it, 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']);
$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']);
$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";
// 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_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>
</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_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 - it is changed or deleted from <a href="transfers.php">Transfers</a>.</div>
<?php } ?>
<!-- The receipt -->
<?php if ($receipt_kind) { ?>
<div class="itflow-file-preview mb-3">
<?php if ($receipt_kind === 'image') { ?>
<a href="<?= $receipt_url ?>&amp;action=view" target="_blank" title="Open full size"><img src="<?= $receipt_url ?>&amp;action=view" alt="Receipt"></a>
<?php } else { ?>
<iframe src="<?= $receipt_url ?>&amp;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']) ?><?= escapeHtml($row['category_name'] ?: 'No category') ?></span>
</div>
<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">&middot; ' . escapeHtml($row['expense_payment_method']) . '</span>' : '' ?></span>
</div>
<?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>
<?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">&middot;
<?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 } ?>
</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 ($can_delete && !$is_transfer) { ?>
<a class="btn btn-outline-danger confirm-link" href="post.php?delete_expense=<?= $expense_id ?>&amp;csrf_token=<?= $_SESSION['csrf_token'] ?>"
data-confirm-text="<?= escapeHtml(expenseLabel($row)) ?> goes for good<?= $row['expense_receipt'] ? ', with its receipt' : '' ?>."><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';