Files
itflow/agent/post/trip.php
johnnyq a2a0823163 Trips stage 2: mileage money - Trips and Mileage in Admin > Settings > Invoice with miles or kilometers, the rate an agent is paid back for their own vehicle and the expense category it goes under, and what a trip bills a client as a trip charge, a rate a mile beyond the distance the charge covers and the invoice line name; the trip form has In their own vehicle starting as the driver's latest trip and Bill the client starting as the ticket is billable, each showing what it pays back or bills; the list gets To pay back and To bill pills, an owed card (Owed to you for agents without financial access), who is owed what and marks on each row; Pay Mileage pays one driver for the ticked trips at the usual or a past rate as a mileage expense per client paid personally by them under the mileage category to a Mileage vendor made on first use, paid back there and then from an account or later with Reimburse; Unbilled Time has Travel to bill beside the expenses at an editable price going on the client's invoice with their tickets and expenses, billing every client includes trips and Not billable writes them off; the invoice gets a Travel card with Add Trips and Take off Invoice, and deleting the line or invoice makes a trip to bill again; paid back and billed trips keep their distance, driver, vehicle, client and billable and can't be deleted, and deleting the mileage expense puts its trips back to pay; bulk Set Vehicle, Set Billable and Pay Mileage; the trip quick look shows what it pays back and bills and the mileage expense lists its trips; dashboard Trips to bill and Mileage to pay back, client overview Trips to bill and the Unbilled Time count include trips; export Vehicle, Paid Back and Billing columns; the Mileage report and dashboard chart follow the unit and the report rate starts at the mileage rate; demo data with recent trips in own vehicles and billable when trips bill; database 3.0.4 with trip_own_vehicle, trip_expense_id, trip_billable, trip_invoice_id, trip_invoice_item_id and the mileage and trip billing settings; CHANGELOG updated
2026-10-16 20:35:51 -04:00

334 lines
13 KiB
PHP

