';
print '';
-print $form->buttonsSaveCancel("Save", '');
+print $formcategory->buttonsSaveCancel("Save", '');
print '';
diff --git a/htdocs/admin/workflow.php b/htdocs/admin/workflow.php
index a58c3378ecd..da82f163a7f 100644
--- a/htdocs/admin/workflow.php
+++ b/htdocs/admin/workflow.php
@@ -161,7 +161,21 @@ $workflowcodes = array(
'position' => 90,
'enabled' => ! empty($conf->expedition->enabled) && ! empty($conf->facture->enabled),
'picto' => 'shipment'
- )
+ ),
+
+ // Automatic link ticket -> contract
+ 'WORKFLOW_TICKET_LINK_CONTRACT' => array(
+ 'family' => 'link_ticket',
+ 'position' => 75,
+ 'enabled' => ! empty($conf->ticket->enabled) && ! empty($conf->contract->enabled),
+ 'picto' => 'ticket'
+ ),
+ 'WORKFLOW_TICKET_USE_PARENT_COMPANY_CONTRACTS' => array(
+ 'family' => 'link_ticket',
+ 'position' => 76,
+ 'enabled' => ! empty($conf->ticket->enabled) && ! empty($conf->contract->enabled),
+ 'picto' => 'ticket'
+ ),
);
if (!empty($conf->modules_parts['workflow']) && is_array($conf->modules_parts['workflow'])) {
@@ -237,6 +251,11 @@ foreach ($workflowcodes as $key => $params) {
if ($reg[1] == 'shipping') {
$header .= ' - '.$langs->trans('Shipment');
}
+ } elseif (preg_match('/link_(.*)/', $params['family'], $reg)) {
+ $header = $langs->trans("AutomaticLinking");
+ if ($reg[1] == 'ticket') {
+ $header .= ' - '.$langs->trans('Ticket');
+ }
} else {
$header = $langs->trans("Description");
}
diff --git a/htdocs/contrat/class/contrat.class.php b/htdocs/contrat/class/contrat.class.php
index 262876c312c..68ac203d955 100644
--- a/htdocs/contrat/class/contrat.class.php
+++ b/htdocs/contrat/class/contrat.class.php
@@ -2136,19 +2136,27 @@ class Contrat extends CommonObject
/**
* Return list of other contracts for same company than current contract
*
- * @param string $option 'all' or 'others'
- * @return array|int Array of contracts id or <0 if error
+ * @param string $option 'all' or 'others'
+ * @param array $status sort contracts having these status
+ * @param array $product_categories sort contracts containing these product categories
+ * @param array $line_status sort contracts where lines have these status
+ * @return array|int Array of contracts id or <0 if error
*/
- public function getListOfContracts($option = 'all')
+ public function getListOfContracts($option = 'all', $status = [], $product_categories = [], $line_status = [])
{
$tab = array();
$sql = "SELECT c.rowid, c.ref";
$sql .= " FROM ".MAIN_DB_PREFIX."contrat as c";
- $sql .= " WHERE fk_soc =".((int) $this->socid);
- if ($option == 'others') {
- $sql .= " AND c.rowid <> ".((int) $this->id);
+ if (!empty($product_categories)) {
+ $sql .= " LEFT JOIN ".MAIN_DB_PREFIX."contratdet as cd ON cd.fk_contrat = c.rowid";
+ $sql .= " INNER JOIN ".MAIN_DB_PREFIX."categorie_product as cp ON cp.fk_product = cd.fk_product AND cp.fk_categorie IN (".$this->db->sanitize(implode(', ', $product_categories)).")";
}
+ $sql .= " WHERE c.fk_soc =".((int) $this->socid);
+ $sql .= ($option == 'others') ? " AND c.rowid <> ".((int) $this->id) : "";
+ $sql .= (!empty($status)) ? " AND c.statut IN (".$this->db->sanitize(implode(', ', $status)).")" : "";
+ $sql .= (!empty($line_status)) ? " AND cd.statut IN (".$this->db->sanitize(implode(', ', $line_status)).")" : "";
+ $sql .= " GROUP BY c.rowid";
dol_syslog(get_class($this)."::getOtherContracts()", LOG_DEBUG);
$resql = $this->db->query($sql);
diff --git a/htdocs/core/class/html.form.class.php b/htdocs/core/class/html.form.class.php
index 0f8a55960c9..2c094ea56c7 100644
--- a/htdocs/core/class/html.form.class.php
+++ b/htdocs/core/class/html.form.class.php
@@ -3708,7 +3708,6 @@ class Form
}
}
-
// phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
/**
* Load into cache list of payment terms
diff --git a/htdocs/core/class/html.formcategory.class.php b/htdocs/core/class/html.formcategory.class.php
index 1a2c02deb54..707b5d5d0ac 100644
--- a/htdocs/core/class/html.formcategory.class.php
+++ b/htdocs/core/class/html.formcategory.class.php
@@ -60,4 +60,44 @@ class FormCategory extends Form
return $filter;
}
+
+ /**
+ * Prints a select form for products categories
+ * @param string $selected Id category pre-selection
+ * @param string $htmlname Name of HTML field
+ * @param int $showempty Add an empty field
+ * @return integer|null
+ */
+ public function selectProductCategory($selected = 0, $htmlname = 'product_category_id', $showempty = 0)
+ {
+ global $conf;
+
+ $sql = "SELECT cp.fk_categorie as cat_index, cat.label FROM `llx_categorie_product` as cp INNER JOIN llx_categorie as cat ON cat.rowid = cp.fk_categorie GROUP BY cp.fk_categorie;";
+
+ dol_syslog(get_class($this)."::selectProductCategory", LOG_DEBUG);
+ $resql = $this->db->query($sql);
+ if ($resql) {
+ print '');
+
+ return $num_rows;
+ } else {
+ dol_print_error($this->db);
+ }
+ }
}
diff --git a/htdocs/core/modules/modTicket.class.php b/htdocs/core/modules/modTicket.class.php
index 86841ce5932..ff7eb1ee18f 100644
--- a/htdocs/core/modules/modTicket.class.php
+++ b/htdocs/core/modules/modTicket.class.php
@@ -111,6 +111,7 @@ class modTicket extends DolibarrModules
5 => array('TICKET_DELAY_BEFORE_FIRST_RESPONSE', 'chaine', '0', 'Maximum wanted elapsed time before a first answer to a ticket (in hours). Display a warning in tickets list if not respected.', 0),
6 => array('TICKET_DELAY_SINCE_LAST_RESPONSE', 'chaine', '0', 'Maximum wanted elapsed time between two answers on the same ticket (in hours). Display a warning in tickets list if not respected.', 0),
7 => array('TICKET_NOTIFY_AT_CLOSING', 'chaine', '0', 'Default notify contacts when closing a module', 0),
+ 8 => array('TICKET_PRODUCT_CATEGORY', 'chaine', 0, 'The category of product that is being used for ticket accounting', 0)
);
diff --git a/htdocs/core/modules/modWorkflow.class.php b/htdocs/core/modules/modWorkflow.class.php
index 4122f347664..dc05bf9dc66 100644
--- a/htdocs/core/modules/modWorkflow.class.php
+++ b/htdocs/core/modules/modWorkflow.class.php
@@ -93,7 +93,9 @@ class modWorkflow extends DolibarrModules
6=>array('WORKFLOW_ORDER_CLASSIFY_RECEIVED_RECEPTION', 'chaine', '1', 'WORKFLOW_ORDER_CLASSIFY_RECEIVED_RECEPTION', 0, 'current', 0),
7=>array('WORKFLOW_ORDER_CLASSIFY_RECEIVED_RECEPTION_CLOSED', 'chaine', '1', 'WORKFLOW_ORDER_CLASSIFY_RECEIVED_RECEPTION_CLOSED', 0, 'current', 0),
8=>array('WORKFLOW_INVOICE_AMOUNT_CLASSIFY_BILLED_SUPPLIER_ORDER', 'chaine', '1', 'WORKFLOW_INVOICE_AMOUNT_CLASSIFY_BILLED_SUPPLIER_ORDER', 0, 'current', 0),
- 9=>array('WORKFLOW_BILL_ON_RECEPTION', 'chaine', '1', 'WORKFLOW_BILL_ON_RECEPTION', 0, 'current', 0)
+ 9=>array('WORKFLOW_BILL_ON_RECEPTION', 'chaine', '1', 'WORKFLOW_BILL_ON_RECEPTION', 0, 'current', 0),
+ 10=>array('WORKFLOW_TICKET_LINK_CONTRACT', 'chaine', '0', 'Automatically link a ticket to available contracts', 0, 'current', 0),
+ 11=>array('WORKFLOW_TICKET_USE_PARENT_COMPANY_CONTRACTS', 'chaine', '0', 'Search among parent companies contracts when automatically linking a ticket to available contracts', 0, 'current', 0)
);
// Boxes
diff --git a/htdocs/core/triggers/interface_20_modWorkflow_WorkflowManager.class.php b/htdocs/core/triggers/interface_20_modWorkflow_WorkflowManager.class.php
index 048dc170c0a..4e98af5c6c7 100644
--- a/htdocs/core/triggers/interface_20_modWorkflow_WorkflowManager.class.php
+++ b/htdocs/core/triggers/interface_20_modWorkflow_WorkflowManager.class.php
@@ -424,6 +424,40 @@ class InterfaceWorkflowManager extends DolibarrTriggers
}
}
+ if ($action == 'TICKET_CREATE') {
+ dol_syslog("Trigger '".$this->name."' for action '$action' launched by ".__FILE__.". id=".$object->id);
+ // Auto link contract
+ if (!empty($conf->contract->enabled) && !empty($conf->ticket->enabled) && !empty($conf->ficheinter->enabled) && !empty($conf->workflow->enabled) && !empty($conf->global->WORKFLOW_TICKET_LINK_CONTRACT) && !empty($conf->global->TICKET_PRODUCT_CATEGORY) && !empty($object->fk_soc)) {
+ $societe = new Societe($this->db);
+ $company_ids = (empty($conf->global->WORKFLOW_TICKET_USE_PARENT_COMPANY_CONTRACTS)) ? [$object->fk_soc] : $societe->getParentsForCompany($object->fk_soc, [$object->fk_soc]);
+
+ $contrat = new Contrat($this->db);
+ $number_contracts_found = 0;
+ foreach ($company_ids as $company_id) {
+ $contrat->socid = $company_id;
+
+ $list = $contrat->getListOfContracts($option = 'all', $status = [Contrat::STATUS_DRAFT, Contrat::STATUS_VALIDATED], $product_categories = [$conf->global->TICKET_PRODUCT_CATEGORY], $line_status = [ContratLigne::STATUS_INITIAL, ContratLigne::STATUS_OPEN]);
+ if (is_array($list) && !empty($list)) {
+ $number_contracts_found = count($list);
+ if ($number_contracts_found == 1) {
+ $contractid = $list[0]->id;
+ $object->setContract($contractid);
+ break;
+ } elseif ($number_contracts_found > 1) {
+ foreach ($list as $linked_contract) {
+ $object->setContract($linked_contract->id);
+ // don't set '$contractid' so it is not used when creating an intervention.
+ }
+ if (empty(NOLOGIN)) setEventMessage($langs->trans('TicketManyContractsLinked'), 'warnings');
+ break;
+ }
+ }
+ }
+ if ($number_contracts_found == 0) {
+ if (empty(NOLOGIN)) setEventMessage($langs->trans('TicketNoContractFoundToLink'), 'mesgs');
+ }
+ }
+ }
return 0;
}
diff --git a/htdocs/install/mysql/migration/15.0.0-16.0.0.sql b/htdocs/install/mysql/migration/15.0.0-16.0.0.sql
index 353eb71eac4..1876d3428b4 100644
--- a/htdocs/install/mysql/migration/15.0.0-16.0.0.sql
+++ b/htdocs/install/mysql/migration/15.0.0-16.0.0.sql
@@ -116,6 +116,8 @@ INSERT INTO llx_c_action_trigger (code,label,description,elementtype,rang) value
ALTER TABLE llx_ticket ADD COLUMN date_last_msg_sent datetime AFTER date_read;
+UPDATE llx_const SET name = 'WORKFLOW_TICKET_LINK_CONTRACT' WHERE name = 'TICKET_AUTO_ASSIGN_CONTRACT_CREATE';
+
CREATE TABLE llx_stock_mouvement_extrafields (
rowid integer AUTO_INCREMENT PRIMARY KEY,
tms timestamp DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
diff --git a/htdocs/langs/en_US/interventions.lang b/htdocs/langs/en_US/interventions.lang
index ef5df43e546..7c117fcd1f2 100644
--- a/htdocs/langs/en_US/interventions.lang
+++ b/htdocs/langs/en_US/interventions.lang
@@ -66,3 +66,4 @@ RepeatableIntervention=Template of intervention
ToCreateAPredefinedIntervention=To create a predefined or recurring intervention, create a common intervention and convert it into intervention template
ConfirmReopenIntervention=Are you sure you want to open back the intervention %s?
GenerateInter=Generate intervention
+FichinterNoContractLinked=Intervention %s has been created without a linked contract.
diff --git a/htdocs/langs/en_US/ticket.lang b/htdocs/langs/en_US/ticket.lang
index 310ac8dd7f6..edd54911bad 100644
--- a/htdocs/langs/en_US/ticket.lang
+++ b/htdocs/langs/en_US/ticket.lang
@@ -145,6 +145,8 @@ TicketsDelayBetweenAnswersHelp=If an unresolved ticket that has already received
TicketsAutoNotifyClose=Automatically notify thirdparty when closing a ticket
TicketsAutoNotifyCloseHelp=When closing a ticket, you will be proposed to send a message to one of thirdparty's contacts. On mass closing, a message will be sent to one contact of the thirdparty linked to the ticket.
TicketWrongContact=Provided contact is not part of current ticket contacts. Email not sent.
+TicketChooseProductCategory=Product category for ticket support
+TicketChooseProductCategoryHelp=Select the product category of ticket support. This will be used to automatically link a contract to a ticket.
#
# Index & list page
@@ -258,6 +260,8 @@ TicketNotCreatedFromPublicInterface=Not available. Ticket was not created from p
ErrorTicketRefRequired=Ticket reference name is required
TicketsDelayForFirstResponseTooLong=Too much time elapsed since ticket opening without any answer.
TicketsDelayFromLastResponseTooLong=Too much time elapsed since last answer on this ticket.
+TicketNoContractFoundToLink=No contract was found to be automatically linked to this ticket. Please link a contract manually.
+TicketManyContractsLinked=Many contracts have been automatically linked to this ticket. Make sure to verify which should be chosen.
#
# Logs
diff --git a/htdocs/langs/en_US/workflow.lang b/htdocs/langs/en_US/workflow.lang
index b65f8449fef..6ddf8d9c6a3 100644
--- a/htdocs/langs/en_US/workflow.lang
+++ b/htdocs/langs/en_US/workflow.lang
@@ -22,9 +22,14 @@ descWORKFLOW_ORDER_CLASSIFY_RECEIVED_RECEPTION=Classify linked source purchase o
descWORKFLOW_ORDER_CLASSIFY_RECEIVED_RECEPTION_CLOSED=Classify linked source purchase order as received when a reception is closed (and if the quantity received by all rceptions is the same as in the purchase order to update)
# Autoclassify purchase invoice
descWORKFLOW_BILL_ON_RECEPTION=Classify receptions to "billed" when a linked supplier order is validated
+# Automatically link ticket to contract
+descWORKFLOW_TICKET_LINK_CONTRACT=When creating a ticket, link available contracts of matching thirdparty
+descWORKFLOW_TICKET_USE_PARENT_COMPANY_CONTRACTS=When linking contracts, search among those of parents companies
# Autoclose intervention
descWORKFLOW_TICKET_CLOSE_INTERVENTION=Close all interventions linked to the ticket when a ticket is closed
AutomaticCreation=Automatic creation
AutomaticClassification=Automatic classification
# Autoclassify shipment
-descWORKFLOW_SHIPPING_CLASSIFY_CLOSED_INVOICE=Classify linked source shipment as closed when customer invoice is validated
\ No newline at end of file
+descWORKFLOW_SHIPPING_CLASSIFY_CLOSED_INVOICE=Classify linked source shipment as closed when customer invoice is validated
+AutomaticClosing=Automatic closing
+AutomaticLinking=Automatic linking
diff --git a/htdocs/societe/class/societe.class.php b/htdocs/societe/class/societe.class.php
index efa868b7aad..015a8c92af6 100644
--- a/htdocs/societe/class/societe.class.php
+++ b/htdocs/societe/class/societe.class.php
@@ -3464,6 +3464,37 @@ class Societe extends CommonObject
}
}
+ /**
+ * Get parents for company
+ *
+ * @param int $company_id ID of company to search parent
+ * @param array $parents List of companies ID found
+ * @return array
+ */
+ public function getParentsForCompany($company_id, $parents = [])
+ {
+ global $langs;
+
+ if ($company_id > 0) {
+ $sql = "SELECT parent FROM " . MAIN_DB_PREFIX . "societe WHERE rowid = $company_id";
+ $resql = $this->db->query($sql);
+ if ($resql) {
+ if ($obj = $this->db->fetch_object($resql)) {
+ $parent = $obj->parent;
+ if ($parent > 0 && !in_array($parent, $parents)) {
+ $parents[] = $parent;
+ return $this->getParentsForCompany($parent, $parents);
+ } else {
+ return $parents;
+ }
+ }
+ $this->db->free($resql);
+ } else {
+ setEventMessage($langs->trans('GetCompanyParentsError', $this->db->lasterror()), 'errors');
+ }
+ }
+ }
+
// phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
/**
* Returns if a profid sould be verified to be unique
diff --git a/htdocs/ticket/card.php b/htdocs/ticket/card.php
index 3e37c2c28e5..1cebeec3506 100755
--- a/htdocs/ticket/card.php
+++ b/htdocs/ticket/card.php
@@ -255,28 +255,12 @@ if (empty($reshook)) {
$object->add_contact($user->id, "SUPPORTTEC", 'internal');
}
- // Auto assign contrat
- $contractid = 0;
- if (!empty($conf->global->TICKET_AUTO_ASSIGN_CONTRACT_CREATE)) {
- $contrat = new Contrat($db);
- $contrat->socid = $object->fk_soc;
- $list = $contrat->getListOfContracts();
-
- if (is_array($list) && !empty($list)) {
- if (count($list) == 1) {
- $contractid = $list[0]->id;
- $object->setContract($contractid);
- } else {
- }
- }
- }
-
// Auto create fiche intervention
if (!empty($conf->global->TICKET_AUTO_CREATE_FICHINTER_CREATE)) {
$fichinter = new Fichinter($db);
$fichinter->socid = $object->fk_soc;
$fichinter->fk_project = $projectid;
- $fichinter->fk_contrat = $contractid;
+ $fichinter->fk_contrat = $object->fk_contract;
$fichinter->author = $user->id;
$fichinter->model_pdf = 'soleil';
$fichinter->origin = $object->element;