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.

跳到安装

来源信息

仓库
Ntizar/NtizarBrainMasterMind
最近来源活动
2026年6月26日 12:05
检测到的 SKILL.md 语言
西班牙语
星标
2
分支
0

安装方式

默认使用会先检查来源的 Prompt;你也可以切换为直接命令,或下载本地副本。

检查来源文件

决定是否安装前,请先阅读 SKILL.md,以及 SkillsMP 当前展示的配套文件。

文件资源管理器
6 个文件

正在显示 SKILL.md

SKILL.md
来源说明 · 只读预览
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.
在 GitHub 查看
这个 SKILL.md 很大,SkillsMP 这里只预览前一段内容。 在 GitHub 查看