<?php
/*
* ITFlow - trips (functions/trips.php does the work).
*
* New, Edit, Copy and Return Trip all post through tripSave(). Anyone logs
* and changes their own trips; someone else's needs write access to
* Financial, deleting them full access. Bulk Set Driver, Set Client, Set
* Vehicle and Set Billable need write access, bulk delete full access - a
* trip paid back for or billed is left as it was. Pay Mileage and taking a
* trip off an invoice need write access to Financial (and Sales to take
* one off).
*/
defined('FROM_POST_HANDLER') || die("Direct file access is not allowed");
if (isset($_POST['add_trip'])) {
validateCSRFToken();
$trip_id = tripSave($_POST, 0, $error);
if (!$trip_id) {
flashAlert(escapeHtml($error), 'error');
redirect();
}
$row = tripRow($trip_id, false);
logAudit("Trip", "Create", "$session_name logged the trip " . escapeSql(tripLabel($row)), intval($row['trip_client_id']), $trip_id);
flashAlert("Trip to <strong>" . escapeHtml($row['trip_destination']) . "</strong> logged - " . escapeHtml(tripMilesText($row['trip_miles'])));
redirect();
}
if (isset($_POST['edit_trip'])) {
validateCSRFToken();
$trip_id = intval($_POST['trip_id'] ?? 0);
if (!tripSave($_POST, $trip_id, $error)) {
flashAlert(escapeHtml($error), 'error');
redirect();
}
$row = tripRow($trip_id, false);
logAudit("Trip", "Edit", "$session_name edited the trip " . escapeSql(tripLabel($row)), intval($row['trip_client_id']), $trip_id);
flashAlert("Trip to <strong>" . escapeHtml($row['trip_destination']) . "</strong> saved");
redirect();
}
if (isset($_GET['delete_trip'])) {
validateCSRFToken();
$row = tripDelete(intval($_GET['delete_trip']), $error);
if (!$row) {
flashAlert(escapeHtml($error), 'error');
redirect();
}
logAudit("Trip", "Delete", "$session_name deleted the trip " . escapeSql(tripLabel($row)), intval($row['trip_client_id']));
flashAlert("Trip to <strong>" . escapeHtml($row['trip_destination']) . "</strong> deleted", 'error');
redirect();
}
/*
* Set the driver (an agent), client (0 for none - the site and ticket go
* with the old one), vehicle (1 their own, 0 a company one) or billable
* (1 or 0) of the ticked trips. One paid back for keeps its driver and
* vehicle, one billed its client and billable.
*/
if (isset($_POST['bulk_edit_trips'])) {
validateCSRFToken();
enforceUserPermission('module_financial', 2);
$field = (string) ($_POST['field'] ?? '');
$value = intval($_POST['value'] ?? 0);
$set = '';
$label = '';
$skip = '';
if ($field === 'driver') {
$drivers = tripDrivers();
if (isset($drivers[$value])) {
$set = "trip_user_id = $value";
$label = 'the driver to ' . $drivers[$value]['user_name'];
$skip = 'trip_expense_id = 0';
}
} elseif ($field === 'client') {
$skip = 'trip_invoice_id = 0';
if (!$value) {
$set = 'trip_client_id = 0, trip_location_id = 0, trip_ticket_id = 0, trip_billable = 0';
$label = 'no client';
} elseif (mysqli_num_rows(mysqli_query($mysqli, "SELECT client_id FROM clients WHERE client_id = $value " . clientScopeSql('client_id')))) {
// The site and ticket first - the client after, as MySQL sets them in order
$set = "trip_location_id = IF(trip_client_id = $value, trip_location_id, 0), trip_ticket_id = IF(trip_client_id = $value, trip_ticket_id, 0), trip_client_id = $value";
$label = 'the client to ' . getFieldById('clients', $value, 'client_name');
}
} elseif ($field === 'vehicle') {
$set = 'trip_own_vehicle = ' . ($value ? 1 : 0);
$label = $value ? 'their own vehicle' : 'a company vehicle';
$skip = 'trip_expense_id = 0';
} elseif ($field === 'billable') {
$set = 'trip_billable = ' . ($value ? 'IF(trip_client_id > 0, 1, 0)' : 0);
$label = $value ? 'billable' : 'not billable';
$skip = 'trip_invoice_id = 0';
}
$trip_ids = array_unique(array_filter(array_map('intval', (array) ($_POST['trip_ids'] ?? []))));
if ($set === '' || !$trip_ids) {
flashAlert($trip_ids ? 'Pick what to set them to' : 'Tick the trips to change first', 'error');
redirect();
}
$count = 0;
$kept = 0;
foreach ($trip_ids as $trip_id) {
$row = tripRow($trip_id);
if (!$row) {
continue;
}
mysqli_query($mysqli, "UPDATE trips SET $set WHERE trip_id = $trip_id" . ($skip !== '' ? " AND $skip" : ''));
if ($skip !== '' && !mysqli_affected_rows($mysqli) && !mysqli_num_rows(mysqli_query($mysqli, "SELECT trip_id FROM trips WHERE trip_id = $trip_id AND $skip"))) {
$kept++;
continue;
}
logAudit("Trip", "Edit", "$session_name set " . escapeSql($label) . " on the trip " . escapeSql(tripLabel($row)), intval($row['trip_client_id']), $trip_id);
$count++;
}
logAudit("Trip", "Bulk Edit", "$session_name set " . escapeSql($label) . " on $count trip" . ($count === 1 ? '' : 's'));
flashAlert("Set " . escapeHtml($label) . " on <strong>$count</strong> trip" . ($count === 1 ? '' : 's')
. ($kept ? " - $kept " . ($kept === 1 ? 'was' : 'were') . ' left as ' . ($kept === 1 ? 'it was' : 'they were') . ', ' . ($skip === 'trip_expense_id = 0' ? 'paid back for already' : 'billed already') : ''), $kept ? 'warning' : 'success');
redirect();
}
if (isset($_POST['bulk_delete_trips'])) {
validateCSRFToken();
enforceUserPermission('module_financial', 3);
$count = 0;
$kept = 0;
foreach (array_unique(array_filter(array_map('intval', (array) ($_POST['trip_ids'] ?? [])))) as $trip_id) {
$row = tripDelete($trip_id, $delete_error);
if (!$row) {
$kept += $delete_error !== '' && strpos($delete_error, 'no longer exists') === false ? 1 : 0;
continue;
}
logAudit("Trip", "Delete", "$session_name deleted the trip " . escapeSql(tripLabel($row)), intval($row['trip_client_id']));
$count++;
}
logAudit("Trip", "Bulk Delete", "$session_name deleted $count trip" . ($count === 1 ? '' : 's'));
flashAlert("<strong>$count</strong> trip" . ($count === 1 ? '' : 's') . " deleted" . ($kept ? " - $kept paid back for or billed " . ($kept === 1 ? 'was' : 'were') . ' kept' : ''), 'error');
redirect();
}
/*
* Pay Mileage - the driver's ticked trips in their own vehicle become a
* mileage expense for each client, paid personally by them at the rate
* typed; paid back from an account there and then when asked.
*/
if (isset($_POST['trip_claim'])) {
validateCSRFToken();
enforceUserPermission('module_financial', 2);
$user_id = intval($_POST['user_id'] ?? 0);
$result = tripClaim($user_id, array_unique(array_filter(array_map('intval', (array) ($_POST['trip_ids'] ?? [])))), [
'rate' => (string) ($_POST['rate'] ?? ''),
'date' => (string) ($_POST['date'] ?? ''),
'by' => stripslashes($session_name),
], $error);
if (!$result) {
flashAlert(escapeHtml($error), 'error');
redirect();
}
$what = '<strong>' . escapeHtml($result['user_name']) . '</strong>\'s mileage - ' . ($result['count'] === 1 ? '1 trip' : $result['count'] . ' trips') . ', '
. escapeHtml(tripDistanceText($result['miles'])) . ' at ' . escapeHtml(tripRateText($result['rate'])) . ': <strong>' . escapeHtml(billingMoney($result['amount'], $result['currency'])) . '</strong>';
if (empty($_POST['pay_now'])) {
flashAlert("$what to pay back - in To reimburse on the expenses");
redirect();
}
$paid = expenseReimburse($user_id, $result['expense_ids'], [
'account_id' => intval($_POST['account'] ?? 0),
'date' => (string) ($_POST['date'] ?? ''),
'payment_method' => (string) ($_POST['payment_method'] ?? ''),
'reference' => (string) ($_POST['reference'] ?? ''),
'by' => stripslashes($session_name),
], $pay_error);
if (!$paid) {
flashAlert("$what to pay back - not paid back yet: " . escapeHtml($pay_error), 'warning');
redirect();
}
flashAlert("$what, paid back");
redirect();
}
/* Take a trip off the invoice it was billed on - it is to bill again. */
if (isset($_GET['trip_unbill'])) {
validateCSRFToken();
enforceUserPermission('module_financial', 2);
enforceUserPermission('module_sales', 2);
$trip_id = intval($_GET['trip_unbill']);
$row = tripRow($trip_id);
$number = $row ? tripUnbill($trip_id, $error) : '';
if (!$row || $number === '') {
flashAlert(escapeHtml($row ? $error : 'That trip no longer exists'), 'error');
redirect();
}
logAudit("Trip", "Unbill", "$session_name took the trip " . escapeSql(tripLabel($row)) . " off invoice " . escapeSql($number), intval($row['trip_client_id']), $trip_id);
if ($row['invoice_status'] !== null) {
billingLog('invoice', intval($row['trip_invoice_id']), $row['invoice_status'], 'Trip taken off by ' . stripslashes($session_name) . ' - ' . tripLabel($row));
}
flashAlert("Trip to <strong>" . escapeHtml($row['trip_destination']) . "</strong> taken off " . escapeHtml($number) . " - it is to bill again");
redirect();
}
/* An invoice's Add Trips - the client's ticked trips to bill, at the prices typed. */
if (isset($_POST['invoice_add_trips'])) {
validateCSRFToken();
enforceUserPermission('module_sales', 2);
enforceUserPermission('module_financial', 2);
$invoice_id = intval($_POST['invoice_id'] ?? 0);
$invoice = billingRow('invoice', $invoice_id);
if (!$invoice) {
flashAlert('That invoice no longer exists.', 'error');
redirect();
}
$client_id = intval($invoice['client_id']);
enforceClientAccess();
$prices = [];
foreach (array_unique(array_filter(array_map('intval', (array) ($_POST['trip_ids'] ?? [])))) as $trip_id) {
$prices[$trip_id] = $_POST['trip_price'][$trip_id] ?? null;
}
$result = tripBillInvoice($client_id, $prices, [
'invoice_id' => $invoice_id,
'tax_id' => intval($_POST['tax_id'] ?? 0),
'by' => stripslashes($session_name),
], $error);
if (!$result) {
flashAlert(escapeHtml($error), 'error');
redirect("invoice.php?invoice_id=$invoice_id");
}
flashAlert('<strong>' . ($result['trips'] === 1 ? '1 trip' : $result['trips'] . ' trips') . '</strong> billed - ' . escapeHtml(billingMoney($result['amount'], $invoice['invoice_currency_code']))
. ' for ' . escapeHtml(tripDistanceText($result['miles'])));
redirect("invoice.php?invoice_id=$invoice_id#items");
}
if (isExportRequest('export_trips')) {
validateCSRFToken();
$format = resolveExportFormat($_POST['export_trips']);
// The same filters as the trips list, over the trips this agent may see
$src = $_POST;
if (!empty($_POST['client_id'])) {
$src['client'] = intval($_POST['client_id']);
}
$where = tripFiltersSql($src, $filter_summary);
$client_id = intval($src['client'] ?? 0);
if ($client_id) {
enforceClientAccess();
}
$sql = mysqli_query($mysqli, "SELECT trips.*, client_name, driver.user_name AS driver_name, location_name, ticket_prefix, ticket_number, ticket_subject,
claim.expense_id AS claim_expense_id, reimbursement_date, invoice_prefix, invoice_number
FROM trips " . tripJoinsSql() . "
LEFT JOIN expenses AS claim ON trips.trip_expense_id > 0 AND claim.expense_id = trips.trip_expense_id
LEFT JOIN reimbursements ON reimbursement_id = claim.expense_reimbursement_id
LEFT JOIN invoices ON trips.trip_invoice_id > 0 AND invoice_id = trips.trip_invoice_id
WHERE $where
ORDER BY trips.trip_date ASC, trips.trip_id ASC");
$num_rows = mysqli_num_rows($sql);
if ($num_rows > 0) {
guardExportPdfRowCount($format, $num_rows);
$export = beginExport('trips', $format, ($client_id ? getFieldById('clients', $client_id, 'client_name') : $session_company_name) . '-Trips', 'Trips', summarizeExportFilters($filter_summary));
while ($row = mysqli_fetch_assoc($sql)) {
$row['user_name'] = $row['driver_name'];
$row['round_trip_display'] = intval($row['round_trip']) ? 'Yes' : 'No';
$row['trip_one_way'] = number_format(tripOneWay($row), 1, '.', '');
$row['ticket_display'] = $row['ticket_number'] !== null ? ticketReference($row) . ' - ' . $row['ticket_subject'] : '';
$row['vehicle_display'] = intval($row['trip_own_vehicle']) ? 'Own' : 'Company';
$row['paid_back_display'] = !intval($row['trip_own_vehicle']) ? '' : ($row['claim_expense_id'] === null ? 'To pay back' : ($row['reimbursement_date'] !== null ? 'Paid back ' . $row['reimbursement_date'] : 'On a mileage expense'));
$row['billing_display'] = $row['invoice_number'] !== null ? 'Billed on ' . $row['invoice_prefix'] . $row['invoice_number'] : (intval($row['trip_billable']) ? 'To bill' : '');
addExportRow($export, $row);
}
finishExport($export);
}
logAudit("Trip", "Export", "$session_name exported $num_rows trip(s) to a " . strtoupper($format) . " file", $client_id);
exit;
}