Files
itflow/admin/database_updates/3.5.0.php
johnnyq 1e5bffff3c Rates, net terms and SLAs come from the client's contract, else the instance defaults: database 3.5.0 adds a default after-hours rate (Settings > Defaults, now Rates and terms), gives each client whose own rate, net terms or SLAs differ from the defaults an active Standard terms contract setting just those (12-month term renewing itself, starting the day the client was added or the day before its earliest contract, archived with an archived client), deletes per-client SLA assignments and drops clients.client_rate and client_net_terms; time bills at the contract's rate, its after-hours rate or the default after-hours rate, else the default rate, new invoices are due on the contract's net terms else the default, and new tickets get the contract's SLA else the default; a client's own terms still apply until the update runs; the client add and edit windows lose the rate, terms and SLA fields for a note saying what the client is billed on and where it comes from, the Clients list loses Set Hourly Rate and Set Net Terms and shows the contract's rate, the overview's Terms shows the after-hours rate and whether terms are a contract's or the defaults, a contract's page and document show what it leaves empty from another contract in force or the default so signed documents stay unchanged, contract windows show the defaults as placeholders, contract names take a proper possessive; the client import and API create make a Standard terms contract for a rate or terms that aren't the defaults (returning contract_id), API update reports them in not_kept; the client export gives the rates, terms and SLAs as billed; demo data puts each demo client's terms on a contract; changelog updated
2026-10-27 22:22:34 -04:00

122 lines
7.5 KiB
PHP

