From af2cf0b66fa9e04bdf21fbce1a5c5c00025c6733 Mon Sep 17 00:00:00 2001
From: Laurent Destailleur <eldy@destailleur.fr>
Date: Sat, 26 Apr 2014 21:36:58 +0200
Subject: [PATCH] Fix: [ bug #1354 ] Tasks disapear in same sub-task

---
 htdocs/core/class/html.formother.class.php |  47 +++++---
 htdocs/core/lib/functions2.lib.php         | 122 +++++++++++++++++++++
 htdocs/install/mysql/migration/repair.sql  |   2 +
 htdocs/projet/class/project.class.php      |  60 ----------
 htdocs/projet/tasks.php                    |  12 +-
 htdocs/projet/tasks/task.php               |   3 +-
 6 files changed, 166 insertions(+), 80 deletions(-)

diff --git a/htdocs/core/class/html.formother.class.php b/htdocs/core/class/html.formother.class.php
index 3ac6ce0eba1..e4d88fab7d7 100644
--- a/htdocs/core/class/html.formother.class.php
+++ b/htdocs/core/class/html.formother.class.php
@@ -424,16 +424,17 @@ class FormOther
     /**
      *	Return list of project and tasks
      *
-     *	@param  int		$selectedtask   	Pre-selected task
-     *  @param  int		$projectid       Project id
-     * 	@param  string	$htmlname    	Name of html select
-     * 	@param	int		$modeproject		1 to restrict on projects owned by user
-     * 	@param	int		$modetask		1 to restrict on tasks associated to user
-     * 	@param	int		$mode			0=Return list of tasks and their projects, 1=Return projects and tasks if exists
-     *  @param  int		$useempty        0=Allow empty values
+     *	@param  int		$selectedtask   		Pre-selected task
+     *  @param  int		$projectid				Project id
+     * 	@param  string	$htmlname    			Name of html select
+     * 	@param	int		$modeproject			1 to restrict on projects owned by user
+     * 	@param	int		$modetask				1 to restrict on tasks associated to user
+     * 	@param	int		$mode					0=Return list of tasks and their projects, 1=Return projects and tasks if exists
+     *  @param  int		$useempty       		0=Allow empty values
+     *  @param	int		$disablechildoftaskid	1=Disable task that are child of the provided task id
      *  @return	void
      */
-    function selectProjectTasks($selectedtask='', $projectid=0, $htmlname='task_parent', $modeproject=0, $modetask=0, $mode=0, $useempty=0)
+    function selectProjectTasks($selectedtask='', $projectid=0, $htmlname='task_parent', $modeproject=0, $modetask=0, $mode=0, $useempty=0, $disablechildoftaskid=0)
     {
         global $user, $langs;
 
@@ -448,7 +449,7 @@ class FormOther
             if ($useempty) print '<option value="0">&nbsp;</option>';
             $j=0;
             $level=0;
-            $this->_pLineSelect($j, 0, $tasksarray, $level, $selectedtask, $projectid);
+            $this->_pLineSelect($j, 0, $tasksarray, $level, $selectedtask, $projectid, $disablechildoftaskid);
             print '</select>';
         }
         else
@@ -458,17 +459,18 @@ class FormOther
     }
 
     /**
-     * Write all lines of a project (if parent = 0)
+     * Write lines of a project (all lines of a project if parent = 0)
      *
      * @param 	int		&$inc					Cursor counter
-     * @param 	int		$parent					Id parent
-     * @param 	Object	$lines					Line object
+     * @param 	int		$parent					Id of parent task we want to see
+     * @param 	array	$lines					Array of task lines
      * @param 	int		$level					Level
      * @param 	int		$selectedtask			Id selected task
      * @param 	int		$selectedproject		Id selected project
+     * @param	int		$disablechildoftaskid	1=Disable task that are child of the provided task id
      * @return	void
      */
