$account_name to $updated_count 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[] = "$revenue_updated_count revenue(s)";
}
if ($invoice_updated_count) {
$updated_summary[] = "$invoice_updated_count 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 = " - $skipped_count record(s) skipped";
}
flashAlert("You assigned category $category_name 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 $payment_method on $updated_count income record(s)");
} else {
flashAlert("No income records were updated", 'error');
}
redirect();
}
if (isset($_POST['export_income'])) {
validateCSRFToken();
enforceUserPermission('module_financial');
$format = resolveExportFormat($_POST['export_income']);
$date_from = escapeSql($_POST['date_from']);
$date_to = escapeSql($_POST['date_to']);
$account = intval($_POST['account']);
// Client Filter - carried over from the client-scoped Income page
$client_id = intval($_POST['client_id'] ?? 0);
if ($client_id) {
enforceClientAccess();
$payment_client_query = "AND invoice_client_id = $client_id";
$revenue_client_query = "AND revenue_client_id = $client_id";
$client_name = getFieldById('clients', $client_id, 'client_name');
$file_name_prepend = "$client_name-";
} else {
// Default - any
$payment_client_query = '';
$revenue_client_query = '';
$file_name_prepend = "$session_company_name-";
}
// Type Filter
$income_types_array = ['Payment', 'Revenue'];
if (!empty($_POST['type']) && in_array($_POST['type'], $income_types_array)) {
$type_query = "AND (income_type = '" . escapeSql($_POST['type']) . "')";
} else {
// Default - any
$type_query = '';
}
// Account Filter
if ($account) {
$account_query = "AND (income_account_id = $account)";
} else {
// Default - any
$account_query = '';
}
// Category Filter - a revenue carries its own category, a payment inherits the one on the
// invoice it was paid against. Both come from the same 'Income' category pool.
$category = intval($_POST['category'] ?? 0);
if ($category) {
$category_query = "AND (income_category_id = $category)";
} else {
// Default - any
$category_query = '';
}
// Payment Method Filter
if (!empty($_POST['method'])) {
$method_query = "AND (income_method = '" . escapeSql($_POST['method']) . "')";
} else {
// Default - any
$method_query = '';
}
// Search Filter - mirrors the income page search box
$q = escapeSql($_POST['q']);
if (!empty($q)) {
$search_query = "AND (income_source LIKE '%$q%' OR income_category LIKE '%$q%' OR income_client LIKE '%$q%' OR income_account LIKE '%$q%' OR income_method LIKE '%$q%' OR income_reference LIKE '%$q%' OR income_amount LIKE '%$q%')";
} else {
// Default - any
$search_query = '';
}
// Date Filter
if (!empty($date_from) && !empty($date_to)) {
$date_query = "AND DATE(income_date) BETWEEN '$date_from' AND '$date_to'";
} else {
$date_query = '';
}
// Filter summary for the export header. This handler was the only one not building it,
// so a filtered PDF came out looking like a full export.
$filter_summary = [];
if ($client_id) {
$filter_summary['Client'] = $client_name;
}
if (!empty($_POST['type']) && in_array($_POST['type'], $income_types_array)) {
$filter_summary['Type'] = $_POST['type'];
}
if ($category) {
$filter_summary['Category'] = getFieldById('categories', $category, 'category_name');
}
if ($account) {
$filter_summary['Account'] = getFieldById('accounts', $account, 'account_name');
}
if (!empty($_POST['method'])) {
$filter_summary['Payment Method'] = $_POST['method'];
}
if (!empty($date_from) && !empty($date_to)) {
$filter_summary['Date'] = "$date_from to $date_to";
}
if (!empty($_POST['q'])) {
$filter_summary['Search'] = $_POST['q'];
}
// Same union as income.php - payments applied to an invoice, and standalone revenues.
// Transfers between accounts are stored as a linked expense + revenue pair, so the revenue leg
// is excluded here (transfer_id IS NULL) - moving your own money is not income.
$sql = mysqli_query(
$mysqli,
"SELECT * FROM (
SELECT
'Payment' AS income_type,
payment_id AS income_id,
payment_date AS income_date,
payment_created_at AS income_created_at,
CONCAT(invoice_prefix, invoice_number) AS income_source,
category_name AS income_category,
IFNULL(invoice_category_id, 0) AS income_category_id,
client_name AS income_client,
payment_amount AS income_amount,
payment_currency_code AS income_currency_code,
payment_method AS income_method,
payment_reference AS income_reference,
payment_account_id AS income_account_id,
account_name AS income_account
FROM payments
LEFT JOIN invoices ON payment_invoice_id = invoice_id
LEFT JOIN clients ON invoice_client_id = client_id
LEFT JOIN accounts ON payment_account_id = account_id
LEFT JOIN categories ON invoice_category_id = category_id
WHERE payment_archived_at IS NULL
$payment_client_query
$access_permission_query
UNION ALL
SELECT
'Revenue',
revenue_id,
revenue_date,
revenue_created_at,
revenue_description,
category_name,
revenue_category_id,
client_name,
revenue_amount,
revenue_currency_code,
revenue_payment_method,
revenue_reference,
revenue_account_id,
account_name
FROM revenues
LEFT JOIN categories ON revenue_category_id = category_id
LEFT JOIN clients ON revenue_client_id = client_id
LEFT JOIN accounts ON revenue_account_id = account_id
LEFT JOIN transfers ON transfer_revenue_id = revenue_id
WHERE revenue_archived_at IS NULL
AND transfer_id IS NULL
$revenue_client_query
) AS income
WHERE 1 = 1
$date_query
$type_query
$category_query
$account_query
$method_query
$search_query
ORDER BY income_date ASC, income_created_at ASC, income_type ASC, income_id ASC"
);
$num_rows = mysqli_num_rows($sql);
if ($num_rows > 0) {
guardExportPdfRowCount($format, $num_rows);
$export = beginExport('income', $format, $file_name_prepend . 'Income', 'Income', summarizeExportFilters($filter_summary));
while ($row = mysqli_fetch_assoc($sql)) {
addExportRow($export, $row);
}
finishExport($export);
}
logAudit("Income", "Export", "$session_name exported $num_rows income record(s) to a " . strtoupper($format) . " file");
exit;
}