# Refactoring Struttura Output e Organizzazione Progetto

> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.

**Goal:** Riscrivere il parser del capitolato per conservare tutte le righe originali, riscrivere il renderer Excel per replicare fedelmente la struttura del capitolato nell'output, e organizzare il codice in più moduli separati con commenti in italiano.

**Architecture:** Il file monolitico `ComparativoPreventivi.py` viene suddiviso in tre moduli:
- `core/lettore.py` — lettura Excel (capitolato e preventivi)
- `core/generatore.py` — scrittura Excel di output
- `interfaccia.py` — GUI tkinter
- `avvia.py` — entry point (sostituisce il `if __name__ == "__main__"` del vecchio file)

Il parser del capitolato abbandona la distinzione `voce/sezione` e conserva **ogni riga** con il suo tipo originale. Il renderer scrive ogni riga così com'è, aggiungendo le colonne dei prezzi solo dove c'è U.M. + quantità totale (colonna C valorizzata).

**Tech Stack:** Python 3.9+, tkinter (GUI), openpyxl (lettura/scrittura Excel)

---

## Struttura cartelle target

```
comparativo_cme/
├── avvia.py                   ← entry point (doppio click per avviare)
├── interfaccia.py             ← GUI tkinter (classe App, PreventivRow, ScrollableFrame)
├── core/
│   ├── __init__.py            ← vuoto
│   ├── config.py              ← costanti di configurazione (colori, soglie)
│   ├── lettore.py             ← leggi_capitolato, leggi_preventivo, funzioni helper
│   └── generatore.py          ← genera_comparativo, _genera_foglio_*, _genera_foglio_riepilogo
├── docs/
│   └── plans/
│       └── ...
├── FILES/
│   └── ...
└── MEMORIA_PROGETTO_ComparativoPreventivi.md
```

---

## Nuova struttura dati — righe del capitolato

`leggi_capitolato()` restituisce una lista piatta di dict, uno per ogni riga del file:

```python
# Riga vuota
{"tipo": "vuota"}

# Riga con solo testo in B (categoria, descrizione estesa, note)
{"tipo": "testo", "col_b": "Allestimento cantiere"}

# Riga con numero in A (inizio voce)
{"tipo": "numero", "num": "1", "col_b": "Allestimento cantiere"}

# Riga con B + D (sub-misura parziale, senza C)
{"tipo": "sub", "col_b": "facciata nord", "col_d": 165.88}

# Riga con C (U.M.) + D (quantità totale) — QUESTA riga riceve i prezzi imprese
{"tipo": "dati", "col_c": "mq", "col_d": 1985.984, "num_voce": "4"}
```

`num_voce` nelle righe `dati` è il numero della voce a cui appartengono (tenuto in memoria durante la scansione).

---

## Task 1: Creare la struttura di cartelle e i moduli vuoti

**Files:**
- Crea: `core/__init__.py`
- Crea: `core/config.py`
- Crea: `core/lettore.py`
- Crea: `core/generatore.py`
- Crea: `interfaccia.py`
- Crea: `avvia.py`

**Step 1: Crea la cartella `core/`**

```bash
mkdir -p "/Users/paolo/Server/Siti Web/_private/comparativo_cme/core"
```

**Step 2: Crea `core/__init__.py` vuoto**

Contenuto: file vuoto (solo commento)

```python
# Pacchetto core — logica Excel
```

**Step 3: Crea `core/config.py` con tutte le costanti**

Sposta qui le costanti di configurazione. Ogni costante deve avere un commento in italiano.

```python
# ─────────────────────────────────────────────────────────────────
# CONFIGURAZIONE — parametri modificabili
# ─────────────────────────────────────────────────────────────────

# Soglia percentuale per evidenziare prezzi anomali (troppo alti o troppo bassi)
SOGLIA_ANOMALIA_PERC = 20

# Nome del font usato nell'Excel di output
FONT_NAME = "Arial"

# ── Colori sfondo celle (codici HEX senza '#') ──
COL_HEADER_BG    = "1F4E79"   # blu scuro — intestazioni principali
COL_SECTION_BG   = "2E75B6"   # blu medio — intestazioni sezioni/imprese
COL_ANOMALO_ALTO = "FFCCCC"   # rosso chiaro — prezzo troppo alto
COL_ANOMALO_BASSO= "CCFFCC"   # verde chiaro — prezzo troppo basso
COL_MEDIA_BG     = "E2EFDA"   # verde pallido — colonna media
COL_MANCANTE_BG  = "FFF2CC"   # giallo — voce senza prezzo
```

**Step 4: Verifica che la struttura esiste**

```bash
ls "/Users/paolo/Server/Siti Web/_private/comparativo_cme/core/"
```

Atteso: `__init__.py  config.py`

---

## Task 2: Riscrivere `leggi_capitolato` in `core/lettore.py`

**Files:**
- Crea/Modifica: `core/lettore.py`

**Descrizione:** Il nuovo `leggi_capitolato` non filtra le righe — le conserva tutte con il loro tipo originale. Tiene in memoria `num_voce_corrente` per annotare le righe `dati`.

**Step 1: Scrivi `core/lettore.py` con le funzioni di lettura**

Contenuto completo del file:

