""" GridFiller — fill utilities condivise tra LinkerSheet e DiritiTab. Entrambi i fogli istanziano un GridFiller con la propria mappa logicalKey → columnIndex; la logica di fill è unica. """ import datetime import re as _re import pandas as pd from PyQt6.QtCore import Qt, QModelIndex from PyQt6.QtGui import QBrush, QColor, QFont, QPainter from PyQt6.QtWidgets import ( QStyledItemDelegate, QStyleOptionViewItem, QTableWidget, QTableWidgetItem, QHeaderView, ) _DATE_SORT_RE = _re.compile(r'^(\d{2})/(\d{2})/(\d{4})$') class DateSortItem(QTableWidgetItem): """QTableWidgetItem che ordina le date gg/mm/aaaa cronologicamente.""" def __lt__(self, other): a, b = self.text(), other.text() ma = _DATE_SORT_RE.match(a) mb = _DATE_SORT_RE.match(b) if ma and mb: return (f'{ma.group(3)}{ma.group(2)}{ma.group(1)}' < f'{mb.group(3)}{mb.group(2)}{mb.group(1)}') return super().__lt__(other) # ------------------------------------------------------------------ # # Delegate — sfondo piatto (niente arrotondamento stile Windows) # # ------------------------------------------------------------------ # class FlatBgDelegate(QStyledItemDelegate): """Disegna lo sfondo delle celle come rettangolo pieno senza arrotondamenti. Usare su ogni QTableWidget che popola celle con setBackground().""" def paint(self, painter, option, index): brush: QBrush = index.data(Qt.ItemDataRole.BackgroundRole) if brush is not None and brush.style() != Qt.BrushStyle.NoBrush: painter.fillRect(option.rect, brush) opt = QStyleOptionViewItem(option) opt.backgroundBrush = QBrush() super().paint(painter, opt, index) else: super().paint(painter, option, index) # ------------------------------------------------------------------ # # Costanti condivise — stile header griglia standard # # ------------------------------------------------------------------ # HEADER_HEIGHT = 33 HEADER_FONT = QFont("Calibri", 8) HEADER_BORDER_COLOR = QColor('#A0A0A0') def paint_std_header_section(painter, rect, text: str, bg: QColor, border: QColor = None): """ Dipinge una sezione di intestazione con lo stile standard condiviso: sfondo colorato, bordo, testo Calibri 8 centrato con word-wrap. Usare da paintSection() di qualsiasi QHeaderView del Linker. """ painter.fillRect(rect, bg) painter.setPen(border or HEADER_BORDER_COLOR) painter.drawRect(rect.adjusted(0, 0, -1, -1)) if text: painter.setPen(QColor('black')) painter.setFont(HEADER_FONT) painter.drawText( rect, Qt.AlignmentFlag.AlignCenter | Qt.TextFlag.TextWordWrap, str(text), ) HEADER_SEL_COLOR = QColor('#CCE8FF') class SelectableVHeader(QHeaderView): """Vertical header condiviso: evidenzia le righe con celle selezionate.""" def __init__(self, table): super().__init__(Qt.Orientation.Vertical, table) self._table = table self.setSectionsClickable(True) self.setDefaultSectionSize(17) self.setMinimumSectionSize(17) self.setMinimumWidth(38) self.setFont(QFont('Calibri', 7)) self.setSectionResizeMode(QHeaderView.ResizeMode.Fixed) def paintSection(self, painter: QPainter, rect, logicalIndex: int): painter.save() sel = self._table.selectionModel() if sel and sel.rowIntersectsSelection(logicalIndex, QModelIndex()): painter.fillRect(rect, HEADER_SEL_COLOR) painter.setPen(QColor('#0078D7')) else: painter.fillRect(rect, QColor('#F5F5F5')) painter.setPen(QColor('#666666')) painter.drawText(rect, Qt.AlignmentFlag.AlignCenter, str(logicalIndex + 1)) painter.setPen(HEADER_BORDER_COLOR) painter.drawLine(rect.bottomLeft(), rect.bottomRight()) painter.drawLine(rect.topRight(), rect.bottomRight()) painter.restore() # ------------------------------------------------------------------ # # Helpers (stessi algoritmi dei metodi statici in LinkerSheet) # # ------------------------------------------------------------------ # def gf_safe(val) -> str: if val is None: return '' try: if pd.isna(val): return '' except (TypeError, ValueError): pass return str(val).strip() def gf_fmt_date(val) -> str: if val is None: return '' if isinstance(val, float): try: if pd.isna(val): return '' except (TypeError, ValueError): pass if hasattr(val, 'day'): return f"{val.day:02d}/{val.month:02d}/{val.year}" s = str(val).strip() if len(s) == 10 and s[4] == '-' and s[7] == '-': return f"{s[8:10]}/{s[5:7]}/{s[0:4]}" return s if s else '' def gf_fmt_num(val, decimals: int = 0) -> str: if val is None: return '' try: if pd.isna(val): return '' except (TypeError, ValueError): pass try: f = float(val) return str(int(round(f))) if decimals == 0 else ( str(int(f)) if f == int(f) else f'{f:.{decimals}f}' ) except (ValueError, TypeError): return '' def gf_to_comparable_date(val) -> str: """Returns a YYYY-MM-DD string for date comparison, '' if unparseable. Handles: date/datetime objects, YYYY-MM-DD[...] strings, DD/MM/YYYY strings.""" if val is None: return '' try: if pd.isna(val): return '' except (TypeError, ValueError): pass if hasattr(val, 'strftime'): return val.strftime('%Y-%m-%d') s = str(val).strip() if not s: return '' if len(s) >= 10 and s[4:5] == '-' and s[7:8] == '-': return s[:10] if len(s) == 10 and s[2:3] == '/' and s[5:6] == '/': return f'{s[6:10]}-{s[3:5]}-{s[0:2]}' return '' def gf_to_date(val) -> datetime.date | None: if val is None: return None if isinstance(val, float): try: if pd.isna(val): return None except (TypeError, ValueError): pass if hasattr(val, 'date'): return val.date() if hasattr(val, 'day'): return val return None # ------------------------------------------------------------------ # # GridFiller # # ------------------------------------------------------------------ # _BG_VAL_GEMMA = QColor('#FFF176') # giallo — sorgente Gemma (G) _BG_VAL_SINOT = QColor('#FFB74D') # arancione — sorgente sinottico (S) _BG_PROPOSTA_GIRO = QColor('#FF69B4') # fucsia — sorgente PROPOSTA GIRO ICR class GridFiller: """ Scrive celle su un QTableWidget usando nomi logici anziché indici. col_indices: { 'SD': 30, 'DECOR': 31, ... } Chiavi mancanti vengono silenziosamente ignorate → la stessa istanza funziona sia sul foglio standard (tutti i tasti) sia su tabelle ridotte. """ def __init__(self, table: QTableWidget, col_indices: dict): self._t = table self._ci = col_indices def _set(self, row: int, key: str, text: str, bg: QColor = None): idx = self._ci.get(key) if idx is None: return s = str(text) if text else '' item = self._t.item(row, idx) if item is not None: item.setText(s) if bg is not None: item.setBackground(bg) else: it = DateSortItem(s) it.setFlags(it.flags() & ~Qt.ItemFlag.ItemIsEditable) if bg is not None: it.setBackground(bg) self._t.setItem(row, idx, it) # ---------------------------------------------------------------- # # Diritti # # ---------------------------------------------------------------- # def fill_diritti(self, row: int, diritti_rows: list): """ diritti_rows: lista di dict/Series con chiavi decr, scad, ragsoc_distr, perc, pass_cons_tot, pass_eff_tot, causale. Riempie: SD, DECOR, SCAD, DISTRIBUTORE, PERC, PASS_CONS, PASS_EFF, CAUS, DECOR_FUT, SCAD_FUT. """ today = datetime.date.today() current, future, expired = [], [], [] for r in diritti_rows: d = gf_to_date(r.get('decr')) s = gf_to_date(r.get('scad')) if d is None or s is None: continue if d <= today <= s: current.append(r) elif d > today: future.append(r) else: expired.append(r) sd = '+' if len(current) > 1 else ('S' if not current and expired else '') self._set(row, 'SD', sd) # current → expired as fallback: "in scadenza" products that already # crossed their SCAD still need their date/distributor shown (SD='S'). cr = current[0] if current else (expired[0] if expired else None) if cr is not None: self._set(row, 'DECOR', gf_fmt_date(cr.get('decr'))) self._set(row, 'SCAD', gf_fmt_date(cr.get('scad'))) self._set(row, 'DISTRIBUTORE', gf_safe(cr.get('ragsoc_distr'))) self._set(row, 'PERC', gf_fmt_num(cr.get('perc'))) self._set(row, 'PASS_CONS', gf_fmt_num(cr.get('pass_cons_tot'))) self._set(row, 'PASS_EFF', gf_fmt_num(cr.get('pass_eff_tot'))) self._set(row, 'CAUS', gf_safe(cr.get('causale'))) else: for k in ('DECOR', 'SCAD', 'DISTRIBUTORE', 'PERC', 'PASS_CONS', 'PASS_EFF', 'CAUS'): self._set(row, k, '') fr = future[0] if future else None self._set(row, 'DECOR_FUT', gf_fmt_date(fr.get('decr')) if fr is not None else '') self._set(row, 'SCAD_FUT', gf_fmt_date(fr.get('scad')) if fr is not None else '') # ---------------------------------------------------------------- # # Emesso (generaliste + tematiche) # # ---------------------------------------------------------------- # def fill_emesso(self, row: int, emesso_row): """Fills RETE, DATA, INIZIO, FASCIA, AUD, SHA.""" if emesso_row is None: for k in ('RETE', 'DATA', 'INIZIO', 'FASCIA', 'AUD', 'SHA'): self._set(row, k, '') return r = emesso_row self._set(row, 'RETE', gf_safe(r.get('rete'))) self._set(row, 'DATA', gf_fmt_date(r.get('data_emissione'))) self._set(row, 'INIZIO', gf_safe(r.get('ora_inizio'))) self._set(row, 'FASCIA', gf_safe(r.get('fascia'))) self._set(row, 'AUD', gf_fmt_num(r.get('audience'), 0)) self._set(row, 'SHA', gf_fmt_num(r.get('share'), 2)) def fill_tematiche(self, row: int, emesso_row): """Fills TEM_RETE, TEM_DATA, TEM_INIZIO, TEM_FASCIA, TEM_AUD, TEM_SHA.""" if emesso_row is None: for k in ('TEM_RETE', 'TEM_DATA', 'TEM_INIZIO', 'TEM_FASCIA', 'TEM_AUD', 'TEM_SHA'): self._set(row, k, '') return r = emesso_row self._set(row, 'TEM_RETE', gf_safe(r.get('rete'))) self._set(row, 'TEM_DATA', gf_fmt_date(r.get('data_emissione'))) self._set(row, 'TEM_INIZIO', gf_safe(r.get('ora_inizio'))) self._set(row, 'TEM_FASCIA', gf_safe(r.get('fascia'))) self._set(row, 'TEM_AUD', gf_fmt_num(r.get('audience'), 0)) self._set(row, 'TEM_SHA', gf_fmt_num(r.get('share'), 2)) # ---------------------------------------------------------------- # # Box office # # ---------------------------------------------------------------- # def fill_boxoffice(self, row: int, bo_row): """Fills BO_DEBUTTO, BO_INCASSO, BO_SPETT, BO_DIST.""" if bo_row is None: for k in ('BO_DEBUTTO', 'BO_INCASSO', 'BO_SPETT', 'BO_DIST'): self._set(row, k, '') return r = bo_row self._set(row, 'BO_DEBUTTO', gf_fmt_date(r.get('data_debutto'))) self._set(row, 'BO_INCASSO', gf_fmt_num(r.get('incasso'), 0)) self._set(row, 'BO_SPETT', gf_fmt_num(r.get('spettatori'), 0)) self._set(row, 'BO_DIST', gf_safe(r.get('distributore'))) # ---------------------------------------------------------------- # # Rete slot / OTT # # ---------------------------------------------------------------- # def fill_rete_slot(self, row: int, value: str): self._set(row, 'RETE_SLOT', value or '') def fill_ott(self, row: int, value: str): self._set(row, 'OTT', value or '') # ---------------------------------------------------------------- # # Valutazioni Gemma # # ---------------------------------------------------------------- # def fill_valutazioni(self, row: int, gem_row): """ gem_row: dict/Series con chiavi A_DISTRIBUTORE, V_RDA, V_C5 … A_DATA. Se None, cancella tutte le celle VAL. """ fields = [ ('VAL_FORNITORE', 'A_DISTRIBUTORE'), ('VAL_RDA', 'V_RDA'), ('VAL_C5', 'V_C5'), ('VAL_I1', 'V_I1'), ('VAL_R4', 'V_R4'), ('VAL_LA5', 'V_LA5'), ('VAL_I2', 'V_I2'), ('VAL_IRIS', 'V_IRIS'), ('VAL_TOP', 'V_TOP'), ('VAL_FOC', 'V_FOC'), ('VAL_C20', 'V_C20'), ('VAL_CI34', 'V_CI34'), ('VAL_C27', 'V_C27'), ('VAL_SUPPORTO', 'A_SUPPORTO'), ('VAL_DATA', 'A_DATA'), ] if gem_row is None: white = QColor('white') for k, _ in fields: self._set(row, k, '', white) self._set(row, 'VAL_GEM_SIN', '', white) return gem_sin = gem_row.get('GEM_SIN', 'G') _bg = _BG_VAL_SINOT if gem_sin == 'S' else _BG_VAL_GEMMA for k, f in fields: self._set(row, k, gf_safe(gem_row.get(f)), _bg) self._set(row, 'VAL_GEM_SIN', gem_sin, _bg) def fill_proposta_giro(self, row: int, extra_row: dict): """Overlay PROPOSTA GIRO ICR (tabValutazioniExtraGemma) sui VAL cols. Rete: '-' se NoIcr=1, altrimenti il valore. GEM_SIN='I'. Sfondo fucsia. Cols non-network (FORNITORE, RDA, SUPPORTO, DATA): mantengono testo, solo ricolorazione.""" fucsia = _BG_PROPOSTA_GIRO no_icr = bool(extra_row.get('NoIcr')) net_fields = [ ('VAL_C5', extra_row.get('C5')), ('VAL_I1', extra_row.get('I1')), ('VAL_R4', extra_row.get('R4')), ('VAL_LA5', extra_row.get('LA5')), ('VAL_I2', extra_row.get('I2')), ('VAL_IRIS', extra_row.get('IRIS')), ('VAL_TOP', extra_row.get('TOPCRIME')), ('VAL_FOC', extra_row.get('FOCUS')), ('VAL_C20', extra_row.get('C20')), ('VAL_CI34', extra_row.get('CINE34')), ('VAL_C27', extra_row.get('TWENTYSEVEN')), ] for col_name, val in net_fields: text = '-' if no_icr else (gf_safe(val) if val else '') self._set(row, col_name, text, fucsia) for col_name in ('VAL_FORNITORE', 'VAL_RDA', 'VAL_SUPPORTO', 'VAL_DATA'): idx = self._ci.get(col_name) if idx is not None: item = self._t.item(row, idx) if item: item.setBackground(fucsia) self._set(row, 'VAL_GEM_SIN', 'I', fucsia)