Schedule Tracking provides integration with Microsoft Project (.mpp) files, allowing teams to maintain “double truth” tracking: real progress for internal management and client-reported progress for contractual milestones.
The system imports the full task hierarchy from MS Project including WBS, dependencies, resources, and baseline dates.
Separate table for “double truth” progress tracking.
class AvanceCronograma(models.Model): """ La doble verdad: Real vs Cliente. Se separa de la tarea para no sobreescribir al re-importar versiones. """ tarea = models.OneToOneField(TareaCronograma, on_delete=models.CASCADE, related_name='avance') porcentaje_real = models.DecimalField(max_digits=5, decimal_places=2, default=0) porcentaje_cliente = models.DecimalField(max_digits=5, decimal_places=2, default=0) comentario = models.TextField(blank=True) fecha_actualizacion = models.DateTimeField(auto_now=True) class Meta: db_table = 'importacion_cronograma_avance'
Why Separate Table?Progress data is stored separately from task definitions to prevent overwriting real/client percentages when re-importing updated .mpp files. This preserves the “double truth” even through schedule revisions.
The system provides an endpoint to retrieve tasks in hierarchical tree format for visualization.
@login_required@require_http_methods(["GET"])def obtener_arbol_mpp(request): """ Obtiene las tareas del cronograma vigente de una OT y las formatea en una estructura jerárquica (_children) para TOAST UI Tree Grid. """ try: ot_id = request.GET.get('ot_id') version_activa = CronogramaVersion.objects.get(id_ot_id=ot_id, es_activo=True) tareas = TareaCronograma.objects.filter(version=version_activa).order_by('id_project') # Build task dictionary tareas_dict = {} for t in tareas: tareas_dict[t.uid_project] = { "wbs": t.wbs, "nombre": t.nombre, "fecha_inicio": t.fecha_inicio.strftime("%d/%m/%Y") if t.fecha_inicio else "", "fecha_fin": t.fecha_fin.strftime("%d/%m/%Y") if t.fecha_fin else "", "duracion_dias": float(t.duracion_dias), "porcentaje_mpp": float(t.porcentaje_mpp), "recursos": t.recursos, "uid_project": t.uid_project, "padre_uid": t.padre_uid, "_attributes": {"expanded": True} # Auto-expand in tree view } # Build hierarchical tree tree_data = [] for uid, tarea in tareas_dict.items(): padre_uid = tarea["padre_uid"] if padre_uid is not None and padre_uid in tareas_dict: # Add as child to parent padre = tareas_dict[padre_uid] if "_children" not in padre: padre["_children"] = [] padre["_children"].append(tarea) else: # Top-level task tree_data.append(tarea) return JsonResponse({"estatus": "ok", "data": tree_data}) except CronogramaVersion.DoesNotExist: return JsonResponse({"estatus": "error", "mensaje": "No hay cronograma activo"}) except Exception as e: import traceback traceback.print_exc() return JsonResponse({"estatus": "error", "mensaje": str(e)}, status=500)
The _children key is used by TOAST UI Tree Grid and similar components to render hierarchical task breakdowns with expand/collapse functionality.
# Activate a specific versionfrom django.db import transactionwith transaction.atomic(): # Deactivate all versions for this OT CronogramaVersion.objects.filter(id_ot_id=ot_id).update(es_activo=False) # Activate target version target_version = CronogramaVersion.objects.get(id=version_id) target_version.es_activo = True target_version.save()
Always use atomic transactions when changing active versions to prevent race conditions.
Resources are stored as comma-separated text in the recursos field:
# Parse resources from tasktask = TareaCronograma.objects.get(uid_project=123)resource_list = [r.strip() for r in task.recursos.split(',') if r.strip()]print(f"Task {task.nombre} assigned to: {', '.join(resource_list)}")# Find all tasks for a specific resourceresource_name = "John Smith"tasks_for_resource = TareaCronograma.objects.filter( version__es_activo=True, recursos__icontains=resource_name)
For advanced resource management (loading, leveling, costing), consider normalizing resources into a separate table with many-to-many relationship.
-- Find orphaned tasksSELECT uid_project, padre_uid, nombreFROM importacion_cronograma_tarea t1WHERE padre_uid IS NOT NULL AND NOT EXISTS ( SELECT 1 FROM importacion_cronograma_tarea t2 WHERE t2.uid_project = t1.padre_uid AND t2.version_id = t1.version_id );
Set orphaned tasks’ padre_uid to NULL.
Progress Data Lost After Reimport
Cause: WBS codes changed between versions.Solution: Maintain stable WBS codes, or implement migration script to match by task name:
# Match progress by task name as fallbackold_avance = AvanceCronograma.objects.get( tarea__wbs='1.2.3', tarea__version__nombre_version='Rev 01')new_tarea = TareaCronograma.objects.get( nombre=old_avance.tarea.nombre, version__nombre_version='Rev 02')AvanceCronograma.objects.create( tarea=new_tarea, porcentaje_real=old_avance.porcentaje_real, porcentaje_cliente=old_avance.porcentaje_cliente)