```python
"""
Modulo lettore — Lettura dei file Excel del capitolato e dei preventivi imprese.

Tutte le funzioni leggono i file .xlsx/.xls con openpyxl e restituiscono
strutture dati Python standard (liste di dizionari).
"""

from openpyxl import load_workbook


# ─────────────────────────────────────────────────────────────────
#  HELPER PRIVATI
# ─────────────────────────────────────────────────────────────────

def _trova_riga_hash(ws):
    """
    Cerca la riga di intestazione della tabella cercando '#' in colonna A.
    Questa riga separa l'intestazione dello studio di architettura
    dalla tabella vera e propria del computo.

    Restituisce il numero di riga (1-based) oppure None se non trovato.
    """
    for riga in ws.iter_rows(max_col=1):
        valore = riga[0].value
        if valore is not None and str(valore).strip() == "#":
            return riga[0].row
    return None


def estrai_firma_intestazione(capitolato_path):
    """
    Legge la riga con '#' in colonna A dal capitolato e restituisce
    i token delle colonne A-F come 'firma' per il riconoscimento
    della riga di intestazione nei file delle imprese.

    Usato da trova_riga_intestazione() per trovare da dove iniziano
    i dati nei preventivi che hanno righe extra in testa.

    Restituisce una lista di stringhe (token in minuscolo).
    """
    wb = load_workbook(capitolato_path, data_only=True)
    ws = wb.active
    firma = []
    riga_hash = _trova_riga_hash(ws)
    if riga_hash:
        for riga in ws.iter_rows(min_row=riga_hash, max_row=riga_hash,
                                  max_col=6, values_only=True):
            for valore in riga:
                token = str(valore).strip().lower() if valore else ""
                firma.append(token)
    wb.close()
    return firma  # es. ['#', 'descrizione delle opere', 'u.m.', ...]


def trova_riga_intestazione(ws, firma_capitolato, soglia_match=2):
    """
    Trova la riga di intestazione della tabella in un worksheet di un preventivo.

    Strategia:
    1. Prima cerca '#' in colonna A (struttura identica al capitolato)
    2. In fallback: confronto testuale con la firma del capitolato
       (almeno soglia_match token devono corrispondere)

    Match unidirezionale: il token del capitolato deve essere contenuto
    nel valore della cella (non viceversa, per evitare falsi positivi).

    Restituisce il numero di riga (1-based) oppure None.
    """
    # Tentativo 1: cerca '#' in colonna A
    for riga in ws.iter_rows(max_col=1):
        if riga[0].value is not None and str(riga[0].value).strip() == "#":
            return riga[0].row

    # Fallback: confronto testuale con la firma
    for riga in ws.iter_rows(max_col=6):
        conteggio_match = 0
        for idx, cella in enumerate(riga):
            if idx >= len(firma_capitolato):
                break
            token_firma = firma_capitolato[idx]
            if not token_firma:
                continue
            valore_cella = str(cella.value).strip().lower() if cella.value else ""
            if token_firma in valore_cella:
                conteggio_match += 1
        if conteggio_match >= soglia_match:
            return riga[0].row

    return None


# ─────────────────────────────────────────────────────────────────
#  LETTURA CAPITOLATO
# ─────────────────────────────────────────────────────────────────

def leggi_capitolato(filepath):
    """
    Legge il capitolato di riferimento e restituisce una lista piatta
    con TUTTE le righe originali del file, ognuna tipizzata.

    Struttura multi-riga per voce nel file Excel:
    - Riga intestazione: col A = '#'  (marca inizio tabella)
    - Riga numero-voce: col A = numero intero, col B = titolo breve
    - Righe testo: col A = None, col B = descrizione/categoria/note
    - Righe sub-misura: col A = None, col B = etichetta, col D = valore parziale
    - Riga dati: col A = None, col C = U.M., col D = quantità totale

    Tipi di riga restituiti:
      {"tipo": "vuota"}
      {"tipo": "testo",  "col_b": str}
      {"tipo": "numero", "num": str, "col_b": str}
      {"tipo": "sub",    "col_b": str, "col_d": float|None}
      {"tipo": "dati",   "col_c": str, "col_d": float, "num_voce": str|None}

    Il campo "num_voce" nelle righe "dati" contiene il numero della voce
    a cui appartiene quella riga (tenuto in memoria durante la scansione).
    """
    wb = load_workbook(filepath, data_only=True)
    ws = wb.active

    # Trova la riga con '#' (inizio della tabella vera)
    riga_intestazione = _trova_riga_hash(ws)
    if riga_intestazione is None:
        riga_intestazione = 1  # fallback: parti dall'inizio

    righe = []
    num_voce_corrente = None  # numero voce attualmente in elaborazione

    for riga in ws.iter_rows(min_row=riga_intestazione + 1, values_only=True):
        # Gestisce righe con meno di 4 colonne
        col_a = riga[0] if len(riga) > 0 else None
        col_b = riga[1] if len(riga) > 1 else None
        col_c = riga[2] if len(riga) > 2 else None
        col_d = riga[3] if len(riga) > 3 else None

        # ── Riga completamente vuota ──
        if col_a is None and col_b is None and col_c is None and col_d is None:
            righe.append({"tipo": "vuota"})
            continue

        num_str = str(col_a).strip() if col_a is not None else ""

        # ── Riga con numero in colonna A → inizio nuova voce ──
        if num_str:
            num_voce_corrente = num_str
            righe.append({
                "tipo":  "numero",
                "num":   num_str,
                "col_b": str(col_b).strip() if col_b else "",
            })

        # ── Riga con U.M. in colonna C → riga dati (qta totale + prezzi imprese) ──
        elif col_c is not None:
            try:
                qta = float(col_d) if col_d is not None else 0.0
            except (ValueError, TypeError):
                qta = 0.0
            righe.append({
                "tipo":      "dati",
                "col_c":     str(col_c).strip(),
                "col_d":     qta,
                "num_voce":  num_voce_corrente,
            })

        # ── Riga con B + D (sub-misura parziale, senza C) ──
        elif col_b is not None and col_d is not None:
            try:
                val_d = float(col_d)
            except (ValueError, TypeError):
                val_d = None
            righe.append({
                "tipo":  "sub",
                "col_b": str(col_b).strip(),
                "col_d": val_d,
            })

        # ── Riga con solo testo in B (categoria, descrizione estesa, note) ──
        elif col_b is not None:
            righe.append({
                "tipo":  "testo",
                "col_b": str(col_b).strip(),
            })

    wb.close()
    return righe


# ─────────────────────────────────────────────────────────────────
#  LETTURA PREVENTIVO IMPRESA
# ─────────────────────────────────────────────────────────────────

def leggi_preventivo(filepath, firma_capitolato, numeri_voce_capitolato):
    """
    Legge un preventivo Excel compilato da un'impresa.

    Struttura attesa: identica al capitolato (multi-riga per voce).
    Il prezzo unitario si trova nella colonna E della riga dati
    (quella con U.M. in colonna C e quantità in colonna D).

    Gestisce:
    - Righe extra in testa: trova la riga di intestazione con '#'
      oppure tramite confronto testuale con la firma del capitolato
    - Note extra intermedie: voci con numero non presente nel
      capitolato vengono raccolte come note separate

    Args:
        filepath: percorso del file Excel dell'impresa
        firma_capitolato: lista token da estrai_firma_intestazione()
        numeri_voce_capitolato: set di numeri voce validi del capitolato

    Returns:
        (voci, note_extra)
        voci: dict{num_voce: prezzo_float_o_None}
        note_extra: list[{"dopo_num": str|None, "testo": str}]
    """
    wb = load_workbook(filepath, data_only=True)
    ws = wb.active

    # Trova la riga di intestazione (con '#' o via firma)
    riga_intestazione = trova_riga_intestazione(ws, firma_capitolato)
    if riga_intestazione is None:
        riga_intestazione = 2  # fallback

    voci = {}
    note_extra = []
    ultimo_num_capitolato = None  # ultimo numero voce confermato nel capitolato
    num_corrente = None            # numero voce in costruzione (ancora senza prezzo)
    num_corrente_in_cap = False    # True se num_corrente è nel capitolato

    for riga in ws.iter_rows(min_row=riga_intestazione + 1, values_only=True):
        if not riga or len(riga) < 2:
            continue

        col_a = riga[0]
        col_b = riga[1] if len(riga) > 1 else None
        col_c = riga[2] if len(riga) > 2 else None
        col_e = riga[4] if len(riga) > 4 else None

        # Salta righe completamente vuote
        if col_a is None and col_b is None and col_c is None:
            continue

        num_str = str(col_a).strip() if col_a is not None else ""

        if num_str:
            # Nuova riga con numero in colonna A
            # Se la voce precedente era del capitolato ma non aveva prezzo,
            # la registra come None (voce non prezzata)
            if num_corrente is not None and num_corrente_in_cap:
                if num_corrente not in voci:
                    voci[num_corrente] = None

            num_corrente = num_str
            num_corrente_in_cap = (num_str in numeri_voce_capitolato)

            if num_corrente_in_cap:
                ultimo_num_capitolato = num_str
            else:
                # Numero non nel capitolato: è una nota extra dell'impresa
                parti = [p for p in [str(col_a).strip(),
                                     str(col_b).strip() if col_b else ""] if p]
                testo = " — ".join(parti) if parti else num_str
                note_extra.append({
                    "dopo_num": ultimo_num_capitolato,
                    "testo":    testo,
                })

        elif col_c is not None:
            # Riga con U.M. in colonna C → il prezzo unitario è in colonna E
            if num_corrente_in_cap and num_corrente is not None:
                try:
                    prezzo = float(col_e) if col_e is not None else None
                except (ValueError, TypeError):
                    prezzo = None
                voci[num_corrente] = prezzo
                # Voce completata: resetta lo stato
                num_corrente = None
                num_corrente_in_cap = False

    # Ultima voce senza riga dati
    if num_corrente is not None and num_corrente_in_cap:
        if num_corrente not in voci:
            voci[num_corrente] = None

    wb.close()
    return voci, note_extra
```

