mirror of
https://github.com/itflow-org/itflow
synced 2026-11-11 07:01:16 +00:00
260 lines
14 KiB
PHP
260 lines
14 KiB
PHP
<?php
|
|
|
|
/*
|
|
* The details of an invoice, quote or recurring invoice - shared by the New
|
|
* and Edit windows for all three. Set before including:
|
|
*
|
|
* $form_type invoice | quote | recurring
|
|
* $form_row billingRow() when editing, null for a new one
|
|
* $form_client_id the client a new one is for (0 = pick it here)
|
|
*
|
|
* Posts the fields the existing handlers read: client_id, scope, category,
|
|
* and then by kind - invoice: date, due (edit), invoice_discount; quote:
|
|
* date, expire, quote_discount; recurring: frequency, start_date (new) or
|
|
* next_date (edit), recurring_invoice_discount, auto_send, status (edit).
|
|
*
|
|
* Should not be accessed directly.
|
|
*/
|
|
|
|
isset($mysqli) || die("Direct file access is not allowed");
|
|
|
|
$form_t = billingType($form_type);
|
|
$form_c = $form_t['col'];
|
|
$form_edit = !empty($form_row);
|
|
$form_id = 'billingForm' . ucfirst($form_type) . ($form_edit ? intval($form_row["{$form_c}_id"]) : 'New');
|
|
$form_value = function ($column, $default = '') use ($form_row, $form_c) {
|
|
return $form_row ? (string) ($form_row["{$form_c}_$column"] ?? $default) : $default;
|
|
};
|
|
$form_currency = $form_edit ? (string) $form_row["{$form_c}_currency_code"] : (string) $session_company_currency;
|
|
$form_category_id = intval($form_value('category_id', '0'));
|
|
$form_discount = floatval($form_value('discount_amount', '0'));
|
|
|
|
// Net terms for a new invoice's due date - a contract in force's, else the
|
|
// default (0 is On Receipt), as the invoice is saved with
|
|
$form_default_terms = billingDefaults()['net_terms'];
|
|
[$form_terms, $form_terms_from] = $form_client_id ? contractNetTermsWhose($form_client_id, date('Y-m-d')) : [$form_default_terms, 'the default'];
|
|
$form_contract_terms = contractNetTermsToday();
|
|
// Before the 3.5.0 update a client still has net terms of its own, which apply without a contract's
|
|
$form_legacy_terms = !billingAfterHoursReady();
|
|
|
|
$form_clients = [];
|
|
if (!$form_edit && !$form_client_id) {
|
|
$sql = mysqli_query($mysqli, "SELECT client_id, client_name, client_abbreviation" . ($form_legacy_terms ? ', client_net_terms' : '') . " FROM clients WHERE client_archived_at IS NULL " . clientScopeSql('clients.client_id') . " ORDER BY client_name ASC");
|
|
while ($row = mysqli_fetch_assoc($sql)) {
|
|
$form_clients[] = $row;
|
|
}
|
|
}
|
|
|
|
$form_scope_placeholder = [
|
|
'invoice' => 'Managed services - October, onsite visit 12 March, new laptop for Sam',
|
|
'quote' => 'Firewall replacement, Microsoft 365 migration, new starter kit',
|
|
'recurring' => 'Monthly managed services, backup and hosting',
|
|
][$form_type];
|
|
|
|
?>
|
|
<div class="row g-3" id="<?= $form_id ?>" data-billing-form="<?= $form_type ?>" data-terms="<?= $form_terms ?>" data-terms-from="<?= escapeHtml($form_terms_from) ?>">
|
|
|
|
<?php if (!$form_edit && !$form_client_id) { ?>
|
|
<div class="col-12">
|
|
<label class="form-label">Client <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="client_id" required data-billing-client>
|
|
<option value="">- Select a client -</option>
|
|
<?php foreach ($form_clients as $form_client) { ?>
|
|
<?php $form_option_terms = isset($form_contract_terms[intval($form_client['client_id'])]) ? [$form_contract_terms[intval($form_client['client_id'])][0], contractPossessive($form_contract_terms[intval($form_client['client_id'])][1])]
|
|
: ($form_legacy_terms && $form_client['client_net_terms'] !== null ? [intval($form_client['client_net_terms']), "the client's"] : [$form_default_terms, 'the default']); ?>
|
|
<option value="<?= intval($form_client['client_id']) ?>" data-terms="<?= $form_option_terms[0] ?>" data-terms-from="<?= escapeHtml($form_option_terms[1]) ?>"<?= selectOptionClient($form_client['client_id'], $form_client['client_name'], $form_client['client_abbreviation'] ?? '') ?>><?= escapeHtml($form_client['client_name']) ?></option>
|
|
<?php } ?>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
<?php } elseif (!$form_edit) { ?>
|
|
<input type="hidden" name="client_id" value="<?= intval($form_client_id) ?>">
|
|
<?php } ?>
|
|
|
|
<div class="col-12">
|
|
<label class="form-label">Scope</label>
|
|
<div class="input-group">
|
|
<span class="input-group-text"><i class="fa fa-fw fa-comment"></i></span>
|
|
<input type="text" class="form-control" name="scope" maxlength="255" placeholder="<?= escapeHtml($form_scope_placeholder) ?>" value="<?= escapeHtml($form_value('scope')) ?>" <?= $form_edit ? '' : 'autofocus' ?>>
|
|
</div>
|
|
<div class="form-text">
|
|
What it is for, in a few words - shown on it and in lists.
|
|
<?php if ($form_type === 'recurring') { ?>
|
|
Put <code>{month}</code>, <code>{quarter}</code>, <code>{year}</code> or <code>{period}</code> in it - here, in the notes or on a line - and each invoice says the period it covers: "Managed services - {month}" reads "Managed services - November 2026".
|
|
<?php } ?>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="col-12">
|
|
<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-tag"></i></span>
|
|
<select class="form-select select2" name="category" required>
|
|
<option value="">- Category -</option>
|
|
<?= categoryOptionsHtml('Income', $form_category_id, ['since' => $form_edit ? $form_value('created_at') : null]) ?>
|
|
</select>
|
|
<?= categoryAddButtonHtml('Income', 'btn btn-secondary') ?>
|
|
</div>
|
|
</div>
|
|
|
|
<?php if ($form_type === 'invoice') { ?>
|
|
<div class="col-sm-6">
|
|
<label class="form-label">Invoice date <strong class="text-danger">*</strong></label>
|
|
<div class="input-group">
|
|
<span class="input-group-text"><i class="fa fa-fw fa-calendar"></i></span>
|
|
<input type="date" class="form-control" name="date" max="2999-12-31" value="<?= escapeHtml($form_value('date', date('Y-m-d'))) ?>" required data-billing-date>
|
|
</div>
|
|
</div>
|
|
<div class="col-sm-6">
|
|
<label class="form-label">Due <?= $form_edit ? '<strong class="text-danger">*</strong>' : '' ?></label>
|
|
<div class="input-group">
|
|
<span class="input-group-text"><i class="fa fa-fw fa-calendar-check"></i></span>
|
|
<?php if ($form_edit) { ?>
|
|
<input type="date" class="form-control" name="due" max="2999-12-31" value="<?= escapeHtml($form_value('due')) ?>" required data-billing-due>
|
|
<?php } else { ?>
|
|
<input type="date" class="form-control" value="<?= date('Y-m-d', strtotime("+$form_terms days")) ?>" disabled data-billing-due>
|
|
<?php } ?>
|
|
</div>
|
|
<?php if (!$form_edit) { ?>
|
|
<div class="form-text" data-billing-terms-note><?= $form_terms ? "$form_terms days from the invoice date" : 'Due on receipt' ?> - <?= escapeHtml($form_terms_from) ?> terms.</div>
|
|
<?php } ?>
|
|
</div>
|
|
<?php } elseif ($form_type === 'quote') { ?>
|
|
<div class="col-sm-6">
|
|
<label class="form-label">Quote date <strong class="text-danger">*</strong></label>
|
|
<div class="input-group">
|
|
<span class="input-group-text"><i class="fa fa-fw fa-calendar"></i></span>
|
|
<input type="date" class="form-control" name="date" max="2999-12-31" value="<?= escapeHtml($form_value('date', date('Y-m-d'))) ?>" required data-billing-date>
|
|
</div>
|
|
</div>
|
|
<div class="col-sm-6">
|
|
<label class="form-label">Valid until <strong class="text-danger">*</strong></label>
|
|
<div class="input-group">
|
|
<span class="input-group-text"><i class="fa fa-fw fa-hourglass-half"></i></span>
|
|
<input type="date" class="form-control" name="expire" max="2999-12-31" value="<?= escapeHtml($form_value('expire', date('Y-m-d', strtotime('+30 days')))) ?>" required data-billing-expire>
|
|
</div>
|
|
<div class="form-text">
|
|
<?php foreach ([15, 30, 60, 90] as $days) { ?>
|
|
<a href="#" class="me-2" data-billing-expire-days="<?= $days ?>"><?= $days ?> days</a>
|
|
<?php } ?>
|
|
</div>
|
|
</div>
|
|
<?php } else { ?>
|
|
<div class="col-sm-6">
|
|
<label class="form-label">Billed <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>
|
|
<?php foreach (recurringInvoiceFrequencies() as $frequency => $label) { ?>
|
|
<option value="<?= $frequency ?>" <?= $form_value('frequency', 'month') === $frequency ? 'selected' : '' ?>><?= $label ?></option>
|
|
<?php } ?>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
<div class="col-sm-6">
|
|
<label class="form-label"><?= $form_edit ? 'Next invoice' : 'First invoice' ?> <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="<?= $form_edit ? 'next_date' : 'start_date' ?>" max="2999-12-31"
|
|
value="<?= escapeHtml($form_edit ? $form_value('next_date') : date('Y-m-01', strtotime('first day of next month'))) ?>" required>
|
|
</div>
|
|
<div class="form-text">Made on this date, then every period after.</div>
|
|
</div>
|
|
<?php } ?>
|
|
|
|
<div class="col-sm-6">
|
|
<label class="form-label">Discount</label>
|
|
<div class="input-group">
|
|
<span class="input-group-text"><i class="fa fa-fw fa-tags"></i></span>
|
|
<input type="text" class="form-control text-end" inputmode="decimal" pattern="-?[0-9]*\.?[0-9]{0,2}"
|
|
name="<?= ['invoice' => 'invoice_discount', 'quote' => 'quote_discount', 'recurring' => 'recurring_invoice_discount'][$form_type] ?>"
|
|
placeholder="0.00" value="<?= $form_discount != 0 ? number_format($form_discount, 2, '.', '') : '' ?>">
|
|
<span class="input-group-text"><?= escapeHtml($form_currency) ?></span>
|
|
</div>
|
|
<div class="form-text">Taken off the total, after the lines.</div>
|
|
</div>
|
|
|
|
<?php if ($form_type === 'recurring') { ?>
|
|
<div class="col-sm-6">
|
|
<label class="form-label">When it is made</label>
|
|
<div class="input-group">
|
|
<span class="input-group-text"><i class="fa fa-fw fa-paper-plane"></i></span>
|
|
<select class="form-select" name="auto_send">
|
|
<option value="1" <?= $form_value('auto_send', '1') === '1' ? 'selected' : '' ?>>Send it automatically</option>
|
|
<option value="0" <?= $form_value('auto_send', '1') === '0' ? 'selected' : '' ?>>Leave a draft to check</option>
|
|
</select>
|
|
</div>
|
|
<?php if (empty($config_recurring_auto_send_invoice)) { ?>
|
|
<div class="form-text text-warning-emphasis">Emailing the invoices recurring invoices make is off in Settings > Notifications - they are made as drafts.</div>
|
|
<?php } ?>
|
|
</div>
|
|
<?php if ($form_edit) { ?>
|
|
<div class="col-12">
|
|
<div class="form-check form-switch">
|
|
<input type="hidden" name="status" value="0">
|
|
<input class="form-check-input" type="checkbox" role="switch" name="status" value="1" id="<?= $form_id ?>Active" <?= intval($form_value('status', '1')) ? 'checked' : '' ?>>
|
|
<label class="form-check-label" for="<?= $form_id ?>Active">Active - invoices are made on schedule</label>
|
|
</div>
|
|
</div>
|
|
<?php } ?>
|
|
<?php } ?>
|
|
</div>
|
|
|
|
<script>
|
|
(function () {
|
|
var root = document.getElementById(<?= json_encode($form_id) ?>);
|
|
if (!root || root.hasAttribute('data-wired')) {
|
|
return;
|
|
}
|
|
root.setAttribute('data-wired', '1');
|
|
|
|
function ymd(d) {
|
|
return d.getFullYear() + '-' + String(d.getMonth() + 1).padStart(2, '0') + '-' + String(d.getDate()).padStart(2, '0');
|
|
}
|
|
function plus(value, days) {
|
|
var parts = String(value || '').split('-');
|
|
var d = parts.length === 3 ? new Date(+parts[0], +parts[1] - 1, +parts[2]) : new Date();
|
|
d.setDate(d.getDate() + days);
|
|
return ymd(d);
|
|
}
|
|
|
|
var date = root.querySelector('[data-billing-date]');
|
|
|
|
// A new invoice: the due date follows the date and the client's terms
|
|
var due = root.querySelector('[data-billing-due][disabled]');
|
|
var client = root.querySelector('[data-billing-client]');
|
|
var note = root.querySelector('[data-billing-terms-note]');
|
|
function terms() {
|
|
var option = client && client.options[client.selectedIndex];
|
|
return option && option.getAttribute('data-terms') ? Number(option.getAttribute('data-terms')) : Number(root.getAttribute('data-terms') || 0);
|
|
}
|
|
function follow() {
|
|
if (due && date) {
|
|
due.value = plus(date.value, terms());
|
|
if (note) {
|
|
var picked = client && client.options[client.selectedIndex];
|
|
var from = picked && picked.value ? picked.getAttribute('data-terms-from') : root.getAttribute('data-terms-from');
|
|
note.textContent = (terms() ? terms() + ' days from the invoice date' : 'Due on receipt') + ' - ' + (from || 'the default') + ' terms.';
|
|
}
|
|
}
|
|
}
|
|
if (due) {
|
|
date.addEventListener('change', follow);
|
|
if (client) {
|
|
client.addEventListener('change', follow);
|
|
}
|
|
}
|
|
|
|
// A quote: valid for so many days from its date
|
|
root.querySelectorAll('[data-billing-expire-days]').forEach(function (link) {
|
|
link.addEventListener('click', function (e) {
|
|
e.preventDefault();
|
|
var expire = root.querySelector('[data-billing-expire]');
|
|
expire.value = plus(date ? date.value : '', Number(link.getAttribute('data-billing-expire-days')));
|
|
});
|
|
});
|
|
})();
|
|
</script>
|