Feature: Add Income Bulk Actions (Account, Payment Method and category

This commit is contained in:
johnnyq
2026-08-03 15:23:50 -04:00
parent a313278623
commit abd79c0aed
6 changed files with 634 additions and 0 deletions

View File

@@ -9,6 +9,304 @@ if (!defined('FROM_POST_HANDLER')) {
exit;
}
// The Income page merges payments and revenues, so its bulk actions have to fan out across both
// tables. Selection parsing is shared - see income_model.php.
// Gating: these are the multi-row form of the row Edit action on the Income page, so they gate the
// same way the payment edit handler does (the strictest of the two row types).
if (isset($_POST['bulk_edit_income_account'])) {
validateCSRFToken();
enforceUserPermission('module_sales', 3);
enforceUserPermission('module_financial', 3);
require_once 'income_model.php';
$account_id = intval($_POST['bulk_account_id']);
// Get Account name for logging and Notification - and confirm it is a real, un-archived account
$sql_account = mysqli_query($mysqli, "SELECT account_name FROM accounts WHERE account_id = $account_id AND account_archived_at IS NULL LIMIT 1");
$row = mysqli_fetch_assoc($sql_account);
if (!$row || !$income_count) {
flashAlert("Nothing to update", 'error');
redirect();
}
$account_name = escapeSql($row['account_name']);
$updated_count = 0;
// Payments - client comes from the invoice the payment was made against
foreach ($payment_ids as $payment_id) {
$sql = mysqli_query($mysqli, "SELECT payment_reference, invoice_client_id FROM payments LEFT JOIN invoices ON payment_invoice_id = invoice_id WHERE payment_id = $payment_id AND payment_archived_at IS NULL");
$row = mysqli_fetch_assoc($sql);
if (!$row) {
continue;
}
$payment_reference = escapeSql($row['payment_reference']);
$client_id = intval($row['invoice_client_id']);
if ($client_id) {
enforceClientAccess($client_id);
}
mysqli_query($mysqli, "UPDATE payments SET payment_account_id = $account_id WHERE payment_id = $payment_id");
logAudit("Payment", "Edit", "$session_name assigned payment $payment_reference to account $account_name", $client_id, $payment_id);
$updated_count++;
}
// Revenues
foreach ($revenue_ids as $revenue_id) {
$sql = mysqli_query($mysqli, "SELECT revenue_description, revenue_client_id FROM revenues WHERE revenue_id = $revenue_id AND revenue_archived_at IS NULL");
$row = mysqli_fetch_assoc($sql);
if (!$row) {
continue;
}
$revenue_description = escapeSql($row['revenue_description']);
$client_id = intval($row['revenue_client_id']);
if ($client_id) {
enforceClientAccess($client_id);
}
mysqli_query($mysqli, "UPDATE revenues SET revenue_account_id = $account_id WHERE revenue_id = $revenue_id");
logAudit("Revenue", "Edit", "$session_name assigned revenue $revenue_description to account $account_name", $client_id, $revenue_id);
$updated_count++;
}
if ($updated_count) {
logAudit("Income", "Bulk Edit", "$session_name assigned $updated_count income record(s) to account $account_name");
flashAlert("You assigned account <strong>$account_name</strong> to <strong>$updated_count</strong> income record(s)");
} else {
flashAlert("No income records were updated", 'error');
}
redirect();
}
if (isset($_POST['bulk_edit_income_category'])) {
validateCSRFToken();
enforceUserPermission('module_sales', 3);
enforceUserPermission('module_financial', 3);
require_once 'income_model.php';
$category_id = intval($_POST['bulk_category_id']);
// Get Category name for logging and Notification - and confirm it is a live Income category
$sql_category = mysqli_query($mysqli, "SELECT category_name FROM categories WHERE category_id = $category_id AND category_type = 'Income' AND category_archived_at IS NULL LIMIT 1");
$row = mysqli_fetch_assoc($sql_category);
if (!$row || !$income_count) {
flashAlert("Nothing to update", 'error');
redirect();
}
$category_name = escapeSql($row['category_name']);
$revenue_updated_count = 0;
$invoice_updated_count = 0;
$skipped_count = 0;
// Revenues carry their own category
foreach ($revenue_ids as $revenue_id) {
$sql = mysqli_query($mysqli, "SELECT revenue_description, revenue_client_id FROM revenues WHERE revenue_id = $revenue_id AND revenue_archived_at IS NULL");
$row = mysqli_fetch_assoc($sql);
if (!$row) {
$skipped_count++;
continue;
}
$revenue_description = escapeSql($row['revenue_description']);
$client_id = intval($row['revenue_client_id']);
if ($client_id) {
enforceClientAccess($client_id);
}
mysqli_query($mysqli, "UPDATE revenues SET revenue_category_id = $category_id WHERE revenue_id = $revenue_id");
logAudit("Revenue", "Edit", "$session_name assigned revenue $revenue_description to category $category_name", $client_id, $revenue_id);
$revenue_updated_count++;
}
// A payment has no category of its own - it inherits the one on the invoice it was paid
// against, so this writes to the INVOICE. Two selected payments against the same invoice
// therefore collapse into a single invoice update, and a payment with no invoice is skipped.
$invoice_ids = [];
foreach ($payment_ids as $payment_id) {
$sql = mysqli_query($mysqli, "SELECT payment_invoice_id FROM payments WHERE payment_id = $payment_id AND payment_archived_at IS NULL");
$row = mysqli_fetch_assoc($sql);
$payment_invoice_id = intval($row['payment_invoice_id'] ?? 0);
if ($payment_invoice_id) {
$invoice_ids[$payment_invoice_id] = $payment_invoice_id;
} else {
$skipped_count++;
}
}
foreach ($invoice_ids as $invoice_id) {
$sql = mysqli_query($mysqli, "SELECT invoice_prefix, invoice_number, invoice_client_id FROM invoices WHERE invoice_id = $invoice_id");
$row = mysqli_fetch_assoc($sql);
if (!$row) {
$skipped_count++;
continue;
}
$invoice_prefix = escapeSql($row['invoice_prefix']);
$invoice_number = intval($row['invoice_number']);
$client_id = intval($row['invoice_client_id']);
enforceClientAccess($client_id);
mysqli_query($mysqli, "UPDATE invoices SET invoice_category_id = $category_id WHERE invoice_id = $invoice_id");
logAudit("Invoice", "Edit", "$session_name assigned invoice $invoice_prefix$invoice_number to category $category_name", $client_id, $invoice_id);
$invoice_updated_count++;
}
// Spell out the invoice leg - the user selected payments, not invoices
$updated_summary = [];
if ($revenue_updated_count) {
$updated_summary[] = "<strong>$revenue_updated_count</strong> revenue(s)";
}
if ($invoice_updated_count) {
$updated_summary[] = "<strong>$invoice_updated_count</strong> invoice(s) behind the selected payment(s)";
}
if ($updated_summary) {
logAudit("Income", "Bulk Edit", "$session_name assigned category $category_name to $revenue_updated_count revenue(s) and $invoice_updated_count invoice(s)");
$skipped_note = '';
if ($skipped_count) {
$skipped_note = " - <strong>$skipped_count</strong> record(s) skipped";
}
flashAlert("You assigned category <strong>$category_name</strong> to " . implode(' and ', $updated_summary) . $skipped_note);
} else {
flashAlert("No income records were categorised - a payment can only take a category from the invoice it was paid against", 'error');
}
redirect();
}
if (isset($_POST['bulk_edit_income_method'])) {
validateCSRFToken();
enforceUserPermission('module_sales', 3);
enforceUserPermission('module_financial', 3);
require_once 'income_model.php';
// The method is stored by name on both tables, so validate it against the lookup list
$payment_method = escapeSql($_POST['bulk_payment_method']);
$sql_payment_method = mysqli_query($mysqli, "SELECT payment_method_name FROM payment_methods WHERE payment_method_name = '$payment_method' LIMIT 1");
$row = mysqli_fetch_assoc($sql_payment_method);
if (!$row || !$income_count) {
flashAlert("Nothing to update", 'error');
redirect();
}
$payment_method = escapeSql($row['payment_method_name']);
$updated_count = 0;
// Payments - client comes from the invoice the payment was made against
foreach ($payment_ids as $payment_id) {
$sql = mysqli_query($mysqli, "SELECT payment_reference, invoice_client_id FROM payments LEFT JOIN invoices ON payment_invoice_id = invoice_id WHERE payment_id = $payment_id AND payment_archived_at IS NULL");
$row = mysqli_fetch_assoc($sql);
if (!$row) {
continue;
}
$payment_reference = escapeSql($row['payment_reference']);
$client_id = intval($row['invoice_client_id']);
if ($client_id) {
enforceClientAccess($client_id);
}
mysqli_query($mysqli, "UPDATE payments SET payment_method = '$payment_method' WHERE payment_id = $payment_id");
logAudit("Payment", "Edit", "$session_name set payment $payment_reference to payment method $payment_method", $client_id, $payment_id);
$updated_count++;
}
// Revenues
foreach ($revenue_ids as $revenue_id) {
$sql = mysqli_query($mysqli, "SELECT revenue_description, revenue_client_id FROM revenues WHERE revenue_id = $revenue_id AND revenue_archived_at IS NULL");
$row = mysqli_fetch_assoc($sql);
if (!$row) {
continue;
}
$revenue_description = escapeSql($row['revenue_description']);
$client_id = intval($row['revenue_client_id']);
if ($client_id) {
enforceClientAccess($client_id);
}
mysqli_query($mysqli, "UPDATE revenues SET revenue_payment_method = '$payment_method' WHERE revenue_id = $revenue_id");
logAudit("Revenue", "Edit", "$session_name set revenue $revenue_description to payment method $payment_method", $client_id, $revenue_id);
$updated_count++;
}
if ($updated_count) {
logAudit("Income", "Bulk Edit", "$session_name set $updated_count income record(s) to payment method $payment_method");
flashAlert("You set payment method <strong>$payment_method</strong> on <strong>$updated_count</strong> income record(s)");
} else {
flashAlert("No income records were updated", 'error');
}
redirect();
}
if (isset($_POST['export_income'])) {
validateCSRFToken();