**Step 2: Verifica sintattica del file**

```bash
/usr/bin/python3 -c "import sys; sys.path.insert(0, '/Users/paolo/Server/Siti Web/_private/comparativo_cme'); from core.lettore import leggi_capitolato, estrai_firma_intestazione; print('OK')"
```

Atteso: `OK`

**Step 3: Test funzionale rapido**

```bash
/usr/bin/python3 - <<'EOF'
import sys
sys.path.insert(0, '/Users/paolo/Server/Siti Web/_private/comparativo_cme')
from core.lettore import leggi_capitolato, estrai_firma_intestazione, leggi_preventivo

cap = 'FILES/Capitolato BASE VUOTO.xlsx'
struttura = leggi_capitolato(cap)
tipi = {}
for r in struttura:
    tipi[r['tipo']] = tipi.get(r['tipo'], 0) + 1
print("Tipi di riga:", tipi)

numeri = {r['num'] for r in struttura if r['tipo'] == 'numero'}
print(f"Voci (righe 'numero'): {len(numeri)}")

firma = estrai_firma_intestazione(cap)
print(f"Firma: {firma}")

voci, note = leggi_preventivo('FILES/brc.xlsx', firma, numeri)
prezzate = sum(1 for v in voci.values() if v is not None)
print(f"BRC: {prezzate} voci prezzate, {len(note)} note extra")
EOF
```

Atteso:
```
Tipi di riga: {'numero': 289, 'dati': ~289, 'testo': ~391, 'sub': ~?, 'vuota': ~?}
Voci (righe 'numero'): 289
Firma: ['#', 'descrizione delle opere', 'u.m.', ...]
BRC: 280 voci prezzate, 19 note extra
```

---

## Task 3: Riscrivere il generatore in `core/generatore.py`

**Files:**
- Crea: `core/generatore.py`

**Descrizione:** Il nuovo `_genera_foglio_comparativo` itera su tutte le righe della struttura e le scrive nell'output fedelmente. Solo le righe di tipo `dati` ricevono le colonne dei prezzi (PU imprese, media, totali).

La funzione `_genera_foglio_riepilogo` è aggiornata per usare le righe `dati` invece delle righe `voce`.

**Step 1: Scrivi `core/generatore.py`**

