#!/usr/bin/env python3 """Estrae i listini prezzi PVC da PVC_LISTINO.xls e genera i file src/assets/data/modelli/pvc//listino.json Uso: python3 scripts/extract_listini_pvc.py Richiede: pip install xlrd """ import json import re import unicodedata from pathlib import Path import xlrd ROOT = Path(__file__).resolve().parent.parent XLS_PATH = ROOT / "PVC_LISTINO.xls" PVC_DIR = ROOT / "src" / "assets" / "data" / "modelli" / "pvc" # Titoli del listino che corrispondono a una o più cartelle pvc/ già esistenti # (catalogo allineato a GAMMA PVC). "FINSETRA 2 ANTE" è il typo presente nel # file excel stesso, non un errore di battitura qui. Un titolo può essere # associato a più cartelle quando non esiste un listino dedicato per ciascun # prodotto (es. "CASSONETTO PVC" riusato per cassonetti/scuretti/scorrevoli). KNOWN_FOLDERS = { "FINESTRA 1 ANTA": "finestra_1_anta", "FINSETRA 2 ANTE": "finestra_2_ante", "FINESTRA 3 ANTE": "finestra_3_ante", "PORTA-FINESTRA 1 ANTA": "portafinestra_1_anta", "PORTA-FINESTRA 2 ANTE": "portafinestra_2_ante", "PORTA-FINESTRA 3 ANTE": "portafinestra_3_ante", "WASISTAS CON MARTELLINA": "finestra_vasistas", "TELAIO FISSO": "finestra_anta_fissa", "BILICO ORIZZONTALE": "bilico", "PORTA 1 ANTA, CON SERRATURA": "porta_1_anta", "PORTA 1 ANTA, ANTIPANICO (APERTURA ESTERNA)": "porta_1_anta_antipanico", "PORTA 2 ANTE, CON SERRATURA": "porta_2_ante", "PORTA 2 ANTE, ANTIPANICO (APERTURA ESTERNA)": "porta_2_ante_antipanico", "CASSONETTO PVC": [ "cassonetto_80", "cassonetto_139", "cassonetto_150", "scuretto_base", "scuretto_pannello_malachite", "scuretto_pannello_alessandrite", "scorrevole_hst_1_anta", "scorrevole_hst_2_ante", "scorrevole_parallelo_1_anta", "scorrevole_sf_1_anta", "scorrevole_sf_2_ante", "traslante_scorrevole_1_anta", ], } # Titoli del listino senza una cartella attiva corrispondente nel catalogo # (prodotti archiviati o senza un modello 3D GAMMA univoco): non vengono # processati, per non ricreare cartelle orfane. SKIP_TITLES = { "PORTA 1 ANTA, CON SERRATURA_(APERTURA ESTERNA)", "PORTA 2 ANTE, CON SERRATURA_(APERTURA ESTERNA)", "SCORREVOLE IN LINEA_2 ANTE", "SCORREVOLE IN LINEA_3 ANTE", "SCORREVOLE IN LINEA_4 ANTE_2 FISSE + 2 APRIBILI", "TRASLANTE_ANTA DX+ANTA FISSA SX", "TRASLANTE_ANTA DX+FISSO SX", "TRASLANTE_ANTA SX+ANTA FISSA DX", "TRASLANTE_ANTA SX+FISSO DX", "ALZANTE 2ANTE_MANO DX", "ALZANTE 2ANTE_MANO SX", "ALZANTE_1ANTA APRIBILE DX+ANTA FISSA SX", "ALZANTE_1ANTA APRIBILE SX+ANTA FISSA DX", "ALZANTE_4ANTE(2 APRIBILI+2 FISSE)", } def slugify(title: str) -> str: text = unicodedata.normalize("NFKD", title).encode("ascii", "ignore").decode() text = text.lower() text = re.sub(r"[^a-z0-9]+", "_", text) return text.strip("_") def parse_blocks(sheet): """Ritorna una lista di (titolo, larghezze, righe) dove righe è una lista di (altezza, [prezzi...]) allineata alle larghezze.""" title_rows = [] for r in range(sheet.nrows): v = sheet.cell_value(r, 0) if isinstance(v, str) and v.strip(): title_rows.append((r, v.strip())) # Un titolo "puro" (es. "CASSONETTO PVC") non ha larghezze sulla propria # riga: la riga successiva è il suo header (es. "mm", 500, 600, ...) e va # scartata dall'elenco titoli, non trattata come un nuovo blocco. filtered = [] for i, (r, title) in enumerate(title_rows): if i > 0: prev_r, prev_title = title_rows[i - 1] prev_header = sheet.row_values(prev_r) prev_is_pure_title = not any(isinstance(v, float) for v in prev_header[1:]) if prev_is_pure_title and r == prev_r + 1: continue filtered.append((r, title)) title_rows = filtered blocks = [] for i, (r, title) in enumerate(title_rows): end = title_rows[i + 1][0] if i + 1 < len(title_rows) else sheet.nrows header_row = r header = sheet.row_values(header_row) # Se la riga del titolo non contiene larghezze numeriche, l'header # (es. "mm", larghezze...) è sulla riga successiva (es. CASSONETTO PVC). if not any(isinstance(v, float) for v in header[1:]): header_row = r + 1 header = sheet.row_values(header_row) larghezze = [] c = 1 while c < len(header) and header[c] != "": larghezze.append(header[c]) c += 1 rows = [] for rr in range(header_row + 1, end): row_values = sheet.row_values(rr) if not row_values or row_values[0] == "": continue altezza = row_values[0] if not isinstance(altezza, float): continue rows.append((altezza, row_values[1:1 + len(larghezze)])) if larghezze and rows: blocks.append((title, larghezze, rows)) return blocks def mm_to_cm(value: float) -> float: cm = value / 10 return int(cm) if cm == int(cm) else round(cm, 2) def round_price(value) -> float: if not isinstance(value, (int, float)) or value == "": return None rounded = round(value, 2) return int(rounded) if rounded == int(rounded) else rounded def block_to_listino(larghezze, rows): listino = [] for altezza, prezzi in rows: for larghezza, prezzo in zip(larghezze, prezzi): price = round_price(prezzo) if price is None: continue listino.append({ "altezza": mm_to_cm(altezza), "larghezza": mm_to_cm(larghezza), "prezzo": price, }) return listino def main(): wb = xlrd.open_workbook(str(XLS_PATH)) sheet = wb.sheet_by_index(0) blocks = parse_blocks(sheet) for title, larghezze, rows in blocks: if title in SKIP_TITLES: print(f"{title!r:55s} -> (saltato, nessuna cartella attiva corrispondente)") continue if title not in KNOWN_FOLDERS: print(f"{title!r:55s} -> (saltato, titolo non mappato)") continue folder_names = KNOWN_FOLDERS[title] if isinstance(folder_names, str): folder_names = [folder_names] listino = block_to_listino(larghezze, rows) for folder_name in folder_names: folder = PVC_DIR / folder_name if not folder.is_dir(): print(f"{title!r:55s} -> cartella {folder_name!r} non trovata, saltato") continue out_path = folder / "listino.json" out_path.write_text(json.dumps(listino, indent=3, ensure_ascii=False) + "\n", encoding="utf-8") print(f"{title!r:55s} -> {out_path.relative_to(ROOT)} ({len(listino)} righe)") if __name__ == "__main__": main()