<?php
/*
* ITFlow - Database update to version 3.5.0 (from 3.4.9)
* Included by admin/database_updates.php - do not access directly
*
* A client has no hourly rate, net terms or SLAs of its own any more: they
* come from its contract in force, else the defaults (Admin > Settings >
* Defaults, Admin > SLAs) - functions/contract_billing.php.
*
* settings.config_default_after_hours_rate the default after-hours rate -
* NULL, none: the hourly rate
* applies after hours too
* clients.client_rate, client_net_terms dropped
* sla_assignments (client_id > 0) deleted - client 0's rows are
* the defaults and stay
*
* A client whose own rate, net terms or SLAs differ from the defaults keeps
* them, on a contract named "Standard terms" made for it here: active,
* 12-month term renewing itself, no notice, setting only what differs. It
* starts the day the client was added - before any contract the client
* already has, so that one's terms still come first where it sets them, as
* they did over the client's own. Its term is carried on to the one running
* today. An archived client's is archived with it. A client SLA pointing at
* no SLA, or at an archived one (the client's tickets got none), can't be
* kept: a contract sets an SLA or leaves the default.
*/
defined('FROM_DB_UPDATER') || die("Direct file access is not allowed");
$itflow_column = function ($table, $column) use ($mysqli) {
$result = mysqli_query($mysqli, "SELECT 1 FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = '$table' AND COLUMN_NAME = '$column' LIMIT 1");
return $result && mysqli_num_rows($result) > 0;
};
if (!$itflow_column('settings', 'config_default_after_hours_rate')) {
mysqli_query($mysqli, "ALTER TABLE `settings` ADD `config_default_after_hours_rate` decimal(15,2) DEFAULT NULL AFTER `config_default_hourly_rate`");
}
if ($itflow_column('clients', 'client_rate') && $itflow_column('clients', 'client_net_terms')) {
$itflow_settings = mysqli_fetch_assoc(mysqli_query($mysqli, "SELECT config_default_hourly_rate, config_default_net_terms FROM settings WHERE company_id = 1")) ?: [];
$itflow_default_rate = round(floatval($itflow_settings['config_default_hourly_rate'] ?? 0), 2);
$itflow_default_net = intval($itflow_settings['config_default_net_terms'] ?? 0);
// The default SLA for each priority - one that is archived counts as none, as it did for tickets
$itflow_parts = ['Low' => 'low', 'Medium' => 'medium', 'High' => 'high', 'Urgent' => 'urgent'];
$itflow_live_slas = [];
$itflow_sql = mysqli_query($mysqli, "SELECT sla_id FROM slas WHERE sla_archived_at IS NULL");
while ($itflow_sql && ($itflow_row = mysqli_fetch_row($itflow_sql))) {
$itflow_live_slas[intval($itflow_row[0])] = true;
}
$itflow_default_slas = array_fill_keys(array_keys($itflow_parts), 0);
$itflow_client_slas = [];
$itflow_sql = mysqli_query($mysqli, "SELECT sla_assignment_client_id, sla_assignment_priority, sla_assignment_sla_id FROM sla_assignments");
while ($itflow_sql && ($itflow_row = mysqli_fetch_assoc($itflow_sql))) {
$itflow_priority = (string) $itflow_row['sla_assignment_priority'];
$itflow_sla_id = intval($itflow_row['sla_assignment_sla_id']);
if (!isset($itflow_parts[$itflow_priority]) || !isset($itflow_live_slas[$itflow_sla_id])) {
continue;
}
if (intval($itflow_row['sla_assignment_client_id']) === 0) {
$itflow_default_slas[$itflow_priority] = $itflow_sla_id;
} else {
$itflow_client_slas[intval($itflow_row['sla_assignment_client_id'])][$itflow_priority] = $itflow_sla_id;
}
}
$itflow_today = date('Y-m-d');
$itflow_sql = mysqli_query($mysqli, "SELECT client_id, client_rate, client_net_terms, client_created_at, client_archived_at,
(SELECT MIN(contract_start_date) FROM contracts WHERE contract_client_id = client_id AND contract_start_date IS NOT NULL) AS first_contract_start
FROM clients ORDER BY client_id");
while ($itflow_sql && ($itflow_client = mysqli_fetch_assoc($itflow_sql))) {
$itflow_client_id = intval($itflow_client['client_id']);
$itflow_set = [];
// Only what differs from the defaults - a rate of nothing was no rate, the default applied
$itflow_rate = round(floatval($itflow_client['client_rate']), 2);
if ($itflow_rate > 0 && $itflow_rate != $itflow_default_rate) {
$itflow_set[] = "contract_rate_standard = " . sprintf('%.2f', $itflow_rate);
}
if ($itflow_client['client_net_terms'] !== null && intval($itflow_client['client_net_terms']) !== $itflow_default_net) {
$itflow_set[] = "contract_net_terms = " . intval($itflow_client['client_net_terms']);
}
foreach ($itflow_parts as $itflow_priority => $itflow_part) {
$itflow_sla_id = intval($itflow_client_slas[$itflow_client_id][$itflow_priority] ?? 0);
if ($itflow_sla_id && $itflow_sla_id !== $itflow_default_slas[$itflow_priority]) {
$itflow_set[] = "contract_sla_{$itflow_part}_id = $itflow_sla_id";
}
}
if (!$itflow_set) {
continue;
}
// From the day the client was added, before any contract it already has
$itflow_start = substr((string) $itflow_client['client_created_at'], 0, 10);
if (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $itflow_start) || $itflow_start === '0000-00-00') {
$itflow_start = $itflow_today;
}
$itflow_first = (string) $itflow_client['first_contract_start'];
if (preg_match('/^\d{4}-\d{2}-\d{2}$/', $itflow_first) && $itflow_first !== '0000-00-00' && $itflow_first <= $itflow_start) {
$itflow_start = date('Y-m-d', strtotime('-1 day', strtotime($itflow_first)));
}
// Its 12-month term carried on to the one running today
$itflow_end = date('Y-m-d', strtotime('+12 months -1 day', strtotime($itflow_start)));
while ($itflow_end < $itflow_today) {
$itflow_end = date('Y-m-d', strtotime('+12 months -1 day', strtotime('+1 day', strtotime($itflow_end))));
}
$itflow_archived = $itflow_client['client_archived_at'] ? "'" . mysqli_real_escape_string($mysqli, $itflow_client['client_archived_at']) . "'" : 'NULL';
mysqli_query($mysqli, "INSERT INTO contracts SET contract_name = 'Standard terms', contract_status = 'Active', contract_type = 'other',
contract_client_id = $itflow_client_id, contract_start_date = '$itflow_start', contract_end_date = '$itflow_end',
contract_term_months = 12, contract_auto_renew = 1, contract_notice_days = 0, contract_archived_at = $itflow_archived,
contract_notes = 'Made by the 26.11 update from the hourly rate, net terms and SLAs the client had of its own, where they differed from the defaults - a client has none of its own from 26.11.',
" . implode(', ', $itflow_set));
}
mysqli_query($mysqli, "DELETE FROM sla_assignments WHERE sla_assignment_client_id > 0");
mysqli_query($mysqli, "ALTER TABLE `clients` DROP `client_rate`, DROP `client_net_terms`");
}