```python
"""
Modulo generatore — Scrittura del file Excel di output (comparativo e riepilogo).

La funzione principale è genera_comparativo(), che coordina la lettura
dei file e la scrittura dell'output.
"""

import os
from datetime import datetime

from openpyxl import Workbook
from openpyxl.styles import Font, PatternFill, Alignment, Border, Side
from openpyxl.utils import get_column_letter

from core.config import (
    SOGLIA_ANOMALIA_PERC, FONT_NAME,
    COL_HEADER_BG, COL_SECTION_BG,
    COL_ANOMALO_ALTO, COL_ANOMALO_BASSO,
    COL_MEDIA_BG, COL_MANCANTE_BG,
)
from core.lettore import (
    leggi_capitolato, estrai_firma_intestazione, leggi_preventivo
)


# ─────────────────────────────────────────────────────────────────
#  FUNZIONE PRINCIPALE
# ─────────────────────────────────────────────────────────────────

def genera_comparativo(capitolato_path, preventivi_paths, nomi_imprese,
                       output_path, log_fn=None):
    """
    Genera il file Excel comparativo delle offerte.

    Args:
        capitolato_path:  path del file Excel capitolato di riferimento
        preventivi_paths: lista di path dei file Excel delle imprese
        nomi_imprese:     lista nomi imprese (stesso ordine di preventivi_paths)
        output_path:      path del file Excel di output da creare
        log_fn:           funzione di log opzionale, riceve una stringa

    Returns:
        output_path se tutto è andato bene
    """
    def log(msg):
        if log_fn:
            log_fn(msg)

    # ── Lettura capitolato ──
    log("📋 Lettura capitolato...")
    struttura = leggi_capitolato(capitolato_path)

    # Estrae numeri voce (righe di tipo 'numero') per confronto con preventivi
    numeri_voce = {r["num"] for r in struttura if r["tipo"] == "numero"}
    # Estrae righe dati per calcolo medie (hanno num_voce valorizzato)
    righe_dati = [r for r in struttura if r["tipo"] == "dati" and r.get("num_voce")]
    log(f"   → {len(numeri_voce)} voci, {len(righe_dati)} righe dati")

    # ── Estrai firma intestazione per riconoscimento nei preventivi ──
    log("🔍 Estrazione firma intestazione...")
    firma = estrai_firma_intestazione(capitolato_path)

    # ── Lettura preventivi imprese ──
    imprese = []
    for path, nome in zip(preventivi_paths, nomi_imprese):
        nome_file = os.path.basename(path)
        log(f"🏗  Lettura: {nome_file}")
        voci, note_extra = leggi_preventivo(path, firma, numeri_voce)
        imprese.append({
            "nome":       nome,
            "voci":       voci,
            "note_extra": note_extra,
            "file":       path,
        })
        prezzate = sum(1 for v in voci.values() if v is not None)
        mancanti = len(numeri_voce) - prezzate
        log(f"   → {nome}: {prezzate} voci prezzate, "
            f"{mancanti} mancanti, {len(note_extra)} note extra")

    n_imprese = len(imprese)

    # ── Calcolo medie per voce (esclude None e 0) ──
    medie = {}
    for r in righe_dati:
        num = r["num_voce"]
        if num in medie:
            continue  # usa solo la prima riga dati per ogni voce
        prezzi_validi = [
            imp["voci"].get(num)
            for imp in imprese
            if imp["voci"].get(num) is not None and imp["voci"].get(num) > 0
        ]
        medie[num] = (sum(prezzi_validi) / len(prezzi_validi)
                      if prezzi_validi else None)

    # ── Scrittura Excel ──
    log("📊 Generazione Excel...")
    wb_out = Workbook()
    ws_comp = wb_out.active
    ws_comp.title = "Comparativo"
    _genera_foglio_comparativo(ws_comp, struttura, imprese, medie, n_imprese)

    ws_riepilogo = wb_out.create_sheet("Riepilogo")
    _genera_foglio_riepilogo(ws_riepilogo, struttura, imprese, medie, n_imprese)

    wb_out.save(output_path)
    log(f"✅ File salvato: {os.path.basename(output_path)}")
    return output_path


# ─────────────────────────────────────────────────────────────────
#  FOGLIO COMPARATIVO
# ─────────────────────────────────────────────────────────────────

def _genera_foglio_comparativo(ws, struttura, imprese, medie, n_imprese):
    """
    Scrive il foglio 'Comparativo' dell'Excel di output.

    Layout colonne:
      A | B | C | D  ←  identiche al capitolato (N°/testo, Desc, U.M., Q.tà)
      E..E+N          ←  prezzi unitari imprese (solo sulle righe 'dati')
      E+N+1           ←  media P.U.
      E+N+2..E+2N+1   ←  totali imprese (formula = D * PU)
      E+2N+2          ←  note anomalie

    I valori nelle colonne A-D vengono copiati fedelmente dal capitolato.
    I prezzi compaiono solo sulle righe di tipo 'dati' (dove c'è U.M. + qta).
    """
    # ── Helper stile ──
    bordo_sottile  = Side(style="thin",   color="BFBFBF")
    bordo_medio    = Side(style="medium", color="2E75B6")
    b  = Border(left=bordo_sottile,  right=bordo_sottile,
                top=bordo_sottile,   bottom=bordo_sottile)
    bs = Border(left=bordo_medio,    right=bordo_medio,
                top=bordo_medio,     bottom=bordo_medio)

    def sfondo(colore):
        return PatternFill("solid", start_color=colore)

    def allineamento(orizzontale="center", verticale="center",
                     a_capo=False, rientro=0):
        return Alignment(horizontal=orizzontale, vertical=verticale,
                         wrap_text=a_capo, indent=rientro)

    # ── Calcolo indici di colonna ──
    # A=1, B=2, C=3, D=4, poi le colonne dinamiche
    col_pu_inizio  = 5                      # prima colonna PU imprese
    col_media      = 5 + n_imprese          # colonna media P.U.
    col_tot_inizio = col_media + 1          # prima colonna totali
    col_note       = col_tot_inizio + n_imprese  # colonna note anomalie
    ultima_col     = get_column_letter(col_note)

    # ── Larghezze colonne ──
    ws.column_dimensions["A"].width = 8
    ws.column_dimensions["B"].width = 52
    ws.column_dimensions["C"].width = 9
    ws.column_dimensions["D"].width = 11
    for i in range(n_imprese):
        ws.column_dimensions[get_column_letter(col_pu_inizio + i)].width = 14
    ws.column_dimensions[get_column_letter(col_media)].width = 14
    for i in range(n_imprese):
        ws.column_dimensions[get_column_letter(col_tot_inizio + i)].width = 14
    ws.column_dimensions[get_column_letter(col_note)].width = 32

    # ── Riga 1: Titolo ──
    ws.row_dimensions[1].height = 28
    ws.merge_cells(f"A1:{ultima_col}1")
    c = ws["A1"]
    c.value = f"COMPARATIVO OFFERTE — {datetime.now().strftime('%d/%m/%Y %H:%M')}"
    c.font      = Font(name=FONT_NAME, bold=True, size=12, color="FFFFFF")
    c.fill      = sfondo(COL_HEADER_BG)
    c.alignment = allineamento("left", rientro=2)

    # ── Riga 2: Legenda colori ──
    ws.row_dimensions[2].height = 16
    ws.merge_cells(f"A2:{ultima_col}2")
    c = ws["A2"]
    c.value = (f"Soglia anomalie: ±{SOGLIA_ANOMALIA_PERC}%  |  "
               "ROSSO = prezzo alto   VERDE = prezzo basso   "
               "GIALLO = voce mancante (usata media)")
    c.font      = Font(name=FONT_NAME, size=8, italic=True, color="404040")
    c.fill      = sfondo("D6E4F0")
    c.alignment = allineamento("left", rientro=2)

    # ── Riga 3: Raggruppamenti colonne ──
    ws.row_dimensions[3].height = 18
    for col in range(1, 5):
        ws.cell(row=3, column=col).fill   = sfondo(COL_HEADER_BG)
        ws.cell(row=3, column=col).border = b

    col_pu_fine  = col_pu_inizio + n_imprese - 1
    col_tot_fine = col_tot_inizio + n_imprese - 1
    ws.merge_cells(f"{get_column_letter(col_pu_inizio)}3:"
                   f"{get_column_letter(col_pu_fine)}3")
    c = ws.cell(row=3, column=col_pu_inizio, value="PREZZI UNITARI (€)")
    c.font      = Font(name=FONT_NAME, bold=True, size=9, color="FFFFFF")
    c.fill      = sfondo(COL_SECTION_BG)
    c.alignment = allineamento()
    for col in range(col_pu_inizio, col_pu_fine + 1):
        ws.cell(row=3, column=col).border = b

    c = ws.cell(row=3, column=col_media, value="")
    c.fill   = sfondo("375623")
    c.border = b

    ws.merge_cells(f"{get_column_letter(col_tot_inizio)}3:"
                   f"{get_column_letter(col_tot_fine)}3")
    c = ws.cell(row=3, column=col_tot_inizio, value="TOTALI (€)")
    c.font      = Font(name=FONT_NAME, bold=True, size=9, color="FFFFFF")
    c.fill      = sfondo(COL_SECTION_BG)
    c.alignment = allineamento()
    for col in range(col_tot_inizio, col_tot_fine + 1):
        ws.cell(row=3, column=col).border = b

    c = ws.cell(row=3, column=col_note, value="")
    c.fill   = sfondo(COL_HEADER_BG)
    c.border = b

    # ── Riga 4: Intestazioni colonne ──
    ws.row_dimensions[4].height = 30
    for col, intestazione in enumerate(["N°", "DESCRIZIONE LAVORAZIONE",
                                        "U.M.", "QUANTITÀ"], 1):
        c = ws.cell(row=4, column=col, value=intestazione)
        c.font      = Font(name=FONT_NAME, bold=True, size=9, color="FFFFFF")
        c.fill      = sfondo(COL_HEADER_BG)
        c.alignment = allineamento(a_capo=True)
        c.border    = b

    for i, imp in enumerate(imprese):
        c = ws.cell(row=4, column=col_pu_inizio + i, value=imp["nome"].upper())
        c.font      = Font(name=FONT_NAME, bold=True, size=9, color="FFFFFF")
        c.fill      = sfondo(COL_SECTION_BG)
        c.alignment = allineamento(a_capo=True)
        c.border    = b

    c = ws.cell(row=4, column=col_media, value="MEDIA P.U.")
    c.font      = Font(name=FONT_NAME, bold=True, size=9, color="1F4E79")
    c.fill      = sfondo(COL_MEDIA_BG)
    c.alignment = allineamento(a_capo=True)
    c.border    = b

    for i, imp in enumerate(imprese):
        c = ws.cell(row=4, column=col_tot_inizio + i, value=imp["nome"].upper())
        c.font      = Font(name=FONT_NAME, bold=True, size=9, color="FFFFFF")
        c.fill      = sfondo(COL_SECTION_BG)
        c.alignment = allineamento(a_capo=True)
        c.border    = b

    c = ws.cell(row=4, column=col_note, value="NOTE ANOMALIE")
    c.font      = Font(name=FONT_NAME, bold=True, size=9, color="FFFFFF")
    c.fill      = sfondo(COL_HEADER_BG)
    c.alignment = allineamento(a_capo=True)
    c.border    = b

    ws.freeze_panes = "A5"

    # ── Variabili di stato per scorrere le righe ──
    riga_corrente = 5           # riga corrente nel foglio Excel
    righe_totali  = []          # lista righe con totali per la somma finale

    # Indice note extra imprese: {num_voce: {idx_impresa: [testi]}}
    note_per_voce = {}
    for idx_imp, imp in enumerate(imprese):
        for nota in imp.get("note_extra", []):
            dopo = nota["dopo_num"]
            if dopo not in note_per_voce:
                note_per_voce[dopo] = {}
            if idx_imp not in note_per_voce[dopo]:
                note_per_voce[dopo][idx_imp] = []
            note_per_voce[dopo][idx_imp].append(nota["testo"])

    # ── Scrittura righe ──
    for riga in struttura:
        tipo = riga["tipo"]

        # ── Riga vuota ──
        if tipo == "vuota":
            ws.row_dimensions[riga_corrente].height = 8
            riga_corrente += 1
            continue

        # ── Riga con solo testo in B (categoria, descrizione estesa, note) ──
        if tipo == "testo":
            ws.row_dimensions[riga_corrente].height = 14
            c = ws.cell(row=riga_corrente, column=2, value=riga["col_b"])
            c.font      = Font(name=FONT_NAME, size=9, italic=True, color="404040")
            c.alignment = Alignment(horizontal="left", vertical="center",
                                    wrap_text=True)
            riga_corrente += 1
            continue

        # ── Riga numero-voce (inizio di una voce) ──
        if tipo == "numero":
            ws.row_dimensions[riga_corrente].height = 16
            # Numero in A (grassetto, colore blu)
            c_num = ws.cell(row=riga_corrente, column=1, value=riga["num"])
            c_num.font      = Font(name=FONT_NAME, bold=True, size=9,
                                   color=COL_HEADER_BG)
            c_num.alignment = allineamento("center")
            c_num.fill      = sfondo("EEF4FB")  # azzurro molto chiaro
            # Titolo breve in B
            c_desc = ws.cell(row=riga_corrente, column=2, value=riga["col_b"])
            c_desc.font      = Font(name=FONT_NAME, bold=True, size=9)
            c_desc.alignment = Alignment(horizontal="left", vertical="center",
                                         wrap_text=True)
            c_desc.fill      = sfondo("EEF4FB")
            riga_corrente += 1
            continue

        # ── Riga sub-misura (B + D, senza C) ──
        if tipo == "sub":
            ws.row_dimensions[riga_corrente].height = 13
            c_b = ws.cell(row=riga_corrente, column=2, value=riga["col_b"])
            c_b.font      = Font(name=FONT_NAME, size=8, italic=True,
                                 color="595959")
            c_b.alignment = Alignment(horizontal="left", vertical="center",
                                      indent=2)
            if riga.get("col_d") is not None:
                c_d = ws.cell(row=riga_corrente, column=4, value=riga["col_d"])
                c_d.font         = Font(name=FONT_NAME, size=8, color="595959")
                c_d.number_format = "#,##0.00"
                c_d.alignment    = allineamento("right")
            riga_corrente += 1
            continue

        # ── Riga dati (U.M. + qta + prezzi imprese) ──
        if tipo == "dati":
            num_voce = riga.get("num_voce")
            qta      = riga["col_d"]
            media    = medie.get(num_voce) if num_voce else None

            ws.row_dimensions[riga_corrente].height = 16

            # Colonne A-D: copiare dal capitolato
            # A = vuota (il numero è sulla riga 'numero')
            c_um = ws.cell(row=riga_corrente, column=3, value=riga["col_c"])
            c_um.font      = Font(name=FONT_NAME, size=9)
            c_um.alignment = allineamento("center")
            c_um.border    = b

            c_qta = ws.cell(row=riga_corrente, column=4, value=qta)
            c_qta.font         = Font(name=FONT_NAME, size=9)
            c_qta.number_format = "#,##0.00"
            c_qta.alignment    = allineamento("right")
            c_qta.border       = b

            # Colonne PU: prezzo unitario per ogni impresa
            note_anomalie = []
            for i, imp in enumerate(imprese):
                prezzo  = imp["voci"].get(num_voce) if num_voce else None
                mancante = prezzo is None or prezzo == 0
                prezzo_display = media if (mancante and media) else (prezzo or 0)

                # Verifica anomalia rispetto alla media
                e_alto = e_basso = False
                if media and media > 0 and not mancante and prezzo_display:
                    delta = (prezzo_display - media) / media * 100
                    if delta > SOGLIA_ANOMALIA_PERC:
                        e_alto = True
                        note_anomalie.append(
                            f"{imp['nome']}: +{delta:.0f}% (ALTO)")
                    elif delta < -SOGLIA_ANOMALIA_PERC:
                        e_basso = True
                        note_anomalie.append(
                            f"{imp['nome']}: {delta:.0f}% (BASSO)")

                c_pu = ws.cell(row=riga_corrente,
                               column=col_pu_inizio + i,
                               value=prezzo_display)
                c_pu.font = Font(
                    name=FONT_NAME, size=9,
                    bold=(e_alto or e_basso),
                    italic=mancante,
                    color="C00000" if e_alto else
                          ("375623" if e_basso else
                           ("7F7F7F" if mancante else "000000")),
                )
                c_pu.number_format = "#,##0.00"
                c_pu.alignment     = allineamento("right")
                c_pu.border        = b
                c_pu.fill          = sfondo(
                    COL_MANCANTE_BG if mancante else
                    (COL_ANOMALO_ALTO if e_alto else
                     (COL_ANOMALO_BASSO if e_basso else "FFFFFF"))
                )

            # Colonna Media P.U.
            c_med = ws.cell(row=riga_corrente, column=col_media,
                            value=media if media else "—")
            c_med.font      = Font(name=FONT_NAME, size=9, bold=True,
                                   color="375623")
            c_med.fill      = sfondo(COL_MEDIA_BG)
            c_med.alignment = allineamento("right")
            c_med.border    = b
            if media:
                c_med.number_format = "#,##0.00"

            # Colonne Totali: formula Excel = D_riga * PU_col_riga
            for i in range(n_imprese):
                col_pu_lettera = get_column_letter(col_pu_inizio + i)
                c_tot = ws.cell(
                    row=riga_corrente,
                    column=col_tot_inizio + i,
                    value=f"=D{riga_corrente}*{col_pu_lettera}{riga_corrente}",
                )
                c_tot.font         = Font(name=FONT_NAME, size=9)
                c_tot.number_format = "€#,##0.00"
                c_tot.alignment    = allineamento("right")
                c_tot.border       = b
                prezzo_i  = imprese[i]["voci"].get(num_voce) if num_voce else None
                mancante_i = prezzo_i is None or prezzo_i == 0
                c_tot.fill = sfondo(COL_MANCANTE_BG if mancante_i else "F8F8F8")

            # Colonna note anomalie
            testo_note = " | ".join(note_anomalie) if note_anomalie else ""
            c_note = ws.cell(row=riga_corrente, column=col_note,
                             value=testo_note)
            c_note.font      = Font(name=FONT_NAME, size=8, italic=True,
                                    color="C00000" if note_anomalie else "7F7F7F")
            c_note.alignment = Alignment(horizontal="left", vertical="center",
                                         wrap_text=True)
            c_note.border    = b

            righe_totali.append(riga_corrente)
            riga_corrente += 1

            # Note extra delle imprese dopo questa voce
            if num_voce and num_voce in note_per_voce:
                for idx_imp, testi in note_per_voce[num_voce].items():
                    for testo in testi:
                        ws.row_dimensions[riga_corrente].height = 14
                        ws.merge_cells(
                            f"A{riga_corrente}:D{riga_corrente}")
                        c = ws.cell(
                            row=riga_corrente, column=1,
                            value=f"↳ [{imprese[idx_imp]['nome']}] {testo}",
                        )
                        c.font      = Font(name=FONT_NAME, size=8,
                                           italic=True, color="595959")
                        c.fill      = sfondo("F2F2F2")
                        c.alignment = Alignment(
                            horizontal="left", vertical="center", indent=2)
                        c.border    = b
                        for col in range(5, col_note + 1):
                            cella = ws.cell(row=riga_corrente, column=col)
                            cella.fill   = sfondo("F2F2F2")
                            cella.border = b
                        riga_corrente += 1

    # ── Riga totale finale ──
    riga_corrente += 1
    ws.row_dimensions[riga_corrente].height = 26
    ws.merge_cells(f"A{riga_corrente}:D{riga_corrente}")
    c = ws.cell(row=riga_corrente, column=1,
                value="TOTALE OFFERTA (IVA ESCLUSA)")
    c.font      = Font(name=FONT_NAME, bold=True, size=10, color="FFFFFF")
    c.fill      = sfondo(COL_HEADER_BG)
    c.alignment = allineamento("right")
    c.border    = b

    for i in range(n_imprese):
        c = ws.cell(row=riga_corrente, column=col_pu_inizio + i, value="—")
        c.font      = Font(name=FONT_NAME, bold=True, color="FFFFFF")
        c.fill      = sfondo(COL_HEADER_BG)
        c.alignment = allineamento()
        c.border    = b

    c = ws.cell(row=riga_corrente, column=col_media, value="—")
    c.font      = Font(name=FONT_NAME, bold=True, color="FFFFFF")
    c.fill      = sfondo(COL_HEADER_BG)
    c.alignment = allineamento()
    c.border    = b

    for i in range(n_imprese):
        col_lettera   = get_column_letter(col_tot_inizio + i)
        formula_somma = "+".join([f"{col_lettera}{r}" for r in righe_totali])
        c_tot = ws.cell(row=riga_corrente, column=col_tot_inizio + i,
                        value=f"={formula_somma}")
        c_tot.font         = Font(name=FONT_NAME, bold=True, size=10,
                                  color="FFFFFF")
        c_tot.fill         = sfondo(COL_HEADER_BG)
        c_tot.number_format = "€#,##0.00"
        c_tot.alignment    = allineamento("right")
        c_tot.border       = b

    c = ws.cell(row=riga_corrente, column=col_note, value="")
    c.fill   = sfondo(COL_HEADER_BG)
    c.border = b


# ─────────────────────────────────────────────────────────────────
#  FOGLIO RIEPILOGO
# ─────────────────────────────────────────────────────────────────

def _genera_foglio_riepilogo(ws_r, struttura, imprese, medie, n_imprese):
    """
    Scrive il foglio 'Riepilogo' dell'Excel di output.

    Mostra il totale per ogni macro-sezione (categoria testuale preceduta
    da riga vuota) e il totale generale.

    Le sezioni sono rilevate come righe 'testo' precedute da riga 'vuota'.
    I totali sono calcolati sommando qta * prezzo per ogni voce nella sezione.
    """
    def sfondo(colore):
        return PatternFill("solid", start_color=colore)

    bordo_sottile = Side(style="thin", color="BFBFBF")
    bordo         = Border(left=bordo_sottile, right=bordo_sottile,
                           top=bordo_sottile,  bottom=bordo_sottile)

    def allineamento(orizzontale="center"):
        return Alignment(horizontal=orizzontale, vertical="center")

    n_cols         = n_imprese + 2  # sezione + N imprese + scarto
    ultima_col_let = get_column_letter(n_cols)

    ws_r.column_dimensions["A"].width = 38
    for i in range(1, n_imprese + 2):
        ws_r.column_dimensions[get_column_letter(i + 1)].width = 18

    # ── Titolo ──
    ws_r.merge_cells(f"A1:{ultima_col_let}1")
    c = ws_r.cell(row=1, column=1, value="RIEPILOGO PER SEZIONE")
    c.font      = Font(name=FONT_NAME, bold=True, size=12, color="FFFFFF")
    c.fill      = sfondo(COL_HEADER_BG)
    c.alignment = allineamento("left")

    # ── Intestazione ──
    ws_r.row_dimensions[2].height = 25
    c = ws_r.cell(row=2, column=1, value="SEZIONE")
    c.font      = Font(name=FONT_NAME, bold=True, size=9, color="FFFFFF")
    c.fill      = sfondo(COL_HEADER_BG)
    c.alignment = allineamento("left")
    c.border    = bordo
    for i, imp in enumerate(imprese):
        c = ws_r.cell(row=2, column=i + 2, value=imp["nome"].upper())
        c.font      = Font(name=FONT_NAME, bold=True, size=9, color="FFFFFF")
        c.fill      = sfondo(COL_SECTION_BG)
        c.alignment = allineamento()
        c.border    = bordo
    c = ws_r.cell(row=2, column=n_imprese + 2, value="SCARTO MAX-MIN")
    c.font      = Font(name=FONT_NAME, bold=True, size=9, color="1F4E79")
    c.fill      = sfondo(COL_MEDIA_BG)
    c.alignment = allineamento()
    c.border    = bordo

    # ── Calcolo totali per sezione ──
    # Identifica le sezioni: righe 'testo' precedute da riga 'vuota'
    sezioni_trovate = []
    for idx, riga in enumerate(struttura):
        if riga["tipo"] == "testo":
            precedente = struttura[idx - 1] if idx > 0 else None
            if precedente and precedente["tipo"] == "vuota":
                sezioni_trovate.append(riga["col_b"])

    totali_sezione = {s: {imp["nome"]: 0.0 for imp in imprese}
                      for s in sezioni_trovate}
    totali_sezione["TOTALE GENERALE"] = {imp["nome"]: 0.0 for imp in imprese}

    sezione_corrente = None
    for riga in struttura:
        if riga["tipo"] == "testo" and riga["col_b"] in totali_sezione:
            sezione_corrente = riga["col_b"]
        elif riga["tipo"] == "dati":
            num  = riga.get("num_voce")
            qta  = riga.get("col_d") or 0
            for imp in imprese:
                prezzo = imp["voci"].get(num) if num else None
                if prezzo is None or prezzo == 0:
                    prezzo = medie.get(num) or 0
                totale_voce = qta * prezzo
                if sezione_corrente and sezione_corrente in totali_sezione:
                    totali_sezione[sezione_corrente][imp["nome"]] += totale_voce
                totali_sezione["TOTALE GENERALE"][imp["nome"]] += totale_voce

    # ── Scrittura righe ──
    riga_corrente = 3
    for nome_sezione, totali_imp in totali_sezione.items():
        e_totale = (nome_sezione == "TOTALE GENERALE")
        colore_sfondo = (COL_HEADER_BG if e_totale else
                         ("F2F2F2" if riga_corrente % 2 == 0 else "FFFFFF"))
        colore_testo  = "FFFFFF" if e_totale else "000000"

        ws_r.row_dimensions[riga_corrente].height = 18
        c = ws_r.cell(row=riga_corrente, column=1, value=nome_sezione)
        c.font      = Font(name=FONT_NAME, bold=e_totale, size=9,
                           color=colore_testo)
        c.fill      = sfondo(colore_sfondo)
        c.alignment = Alignment(horizontal="left", vertical="center", indent=1)
        c.border    = bordo

        valori = []
        for i, imp in enumerate(imprese):
            val = totali_imp[imp["nome"]]
            valori.append(val)
            c = ws_r.cell(row=riga_corrente, column=i + 2, value=val)
            c.font         = Font(name=FONT_NAME, bold=e_totale, size=9,
                                  color=colore_testo)
            c.fill         = sfondo(colore_sfondo)
            c.number_format = "€#,##0.00"
            c.alignment    = allineamento("right")
            c.border       = bordo

        scarto = max(valori) - min(valori) if len(valori) >= 2 else 0
        scarto_pct = (scarto / min(valori) * 100
                      if len(valori) >= 2 and min(valori) > 0 else 0)
        testo_scarto = f"€{scarto:,.0f}  ({scarto_pct:.1f}%)"
        c = ws_r.cell(row=riga_corrente, column=n_imprese + 2,
                      value=testo_scarto)
        c.font      = Font(
            name=FONT_NAME, bold=e_totale, size=9,
            color=colore_testo if e_totale else
                  ("C00000" if scarto_pct > 15 else "000000"),
        )
        c.fill      = sfondo(colore_sfondo if e_totale else COL_MEDIA_BG)
        c.alignment = allineamento("right")
        c.border    = bordo
        riga_corrente += 1

    ws_r.freeze_panes = "A3"
```

