License templates stage 2: templates carry the seats a license starts with, a renewal term, a vendor from the vendor templates and the product they are sold as (database 3.2.9); a license made from one starts with its seats, gets a renewal date worked out from the purchase date and uses the client's vendor from that vendor template, or one with the same name, or has one made from the template by someone who can add vendors; New License fills in the seats, vendor and renewal date from the template and keeps the renewal date in step with the purchase date until one is typed in; Renew starts on the template's term; new licenses can be made for several clients at once with an option to skip clients that already have one; the template page and list show the new fields and the list search covers vendor and product; the license quick look shows the product and price to Sales users; deleting a vendor template or product removes it from the templates that used it

This commit is contained in:
johnnyq
2026-10-18 20:23:27 -04:00
parent f74ad10dd8
commit 0c12459eaf
16 changed files with 595 additions and 65 deletions

View File

@@ -9,6 +9,13 @@
* (software.software_template_id, from database 3.2.8 on), so each template's
* use is counted: how many licenses, for how many clients, with how many seats.
*
* From database 3.2.9 on a template also carries the seats a license starts
* with, how often it renews - its renewal date worked out from when it was
* bought, and Renew offering that term - the vendor template its vendor comes
* from (the client's own vendor made from it, or one of that name, or one made
* from it there and then) and the product it is sold as. Licenses can be made
* from one for several clients at once.
*
* Shared by admin/software_templates.php, admin/software_template.php,
* admin/modals/software_template/*, admin/post/software_template.php and the
* New License window and its handlers on the agent side
@@ -23,12 +30,16 @@ function softwareTemplateRow($software_template_id) {
return null;
}
$row = mysqli_fetch_assoc(mysqli_query($mysqli, "SELECT software_template_id, software_template_name, software_template_description, software_template_version,
software_template_type, software_template_license_type, software_template_notes, software_template_created_at, software_template_updated_at,
software_template_type, software_template_license_type, software_template_notes, software_template_seats, software_template_term,
software_template_vendor_template_id, software_template_product_id, software_template_created_at, software_template_updated_at,
software_template_archived_at, software_template_created_by, software_template_updated_by,
creator.user_name AS created_by_name, updater.user_name AS updated_by_name
creator.user_name AS created_by_name, updater.user_name AS updated_by_name,
vendor_template_name, vendor_template_archived_at, product_name, product_price, product_cost, product_currency_code, product_frequency, product_seat_source, product_archived_at
FROM software_templates
LEFT JOIN users creator ON creator.user_id = software_template_created_by AND software_template_created_by > 0
LEFT JOIN users updater ON updater.user_id = software_template_updated_by AND software_template_updated_by > 0
LEFT JOIN vendor_templates ON vendor_template_id = software_template_vendor_template_id AND software_template_vendor_template_id > 0
LEFT JOIN products ON product_id = software_template_product_id AND software_template_product_id > 0
WHERE software_template_id = $software_template_id"));
return $row ?: null;
}
@@ -63,7 +74,10 @@ function softwareTemplateUsage(array $software_template_ids) {
/*
* The template window's fields, checked - or null with $error saying why.
* $post: name, version, description (a line on what it is), type (a software
* type from Admin > Categories), license_type and notes.
* type from Admin > Categories), license_type, notes, seats (blank for no
* limit), term (one of SOFTWARE_RENEWAL_TERMS, or blank), vendor_template_id
* (a vendor template, or none) and product_id (a product, or none) - an
* archived one stays, the window only offering it to the template that has it.
*/
function softwareTemplateFields(array $post, &$error = '') {
$error = '';
@@ -91,6 +105,10 @@ function softwareTemplateFields(array $post, &$error = '') {
'type' => mb_substr($type, 0, 200),
'license_type' => mb_substr($line('license_type'), 0, 200),
'notes' => trim(strip_tags((string) ($post['notes'] ?? ''))),
'seats' => trim((string) ($post['seats'] ?? '')) === '' ? null : max(0, min(1000000, intval($post['seats']))),
'term' => isset(SOFTWARE_RENEWAL_TERMS[(string) ($post['term'] ?? '')]) ? (string) $post['term'] : '',
'vendor_template_id' => softwareTemplateLinkId('vendor_templates', 'vendor_template_id', $post['vendor_template_id'] ?? 0),
'product_id' => softwareTemplateLinkId('products', 'product_id', $post['product_id'] ?? 0),
];
return $fields;
}
@@ -102,7 +120,10 @@ function softwareTemplateSave($software_template_id, array $f) {
$user_id = intval($session_user_id ?? 0);
$set = "software_template_name = '" . escapeSql($f['name']) . "', software_template_version = '" . escapeSql($f['version']) . "',
software_template_description = '" . escapeSql($f['description']) . "', software_template_type = '" . escapeSql($f['type']) . "',
software_template_license_type = '" . escapeSql($f['license_type']) . "', software_template_notes = '" . escapeSql($f['notes']) . "'";
software_template_license_type = '" . escapeSql($f['license_type']) . "', software_template_notes = '" . escapeSql($f['notes']) . "',
software_template_seats = " . (($f['seats'] ?? null) === null ? 'NULL' : intval($f['seats'])) . ",
software_template_term = " . (($f['term'] ?? '') !== '' ? "'" . escapeSql($f['term']) . "'" : 'NULL') . ",
software_template_vendor_template_id = " . intval($f['vendor_template_id'] ?? 0) . ", software_template_product_id = " . intval($f['product_id'] ?? 0);
if ($software_template_id) {
mysqli_query($mysqli, "UPDATE software_templates SET $set, software_template_updated_by = $user_id WHERE software_template_id = $software_template_id");
return $software_template_id;
@@ -124,6 +145,10 @@ function softwareTemplateCopy($software_template_id) {
'type' => (string) $row['software_template_type'],
'license_type' => (string) $row['software_template_license_type'],
'notes' => (string) $row['software_template_notes'],
'seats' => $row['software_template_seats'] === null ? null : intval($row['software_template_seats']),
'term' => (string) $row['software_template_term'],
'vendor_template_id' => intval($row['software_template_vendor_template_id']),
'product_id' => intval($row['software_template_product_id']),
]);
}
@@ -141,19 +166,88 @@ function softwareTemplateLiveId($software_template_id) {
return $row && !$row['software_template_archived_at'] ? intval($row['software_template_id']) : 0;
}
/** A vendor template or product a template links to, when it exists - or 0. */
function softwareTemplateLinkId($table, $column, $id) {
global $mysqli;
$id = intval($id);
return $id > 0 && mysqli_num_rows(mysqli_query($mysqli, "SELECT 1 FROM $table WHERE $column = $id")) ? $id : 0;
}
/*
* When a license bought on $purchase (Y-m-d, or today when blank) renews
* with a template's $term - Y-m-d, or '' without a term.
*/
function softwareTemplateRenewal($term, $purchase = '') {
$intervals = ['1m' => 'P1M', '3m' => 'P3M', '6m' => 'P6M', '1y' => 'P1Y', '2y' => 'P2Y', '3y' => 'P3Y'];
if (!isset($intervals[(string) $term])) {
return '';
}
$from = softwareFormDate($purchase);
return (new DateTime($from ?: 'today'))->add(new DateInterval($intervals[$term]))->format('Y-m-d');
}
/** A template's price as it is sold - "$22.00 · Monthly, per user" - or ''. */
function softwareTemplatePriceText(array $template) {
if (empty($template['product_name'])) {
return '';
}
$billed = productBilledText($template['product_frequency'], $template['product_seat_source']);
return billingMoney($template['product_price'], $template['product_currency_code']) . ' · ' . $billed;
}
/*
* A client's vendor for a vendor template - the client's current vendor made
* from it, or one of its name; or, with $make, one made from the template
* there and then ($made says so). 0 when there is none, or the template is
* gone or archived.
*/
function softwareVendorFromTemplate($vendor_template_id, $client_id, $make = true, &$made = false) {
global $mysqli;
$made = false;
$client_id = intval($client_id);
$template = vendorTemplateRow($vendor_template_id);
if (!$template || !$client_id) {
return 0;
}
$vendor_template_id = intval($template['vendor_template_id']);
$row = mysqli_fetch_row(mysqli_query($mysqli, "SELECT vendor_id FROM vendors WHERE vendor_client_id = $client_id AND vendor_archived_at IS NULL
AND (vendor_template_id = $vendor_template_id OR LOWER(vendor_name) = LOWER('" . escapeSql($template['vendor_template_name']) . "'))
ORDER BY vendor_template_id = $vendor_template_id DESC, vendor_id ASC LIMIT 1"));
if ($row) {
return intval($row[0]);
}
if (!$make) {
return 0;
}
$set = vendorFormSet(vendorTemplateFormFields($template));
if ($set === null) {
return 0;
}
mysqli_query($mysqli, "INSERT INTO vendors SET $set, vendor_client_id = $client_id");
$made = true;
return intval(mysqli_insert_id($mysqli));
}
/*
* Make a client's license from a template - its name, version, summary, type,
* license type and notes - with what $post gives it: seats (blank is no
* limit), key, purchase_reference, purchase and expire (Y-m-d). It remembers
* the template. The new license's id, or 0 when the template is gone or
* archived.
* license type and notes, its vendor (softwareVendorFromTemplate() - made for
* the client when it hasn't got it, with $make_vendor) - with what $post
* gives it: seats (the template's when not posted at all; blank is no
* limit), key, purchase_reference, purchase and expire (Y-m-d - blank for the
* template's term from the purchase date). It remembers the template. The new
* license's id, or 0 when the template is gone or archived; $vendor_made says
* when a vendor was made for it.
*/
function softwareTemplateMakeLicense($software_template_id, $client_id, array $post = []) {
function softwareTemplateMakeLicense($software_template_id, $client_id, array $post = [], $make_vendor = true, &$vendor_made = false) {
global $mysqli;
$vendor_made = false;
$row = softwareTemplateRow($software_template_id);
if (!$row || $row['software_template_archived_at']) {
return 0;
}
$client_id = intval($client_id);
$seats = array_key_exists('seats', $post) ? trim((string) $post['seats']) : (string) $row['software_template_seats'];
$expire = trim((string) ($post['expire'] ?? ''));
$set = softwareFormSet([
'name' => $row['software_template_name'],
'version' => $row['software_template_version'],
@@ -161,12 +255,49 @@ function softwareTemplateMakeLicense($software_template_id, $client_id, array $p
'type' => $row['software_template_type'],
'license_type' => $row['software_template_license_type'],
'notes' => $row['software_template_notes'],
'seats' => $post['seats'] ?? '',
'seats' => $seats,
'key' => $post['key'] ?? '',
'purchase_reference' => $post['purchase_reference'] ?? '',
'purchase' => $post['purchase'] ?? '',
'expire' => $post['expire'] ?? '',
], intval($client_id));
mysqli_query($mysqli, "INSERT INTO software SET $set, software_client_id = " . intval($client_id) . ", software_template_id = " . intval($row['software_template_id']));
'expire' => $expire !== '' ? $expire : softwareTemplateRenewal($row['software_template_term'], $post['purchase'] ?? ''),
'vendor' => softwareVendorFromTemplate($row['software_template_vendor_template_id'], $client_id, $make_vendor, $vendor_made),
], $client_id);
mysqli_query($mysqli, "INSERT INTO software SET $set, software_client_id = $client_id, software_template_id = " . intval($row['software_template_id']));
return intval(mysqli_insert_id($mysqli));
}
/*
* Licenses from a template for several clients at once, each with the same
* $post (softwareTemplateMakeLicense()). With $skip_existing, a client with a
* current license from it is passed over. ['made' => [client id => license
* id], 'skipped' => [client id => name], 'vendors' => [client id => true] for
* the clients a vendor was made for] - only current clients.
*/
function softwareTemplateMakeForClients($software_template_id, array $client_ids, array $post, $skip_existing = true, $make_vendor = true) {
global $mysqli;
$software_template_id = intval($software_template_id);
$result = ['made' => [], 'skipped' => [], 'vendors' => []];
$client_ids = array_values(array_unique(array_filter(array_map('intval', $client_ids))));
if (!$client_ids) {
return $result;
}
$sql = mysqli_query($mysqli, "SELECT client_id, client_name, EXISTS (SELECT 1 FROM software WHERE software_client_id = client_id
AND software_template_id = $software_template_id AND software_archived_at IS NULL) AS has_one
FROM clients WHERE client_archived_at IS NULL AND client_id IN (" . implode(',', $client_ids) . ") ORDER BY client_name ASC");
while ($row = mysqli_fetch_assoc($sql)) {
$client_id = intval($row['client_id']);
if ($skip_existing && intval($row['has_one'])) {
$result['skipped'][$client_id] = (string) $row['client_name'];
continue;
}
$software_id = softwareTemplateMakeLicense($software_template_id, $client_id, $post, $make_vendor, $vendor_made);
if (!$software_id) {
break;
}
$result['made'][$client_id] = $software_id;
if ($vendor_made) {
$result['vendors'][$client_id] = true;
}
}
return $result;
}