-    private function _pLineSelect(&$inc, $parent, $lines, $level=0, $selectedtask=0, $selectedproject=0)
+    private function _pLineSelect(&$inc, $parent, $lines, $level=0, $selectedtask=0, $selectedproject=0, $disablechildoftaskid=0)
     {
         global $langs, $user, $conf;
 
@@ -481,12 +483,12 @@ class FormOther
             {
                 $var = !$var;
 
-				//var_dump($selectedtask."--".$selectedtask."--".$lines[$i]->fk_project."_".$lines[$i]->id);
+				//var_dump($selectedproject."--".$selectedtask."--".$lines[$i]->fk_project."_".$lines[$i]->id);
 
                 // Break on a new project
-                if ($parent == 0)
+                if ($parent == 0)	// We are on a task at first level
                 {
-                    if ($lines[$i]->fk_project != $lastprojectid)
+                    if ($lines[$i]->fk_project != $lastprojectid)	// Break found on project
                     {
                         if ($i > 0 && $conf->browser->firefox) print '<option value="0" disabled="disabled">----------</option>';
                         print '<option value="'.$lines[$i]->fk_project.'_0"';
@@ -509,11 +511,22 @@ class FormOther
                     }
                 }
 
+                $newdisablechildoftaskid=$disablechildoftaskid;
+
                 // Print task
                 if ($lines[$i]->id >= 0)
                 {
+                	// Check if we must disable entry
+                	$disabled=0;
+                	if ($disablechildoftaskid && (($lines[$i]->id == $disablechildoftaskid || $lines[$i]->fk_parent == $disablechildoftaskid)))
+                	{
+               			$disabled++;
+               			if ($lines[$i]->fk_parent == $disablechildoftaskid) $newdisablechildoftaskid=$lines[$i]->id;	// If task is child of a disabled parent, we will propagate id to disable next child too
+                	}
+
                     print '<option value="'.$lines[$i]->fk_project.'_'.$lines[$i]->id.'"';
                     if (($lines[$i]->id == $selectedtask) || ($lines[$i]->fk_project.'_'.$lines[$i]->id == $selectedtask)) print ' selected="selected"';
+                    if ($disabled) print ' disabled="disabled"';
                     print '>';
                     print $langs->trans("Project").' '.$lines[$i]->projectref;
                     if (empty($lines[$i]->public))
@@ -534,7 +547,7 @@ class FormOther
                 }
 
                 $level++;
-                if ($lines[$i]->id) $this->_pLineSelect($inc, $lines[$i]->id, $lines, $level, $selectedtask, $selectedproject);
+                if ($lines[$i]->id) $this->_pLineSelect($inc, $lines[$i]->id, $lines, $level, $selectedtask, $selectedproject, $newdisablechildoftaskid);
                 $level--;
             }
         }
diff --git a/htdocs/core/lib/functions2.lib.php b/htdocs/core/lib/functions2.lib.php
index 20af2917fa4..01cb46f20f2 100644
--- a/htdocs/core/lib/functions2.lib.php
+++ b/htdocs/core/lib/functions2.lib.php
@@ -1532,3 +1532,125 @@ function dolGetElementUrl($objectid,$objecttype,$withpicto=0,$option='')
 	}
 	return $ret;
 }
