diff --git a/front/contract_item.form.php b/front/contract_item.form.php index 1387648d918..5beb93a4cdf 100644 --- a/front/contract_item.form.php +++ b/front/contract_item.form.php @@ -42,8 +42,6 @@ Session::checkCentralAccess(); -$contract_item = new Contract_Item(); - if (isset($_POST["add"])) { if (!isset($_POST['contracts_id']) || empty($_POST['contracts_id'])) { $message = sprintf( @@ -54,6 +52,15 @@ Html::back(); } + if (isset($_POST['itemtype']) && $_POST['itemtype'] == 'User') { + $contract_item = new Contract_User(); + // convert form data to match the Contract_User case + $_POST['users_id'] = $_POST['items_id']; + unset($_POST['itemtype'], $_POST['items_id']); + } else { + $contract_item = new Contract_Item(); + } + $contract_item->check(-1, CREATE, $_POST); if ($contract_item->add($_POST)) { Event::log( diff --git a/inc/relation.constant.php b/inc/relation.constant.php index 4ba2f1c56b7..3caec3ab2d5 100644 --- a/inc/relation.constant.php +++ b/inc/relation.constant.php @@ -272,6 +272,7 @@ '_glpi_contracts_suppliers' => 'contracts_id', 'glpi_entities' => 'contracts_id_default', '_glpi_tickets_contracts' => 'contracts_id', + '_glpi_contracts_users' => 'contracts_id', ], 'glpi_contracttypes' => [ @@ -1656,6 +1657,7 @@ 'users_id', 'users_id_substitute', ], + '_glpi_contracts_users' => 'users_id', ], 'glpi_usertitles' => [ diff --git a/install/migrations/update_10.0.x_to_11.0.0/contract_user.php b/install/migrations/update_10.0.x_to_11.0.0/contract_user.php new file mode 100644 index 00000000000..ca55283f274 --- /dev/null +++ b/install/migrations/update_10.0.x_to_11.0.0/contract_user.php @@ -0,0 +1,55 @@ +. + * + * --------------------------------------------------------------------- + */ + +/** + * @var \Migration $migration + * @var array $ADDTODISPLAYPREF + * @var \DBmysql $DB + */ + +$default_charset = DBConnection::getDefaultCharset(); +$default_collation = DBConnection::getDefaultCollation(); + +if (!$DB->tableExists('glpi_contracts_users')) { + $query = "CREATE TABLE `glpi_contracts_users` ( + `id` int unsigned NOT NULL AUTO_INCREMENT, + `contracts_id` int unsigned NOT NULL DEFAULT '0', + `users_id` int unsigned NOT NULL DEFAULT '0', + PRIMARY KEY (`id`), + UNIQUE KEY `unicity` (`contracts_id`,`users_id`), + KEY `item` (`users_id`) + ) ENGINE=InnoDB DEFAULT CHARSET = {$default_charset} COLLATE = {$default_collation} ROW_FORMAT=DYNAMIC"; + $DB->doQuery($query); +} diff --git a/install/mysql/glpi-empty.sql b/install/mysql/glpi-empty.sql index 6d99d88ac5d..f213a22c1fe 100644 --- a/install/mysql/glpi-empty.sql +++ b/install/mysql/glpi-empty.sql @@ -10086,4 +10086,14 @@ CREATE TABLE `glpi_assets_customfielddefinitions` ( KEY `system_name` (`system_name`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=DYNAMIC; +DROP TABLE IF EXISTS `glpi_contracts_users`; +CREATE TABLE `glpi_contracts_users` ( + `id` int unsigned NOT NULL AUTO_INCREMENT, + `contracts_id` int unsigned NOT NULL DEFAULT '0', + `users_id` int unsigned NOT NULL DEFAULT '0', + PRIMARY KEY (`id`), + UNIQUE KEY `unicity` (`contracts_id`, `users_id`), + KEY `item` (`users_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=DYNAMIC; + SET FOREIGN_KEY_CHECKS=1; diff --git a/phpunit/functional/ContractTest.php b/phpunit/functional/ContractTest.php index 3267e483d78..d61db2c3a87 100644 --- a/phpunit/functional/ContractTest.php +++ b/phpunit/functional/ContractTest.php @@ -188,4 +188,38 @@ public function testGetSpecificValueToDisplay($field, $values, $expected) $contract = new \Contract(); $this->assertEquals($expected, $contract->getSpecificValueToDisplay($field, $values)); } + + public function testLinkUser() + { + $this->login(); + $this->setEntity('_test_root_entity', true); + + $contract = new \Contract(); + $input = [ + 'name' => 'A test contract', + 'entities_id' => 0 + ]; + $cid = $contract->add($input); + $this->assertGreaterThan(0, $cid); + + $user = new \User(); + $uid = $user->add([ + 'name' => 'Test User', + 'firstname' => 'Test', + 'realname' => 'User', + 'entities_id' => 0 + ]); + $this->assertGreaterThan(0, $uid); + + $link_user = new \Contract_User(); + $link_id = $link_user->add([ + 'users_id' => $uid, + 'contracts_id' => $cid + ]); + $this->assertGreaterThan(0, $link_id); + + $this->assertTrue($link_user->getFromDB($link_id)); + $relation_items = $link_user->getItemsAssociatedTo($contract->getType(), $cid); + $this->assertCount(1, $relation_items, 'Original Contract_User not found!'); + } } diff --git a/src/Contract_Item.php b/src/Contract_Item.php index d31bc188521..f687f44b7a9 100644 --- a/src/Contract_Item.php +++ b/src/Contract_Item.php @@ -208,14 +208,11 @@ public function getTabNameForItem(CommonGLPI $item, $withtemplate = 0) case Contract::class: if ($_SESSION['glpishow_count_on_tabs']) { $nb = self::countForMainItem($item); + $nb += countElementsInTable(Contract_User::getTable(), ['contracts_id' => $item->fields['id']]); } - return self::createTabEntry(_n('Item', 'Items', Session::getPluralNumber()), $nb, $item::class, 'ti ti-package'); - + return self::createTabEntry(_n('Affected item', 'Affected items', Session::getPluralNumber()), $nb, $item::class, 'ti ti-package'); default: - if ( - $_SESSION['glpishow_count_on_tabs'] - && in_array($item::class, $CFG_GLPI["contract_types"], true) - ) { + if (in_array($item::class, $CFG_GLPI["contract_types"], true)) { $nb = self::countForItem($item); } return self::createTabEntry(Contract::getTypeName(Session::getPluralNumber()), $nb, $item::class); @@ -394,8 +391,11 @@ public static function showForItem(CommonDBTM $item, $withtemplate = 0) **/ public static function showForContract(Contract $contract, $withtemplate = 0) { - /** @var \DBmysql $DB */ - global $DB; + /** + * @var \DBmysql $DB + * @var array $CFG_GLPI + */ + global $DB, $CFG_GLPI; $instID = $contract->fields['id']; @@ -482,10 +482,43 @@ public static function showForContract(Contract $contract, $withtemplate = 0) foreach ($iterator as $objdata) { $data[$itemtype][$objdata['id']] = $objdata; $used[$itemtype][$objdata['id']] = $objdata['id']; + $totalnb++; } } } + // Add contract users + $contract_users_table = Contract_User::getTable(); + $users_table = User::getTable(); + $user_params = [ + 'SELECT' => [ + "$users_table.*", + "$contract_users_table.id AS linkid", + ], + 'FROM' => $contract_users_table, + 'LEFT JOIN' => [ + $users_table => [ + 'FKEY' => [ + $contract_users_table => 'users_id', + $users_table => 'id' + ] + ], + ], + 'WHERE' => [ + "$contract_users_table.contracts_id" => $instID + ], + 'ORDER' => "$users_table.name" + ]; + + $user_iterator = $DB->request($user_params); + + $data[User::class] = []; + foreach ($user_iterator as $userdata) { + $data[User::class][$userdata['id']] = $userdata; + $used[User::class][$userdata['id']] = $userdata['id']; + $totalnb++; + } + if ( $canedit && (((int) $contract->fields['max_links_allowed'] === 0) @@ -494,6 +527,7 @@ public static function showForContract(Contract $contract, $withtemplate = 0) ) { $twig_params = [ 'contract' => $contract, + 'contract_types' => array_merge($CFG_GLPI["contract_types"], [User::class]), 'entity_restrict' => $contract->fields['is_recursive'] ? getSonsOf('glpi_entities', $contract->fields['entities_id']) : $contract->fields['entities_id'], @@ -509,7 +543,7 @@ public static function showForContract(Contract $contract, $withtemplate = 0) {{ fields.dropdownItemsFromItemtypes('', null, { - itemtypes: config('contract_types'), + itemtypes: contract_types, entity_restrict: entity_restrict, checkright: true, used: used @@ -531,7 +565,7 @@ public static function showForContract(Contract $contract, $withtemplate = 0) foreach ($data as $itemtype => $datas) { foreach ($datas as $objdata) { $entry = [ - 'itemtype' => self::class, + 'itemtype' => $itemtype === User::class ? Contract_User::class : self::class, 'id' => $objdata['linkid'], 'row_class' => isset($objdata['is_deleted']) && $objdata['is_deleted'] ? 'table-danger' : '', 'type' => $itemtype::getTypeName(1), @@ -540,23 +574,31 @@ public static function showForContract(Contract $contract, $withtemplate = 0) $item->getFromResultSet($objdata); $entry['name'] = $item->getLink(); - if (!isset($entity_cache[$objdata['entity']])) { - $entity_cache[$objdata['entity']] = Dropdown::getDropdownName( - "glpi_entities", - $objdata['entity'] - ); + if (isset($objdata['entity'])) { + if (!isset($entity_cache[$objdata['entity']])) { + $entity_cache[$objdata['entity']] = Dropdown::getDropdownName( + "glpi_entities", + $objdata['entity'] + ); + } + $entry['entity'] = $entity_cache[$objdata['entity']]; + } else { + $entry['entity'] = '-'; } - $entry['entity'] = $entity_cache[$objdata['entity']]; $entry['serial'] = $objdata['serial'] ?? '-'; $entry['otherserial'] = $objdata['otherserial'] ?? '-'; - if (!isset($state_cache[$objdata['states_id']])) { - $state_cache[$objdata['states_id']] = Dropdown::getDropdownName( - "glpi_states", - $objdata['states_id'] - ); + if (isset($objdata['states_id'])) { + if (!isset($state_cache[$objdata['states_id']])) { + $state_cache[$objdata['states_id']] = Dropdown::getDropdownName( + "glpi_states", + $objdata['states_id'] + ); + } + $entry['status'] = $state_cache[$objdata['states_id']]; + } else { + $entry['status'] = '-'; } - $entry['status'] = $state_cache[$objdata['states_id']]; $entries[] = $entry; } } diff --git a/src/Contract_User.php b/src/Contract_User.php new file mode 100644 index 00000000000..e7a99bbd074 --- /dev/null +++ b/src/Contract_User.php @@ -0,0 +1,238 @@ +. + * + * --------------------------------------------------------------------- + */ + +use Glpi\Application\View\TemplateRenderer; + +class Contract_User extends CommonDBRelation +{ + // From CommonDBRelation + public static $itemtype_1 = 'Contract'; + public static $items_id_1 = 'contracts_id'; + + public static $itemtype_2 = 'User'; + public static $items_id_2 = 'users_id'; + + public static $check_entity_coherency = false; // `entities_id`/`is_recursive` fields from user should not be used here + + public function getForbiddenStandardMassiveAction() + { + $forbidden = parent::getForbiddenStandardMassiveAction(); + $forbidden[] = 'update'; + return $forbidden; + } + + public function canCreateItem(): bool + { + // Try to load the contract + $contract = $this->getConnexityItem(static::$itemtype_1, static::$items_id_1); + if ($contract === false) { + return false; + } + + // Don't create a Contract_User on contract that is already max used + if ( + ($contract->fields['max_links_allowed'] > 0) + && (countElementsInTable( + static::getTable(), + ['contracts_id' => $this->input['contracts_id']] + ) + >= $contract->fields['max_links_allowed']) + ) { + return false; + } + + return parent::canCreateItem(); + } + + public static function getTypeName($nb = 0) + { + return User::getTypeName($nb); + } + + public function getTabNameForItem(CommonGLPI $item, $withtemplate = 0) + { + if (Contract::canView() && $item::class === User::class) { + $nb = 0; + if ($_SESSION['glpishow_count_on_tabs']) { + $nb = countElementsInTable(Contract_User::getTable(), ['users_id' => $item->fields['id']]); + } + return self::createTabEntry(Contract::getTypeName(Session::getPluralNumber()), $nb, $item::class); + } + + return ''; + } + + public static function displayTabContentForItem(CommonGLPI $item, $tabnum = 1, $withtemplate = 0) + { + if (Contract::canView() && $item::class === User::class) { + self::showForUser($item, $withtemplate); + } + + return true; + } + + private static function showForUser(User $user, $withtemplate = 0): void + { + $ID = $user->fields['id']; + + if ( + !Contract::canView() + || !$user->can($ID, READ) + ) { + return; + } + + $canedit = $user->can($ID, UPDATE); + $rand = mt_rand(); + + $iterator = self::getListForItem($user); + + $contracts = []; + $used = []; + foreach ($iterator as $data) { + $contracts[$data['id']] = $data; + $used[$data['id']] = $data['id']; + } + if ($canedit && ((int) $withtemplate !== 2)) { + $twig_params = [ + 'user' => $user, + 'used' => $used, + 'btn_label' => _x('button', 'Add'), + ]; + // language=Twig + echo TemplateRenderer::getInstance()->renderFromStringTemplate(<< +
+ + + +
+ {{ fields.dropdownField('Contract', 'contracts_id', 0, null, { + used: used, + expired: false, + }) }} + {% set btn %} + + {% endset %} + {{ fields.htmlField('', btn, null) }} +
+
+ +TWIG, $twig_params); + } + + $entries = []; + $entity_cache = []; + $type_cache = []; + foreach ($contracts as $data) { + $entry = [ + 'itemtype' => self::class, + 'id' => $data['linkid'], + 'row_class' => $data['is_deleted'] ? 'table-danger' : '', + 'num' => $data['num'] + ]; + $contract = new Contract(); + $contract->getFromResultSet($data); + + $entry['name'] = $contract->getLink(); + + if (!isset($entity_cache[$contract->fields["entities_id"]])) { + $entity_cache[$contract->fields["entities_id"]] = Dropdown::getDropdownName( + "glpi_entities", + $contract->fields["entities_id"] + ); + } + $entry['entity'] = $entity_cache[$contract->fields["entities_id"]]; + + if (!isset($type_cache[$contract->fields["contracttypes_id"]])) { + $type_cache[$contract->fields["contracttypes_id"]] = Dropdown::getDropdownName( + "glpi_contracttypes", + $contract->fields["contracttypes_id"] + ); + } + $entry['type'] = $type_cache[$contract->fields["contracttypes_id"]]; + + $entry['supplier'] = $contract->getSuppliersNames(); + $entry['begin_date'] = $contract->fields["begin_date"]; + + $duration = sprintf( + __('%1$s %2$s'), + $contract->fields["duration"], + _n('month', 'months', $contract->fields["duration"]) + ); + + if (!empty($contract->fields["begin_date"])) { + $duration .= ' -> ' . Infocom::getWarrantyExpir( + $contract->fields["begin_date"], + $contract->fields["duration"], + 0, + true + ); + } + $entry['duration'] = $duration; + + $entries[] = $entry; + } + + TemplateRenderer::getInstance()->display('components/datatable.html.twig', [ + 'is_tab' => true, + 'nopager' => true, + 'nofilter' => true, + 'nosort' => true, + 'columns' => [ + 'name' => __('Name'), + 'entity' => Entity::getTypeName(1), + 'type' => _n('Type', 'Types', 1), + 'supplier' => Supplier::getTypeName(1), + 'begin_date' => __('Start date'), + 'duration' => __('Initial contract period'), + ], + 'formatters' => [ + 'name' => 'raw_html', + 'supplier' => 'raw_html', + 'begin_date' => 'date', + ], + 'entries' => $entries, + 'total_number' => count($entries), + 'filtered_number' => count($entries), + 'showmassiveactions' => $canedit && (int) $withtemplate !== 2, + 'massiveactionparams' => [ + 'num_displayed' => count($entries), + 'container' => 'mass' . static::class . $rand, + ] + ]); + } +} diff --git a/src/User.php b/src/User.php index 6776221739f..9ae77a29470 100644 --- a/src/User.php +++ b/src/User.php @@ -405,6 +405,7 @@ public function defineTabs($options = []) $this->addStandardTab('Auth', $ong, $options); $this->addStandardTab('ManualLink', $ong, $options); $this->addStandardTab('Certificate_Item', $ong, $options); + $this->addStandardTab(Contract_User::class, $ong, $options); $this->addStandardTab('Log', $ong, $options); return $ong;