@csrf_exemptdef importar_anexo_ot(request): """ Procesa el archivo Excel (.xlsx o .xlsm). """ if request.method == "POST": ot_id = request.POST.get('ot_id') archivo = request.FILES.get('archivo') modo_actualizacion = request.POST.get('modo_actualizacion') == 'true' if not ot_id or not archivo: return JsonResponse({'exito': False, 'tipo_aviso':'advertencia', 'detalles': 'Faltan datos (OT o Archivo)'})
The system intelligently locates the correct sheet and header row:
operaciones/views/ote.py
target_sheet = Noneheader_row_index = -1sheet_names = sorted(xls.sheet_names, key=lambda x: 0 if ('ANEXO C' in x.upper() or 'RECURSOS PROGRAMADOS' in x.upper()) else 1)logs_busqueda = []for sheet in sheet_names: try: df_temp = pd.read_excel(xls, sheet_name=sheet, header=None, nrows=100) found = False for idx, row in df_temp.iterrows(): row_str = [str(x).strip().upper() for x in row.values] match_partida = 'PARTIDA' in row_str match_concepto = 'CONCEPTO' in row_str match_unidad = 'UNIDAD' in row_str match_volumen = 'VOLUMEN PTE' in row_str match_precio = ('P.U. M.N.' in row_str) or ('P.U.M.N.' in row_str) if match_partida and match_concepto and match_unidad and match_volumen and match_precio: header_row_index = idx target_sheet = sheet found = True if 'ANEXO' not in row_str: logs_busqueda.append(f"[{sheet}]: Se encontraron cabeceras pero falta columna 'ANEXO'.") break
1
Sort Sheets by Priority
Sheets with “ANEXO C” or “RECURSOS PROGRAMADOS” in the name are checked first
2
Search for Header Row
Scan first 100 rows looking for required column names
If validation errors are found, an Excel error report is generated:
operaciones/views/ote.py
if errores_validacion: df_errores = pd.DataFrame(errores_validacion) cols_limpias = [c for c in df_errores.columns if "UNNAMED" not in str(c).upper()] df_errores = df_errores[cols_limpias] cols_ordenadas = ['OBSERVACIONES_SISTEMA'] + [c for c in cols_limpias if c != 'OBSERVACIONES_SISTEMA'] cols_finales = [c for c in cols_ordenadas if c in df_errores.columns] df_errores = df_errores[cols_finales] output = io.BytesIO() with pd.ExcelWriter(output, engine='openpyxl') as writer: df_errores.to_excel(writer, index=False, sheet_name='Errores Importacion') worksheet = writer.sheets['Errores Importacion'] if 'A' in worksheet.column_dimensions: worksheet.column_dimensions['A'].width = 80
When errors are found, the response includes the error Excel file as an attachment with header X-Validation-Error: True. No database changes are committed.
if hay_programacion_diaria: objs_proyeccion = [] for partida_db, data_original in zip(partidas_creadas, partidas_validas): schedule = data_original.get('schedule', []) for item_prog in schedule: objs_proyeccion.append(PartidaProyectada( ot=ot, partida_anexo=partida_db, fecha=item_prog['fecha'], volumen_programado=item_prog['volumen'] )) if objs_proyeccion: PartidaProyectada.objects.bulk_create(objs_proyeccion)
Without Date Columns
Only total projected volume per line item is imported
With Date Columns
Daily volume breakdown is created for detailed production planning
Update mode allows refreshing annexes without losing existing data:
operaciones/views/ote.py
if modo_actualizacion: if not importacion_activa: return JsonResponse({ 'exito': False, 'tipo_aviso': 'advertencia', 'detalles': 'No se puede actualizar: No existe un Anexo cargado previamente.' }) existentes_qs = PartidaAnexoImportada.objects.filter(importacion_anexo=importacion_activa) mapa_db = {p.id_partida.strip().upper(): p for p in existentes_qs} objs_nuevos = [] objs_actualizar_anexo = [] lista_para_proyectar = [] for data_excel in partidas_validas: codigo = data_excel['codigo'].strip().upper() if codigo in mapa_db: partida_existente = mapa_db[codigo] if partida_existente.anexo != data_excel['anexo_row']: partida_existente.anexo = data_excel['anexo_row'] objs_actualizar_anexo.append(partida_existente)
1
Check Existing Import
Verify an active import exists for the work order
2
Match Line Items by Code
Compare Excel data against existing records by id_partida
3
Update Annexes Only
Only update the anexo field for matching items
4
Add New Items
Create records for any new line items not previously imported
@csrf_exempt@login_requireddef importar_mpp_ot(request): """ Procesa un archivo .mpp """ if request.method != 'POST': return JsonResponse({'exito': False, 'tipo_aviso': 'advertencia', 'detalles': 'Método no permitido'}) ot_id = request.POST.get('ot_id') archivo = request.FILES.get('archivo') if not ot_id or not archivo: return JsonResponse({'exito': False, 'tipo_aviso': 'advertencia', 'detalles': 'Faltan datos (OT o Archivo)'}) if not archivo.name.lower().endswith('.mpp'): return JsonResponse({'exito': False, 'tipo_aviso': 'advertencia', 'detalles': 'El archivo debe tener extensión .mpp'})
MPP import requires jpype1 and mpxj Python libraries. The system will return an error if these are not installed.
The system automatically detects custom fields for weight (ponderador) and progress:
operaciones/views/ote.py
columna_pond_oficial = Nonecolumna_avance_oficial = Nonefor cf in project.getCustomFields(): alias_original = str(cf.getAlias() or '') alias = alias_original.strip().lower() if alias in ['pond.', 'pond', 'ponderador', 'peso']: columna_pond_oficial = cf.getFieldType() breakfor cf in project.getCustomFields(): alias_original = str(cf.getAlias() or '') alias = alias_original.strip().lower() if alias in ['avance real', '% avance', 'fisico', 'físico']: columna_avance_oficial = cf.getFieldType() break
Ponderador (Weight) Field
Looks for custom fields named: “pond.”, “pond”, “ponderador”, “peso” or containing “pond”/“peso”
Avance (Progress) Field
Looks for: “avance real”, ”% avance”, “fisico”, “físico” or containing these terms
tasks_to_create = []dependencies_to_create = []for task in project.getTasks(): if task.getID() is None: continue padre = task.getParentTask() padre_uid = padre.getUniqueID() if padre and padre.getID() is not None else None recursos_str = '' assignments = task.getResourceAssignments() if assignments: nombres = [] for assignment in assignments: res = assignment.getResource() if res: nombres.append(str(res.getName())) recursos_str = ', '.join(nombres) duracion = task.getDuration() duracion_dias = float(duracion.getDuration()) if duracion else 0.0 tasks_to_create.append(TareaCronograma( version=version, uid_project=task.getUniqueID(), id_project=task.getID(), wbs=str(task.getWBS() or ''), nivel_esquema=task.getOutlineLevel() or 0, es_resumen=task.hasChildTasks(), padre_uid=padre_uid, nombre=str(task.getName() or ''), fecha_inicio=java_date_to_py(task.getStart()), fecha_fin=java_date_to_py(task.getFinish()), duracion_dias=duracion_dias, porcentaje_mpp=ponderador, porcentaje_completado=avance_real, recursos=recursos_str, ))
Tasks and dependencies are inserted in batches of 2000 records for optimal performance. The entire import is wrapped in a transaction to ensure data consistency.