versione 0.8

This commit is contained in:
2026-07-13 11:58:30 +02:00
commit 761db06329
1723 changed files with 159016 additions and 0 deletions
+198
View File
@@ -0,0 +1,198 @@
#!/usr/bin/env python3
"""Estrae i listini prezzi PVC da PVC_LISTINO.xls e genera i file
src/assets/data/modelli/pvc/<modello>/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()