mirror of
https://github.com/itflow-org/itflow
synced 2026-11-11 15:07:24 +00:00
368 lines
20 KiB
PHP
368 lines
20 KiB
PHP
<?php
|
|
|
|
/*
|
|
* The fields of a recurring expense - shared by New Recurring Expense and
|
|
* Edit. Set before including:
|
|
*
|
|
* $form_row recurringExpenseRow() when editing, null for a new one
|
|
* $form_vendor_id the vendor a new one starts with (0 to pick)
|
|
* $form_client_id the client a new one starts with (0 for none)
|
|
*
|
|
* Posts vendor, amount, tax_id and tax_amount (expense_tax_fields.php),
|
|
* frequency, next_date, end_date, category, account, payment_method,
|
|
* client_id, reference and description. The schedule reads
|
|
* back in words as it is set; picking a vendor on a new one fills in what
|
|
* the last expense with them used, unless that was changed by hand.
|
|
*
|
|
* Should not be accessed directly.
|
|
*/
|
|
|
|
isset($mysqli) || die("Direct file access is not allowed");
|
|
|
|
$form_edit = !empty($form_row);
|
|
$form_id = 'recurringExpenseForm' . ($form_edit ? intval($form_row['recurring_expense_id']) : 'New');
|
|
$form_value = function ($column, $default = '') use ($form_row) {
|
|
return $form_row ? (string) ($form_row[$column] ?? $default) : $default;
|
|
};
|
|
|
|
$form_defaults = expenseVendorDefaults();
|
|
$form_vendor = $form_edit ? intval($form_row['recurring_expense_vendor_id']) : intval($form_vendor_id ?? 0);
|
|
$form_category = intval($form_value('recurring_expense_category_id', '0'));
|
|
$form_account = $form_edit ? intval($form_row['recurring_expense_account_id']) : intval($config_default_expense_account);
|
|
$form_method = $form_edit ? $form_value('recurring_expense_payment_method') : (string) $config_default_expense_payment_method;
|
|
if (!$form_edit && $form_vendor && isset($form_defaults[$form_vendor])) {
|
|
$form_category = $form_defaults[$form_vendor]['category'];
|
|
$form_account = $form_defaults[$form_vendor]['account'] ?: $form_account;
|
|
$form_method = $form_defaults[$form_vendor]['method'] !== '' ? $form_defaults[$form_vendor]['method'] : $form_method;
|
|
}
|
|
$form_client = $form_edit ? intval($form_row['recurring_expense_client_id']) : intval($form_client_id ?? 0);
|
|
$form_frequency = $form_value('recurring_expense_frequency', 'month');
|
|
$form_next = $form_edit ? $form_value('recurring_expense_next_date') : date('Y-m-d', strtotime('first day of next month'));
|
|
$form_end = $form_value('recurring_expense_end_date');
|
|
$form_currency = $form_edit ? $form_value('recurring_expense_currency_code') : (string) $session_company_currency;
|
|
|
|
$form_vendors = expenseVendors($form_edit ? $form_vendor : 0);
|
|
$form_accounts = paymentAccounts($form_edit ? $form_account : 0);
|
|
$form_methods = paymentMethods('out');
|
|
if ($form_method !== '' && !in_array($form_method, $form_methods, true)) {
|
|
$form_methods[] = $form_method;
|
|
}
|
|
$form_clients = [];
|
|
$sql = mysqli_query($mysqli, "SELECT client_id, client_name, client_abbreviation FROM clients
|
|
WHERE (client_archived_at IS NULL OR client_id = $form_client) " . clientScopeSql('clients.client_id') . " ORDER BY client_name ASC");
|
|
while ($row = mysqli_fetch_assoc($sql)) {
|
|
$form_clients[] = $row;
|
|
}
|
|
|
|
// Billed on to the client each time, or covered by one of their recurring invoices
|
|
$form_covered_by = $form_edit ? intval($form_row['recurring_expense_recurring_invoice_id']) : intval($form_recurring_invoice_id ?? 0);
|
|
$form_billing = $form_covered_by ? 'covered' : ($form_edit && intval($form_row['recurring_expense_billable']) ? 'bill' : '');
|
|
$form_markup = $form_edit ? floatval($form_row['recurring_expense_markup']) : expenseMarkupDefault();
|
|
$form_covers = [];
|
|
$sql = mysqli_query($mysqli, "SELECT recurring_invoice_id, recurring_invoice_prefix, recurring_invoice_number, recurring_invoice_scope, recurring_invoice_amount,
|
|
recurring_invoice_frequency, recurring_invoice_currency_code, recurring_invoice_client_id
|
|
FROM recurring_invoices
|
|
WHERE (recurring_invoice_archived_at IS NULL OR recurring_invoice_id = $form_covered_by) " . clientScopeSql('recurring_invoice_client_id') . "
|
|
ORDER BY recurring_invoice_status DESC, recurring_invoice_number ASC");
|
|
while ($row = mysqli_fetch_assoc($sql)) {
|
|
$form_covers[intval($row['recurring_invoice_client_id'])][] = [intval($row['recurring_invoice_id']),
|
|
billingNumber('recurring', $row) . (trim((string) $row['recurring_invoice_scope']) !== '' ? ' - ' . $row['recurring_invoice_scope'] : '')
|
|
. ' (' . billingMoney(recurringInvoiceMonthly($row['recurring_invoice_amount'], $row['recurring_invoice_frequency']), $row['recurring_invoice_currency_code']) . ' a month)'];
|
|
}
|
|
|
|
?>
|
|
<div class="row g-3" id="<?= $form_id ?>" data-recurring-expense-form>
|
|
|
|
<div class="col-md-7">
|
|
<label class="form-label">Vendor <strong class="text-danger">*</strong></label>
|
|
<div class="input-group">
|
|
<span class="input-group-text"><i class="fa fa-fw fa-building"></i></span>
|
|
<select class="form-select select2" name="vendor" required data-expense-vendor>
|
|
<option value="">- Who it is paid to -</option>
|
|
<?php foreach ($form_vendors as $vendor_id => $vendor) { ?>
|
|
<option value="<?= $vendor_id ?>" <?= $form_vendor === $vendor_id ? 'selected' : '' ?><?= selectOptionVendor($vendor['vendor_name'], $vendor['vendor_archived_at'] ? 'Archived' : (string) $vendor['vendor_description']) ?>><?= escapeHtml($vendor['vendor_name']) ?></option>
|
|
<?php } ?>
|
|
</select>
|
|
<a class="btn btn-outline-secondary" title="Add a vendor - opens Vendors in a new tab" href="vendors.php" target="_blank"><i class="fas fa-fw fa-plus"></i></a>
|
|
</div>
|
|
<div class="form-text" data-expense-filled hidden>Filled in from the last expense with them.</div>
|
|
</div>
|
|
|
|
<div class="col-md-5">
|
|
<label class="form-label">Amount each time <strong class="text-danger">*</strong></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 text-end fw-semibold" inputmode="decimal" pattern="[0-9,]*\.?[0-9]{0,2}" name="amount" placeholder="0.00" required
|
|
value="<?= $form_edit ? number_format(floatval($form_row['recurring_expense_amount']), 2, '.', '') : '' ?>" data-recurring-amount>
|
|
<span class="input-group-text"><?= escapeHtml($form_currency) ?></span>
|
|
</div>
|
|
</div>
|
|
|
|
<?php
|
|
$tax_form_id = $form_id;
|
|
$tax_selected = $form_edit ? intval($form_row['recurring_expense_tax_id']) : 0;
|
|
$tax_amount = $form_edit ? floatval($form_row['recurring_expense_tax_amount']) : null;
|
|
$tax_currency = $form_currency;
|
|
$tax_lock = false;
|
|
require __DIR__ . '/../expense/expense_tax_fields.php';
|
|
?>
|
|
|
|
<div class="col-md-4">
|
|
<label class="form-label">How often <strong class="text-danger">*</strong></label>
|
|
<div class="input-group">
|
|
<span class="input-group-text"><i class="fa fa-fw fa-sync-alt"></i></span>
|
|
<select class="form-select" name="frequency" required data-recurring-frequency>
|
|
<?php foreach (recurringInvoiceFrequencies() as $key => $label) { ?>
|
|
<option value="<?= $key ?>" <?= $form_frequency === $key ? 'selected' : '' ?>><?= $label ?></option>
|
|
<?php } ?>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="col-md-4">
|
|
<label class="form-label"><?= $form_edit ? 'Next one on' : 'First one on' ?> <strong class="text-danger">*</strong></label>
|
|
<div class="input-group">
|
|
<span class="input-group-text"><i class="fa fa-fw fa-calendar-day"></i></span>
|
|
<input type="date" class="form-control" name="next_date" max="2999-12-31" value="<?= escapeHtml($form_next) ?>" required data-recurring-next>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="col-md-4">
|
|
<label class="form-label">Ends</label>
|
|
<div class="input-group">
|
|
<span class="input-group-text"><i class="fa fa-fw fa-flag-checkered"></i></span>
|
|
<input type="date" class="form-control" name="end_date" max="2999-12-31" value="<?= escapeHtml($form_end) ?>" data-recurring-end>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="col-12">
|
|
<div class="small text-body-secondary"><i class="fas fa-fw fa-redo-alt me-1"></i><span data-recurring-summary><?= $form_edit ? escapeHtml(recurringExpenseScheduleText($form_row)) : '' ?></span>. Leave Ends empty unless it stops - a lease or financing that is paid off on a date.</div>
|
|
</div>
|
|
|
|
<div class="col-md-7">
|
|
<label class="form-label">Category <strong class="text-danger">*</strong></label>
|
|
<div class="input-group">
|
|
<span class="input-group-text"><i class="fa fa-fw fa-list"></i></span>
|
|
<select class="form-select select2" name="category" required data-expense-category>
|
|
<option value="">- What it is for -</option>
|
|
<?= categoryOptionsHtml('Expense', $form_category) ?>
|
|
</select>
|
|
<?= categoryAddButtonHtml('Expense') ?>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="col-md-5">
|
|
<label class="form-label">Client</label>
|
|
<div class="input-group">
|
|
<span class="input-group-text"><i class="fa fa-fw fa-user"></i></span>
|
|
<select class="form-select select2" name="client_id" data-recurring-client>
|
|
<option value="0">- Not for a client -</option>
|
|
<?php foreach ($form_clients as $client) { ?>
|
|
<option value="<?= intval($client['client_id']) ?>" <?= $form_client === intval($client['client_id']) ? 'selected' : '' ?><?= selectOptionClient($client['client_id'], $client['client_name'], $client['client_abbreviation'] ?? '') ?>><?= escapeHtml($client['client_name']) ?></option>
|
|
<?php } ?>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="col-12" data-recurring-billing <?= $form_client ? '' : 'hidden' ?>>
|
|
<div class="row g-2 align-items-start p-2 rounded border bg-body-tertiary mx-0">
|
|
<div class="col-md-5">
|
|
<label class="form-label small mb-1">For them</label>
|
|
<div class="input-group">
|
|
<span class="input-group-text"><i class="fa fa-fw fa-file-invoice-dollar"></i></span>
|
|
<select class="form-select" name="billing" data-recurring-billing-mode>
|
|
<option value="" <?= $form_billing === '' ? 'selected' : '' ?>>Our cost - not billed on</option>
|
|
<option value="bill" <?= $form_billing === 'bill' ? 'selected' : '' ?>>Billed on to them each time</option>
|
|
<option value="covered" <?= $form_billing === 'covered' ? 'selected' : '' ?>>Covered by their recurring invoice</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-3" data-recurring-billing-for="bill">
|
|
<label class="form-label small mb-1">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 text-end" name="markup" inputmode="decimal" value="<?= rtrim(rtrim(number_format($form_markup, 2, '.', ''), '0'), '.') ?>" aria-label="Markup">
|
|
</div>
|
|
</div>
|
|
<div class="col-md-7" data-recurring-billing-for="covered">
|
|
<label class="form-label small mb-1">Recurring invoice</label>
|
|
<div class="input-group">
|
|
<span class="input-group-text"><i class="fa fa-fw fa-redo-alt"></i></span>
|
|
<select class="form-select" name="recurring_invoice_id" data-recurring-cover data-selected="<?= $form_covered_by ?>"></select>
|
|
</div>
|
|
</div>
|
|
<div class="col-12 small text-body-secondary" data-recurring-billing-note></div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="col-md-7">
|
|
<label class="form-label">Paid from <strong class="text-danger">*</strong></label>
|
|
<div class="input-group">
|
|
<span class="input-group-text"><i class="fa fa-fw fa-piggy-bank"></i></span>
|
|
<select class="form-select select2" name="account" required data-expense-account>
|
|
<option value="">- Which account -</option>
|
|
<?php foreach ($form_accounts as $account_id => $account) { ?>
|
|
<option value="<?= $account_id ?>" <?= $form_account === $account_id ? 'selected' : '' ?> data-icon="fas fa-piggy-bank"
|
|
data-description="<?= escapeHtml(billingMoney($account['balance'], $account['account_currency_code'])) ?><?= $account['account_archived_at'] ? ' - archived' : '' ?>"><?= escapeHtml($account['account_name']) ?></option>
|
|
<?php } ?>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="col-md-5">
|
|
<label class="form-label">Paid by</label>
|
|
<div class="input-group">
|
|
<span class="input-group-text"><i class="fa fa-fw fa-credit-card"></i></span>
|
|
<select class="form-select select2" name="payment_method" data-expense-method>
|
|
<option value="">- How it is paid -</option>
|
|
<?php foreach ($form_methods as $method) { ?>
|
|
<option value="<?= escapeHtml($method) ?>" <?= $form_method === $method ? 'selected' : '' ?>><?= escapeHtml($method) ?></option>
|
|
<?php } ?>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="col-12">
|
|
<label class="form-label">Reference</label>
|
|
<div class="input-group">
|
|
<span class="input-group-text"><i class="fa fa-fw fa-hashtag"></i></span>
|
|
<input type="text" class="form-control" name="reference" maxlength="255" placeholder="Account or contract number - it goes on each expense" value="<?= escapeHtml($form_value('recurring_expense_reference')) ?>">
|
|
</div>
|
|
</div>
|
|
|
|
<div class="col-12">
|
|
<label class="form-label">Description</label>
|
|
<div class="input-group">
|
|
<span class="input-group-text align-items-start pt-2"><i class="fa fa-fw fa-align-left"></i></span>
|
|
<textarea class="form-control" rows="2" name="description" placeholder="What it is - office rent, colocation, M365 partner licensing"><?= escapeHtml($form_value('recurring_expense_description')) ?></textarea>
|
|
</div>
|
|
</div>
|
|
|
|
</div>
|
|
|
|
<script>
|
|
(function () {
|
|
var root = document.getElementById(<?= json_encode($form_id) ?>);
|
|
if (!root || root.dataset.wired) {
|
|
return;
|
|
}
|
|
root.dataset.wired = '1';
|
|
|
|
// The schedule in words - the same as recurringExpenseScheduleText()
|
|
var frequency = root.querySelector('[data-recurring-frequency]');
|
|
var next = root.querySelector('[data-recurring-next]');
|
|
var end = root.querySelector('[data-recurring-end]');
|
|
var summary = root.querySelector('[data-recurring-summary]');
|
|
var months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
|
|
function parse(value) {
|
|
var m = /^(\d{4})-(\d{2})-(\d{2})$/.exec(value || '');
|
|
return m ? { y: +m[1], m: +m[2], d: +m[3] } : null;
|
|
}
|
|
function ordinal(n) {
|
|
var s = (n % 100 >= 11 && n % 100 <= 13) ? 'th' : (['th', 'st', 'nd', 'rd'][n % 10] || 'th');
|
|
return n + s;
|
|
}
|
|
function update() {
|
|
var d = parse(next.value);
|
|
if (!d) {
|
|
summary.textContent = 'Set the date of the ' + <?= json_encode($form_edit ? 'next' : 'first') ?> + ' one';
|
|
return;
|
|
}
|
|
var text = frequency.value === 'year' ? 'Every year on ' + months[d.m - 1] + ' ' + d.d
|
|
: 'Every ' + (frequency.value === 'quarter' ? 'quarter' : 'month') + ' on the ' + ordinal(d.d);
|
|
text += ', ' + <?= json_encode($form_edit ? 'next on ' : 'starting ') ?> + months[d.m - 1] + ' ' + d.d + ', ' + d.y;
|
|
var e = parse(end.value);
|
|
if (e) {
|
|
text += (end.value < next.value ? ' - but that is after it ends on ' : ', until ') + months[e.m - 1] + ' ' + e.d + ', ' + e.y;
|
|
}
|
|
summary.textContent = text;
|
|
}
|
|
[frequency, next, end].forEach(function (el) {
|
|
el.addEventListener('change', update);
|
|
el.addEventListener('input', update);
|
|
});
|
|
update();
|
|
|
|
// For a client: billed on each time, or covered by one of their recurring invoices
|
|
var covers = <?= json_encode((object) $form_covers) ?>;
|
|
var client = root.querySelector('[data-recurring-client]');
|
|
var billingBox = root.querySelector('[data-recurring-billing]');
|
|
var mode = root.querySelector('[data-recurring-billing-mode]');
|
|
var cover = root.querySelector('[data-recurring-cover]');
|
|
var billingNote = root.querySelector('[data-recurring-billing-note]');
|
|
function billing() {
|
|
var options = covers[client.value] || [];
|
|
billingBox.hidden = !client.value || client.value === '0';
|
|
var selected = cover.value || cover.getAttribute('data-selected');
|
|
cover.innerHTML = '';
|
|
options.forEach(function (o) {
|
|
var option = document.createElement('option');
|
|
option.value = o[0];
|
|
option.textContent = o[1];
|
|
option.selected = String(o[0]) === String(selected);
|
|
cover.appendChild(option);
|
|
});
|
|
mode.querySelector('option[value="covered"]').disabled = !options.length;
|
|
if (mode.value === 'covered' && !options.length) {
|
|
mode.value = '';
|
|
}
|
|
root.querySelectorAll('[data-recurring-billing-for]').forEach(function (el) {
|
|
el.hidden = el.getAttribute('data-recurring-billing-for') !== mode.value;
|
|
el.querySelectorAll('input, select').forEach(function (input) {
|
|
input.disabled = el.hidden;
|
|
});
|
|
});
|
|
billingNote.textContent = mode.value === 'bill' ? 'Each one it records waits in Unbilled Time to go on their next invoice, with the markup.'
|
|
: mode.value === 'covered' ? 'Not billed on by itself - the recurring invoice shows it as a cost against what it bills, with the margin.'
|
|
: (options.length ? 'Filed against them as a cost. It can also be shown against one of their recurring invoices.' : 'Filed against them as a cost - they have no recurring invoice to cover it.');
|
|
}
|
|
client.addEventListener('change', billing);
|
|
mode.addEventListener('change', billing);
|
|
billing();
|
|
|
|
<?php if (!$form_edit) { ?>
|
|
// Picking a vendor fills in what the last expense with them used, unless changed by hand
|
|
var defaults = <?= json_encode((object) $form_defaults) ?>;
|
|
var vendor = root.querySelector('[data-expense-vendor]');
|
|
var note = root.querySelector('[data-expense-filled]');
|
|
var fields = {
|
|
category: root.querySelector('[data-expense-category]'),
|
|
account: root.querySelector('[data-expense-account]'),
|
|
method: root.querySelector('[data-expense-method]'),
|
|
tax: root.querySelector('[data-expense-tax-rate]')
|
|
};
|
|
var touched = {};
|
|
var filling = false;
|
|
Object.keys(fields).forEach(function (key) {
|
|
fields[key].addEventListener('change', function () {
|
|
if (!filling) {
|
|
touched[key] = true;
|
|
}
|
|
});
|
|
});
|
|
vendor.addEventListener('change', function () {
|
|
var d = defaults[vendor.value];
|
|
var filled = false;
|
|
if (!d) {
|
|
note.hidden = true;
|
|
return;
|
|
}
|
|
filling = true;
|
|
Object.keys(fields).forEach(function (key) {
|
|
var value = String(d[key] || '');
|
|
if (touched[key] || value === '' || value === '0' || !fields[key].querySelector('option[value="' + CSS.escape(value) + '"]')) {
|
|
return;
|
|
}
|
|
if (fields[key].value !== value) {
|
|
fields[key].value = value;
|
|
fields[key].dispatchEvent(new Event('change', { bubbles: true }));
|
|
filled = true;
|
|
}
|
|
});
|
|
filling = false;
|
|
note.hidden = !filled;
|
|
});
|
|
<?php } ?>
|
|
})();
|
|
</script>
|