**Step 2: Verifica sintattica**

```bash
/usr/bin/python3 -c "
import sys
sys.path.insert(0, '/Users/paolo/Server/Siti Web/_private/comparativo_cme')
from core.generatore import genera_comparativo
print('OK')
"
```

Atteso: `OK`

---

## Task 4: Riscrivere `interfaccia.py`

**Files:**
- Crea: `interfaccia.py`

**Descrizione:** Sposta le classi GUI `PreventivRow`, `ScrollableFrame`, `App` in un file separato. Importa da `core.generatore` e `core.config`. Tutti i commenti in italiano.

**Step 1: Scrivi `interfaccia.py`**

Prendi le classi `PreventivRow`, `ScrollableFrame`, `App` (righe 770-1170 del file originale) e spostale in questo file con:
- Commenti docstring in italiano su ogni classe e metodo
- Import da `core.config` e `core.generatore`
- Stessa logica funzionale invariata

```python
"""
Modulo interfaccia — Interfaccia grafica dell'applicazione.

Contiene le classi:
  - PreventivRow:    widget riga per un singolo file preventivo
  - ScrollableFrame: frame con scrollbar verticale
  - App:             finestra principale dell'applicazione
"""

import os
import threading
from datetime import datetime
from tkinter import (Tk, Frame, Label, Button, StringVar, filedialog,
                     messagebox, Text, Scrollbar, END, WORD, DISABLED, NORMAL)
import tkinter as tk

from core.config import SOGLIA_ANOMALIA_PERC
from core.generatore import genera_comparativo


# ── (incolla le classi PreventivRow, ScrollableFrame, App qui) ──
# ── aggiornando gli import e i commenti in italiano ──
```

