Skip to main content

budget-comparison

Extraer, estructurar y comparar presupuestos de obra (PEMs) de construccion generados en PDFs (CYPE Arquimedes, Presto). Comparar oferta de constructora contra presupuesto base de ejecucion material. Generar informes y herramientas HTML interactivas.

Zur Installation springen

Quellinformationen

Repository
Ntizar/NtizarBrainMasterMind
Letzte Quellaktivität
26. Juni 2026 um 12:05
Erkannte Sprache von SKILL.md
Spanisch
Sterne
2
Forks
0

Installationsoptionen

Standardmäßig ist der Prompt ausgewählt, der zuerst die Quelle prüft. Sie können zu einem direkten Befehl wechseln oder eine lokale Kopie herunterladen.

Quelldateien prüfen

Lesen Sie SKILL.md und alle von SkillsMP angezeigten Begleitdateien, bevor Sie sich für eine Installation entscheiden.

Datei-Explorer
6 Dateien

SKILL.md wird angezeigt

SKILL.md
Quellanweisungen · Schreibgeschützte Vorschau
name
budget-comparison
description
Extraer, estructurar y comparar presupuestos de obra (PEMs) de construccion generados en PDFs (CYPE Arquimedes, Presto). Comparar oferta de constructora contra presupuesto base de ejecucion material. Generar informes y herramientas HTML interactivas.
version
1.1.0
tags
["presupuesto","construccion","CYPE","Presto","comparacion","PDF","obra"]
# Budget Comparison — Extraccion y comparacion de presupuestos de obra ## Cuándo usarlo - El usuario adjunta un PDF de presupuesto de obra (PEM) y quiere compararlo - El usuario pide \"comparar presupuestos\", \"detectar diferencias entre ofertas\" - Hay un presupuesto base (proyecto) y una/multiple ofertas de constructoras - Se necesita extraer datos estructurados de PDFs de construccion (300+ paginas) ## No es para - Informes institucionales/regulatorios (usar `documentos-institucionales`) - Presupuestos de software/IT (usar `pdf-to-dashboard`) - Analisis de facturas (usar `liteparse-rust-pdf-ocr`) ## PDF-to-Dashboard — Extracción general de datos estructurados (absorbido de `pdf-to-dashboard`) ### Cuándo usarlo - El usuario adjunta un PDF con datos tabulares/estructurados (presupuestos, informes técnicos, documentos de construcción) - Se necesita extraer datos numéricos y generar visualización interactiva HTML con Aurora Design System ### Pipeline completo 1. **Extracción:** pdftotext → pdfplumber (recomendado) → pdfjs-dist (fallback) 2. **Análisis de estructura:** Entender patrón de texto (capítulos, partidas, importes) 3. **Parsing estructurado:** Regex para códigos, importes españoles, tablas 4. **Generación HTML Dashboard:** Aurora Design System, Chart.js, navegación por capítulos ### Patrones específicos de presupuestos CYPE Arquímedes - Página 1: resumen mínimo, Páginas 2-N-1: detalle, Página N: resumen final con totales - Cabeceras: "Presupuesto parcial nº X NOMBRE" - Formato: `Cap-1`, `Cap-2`... (no `Cap-01`) - Normalizar claves: `offer_key = f"Cap-{cap_num.zfill(2)}"` para matching con Presto ### ⚠️ pdfjs-dist compatibility Usar `pdfjs-dist@3.11.174` — v2.x tiene API incompatible. Usar `execute_code` para scripts largos, no `terminal()`. ### ⚠️ Formatos numéricos españoles `1.234,56` → `parseFloat(str.replace(/\./g, '').replace(',', '.'))` --- ## Documentos Institucionales — Análisis de informes públicos (absorbido de `documentos-institucionales`) ### Cuándo usarlo - Informes institucionales/regulatorios públicos (CNMC, REE, MITMS, BOE, Comisión Europea) - Se necesita extracción PDF → auditoría estructurada → multi-formato (HTML resumen, LinkedIn, nota técnica) ### Diferencia con pdf-to-dashboard - `pdf-to-dashboard`: datos tabulares → dashboard HTML interactivo - `documentos-institucionales`: informe texto largo → análisis estructurado → múltiples formatos de salida --- ## Flujo de trabajo ### Paso 1: Extraer presupuesto base (referencia) ```python import pdfplumber # PDF CYPE Arquimedes — estructura fija: # Pagina 1: resumen minimo # Paginas 2-N-1: detalle de partidas # Pagina N (ultima): resumen final con totales with pdfplumber.open(pdf_path) as pdf: # Extraer resumen final (ultima pagina) page = pdf.pages[-1] text = page.extract_text() # Parsear capitulos: "1. MOVIMIENTO DE TIERRAS .............................… 873,19" pattern = r'(\d+)\.\s+(.+?)\.{2,}…?\s+([\d.,]+)' # Extraer detalle (paginas intermedias) for i in range(1, len(pdf.pages) - 1): page = pdf.pages[i] text = page.extract_text() tables = page.extract_tables() ``` ### Paso 2: Extraer oferta de constructora Los PDFs de ofertas suelen tener estructura diferente: - **Pagina 1:** Portada (datos empresa, expediente, fecha) - **Pagina 2-3:** Resumen con capitulos + totales + IVA + total contrata - **Pagina 4-N:** Detalle de partidas ```python # Resumen de oferta (pagina 2-3) page2 = pdf.pages[1] page3 = pdf.pages[2] text2 = page2.extract_text() text3 = page3.extract_text() # Pattern: "01 MOVIMIENTO DE TIERRAS 1.863,37 0,17" pattern = r'(\d{2})\s+(.+?)\s+([\d.,]+)\s+([\d.,]+)' # Totales total_match = re.search(r'TOTAL EJECUCION MATERIAL\s+([\d.,]+)', text) iva_match = re.search(r'%\s*I\.?V\.?A\.?\s+([\d.,]+)', text) contrata_match = re.search(r'TOTAL PRESUPUESTO CONTRATA\s+([\d.,]+)', text) ``` ### Paso 3: Normalizar y comparar **⚠️ Normalizacion de claves:** CYPE usa `Cap-1`, `Cap-2`... ofertas Presto usan `Cap-01`, `Cap-02`. ```python # Normalizar claves para matching def normalize_key(key): # Extraer numero de capitulo num = re.search(r'(\d+)', key) if num: return f"Cap-{num.group(1).zfill(2)}" return key # Comparar por capitulo for ref_key, ref_data in referencia['capitulos'].items(): offer_key = normalize_key(ref_key) if offer_key in oferta['capitulos']: diff = oferta['capitulos'][offer_key]['total'] - ref_data['total'] diff_pct = (diff / ref_data['total'] * 100) if ref_data['total'] > 0 else 0 else: diff = ref_data['total'] # capitulo eliminado en oferta diff_pct = -100 ``` ### Paso 4: Generar herramienta HTML Crear un HTML autocontenido con: - Tabla comparativa con filtros y ordenacion - Grafico de barras (referencia vs oferta + diferencias) - Ranking de capitulos por mayor diferencia - Detalle de partidas ## Patrones de parsing por herramienta ### CYPE Arquimedes - Cabeceras: "Presupuesto parcial nº X NOMBRE" o "CAPÍTULO X NOMBRE" - Resumen final en la ÚLTIMA pagina del PDF - Formato: `Cap-1`, `Cap-2`... - Partidas con codigos alfanumericos largos (ej: `m23E02AM010`) - Subpartidas con referencias de planos (VC.T-2.1 [P16-P15]) ### Presto - Cabeceras: "CAPÍTULO X NOMBRE" con ceros a la izquierda (01, 02...) - Formato: `Cap-01`, `Cap-02`... - Resumen en paginas 2-3 ### Presto moderno (GLAM-style) — Tablas rotas + extract_words por coordenadas X **Problema:** Algunos PDFs Presto modernos tienen tablas donde la fila del capítulo entero está en una sola celda. `extract_tables()` devuelve filas con todo el texto junto y las columnas vacías. **Solución:** Usar `extract_words()` con coordenadas X para mapear columnas: ```python with pdfplumber.open(pdf_path) as pdf: for page in pdf.pages: words = page.extract_words() # Agrupar palabras por linea Y lines = {} for w in words: y_key = round(w['top'], 0) if y_key not in lines: lines[y_key] = [] lines[y_key].append(w) for y_key in sorted(lines.keys()): line_words = sorted(lines[y_key], key=lambda w: w['x0']) # Detectar linea de capítulo (tiene "Capítulo") if 'Capítulo' not in [w['text'] for w in line_words]: continue # Columnas conocidas: # Código: x~52 | Nat: x~109 | Ud: x~129 | Resumen: x~258 # CanPres: x~409 | PrPres: x~453 | ImpPres: x~501 # Extraer código (primera palabra) code = line_words[0]['text'] # Extraer nombre (entre "Capítulo" y x~400) name_words = [w['text'] for w in line_words if w['text'] != 'Capítulo' and w['x0'] < 400] name = ' '.join(name_words) # Extrair total (ImpPres: x >= 500) imp_words = [w for w in line_words if w['x0'] >= 500 and w['text'] != '€'] if imp_words: imp_str = ''.join(w['text'] for w in imp_words) total = parse_es_amount(imp_str) ``` > **Pitfall:** Los números españoles "2.041,83" se extraen como una sola palabra. Pero números con más de 4 dígitos como "2 0.316,59" se separan en palabras distintas ("2", "0.316,59"). Hay que concatenar todas las palabras en la columna ImpPres ANTES de parsear. ### ⚡ CRÍTICO: Regex para extraer precios — el bug del split numérico **Problema:** El regex `([\d\s.,]+)\s*€` captura partes incorrectas cuando el número tiene espacios internos. Ejemplo: ``` LÍNEA: "01 Capítulo MOVIMIENTO DE TIERRAS 1,00 2.041,83 € 2.041,83 €" price_parts = [' 1,00 2.041,83 ', ' 2.041,83 '] # Si concatenas las dos últimas partes: "1,00 2.041,832.041,83" → parse error ``` **Solución (PROMOVER):** Usar el ÚLTIMO valor antes del último `€`, no concatenar partes: ```python # CORRECTO: encontrar el último € y tomar todo lo que hay antes last_euro_idx = rest.rfind('€') before_last_euro = rest[:last_euro_idx].strip() num_match = re.search(r'([\d][\d\s.,]*)$', before_last_euro) num_str = num_match.group(1).strip() clean = num_str.replace(' ', '').replace('.', '').replace(',', '.') total = float(clean) ``` > **Por qué funciona:** En Presto, la columna `ImpPres` (importe total) siempre es el último valor antes del último `€`. No necesitas concatenar partes — solo tomar el último número. > **Pitfall alternativo:** Si usas `extract_words()` por coordenadas X, los números se extraen como palabras individuales. En ese caso sí necesitas concatenar palabras adyacentes en la columna ImpPres. ## ⚠️ CRÍTICO: Validación del total extraído **Problema frecuente:** La suma de TODAS las líneas de "Capítulo" extraídas da un número MUY superior al Presupuesto General del documento. Esto ocurre porque: 1. **Padres e hijos duplicados:** Presto tiene capítulos padre (ej: `SAN.07.01`) que incluyen sus hijos (`SAN.07.01.01`, `.02`, `.03`). Si sumas todo, los duplicas. 2. **Complementos de materiales incluidos:** `CA` (Micropilotes), `EA` (Acero), `EH` (Hormigón armado) están incluidos en capítulos 03, 04, 05. No se suman aparte. 3. **Instalaciones dentro de capítulos:** ELE, FON, TEL, TER, VMC están DENTRO de los capítulos numéricos (13, 15, 14, 17, 18). No son capítulos independientes. **Procedimiento de validación (PASO OBLIGATORIO):** 1. **Siempre leer el total del documento primero:** ```python # Última página del PDF — buscar "PRESUPUESTO_P" o "PRESUPUESTO GENERAL" last_page = pdf.pages[-1] text = last_page.extract_text() # Buscar línea con "PRESUPUESTO_P" o "PRESUPUESTO GENERAL" total_match = re.search(r'PRESUPUESTO[_\s]*(?:GENERAL)?[_\s]*P?\s+[\d\s.,]+\s*€', text) documento_total = extract_total_from_line(last_page, text) ``` 2. **Extraer TODAS las líneas de "Capítulo"** (sin filtrar). 3. **Identificar padres e hijos:** - Un código es hijo si otro código en la lista empieza con `codigo + '.'` - Ej: `SAN.07.01.01` es hijo de `SAN.07.01` 4. **Para cada grupo padre-hijos, decidir cuál usar:** - Si `padre ≈ hijos` (ratio 0.9-1.1): usar padre (incluye hijos) - Si `hijos > padre * 1.1`: usar hijos (padre es sección, no total) - Si `padre < hijos < padre * 1.1`: usar padre (es resumen parcial) - **⚠️ Si `padre > hijos`: el padre puede incluir MÁS cosas no listadas** → usar padre 5. **Sumar solo los capítulos seleccionados** y comparar con `documento_total`. - Si la diferencia es < 1% → correcto. - Si no, revisar qué capítulos sobran o faltan. **⚠️ Pitfall avanzado:** Algunos padres tienen ratio ~0.6 (ej: SAN.07.01 = 20.316 € vs hijos = 12.373 €). Esto significa que el padre es un resumen PARCIAL que no incluye todos los hijos. En este caso, usar el padre (es el valor oficial del capítulo). **⚠️ Pitfall avanzado 2:** Algunos padres tienen ratio > 1.5 (ej: 25 = 3.589 € vs hijos = 36.906 €). Esto significa que el padre es un NOMBRE DE SECCIÓN, no un total. Los hijos son los capítulos reales. Usar hijos. **⚠️ Pitfall avanzado 3:** En Presto moderno, TODOS los códigos pueden ser "hijos" de algún otro código (ej: "03.04.01" no es hijo de "01" aunque contiene "01"). La función `is_child()` debe verificar `code.startswith(other + '.')` EXACTO, no solo contiene. **Patrón común en Presto/GLAM:** - Capítulos principales: `01` a `27` (obra civil, acabados, instalaciones incluidas) - SAN.07.01 puede estar fuera de los capítulos numéricos (saneamiento de red) - ELE.03.01-07 están dentro del capítulo 13 - FON.02.01.01-05 están dentro del capítulo 15 - TEL.10.01-04 están dentro del capítulo 14 - TER.01.01-05 están dentro del capítulo 17 - VMC.09.01.01-03 + VMC09.02 están dentro del capítulo 18 - CA, EA, EH son complementos de materiales (excluir) ### Herramienta recomendada **pdfplumber** (no PyMuPDF/fitz) — funciona mejor con CYPE: ```bash /opt/hermes/.venv/bin/python3 -c "import pdfplumber; print('ok')" ``` > **Pitfall:** pdfplumber necesita el venv de Hermes (`/opt/hermes/.venv/bin/python3`). No funciona con el python3 del sistema. ## ⚠️ CRÍTICO: GLAM puede ser igual al CYPE/Dmarche **Problema frecuente:** El presupuesto de GLAM (constructora) tiene los mismos importes base que el CYPE/Dmarche (proyecto básico). Esto ocurre porque GLAM usa los mismos precios base del proyecto. **Solución:** NO asumir que GLAM siempre tiene importes diferentes al CYPE. Verificar siempre con la extracción real del PDF. **Regla:** El comparador debe mostrar SIEMPRE las 3 columnas: Referencia (Dmarche/CYPE), Constructora 1 (GLAM), Constructora 2 (Trevicon/u otra). NO eliminar ninguna columna automáticamente. El usuario decide qué significa cada una.
Auf GitHub ansehen
Diese SKILL.md ist sehr gross, daher zeigt SkillsMP hier nur den ersten Abschnitt. Auf GitHub ansehen