Files
itflow/admin/settings_invoice.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

371 lines
25 KiB
PHP

<?php
require_once "includes/inc_all_admin.php";
?>
<div class="card card-dark">
<div class="card-header py-3">
<h3 class="card-title"><i class="fas fa-fw fa-file-invoice me-2"></i>Invoice Settings</h3>
</div>
<div class="card-body">
<form action="post.php" method="post" autocomplete="off">
<input type="hidden" name="csrf_token" value="<?= $_SESSION['csrf_token'] ?>">
<h4>Invoice</h4>
<div class="mb-3">
<label>Invoice Prefix</label>
<div class="input-group">
<span class="input-group-text"><i class="fa fa-fw fa-barcode"></i></span>
<input type="text" class="form-control" name="config_invoice_prefix" placeholder="Invoice Prefix" maxlength="200" value="<?= escapeHtml($config_invoice_prefix) ?>" required>
</div>
</div>
<div class="mb-3">
<label>Next Number</label>
<div class="input-group">
<span class="input-group-text"><i class="fa fa-fw fa-barcode"></i></span>
<input type="number" min="0" class="form-control" name="config_invoice_next_number" placeholder="Next Invoice Number" value="<?= intval($config_invoice_next_number) ?>" required>
</div>
</div>
<div class="mb-3">
<label>Invoice Footer</label>
<textarea class="form-control" rows="4" name="config_invoice_footer"><?= escapeHtml($config_invoice_footer) ?></textarea>
</div>
<h5>Show Tax ID On Invoices</h5>
<div class="mb-3">
<div class="form-check form-switch">
<input type="checkbox" class="form-check-input" name="config_invoice_show_tax_id" <?php if ($config_invoice_show_tax_id == 1) { echo "checked"; } ?> value="1" id="customSwitch1">
<label class="form-check-label" for="customSwitch1">Show Tax ID</label>
</div>
</div>
<h5>Invoice Late Fees</h5>
<div class="mb-3">
<div class="form-check form-switch">
<input type="checkbox" class="form-check-input" name="config_invoice_late_fee_enable" <?php if ($config_invoice_late_fee_enable == 1) { echo "checked"; } ?> value="1" id="customSwitch2">
<label class="form-check-label" for="customSwitch2">Enable Late Fee</label>
</div>
</div>
<div class="mb-3">
<label>Late Fee %</label>
<div class="input-group">
<span class="input-group-text"><i class="fa fa-fw fa-percent"></i></span>
<input type="number" class="form-control" min="0" max="100" step="0.01" name="config_invoice_late_fee_percent" value="<?= $config_invoice_late_fee_percent ?>">
</div>
<small class="text-secondary">We recommend updating the invoice footer to include policies on your late charges. It is added as a line every 30 days after the invoice's due date, while payment reminders are on.</small>
</div>
<div class="mb-3">
<label>Email address to notify when invoices are paid online <small class="text-secondary">(Ideally a distribution list/shared mailbox)</small></label>
<div class="input-group">
<span class="input-group-text"><i class="fa fa-fw fa-bell"></i></span>
<input type="email" class="form-control" name="config_invoice_paid_notification_email" placeholder="Address to notify for paid invoices, leave blank for none" maxlength="200" value="<?= escapeHtml($config_invoice_paid_notification_email) ?>">
</div>
</div>
<hr>
<?php $reminder_rules = billingReminderRules();
$reminder_before = implode(', ', array_reverse(array_map('abs', array_filter($reminder_rules['days'], function ($day) { return $day < 0; }))));
$reminder_after = implode(', ', array_filter($reminder_rules['days'], function ($day) { return $day > 0; })); ?>
<h4 id="reminders">Reminders and Statements</h4>
<p class="text-secondary small">Payment reminders go to each client's primary and billing contacts while an invoice is still owed, with a button to view and pay it. A client can be left out of reminders and statements on the Billing tab of its Edit window. Late fees above come every 30 days late, while reminders are on.</p>
<div class="mb-3">
<div class="form-check form-switch">
<input type="checkbox" class="form-check-input" name="config_send_invoice_reminders" value="1" id="settingsReminders" <?= $reminder_rules['enabled'] ? 'checked' : '' ?> data-reminder-input>
<label class="form-check-label" for="settingsReminders">Email payment reminders</label>
</div>
</div>
<div class="row" data-reminder-fields>
<div class="col-md-6 mb-3">
<label>Before it is due</label>
<div class="input-group">
<span class="input-group-text"><i class="fa fa-fw fa-hourglass-start"></i></span>
<input type="text" class="form-control" name="reminder_before" maxlength="60" value="<?= escapeHtml($reminder_before) ?>" placeholder="None - or 3, or 3, 7" data-reminder-input>
<span class="input-group-text">days</span>
</div>
</div>
<div class="col-md-6 mb-3">
<label>After it is due</label>
<div class="input-group">
<span class="input-group-text"><i class="fa fa-fw fa-exclamation-circle"></i></span>
<input type="text" class="form-control" name="reminder_after" maxlength="120" value="<?= escapeHtml($reminder_after) ?>" placeholder="None - or 7, 14, 30" data-reminder-input>
<span class="input-group-text">days late</span>
</div>
</div>
<div class="col-md-6 mb-3">
<label>Then every</label>
<div class="input-group">
<span class="input-group-text"><i class="fa fa-fw fa-redo-alt"></i></span>
<input type="number" class="form-control" name="reminder_repeat" min="0" max="365" value="<?= intval($reminder_rules['repeat']) ?>" data-reminder-input>
<span class="input-group-text">days until paid</span>
</div>
<small class="text-secondary">0 for no more after the last one.</small>
</div>
<div class="col-md-6 mb-3 d-flex align-items-center">
<div class="form-check form-switch mt-md-3">
<input type="checkbox" class="form-check-input" name="reminder_on_due" value="1" id="settingsReminderOnDue" <?= in_array(0, $reminder_rules['days'], true) ? 'checked' : '' ?> data-reminder-input>
<label class="form-check-label" for="settingsReminderOnDue">And on the due date</label>
</div>
</div>
<div class="col-12 mb-3">
<div class="small text-body-secondary"><i class="fa fa-fw fa-bell me-1"></i>Reminders go <strong data-reminder-summary><?= escapeHtml(billingReminderScheduleText($reminder_rules)) ?></strong>. An invoice a recurring autopay charges on its due date isn't reminded before then.</div>
</div>
</div>
<div class="row">
<div class="col-md-8 mb-3">
<div class="form-check form-switch">
<input type="checkbox" class="form-check-input" name="config_statement_monthly" value="1" id="settingsStatements" <?= $reminder_rules['statement_monthly'] ? 'checked' : '' ?>>
<label class="form-check-label" for="settingsStatements">Email a statement each month to every client with a balance</label>
</div>
<small class="text-secondary">Their open invoices, how late each is and what is owed, as a PDF too.</small>
</div>
<div class="col-md-4 mb-3">
<div class="input-group">
<span class="input-group-text"><i class="fa fa-fw fa-calendar-day"></i></span>
<select class="form-select" name="config_statement_day" aria-label="Day of the month">
<?php for ($day = 1; $day <= 28; $day++) { ?>
<option value="<?= $day ?>" <?= $reminder_rules['statement_day'] === $day ? 'selected' : '' ?>>On the <?= $day . ([1 => 'st', 2 => 'nd', 3 => 'rd', 21 => 'st', 22 => 'nd', 23 => 'rd'][$day] ?? 'th') ?></option>
<?php } ?>
</select>
</div>
</div>
</div>
<div class="mb-3">
<div class="form-check form-switch">
<input type="checkbox" class="form-check-input" name="config_billing_email_attach_pdf" value="1" id="settingsAttachPdf" <?= $reminder_rules['attach_pdf'] ? 'checked' : '' ?>>
<label class="form-check-label" for="settingsAttachPdf">Attach the PDF to invoice, quote, reminder and statement emails</label>
</div>
<small class="text-secondary">The default in each Send window, and what reminders, statements and recurring invoices do.</small>
</div>
<script>
(function () {
// The schedule in words as it is typed - the same as billingReminderScheduleText()
var form = document.currentScript.closest('form');
var summary = form.querySelector('[data-reminder-summary]');
function days(name) {
var seen = {};
return String(form.querySelector('[name="' + name + '"]').value).split(/[\s,;]+/).map(Number).filter(function (n) {
var ok = n > 0 && n <= 365 && Math.floor(n) === n && !seen[n];
seen[n] = true;
return ok;
}).sort(function (a, b) { return a - b; });
}
function list(numbers) {
return numbers.length > 1 ? numbers.slice(0, -1).join(', ') + ' and ' + numbers[numbers.length - 1] : String(numbers[0]);
}
function update() {
var on = form.querySelector('[name="config_send_invoice_reminders"]').checked;
form.querySelector('[data-reminder-fields]').classList.toggle('opacity-50', !on);
var before = days('reminder_before').filter(function (n) { return n <= 60; });
var after = days('reminder_after');
var onDue = form.querySelector('[name="reminder_on_due"]').checked;
var repeat = Math.max(0, Math.min(365, parseInt(form.querySelector('[name="reminder_repeat"]').value, 10) || 0));
var parts = [];
if (before.length) {
parts.push(list(before) + ' day' + (before.length === 1 && before[0] === 1 ? '' : 's') + ' before it is due');
}
if (onDue) {
parts.push('on the due date');
}
if (after.length) {
parts.push(list(after) + ' day' + (after.length === 1 && after[0] === 1 ? '' : 's') + ' late');
}
if (repeat) {
parts.push((parts.length ? 'then ' : '') + 'every ' + repeat + ' day' + (repeat === 1 ? '' : 's') + (after.length || onDue ? '' : ' after it is due') + ' until it is paid');
}
summary.textContent = on ? (parts.length ? parts.join(', ') : 'never') : 'never - reminders are off';
}
form.addEventListener('input', update);
form.addEventListener('change', update);
update();
})();
</script>
<hr>
<h4>Recurring Invoice</h4>
<div class="mb-3">
<label>Recurring Prefix</label>
<div class="input-group">
<span class="input-group-text"><i class="fa fa-fw fa-barcode"></i></span>
<input type="text" class="form-control" name="config_recurring_invoice_prefix" placeholder="Recurring Invoice Prefix" maxlength="200" value="<?= escapeHtml($config_recurring_invoice_prefix) ?>" required>
</div>
</div>
<div class="mb-3">
<label>Next Number</label>
<div class="input-group">
<span class="input-group-text"><i class="fa fa-fw fa-barcode"></i></span>
<input type="number" min="0" class="form-control" name="config_recurring_invoice_next_number" placeholder="Next Recurring Invoice Number" value="<?= intval($config_recurring_invoice_next_number) ?>" required>
</div>
</div>
<hr>
<?php $time_rules = billingTimeRules(); ?>
<h4 id="billing-time">Billing Time</h4>
<p class="text-secondary small">How the time logged on a billable ticket becomes hours on an invoice - in Unbilled Time, a ticket's Invoice window and Add Tickets. Each ticket's time is rounded once, as a whole. It is billed at the client's hourly rate, or the default rate in Defaults when the client has none.</p>
<div class="row">
<div class="col-md-6 mb-3">
<label>Round to</label>
<div class="input-group">
<span class="input-group-text"><i class="fa fa-fw fa-stopwatch"></i></span>
<select class="form-select" name="config_time_increment">
<?php foreach (billingTimeIncrements() as $minutes => $label) { ?>
<option value="<?= $minutes ?>" <?= $time_rules['increment'] === $minutes ? 'selected' : '' ?>><?= escapeHtml($label) ?></option>
<?php } ?>
</select>
</div>
</div>
<div class="col-md-6 mb-3">
<label>Rounding</label>
<div class="input-group">
<span class="input-group-text"><i class="fa fa-fw fa-sort-amount-up"></i></span>
<select class="form-select" name="config_time_rounding">
<option value="nearest" <?= $time_rules['rounding'] === 'nearest' ? 'selected' : '' ?>>To the nearest - under half a step rounds down</option>
<option value="up" <?= $time_rules['rounding'] === 'up' ? 'selected' : '' ?>>Always up - any part of a step bills the whole step</option>
</select>
</div>
</div>
<div class="col-md-6 mb-3">
<label>Least a ticket bills</label>
<div class="input-group">
<span class="input-group-text"><i class="fa fa-fw fa-arrow-down"></i></span>
<select class="form-select" name="config_time_minimum">
<?php foreach ([0 => 'No minimum', 15 => '15 minutes', 30 => '30 minutes', 60 => '1 hour', 120 => '2 hours'] as $minutes => $label) { ?>
<option value="<?= $minutes ?>" <?= $time_rules['minimum'] === $minutes ? 'selected' : '' ?>><?= $label ?></option>
<?php } ?>
</select>
</div>
<small class="text-secondary">Once it has any time logged. A ticket with none bills nothing unless the hours are typed.</small>
</div>
<div class="col-md-6 mb-3">
<label>Invoice line</label>
<div class="input-group">
<span class="input-group-text"><i class="fa fa-fw fa-cube"></i></span>
<input type="text" class="form-control" name="config_time_item_name" maxlength="200" value="<?= escapeHtml($time_rules['item_name']) ?>" placeholder="Support [Hourly]" required>
</div>
<small class="text-secondary">The name of the line the time goes on. The ticket number, subject and date go in its description.</small>
</div>
</div>
<hr>
<h4 id="billable-expenses">Billable Expenses</h4>
<p class="text-secondary small">An expense for a client can be billed on to them - it waits in Unbilled Time and goes on their invoice at what it cost, less any refund, plus a markup. A recurring expense can be billed on each time it records one.</p>
<div class="row">
<div class="col-md-6 mb-3">
<label>Default markup</label>
<div class="input-group">
<span class="input-group-text"><i class="fa fa-fw fa-percent"></i></span>
<input type="text" class="form-control" name="config_expense_markup" inputmode="decimal" value="<?= rtrim(rtrim(number_format(expenseMarkupDefault(), 2, '.', ''), '0'), '.') ?>" placeholder="0">
</div>
<small class="text-secondary">What a billable expense starts with - 0 bills it at cost. Each expense can have its own.</small>
</div>
</div>
<hr>
<?php $trip_rules = tripRules(); ?>
<h4 id="mileage">Trips and Mileage</h4>
<p class="text-secondary small">An agent driving their own vehicle is paid back for the distance: Pay Mileage on the Trips page makes their trips a mileage expense they paid personally, to pay back with Reimburse. A trip for a client can be billed to them from Unbilled Time, beside their tickets and expenses.</p>
<div class="row">
<div class="col-md-6 mb-3">
<label>Distance in</label>
<div class="input-group">
<span class="input-group-text"><i class="fa fa-fw fa-ruler"></i></span>
<select class="form-select" name="config_mileage_unit">
<option value="mi" <?= $trip_rules['unit'] === 'mi' ? 'selected' : '' ?>>Miles</option>
<option value="km" <?= $trip_rules['unit'] === 'km' ? 'selected' : '' ?>>Kilometers</option>
</select>
</div>
<small class="text-secondary">For every trip, the rates below and the reports. Distances already logged aren't converted.</small>
</div>
<div class="col-md-6 mb-3">
<label>Paid back a <?= tripUnitOne() ?></label>
<div class="input-group">
<span class="input-group-text"><i class="fa fa-fw fa-hand-holding-usd"></i></span>
<input type="text" class="form-control" name="config_mileage_rate" inputmode="decimal" value="<?= $trip_rules['rate'] > 0 ? rtrim(rtrim(number_format($trip_rules['rate'], 4, '.', ''), '0'), '.') : '' ?>" placeholder="0.70">
</div>
<small class="text-secondary">For driving their own vehicle - your reimbursement rate, such as the IRS standard mileage rate. Empty turns paying back off.</small>
</div>
<div class="col-md-6 mb-3">
<label>Mileage goes under</label>
<div class="input-group">
<span class="input-group-text"><i class="fa fa-fw fa-list"></i></span>
<select class="form-select" name="config_mileage_category_id">
<option value="0">- Mileage, or Travel -</option>
<?php foreach (expenseCategories($trip_rules['category_id']) as $category_id => $category) { ?>
<option value="<?= intval($category_id) ?>" <?= intval($category_id) === $trip_rules['category_id'] ? 'selected' : '' ?>><?= escapeHtml($category['category_name']) ?></option>
<?php } ?>
</select>
</div>
<small class="text-secondary">The expense category of the mileage paid back. The expenses are to the vendor Mileage, made the first time.</small>
</div>
</div>
<h5 class="mt-2">Billing trips</h5>
<div class="row">
<div class="col-md-6 mb-3">
<label>Trip charge</label>
<div class="input-group">
<span class="input-group-text"><i class="fa fa-fw fa-dollar-sign"></i></span>
<input type="text" class="form-control" name="config_trip_charge" inputmode="decimal" value="<?= $trip_rules['charge'] > 0 ? number_format($trip_rules['charge'], 2, '.', '') : '' ?>" placeholder="0.00">
</div>
<small class="text-secondary">What each billable trip bills to start with.</small>
</div>
<div class="col-md-6 mb-3">
<label>Billed a <?= tripUnitOne() ?></label>
<div class="input-group">
<span class="input-group-text"><i class="fa fa-fw fa-road"></i></span>
<input type="text" class="form-control" name="config_trip_bill_rate" inputmode="decimal" value="<?= $trip_rules['bill_rate'] > 0 ? rtrim(rtrim(number_format($trip_rules['bill_rate'], 4, '.', ''), '0'), '.') : '' ?>" placeholder="0.00">
</div>
<small class="text-secondary">For the whole trip, there and back.</small>
</div>
<div class="col-md-6 mb-3">
<label>The charge covers the first</label>
<div class="input-group">
<span class="input-group-text"><i class="fa fa-fw fa-route"></i></span>
<input type="text" class="form-control" name="config_trip_included" inputmode="decimal" value="<?= $trip_rules['included'] > 0 ? rtrim(rtrim(number_format($trip_rules['included'], 1, '.', ''), '0'), '.') : '' ?>" placeholder="0">
<span class="input-group-text"><?= tripUnit(true) ?></span>
</div>
<small class="text-secondary">The rate bills only the distance beyond it. Empty bills all of it.</small>
</div>
<div class="col-md-6 mb-3">
<label>Invoice line</label>
<div class="input-group">
<span class="input-group-text"><i class="fa fa-fw fa-cube"></i></span>
<input type="text" class="form-control" name="config_trip_item_name" maxlength="200" value="<?= escapeHtml($trip_rules['item_name']) ?>" placeholder="Travel" required>
</div>
<small class="text-secondary">What a trip is called on the invoice. What it was for, the route, date and distance go in its description.</small>
</div>
</div>
<p class="text-secondary small mb-3"><?= tripBillingOn() ? 'A trip bills ' . escapeHtml(tripBillRuleText()) . '.' : 'With no charge and no rate, trips aren\'t billed.' ?> The price can be changed in Unbilled Time before it goes on.</p>
<hr>
<button type="submit" name="edit_invoice_settings" class="btn btn-primary text-bold"><i class="fas fa-check me-2"></i>Save</button>
</form>
</div>
</div>
<?php
require_once "../includes/footer.php";