**Step 2: Verifica sintattica (senza avviare la GUI)**

```bash
/usr/bin/python3 -c "
import sys
sys.path.insert(0, '/Users/paolo/Server/Siti Web/_private/comparativo_cme')
import unittest.mock as mock
sys.modules['tkinter'] = mock.MagicMock()
sys.modules['tkinter.ttk'] = mock.MagicMock()
import interfaccia
print('OK')
"
```

Atteso: `OK`

---

## Task 5: Creare `avvia.py` (entry point)

**Files:**
- Crea: `avvia.py`

**Descrizione:** File minimale di avvio che verifica openpyxl e lancia la GUI. Il vecchio `ComparativoPreventivi.py` può essere mantenuto come backup oppure rimosso.

**Step 1: Scrivi `avvia.py`**

```python
"""
COMPARATIVO PREVENTIVI — Applicazione con interfaccia grafica
Studio di Architettura

INSTALLAZIONE (una sola volta):
  1. Installare Python da https://www.python.org/downloads/
     → Spuntare "Add Python to PATH" durante l'installazione
  2. Aprire il Prompt dei comandi (cmd) e digitare:
     pip install openpyxl

UTILIZZO:
  Doppio click su questo file per avviare l'applicazione.
"""

import sys

# Verifica disponibilità libreria openpyxl
try:
    import openpyxl  # noqa: F401
    OPENPYXL_OK = True
except ImportError:
    OPENPYXL_OK = False

if __name__ == "__main__":
    if not OPENPYXL_OK:
        # Mostra messaggio di errore anche senza GUI disponibile
        try:
            import tkinter as tk
            from tkinter import messagebox
            root = tk.Tk()
            root.withdraw()
            messagebox.showerror(
                "Libreria mancante",
                "La libreria 'openpyxl' non è installata.\n\n"
                "Apri il Prompt dei comandi e digita:\n\n"
                "    pip install openpyxl\n\n"
                "Poi riavvia l'applicazione.",
            )
            root.destroy()
        except Exception:
            print("ERRORE: esegui 'pip install openpyxl' e riavvia.")
        sys.exit(1)

    # Avvia l'interfaccia grafica
    from interfaccia import App
    app = App()
    app.mainloop()
```