+
+
+/**
+ * Clean corrupted tree (orphelins linked to a not existing parent), record linked to themself and child-parent loop
+ *
+ * @param	string	$tabletocleantree	Table to clean
+ * @param	string	$fieldfkparent		Field name that contains id of parent
+ * @return	int							Nb of records fixed/deleted
+ */
+function cleanCorruptedTree($db, $tabletocleantree, $fieldfkparent)
+{
+	$totalnb=0;
+	$listofid=array();
+	$listofparentid=array();
+
+	// Get list of all id in array listofid and all parents in array listofparentid
+	$sql='SELECT rowid, '.$fieldfkparent.' as parent_id FROM '.MAIN_DB_PREFIX.$tabletocleantree;
+	$resql = $db->query($sql);
+	if ($resql)
+	{
+		$num = $db->num_rows($resql);
+		$i = 0;
+		while ($i < $num)
+		{
+			$obj = $db->fetch_object($resql);
+			$listofid[]=$obj->rowid;
+			if ($obj->parent_id > 0) $listofparentid[$obj->rowid]=$obj->parent_id;
+			$i++;
+		}
+	}
+	else
+	{
+		dol_print_error($db);
+	}
+
+	if (count($listofid))
+	{
+		print 'Code requested to clean tree (may be to solve data corruption), so we check/clean orphelins and loops.'."<br>\n";
+
+		// Check loops on each other
+		$sql = "UPDATE ".MAIN_DB_PREFIX.$tabletocleantree." SET ".$fieldfkparent." = 0 WHERE ".$fieldfkparent." = rowid";	// So we update only records linked to themself
+		dol_syslog("sql=".$sql);
+		$resql = $db->query($sql);
+		if ($resql)
+		{
+			$nb=$db->affected_rows($sql);
+			if ($nb > 0)
+			{
+				print '<br>Some record that were parent of themself were cleaned.';
+			}
+
+			$totalnb+=$nb;
+		}
+		//else dol_print_error($db);
+
+		// Check other loops
+		$listofidtoclean=array();
+		foreach($listofparentid as $id => $pid)
+		{
+			// Check depth
+			//print 'Analyse record id='.$id.' with parent '.$pid.'<br>';
+
+			$cursor=$id; $arrayidparsed=array();	// We start from child $id
+			while ($cursor > 0)
+			{
+				$arrayidparsed[$cursor]=1;
+				if ($arrayidparsed[$listofparentid[$cursor]])	// We detect a loop. A record with a parent that was already into child
+				{
+					print 'Found a loop between id '.$id.' - '.$cursor.'<br>';
+					unset($arrayidparsed);
+					$listofidtoclean[$cursor]=$id;
+					break;
+				}
+				$cursor=$listofparentid[$cursor];
+			}
+
+			if (count($listofidtoclean)) break;
+		}
+
+		$sql = "UPDATE ".MAIN_DB_PREFIX.$tabletocleantree;
+		$sql.= " SET ".$fieldfkparent." = 0";
+		$sql.= " WHERE rowid IN (".join(',',$listofidtoclean).")";	// So we update only records detected wrong
+		dol_syslog("sql=".$sql);
+		$resql = $db->query($sql);
+		if ($resql)
+		{
+			$nb=$db->affected_rows($sql);
+			if ($nb > 0)
+			{
+				// Removed orphelins records
+				print '<br>Some records were detected to have parent that is a child, we set them as root record for id: ';
+				print join(',',$listofidtoclean);
+			}
+
+			$totalnb+=$nb;
+		}
+		//else dol_print_error($db);
+
+		// Check and clean orphelins
+		$sql = "UPDATE ".MAIN_DB_PREFIX.$tabletocleantree;
+		$sql.= " SET ".$fieldfkparent." = 0";
+		$sql.= " WHERE ".$fieldfkparent." NOT IN (".join(',',$listofid).")";	// So we update only records linked to a non existing parent
+		dol_syslog("sql=".$sql);
+		$resql = $db->query($sql);
+		if ($resql)
+		{
+			$nb=$db->affected_rows($sql);
+			if ($nb > 0)
+			{
+				// Removed orphelins records
+				print '<br>Some orphelins were found and modified to be parent so records are visible again for id: ';
+				print join(',',$listofid);
+			}
+
+			$totalnb+=$nb;
+		}
+		//else dol_print_error($db);
+
+		print '<br>We fixed '.$totalnb.' record(s). Some records may still be corrupted. New check may be required.';
+		return $totalnb;
+	}
+}
diff --git a/htdocs/install/mysql/migration/repair.sql b/htdocs/install/mysql/migration/repair.sql
index 7a46fbd48ed..ca2fbf78997 100644
--- a/htdocs/install/mysql/migration/repair.sql
+++ b/htdocs/install/mysql/migration/repair.sql
@@ -142,3 +142,5 @@ update llx_societe set barcode = null where (rowid, barcode) in (select max_rowi
 drop table tmp_societe_double;
 
 
+UPDATE llx_projet_task SET fk_task_parent = 0 WHERE fk_task_parent = rowid
+
diff --git a/htdocs/projet/class/project.class.php b/htdocs/projet/class/project.class.php
index 719c7c5f034..277b241f259 100644
--- a/htdocs/projet/class/project.class.php
+++ b/htdocs/projet/class/project.class.php
@@ -1243,66 +1243,6 @@ class Project extends CommonObject
 	    return $result;
 	}
 