**Step 2: Verifica sintattica**

```bash
/usr/bin/python3 -c "
import ast, sys
with open('/Users/paolo/Server/Siti Web/_private/comparativo_cme/avvia.py') as f:
    ast.parse(f.read())
print('OK sintassi avvia.py')
"
```

Atteso: `OK sintassi avvia.py`

---

## Task 6: Test end-to-end con file reali

**Step 1: Esegui il test completo**

```bash
/usr/bin/python3 - <<'EOF'
import sys, unittest.mock as mock
sys.modules['tkinter'] = mock.MagicMock()
sys.modules['tkinter.ttk'] = mock.MagicMock()
sys.path.insert(0, '/Users/paolo/Server/Siti Web/_private/comparativo_cme')

from core.generatore import genera_comparativo
import os

cap  = 'FILES/Capitolato BASE VUOTO.xlsx'
prev = ['FILES/brc.xlsx', 'FILES/conio.xlsx', 'FILES/giò.xlsx']
nomi = ['BRC', 'CONIO', 'GIÒ']
out  = '/tmp/comparativo_test_v2.xlsx'

def log(msg): print(msg)

genera_comparativo(cap, prev, nomi, out, log_fn=log)
print(f"\nDimensione: {os.path.getsize(out):,} bytes")
print("✅ Test end-to-end superato!")
EOF
```

Atteso: log con 289 voci, 280/246/238 prezzate, file salvato.

**Step 2: Verifica struttura foglio output**

```bash
/usr/bin/python3 - <<'EOF'
import sys
sys.path.insert(0, '/Users/paolo/Server/Siti Web/_private/comparativo_cme')
from openpyxl import load_workbook

wb = load_workbook('/tmp/comparativo_test_v2.xlsx', data_only=True)
ws = wb['Comparativo']

print("=== Prime 25 righe del Comparativo ===")
for i, row in enumerate(ws.iter_rows(min_row=1, max_row=25,
                                      max_col=8, values_only=True), start=1):
    parti = []
    nomi_col = list('ABCDEFGH')
    for j, v in enumerate(row):
        if v is not None:
            parti.append(f"{nomi_col[j]}={repr(str(v)[:25])}")
    print(f"  R{i:3d}: {' | '.join(parti) if parti else '(vuota)'}")

wb.close()
EOF
```

Atteso: la struttura deve rispecchiare quella del capitolato originale (riga vuota → categoria → vuota → numero voce → descrizione estesa → sub-misure → riga dati con prezzi).

**Step 3: Copia output su Desktop per controllo visivo**

```bash
cp /tmp/comparativo_test_v2.xlsx ~/Desktop/comparativo_test_v2.xlsx
echo "Copiato su Desktop"
```

---

## Task 7: Aggiornare `MEMORIA_PROGETTO_ComparativoPreventivi.md`

**Files:**
- Modifica: `MEMORIA_PROGETTO_ComparativoPreventivi.md`

**Step 1: Aggiungi sezione per la nuova struttura modulare**

Aggiungi in fondo al file una sezione `## v3 — Struttura modulare e output fedele al capitolato` che documenta:
- Nuova struttura cartelle (`core/`, `avvia.py`, `interfaccia.py`)
- Nuovi tipi di riga della struttura dati (`vuota`, `testo`, `numero`, `sub`, `dati`)
- Logica di rendering (solo righe `dati` ricevono prezzi)
- File originale `ComparativoPreventivi.py` mantenuto come backup

---

## Note implementative

### Vecchio file `ComparativoPreventivi.py`
Il file originale deve essere **mantenuto** (non cancellato) finché il nuovo sistema non è testato. Può essere rinominato in `ComparativoPreventivi_backup.py` o lasciato così.

### Import circolari
`interfaccia.py` importa da `core.generatore` ma NON viceversa. `core.generatore` importa da `core.lettore` e `core.config`. Nessun import circolare.

### SOGLIA_ANOMALIA_PERC modificabile dalla GUI
La GUI modifica `SOGLIA_ANOMALIA_PERC` globalmente prima di chiamare `genera_comparativo`. Nel nuovo sistema, la GUI deve passare la soglia come parametro oppure aggiornare `core.config.SOGLIA_ANOMALIA_PERC`. La soluzione più semplice: passarla come parametro opzionale a `genera_comparativo`.

Aggiorna la firma:
```python
def genera_comparativo(capitolato_path, preventivi_paths, nomi_imprese,
                       output_path, log_fn=None, soglia_anomalie=None):
    # All'inizio della funzione:
    soglia = soglia_anomalie if soglia_anomalie is not None else SOGLIA_ANOMALIA_PERC
```

E nella GUI:
```python
genera_comparativo(cap, paths, nomi, output_path,
                   log_fn=self._log,
                   soglia_anomalie=self.soglia_var.get())
```