-	/**
-	 * Clean tasks not linked to an existing parent
-	 *
-	 * @return	int				Nb of records deleted
-	 */
-	function clean_orphelins()
-	{
-		$nb=0;
-
-		// There is orphelins. We clean that
-		$listofid=array();
-
-		// Get list of all id in array listofid
-		$sql='SELECT rowid FROM '.MAIN_DB_PREFIX.'projet_task';
-		$resql = $this->db->query($sql);
-		if ($resql)
-		{
-			$num = $this->db->num_rows($resql);
-			$i = 0;
-			while ($i < $num && $i < 100)
-			{
-				$obj = $this->db->fetch_object($resql);
-				$listofid[]=$obj->rowid;
-				$i++;
-			}
-		}
-		else
-		{
-			dol_print_error($this->db);
-		}
-
-		if (count($listofid))
-		{
-			print 'Code asked to check and clean orphelins.';
-
-			$sql = "UPDATE ".MAIN_DB_PREFIX."projet_task";
-			$sql.= " SET fk_task_parent = 0";
-			$sql.= " WHERE fk_task_parent NOT IN (".join(',',$listofid).")";	// So we update only records linked to a non existing parent
-
-			$resql = $this->db->query($sql);
-			if ($resql)
-			{
-				$nb=$this->db->affected_rows($sql);
-
-				if ($nb > 0)
-				{
-					// Removed orphelins records
-					print 'Some orphelins were found and modified to be parent so records are visible again: ';
-					print join(',',$listofid);
-				}
-
-				return $nb;
-			}
-			else
-			{
-				return -1;
-			}
-		}
-	}
-
 
 	 /**
 	  *    Associate element to a project
diff --git a/htdocs/projet/tasks.php b/htdocs/projet/tasks.php
index 34dc9232e81..36792d4d0ad 100644
--- a/htdocs/projet/tasks.php
+++ b/htdocs/projet/tasks.php
@@ -438,11 +438,19 @@ else
 	{
 		if ($mode=='mine')
 		{
-			if ($nboftaskshown < count($tasksrole)) $object->clean_orphelins();
+			if ($nboftaskshown < count($tasksrole))
+			{
+				include_once DOL_DOCUMENT_ROOT.'/core/lib/functions2.lib.php';
+				cleanCorruptedTree($db, 'projet_task', 'fk_task_parent');
+			}
 		}
 		else
 		{
-			if ($nboftaskshown < count($tasksarray)) $object->clean_orphelins();
+			if ($nboftaskshown < count($tasksarray))
+			{
+				include_once DOL_DOCUMENT_ROOT.'/core/lib/functions2.lib.php';
+				cleanCorruptedTree($db, 'projet_task', 'fk_task_parent');
+			}
 		}
 	}
 }
diff --git a/htdocs/projet/tasks/task.php b/htdocs/projet/tasks/task.php
index 60fdbda2c70..cab79eb7326 100644
--- a/htdocs/projet/tasks/task.php
+++ b/htdocs/projet/tasks/task.php
@@ -317,7 +317,8 @@ if ($id > 0 || ! empty($ref))
 
 			// Task parent
 			print '<tr><td>'.$langs->trans("ChildOfTask").'</td><td>';
-			print $formother->selectProjectTasks($object->fk_task_parent,$projectstatic->id, 'task_parent', $user->admin?0:1, 0);
+//			print $formother->selectProjectTasks($object->fk_task_parent, $projectstatic->id, 'task_parent', ($user->admin?0:1), 0, 0, 0, $object->id);
+			print $formother->selectProjectTasks($object->fk_task_parent, $projectstatic->id, 'task_parent', ($user->admin?0:1), 0, 0, 0, 0);
 			print '</td></tr>';
 
 			// Date start
-- 
GitLab