Roudrigus commited on
Commit
6de6fd2
·
verified ·
1 Parent(s): 93492ab

Update recebimento.py

Browse files
Files changed (1) hide show
  1. recebimento.py +450 -146
recebimento.py CHANGED
@@ -17,6 +17,8 @@ try:
17
  except Exception:
18
  ALT_AVAILABLE = False
19
 
 
 
20
  from banco import SessionLocal
21
  from models import RecebimentoRegistro
22
 
@@ -973,6 +975,309 @@ def _kpis_metas(total_reg: int, datas: pd.Series, meta_diaria: float, meta_mensa
973
  c6.metric("Ating. total vs soma metas diárias", "—")
974
 
975
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
976
  # ==========================================================
977
  # Helpers de UI (preview + processamento)
978
  # ==========================================================
@@ -1135,7 +1440,7 @@ def formulario(payload: Optional[Dict[str, Any]] = None, key_prefix: str = "new"
1135
  return {
1136
  "id_planilha": (payload.get("id_planilha") or _next_id_planilha()),
1137
 
1138
- "DATA": None, # apenas para referência visual no form
1139
  "data": data,
1140
  "data_emissao": data_emissao,
1141
  "nota_fiscal": nf,
@@ -1596,165 +1901,165 @@ def main():
1596
  st.session_state["__idx_iguais_db__"] = []
1597
  st.info("Prévia descartada. Clique em **⚙️ Processar agora** para gerar novamente.")
1598
 
1599
- # ------------------ LISTA ------------------
1600
  with aba_reg:
1601
- st.header("Registros (com filtros)")
1602
 
1603
  db = _get_db()
1604
  try:
1605
- regs = (
1606
- db.query(RecebimentoRegistro)
1607
- .order_by(RecebimentoRegistro.created_at.desc())
1608
- .limit(5000)
1609
- .all()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1610
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1611
  finally:
1612
  db.close()
1613
 
1614
- if not regs:
1615
- st.info("Nenhum registro encontrado.")
1616
- st.stop()
1617
-
1618
- df_base = pd.DataFrame([{c.name: getattr(r, c.name) for c in r.__table__.columns} for r in regs])
1619
-
1620
- if not df_base.empty and "data" in df_base.columns:
1621
- df_base["data"] = pd.to_datetime(df_base["data"], errors="coerce").dt.date
1622
-
1623
- def rotulo_to_campo(rotulo: str) -> Optional[str]:
1624
- info = COLUMN_MAP.get(rotulo)
1625
- return info[0] if info else None
1626
 
1627
- ordem_rotulos = []
1628
- if "ID" in OPTIONAL_COLUMNS:
1629
- ordem_rotulos.append("ID")
1630
- ordem_rotulos.extend(OFFICIAL_COLUMNS)
1631
-
1632
- colunas_display = []
1633
- for rot in ordem_rotulos:
1634
- campo = rotulo_to_campo(rot)
1635
- if campo and campo in df_base.columns:
1636
- colunas_display.append((rot, campo))
1637
-
1638
- df_disp = pd.DataFrame()
1639
- for rot, campo in colunas_display:
1640
- df_disp[rot] = df_base[campo]
1641
-
1642
- usados = {campo for _rot, campo in colunas_display}
1643
- extras = [c for c in df_base.columns if c not in usados]
1644
- for extra in extras:
1645
- df_disp[extra] = df_base[extra]
1646
-
1647
- with st.expander("🔎 Filtros", expanded=True):
1648
- c1, c2, c3 = st.columns(3)
1649
- f_po = c1.text_input("P.O (campo: po)", placeholder="ex.: 4500...", key="reg__f_po")
1650
- f_pn = c2.text_input("PN (campo: pn)", placeholder="ex.: 1Z23...", key="reg__f_pn")
1651
- f_lot = c3.text_input("LOT BATCH (campo: lot_batch)", placeholder="ex.: L123...", key="reg__f_lot")
1652
-
1653
- c4, c5, c6 = st.columns(3)
1654
- f_nf = c4.text_input("Nota Fiscal (campo: nota_fiscal)", placeholder="ex.: 12345", key="reg__f_nf")
1655
- f_forn = c5.text_input("Fornecedor (campo: fornecedor)", placeholder="ex.: ACME", key="reg__f_forn")
1656
- f_placa = c6.text_input("Placa do Veículo (campo: placa_veiculo)", placeholder="ex.: ABC1D23", key="reg__f_placa")
1657
-
1658
- c7, c8, c9 = st.columns([1, 1, 1])
1659
- f_data_ini = c7.date_input("Data inicial (campo: data)", value=None, key="reg__f_data_ini")
1660
- f_data_fim = c8.date_input("Data final (campo: data)", value=None, key="reg__f_data_fim")
1661
- limpar = c9.button("Limpar filtros", key="reg__btn_limpar_filtros")
1662
-
1663
- if limpar:
1664
- for k in ["reg__f_po", "reg__f_pn", "reg__f_lot", "reg__f_nf", "reg__f_forn", "reg__f_placa", "reg__f_data_ini", "reg__f_data_fim"]:
1665
- if k in st.session_state:
1666
- del st.session_state[k]
1667
- st.rerun()
1668
-
1669
- df_filtrado = df_base.copy()
1670
-
1671
- def _contains(df, col, term):
1672
- if not term or col not in df.columns:
1673
- return pd.Series([True] * len(df))
1674
- return df[col].astype(str).str.contains(str(term), case=False, na=False)
1675
-
1676
- if f_po: df_filtrado = df_filtrado[_contains(df_filtrado, "po", f_po)]
1677
- if f_pn: df_filtrado = df_filtrado[_contains(df_filtrado, "pn", f_pn)]
1678
- if f_lot: df_filtrado = df_filtrado[_contains(df_filtrado, "lot_batch", f_lot)]
1679
- if f_nf: df_filtrado = df_filtrado[_contains(df_filtrado, "nota_fiscal", f_nf)]
1680
- if f_forn: df_filtrado = df_filtrado[_contains(df_filtrado, "fornecedor", f_forn)]
1681
- if f_placa:df_filtrado = df_filtrado[_contains(df_filtrado, "placa_veiculo", f_placa)]
1682
- if "data" in df_filtrado.columns:
1683
- if f_data_ini:
1684
- df_filtrado = df_filtrado[df_filtrado["data"] >= f_data_ini]
1685
- if f_data_fim:
1686
- df_filtrado = df_filtrado[df_filtrado["data"] <= f_data_fim]
1687
-
1688
- df_disp_filtrado = pd.DataFrame()
1689
- for rot, campo in colunas_display:
1690
- if campo in df_filtrado.columns:
1691
- df_disp_filtrado[rot] = df_filtrado[campo]
1692
- for extra in extras:
1693
- if extra in df_filtrado.columns:
1694
- df_disp_filtrado[extra] = df_filtrado[extra]
1695
-
1696
- total_filtrado = len(df_disp_filtrado)
1697
- if "DATA" in df_disp_filtrado.columns:
1698
- datas_validas = pd.to_datetime(df_disp_filtrado["DATA"], errors="coerce").dt.date.dropna()
1699
  if not datas_validas.empty:
1700
- st.caption(
1701
- f"Exibindo **{total_filtrado}** registro(s). "
1702
- f"Primeira data: **{datas_validas.min()}** — Última data: **{datas_validas.max()}**."
1703
- )
1704
  else:
1705
- st.caption(f"Exibindo **{total_filtrado}** registro(s).")
 
1706
  else:
1707
- st.caption(f"Exibindo **{total_filtrado}** registro(s).")
 
1708
 
1709
- st.markdown("**Colunas visíveis**")
1710
- final_labels_order = list(df_disp_filtrado.columns)
1711
 
1712
- vis_key = "__cols_visiveis_labels__"
1713
- if vis_key not in st.session_state:
1714
- st.session_state[vis_key] = set(final_labels_order)
1715
- else:
1716
- st.session_state[vis_key] = {lbl for lbl in st.session_state[vis_key] if lbl in final_labels_order}
1717
- if not st.session_state[vis_key]:
1718
- st.session_state[vis_key] = set(final_labels_order)
1719
-
1720
- def render_columns_selector(title: str, labels: List[str], state_key: str):
1721
- container_supported = hasattr(st, "popover")
1722
- ctx_mgr = st.popover(title) if container_supported else st.expander(title, expanded=False)
1723
- with ctx_mgr:
1724
- st.write("Marque as colunas que deseja **exibir**:")
1725
- ac1, ac2 = st.columns(2)
1726
- if ac1.button("Selecionar tudo"):
1727
- st.session_state[state_key] = set(labels)
1728
- if ac2.button("Limpar"):
1729
- st.session_state[state_key] = set()
1730
-
1731
- left, right = st.columns(2)
1732
- half = (len(labels) + 1) // 2
1733
- for i, lbl in enumerate(labels):
1734
- col = left if i < half else right
1735
- checked = lbl in st.session_state[state_key]
1736
- new_val = col.checkbox(lbl, value=checked, key=f"__chk_col_{state_key}_{lbl}")
1737
- if new_val and lbl not in st.session_state[state_key]:
1738
- st.session_state[state_key].add(lbl)
1739
- if (not new_val) and (lbl in st.session_state[state_key]):
1740
- st.session_state[state_key].discard(lbl)
1741
-
1742
- render_columns_selector("⚙️ Definir colunas", final_labels_order, vis_key)
1743
-
1744
- visible_labels_sorted = [lbl for lbl in final_labels_order if lbl in st.session_state[vis_key]]
1745
- if not visible_labels_sorted:
1746
- st.warning("Nenhuma coluna selecionada. Selecione pelo menos uma para visualizar a tabela.")
1747
- else:
1748
- st.dataframe(df_disp_filtrado[visible_labels_sorted], use_container_width=True)
1749
-
1750
- cexp1, cexp2 = st.columns([1, 1])
1751
- csv_data = df_disp_filtrado.to_csv(index=False).encode("utf-8-sig")
1752
- cexp1.download_button("⬇️ Exportar CSV (filtrados)", data=csv_data, file_name="registros_filtrados.csv")
1753
- xlsx_all = _df_to_excel_bytes(df_disp_filtrado)
1754
- if xlsx_all:
1755
- cexp2.download_button("⬇️ Exportar Excel (filtrados)", data=xlsx_all, file_name="registros_filtrados.xlsx")
1756
  else:
1757
- cexp2.caption("Excel indisponível (openpyxl ausente).")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1758
 
1759
  # ------------------ RELATÓRIOS ------------------
1760
  with aba_rel:
@@ -1789,7 +2094,6 @@ def main():
1789
  "Aprovado": "SIM" if getattr(r, "aprovado", None) is True else ("NÃO" if getattr(r, "aprovado", None) is False else "N/A"),
1790
  "SKU": getattr(r, "qtd_sku", None),
1791
  "Tipo de Operação": getattr(r, "tipo_operacao", None),
1792
- # Removido campo "Divergência" genérico — não faz parte do layout oficial
1793
  })
1794
  dr = pd.DataFrame(rows)
1795
 
 
17
  except Exception:
18
  ALT_AVAILABLE = False
19
 
20
+ from sqlalchemy import func, or_, and_
21
+
22
  from banco import SessionLocal
23
  from models import RecebimentoRegistro
24
 
 
975
  c6.metric("Ating. total vs soma metas diárias", "—")
976
 
977
 
978
+ # ==========================================================
979
+ # Helpers — Registros (consulta completa, filtros dinâmicos, paginação)
980
+ # ==========================================================
981
+ def _sa_col_python_type(sa_col) -> type:
982
+ """Tenta inferir o tipo Python do campo SQLAlchemy; fallback para str."""
983
+ try:
984
+ return sa_col.type.python_type # nem todo dialecto implementa
985
+ except Exception:
986
+ return str
987
+
988
+ def _all_model_columns():
989
+ """Retorna lista [(nome_attr, coluna_sqlalchemy)] do modelo RecebimentoRegistro."""
990
+ return [(c.name, c) for c in RecebimentoRegistro.__table__.columns]
991
+
992
+ def _campo_to_label():
993
+ """Mapeia campo interno -> rótulo oficial (se existir), senão retorna o próprio nome do campo."""
994
+ lbl = {}
995
+ for rot, (campo, _conv) in COLUMN_MAP.items():
996
+ lbl[campo] = rot
997
+ return lbl
998
+
999
+ def _label_to_campo():
1000
+ """Mapeia rótulo -> campo interno."""
1001
+ return {rot: campo for rot, (campo, _conv) in COLUMN_MAP.items()}
1002
+
1003
+ def _make_filters_ui(db, key_prefix: str = "reg"):
1004
+ """
1005
+ Renderiza UI de filtros executiva:
1006
+ - Busca global (texto) em todos os campos textuais
1007
+ - Filtros avançados dinâmicos e tipados
1008
+ - Ordenação + paginação
1009
+ Retorna dicionário de configuração.
1010
+ """
1011
+ # Descobrir campos e tipos
1012
+ cols = _all_model_columns()
1013
+ campo_to_sa = {name: col for name, col in cols}
1014
+ campo_types = {name: _sa_col_python_type(col) for name, col in cols}
1015
+
1016
+ # Mapas de rótulos
1017
+ campo2label = _campo_to_label()
1018
+ label2campo = _label_to_campo()
1019
+
1020
+ # Lista de rótulos disponíveis (prefere OFFICIAL_COLUMNS + ID; adiciona extras existentes)
1021
+ rotulos_base = []
1022
+ if "ID" in OPTIONAL_COLUMNS:
1023
+ rotulos_base.append("ID")
1024
+ rotulos_base.extend(OFFICIAL_COLUMNS)
1025
+
1026
+ # Converter rótulos -> campos (existentes no modelo)
1027
+ campos_oficiais = []
1028
+ for rot in rotulos_base:
1029
+ if rot == "ID":
1030
+ continue
1031
+ campo = label2campo.get(rot)
1032
+ if campo and campo in campo_to_sa:
1033
+ campos_oficiais.append((rot, campo))
1034
+
1035
+ # Extras: qualquer coluna do modelo que não esteja no mapping acima
1036
+ usados = {c for _, c in campos_oficiais}
1037
+ extras = [(campo2label.get(c, c).upper(), c) for c in campo_to_sa.keys() if c not in usados]
1038
+
1039
+ # Busca global
1040
+ st.subheader("🔎 Consulta Executiva")
1041
+ global_q = st.text_input(
1042
+ "Pesquisa Global (em textos e códigos)",
1043
+ placeholder="Digite um termo para buscar em múltiplos campos (ex.: NF, Fornecedor, PO, Placa, Projeto...)",
1044
+ key=f"{key_prefix}__global_q"
1045
+ ).strip()
1046
+ global_q = global_q or None
1047
+
1048
+ # Filtros avançados (dinâmicos)
1049
+ with st.expander("⚙️ Filtros avançados por campo", expanded=False):
1050
+ # Selecionar campos para filtrar
1051
+ options_labels = [rot for rot, _c in campos_oficiais] + [rot for rot, _c in extras]
1052
+ # Remover duplicidades mantendo ordem
1053
+ seen = set()
1054
+ options_labels = [x for x in options_labels if not (x in seen or seen.add(x))]
1055
+ sel_labels = st.multiselect(
1056
+ "Escolha os campos que deseja filtrar:",
1057
+ options=options_labels,
1058
+ key=f"{key_prefix}__sel_campos"
1059
+ )
1060
+
1061
+ # Renderizar widgets para cada campo selecionado
1062
+ field_filters: Dict[str, Dict[str, Any]] = {}
1063
+ cols_grid = st.columns(2)
1064
+ for i, rot in enumerate(sel_labels):
1065
+ col = cols_grid[i % 2]
1066
+ campo = label2campo.get(rot, None)
1067
+ if not campo:
1068
+ # extras foram upper(); tente casar pelo label upper do mapping
1069
+ match = [c for c, lbl in _campo_to_label().items() if lbl.upper() == rot]
1070
+ campo = match[0] if match else (rot.lower() if rot.lower() in campo_to_sa else None)
1071
+
1072
+ if not campo or campo not in campo_to_sa:
1073
+ continue
1074
+
1075
+ py_type = campo_types.get(campo, str)
1076
+
1077
+ with col:
1078
+ st.caption(f"Filtro — **{rot}**")
1079
+
1080
+ # DATA / DATETIME
1081
+ if py_type in (date, datetime):
1082
+ d_ini = st.date_input(f"De ({rot})", value=None, key=f"{key_prefix}__f_{campo}_ini")
1083
+ d_fim = st.date_input(f"Até ({rot})", value=None, key=f"{key_prefix}__f_{campo}_fim")
1084
+ field_filters[campo] = {"op": "between_date", "ini": d_ini, "fim": d_fim}
1085
+
1086
+ # BOOLEAN
1087
+ elif py_type is bool:
1088
+ val = st.selectbox(
1089
+ rot,
1090
+ options=["Todos", "SIM", "NÃO"],
1091
+ key=f"{key_prefix}__f_{campo}_bool"
1092
+ )
1093
+ field_filters[campo] = {"op": "eq_bool", "value": None if val == "Todos" else (val == "SIM")}
1094
+
1095
+ # NUMÉRICO
1096
+ elif py_type in (int, float):
1097
+ c1, c2 = st.columns(2)
1098
+ vmin = c1.number_input(f"Mín ({rot})", value=0.0 if py_type is float else 0, key=f"{key_prefix}__f_{campo}_min")
1099
+ vmax = c2.number_input(f"Máx ({rot})", value=0.0 if py_type is float else 0, key=f"{key_prefix}__f_{campo}_max")
1100
+ field_filters[campo] = {"op": "between_num", "min": vmin, "max": vmax, "is_float": py_type is float}
1101
+
1102
+ # TEXTO (inclui horas em 'HH:MM:SS' e códigos)
1103
+ else:
1104
+ term = st.text_input(f"Contém ({rot})", value="", key=f"{key_prefix}__f_{campo}_txt").strip()
1105
+ if term != "":
1106
+ field_filters[campo] = {"op": "contains", "value": term}
1107
+
1108
+ # Ordenação e Paginação (básicos; primeira/última/ir p/ página serão aplicados depois de saber o total)
1109
+ st.divider()
1110
+ st.subheader("📑 Ordenação & Paginação")
1111
+
1112
+ # Lista de campos ordenáveis (todos)
1113
+ sort_labels = [rot for rot, _ in campos_oficiais] + [rot for rot, _ in extras]
1114
+ # remover duplicatas mantendo ordem
1115
+ seen_s = set()
1116
+ sort_labels = [x for x in sort_labels if not (x in seen_s or seen_s.add(x))]
1117
+
1118
+ sort_sel = st.selectbox(
1119
+ "Ordenar por",
1120
+ options=sort_labels,
1121
+ index=sort_labels.index("DATA") if "DATA" in sort_labels else 0,
1122
+ key=f"{key_prefix}__sort_label"
1123
+ )
1124
+
1125
+ sort_campo = label2campo.get(sort_sel)
1126
+ if not sort_campo:
1127
+ # extras: tentar inferir pelo label uppercase
1128
+ m = [c for c, lbl in _campo_to_label().items() if lbl.upper() == sort_sel]
1129
+ sort_campo = m[0] if m else (sort_sel.lower() if sort_sel.lower() in campo_to_sa else None)
1130
+ sort_dir = st.radio("Direção", options=["Ascendente", "Descendente"], horizontal=True, key=f"{key_prefix}__sort_dir")
1131
+ sort_dir = "asc" if sort_dir == "Ascendente" else "desc"
1132
+
1133
+ cpg1, cpg2, cpg3 = st.columns([1, 1, 2])
1134
+ page_size = cpg1.selectbox("Registros por página", options=[50, 100, 250, 1000], index=1, key=f"{key_prefix}__page_size")
1135
+ # Estado da página
1136
+ page_state_key = f"{key_prefix}__page_idx"
1137
+ if page_state_key not in st.session_state:
1138
+ st.session_state[page_state_key] = 0
1139
+ # Botões navegação básicos
1140
+ prev = cpg2.button("⬅️ Anterior", key=f"{key_prefix}__prev")
1141
+ next_ = cpg3.button("Próxima ➡️", key=f"{key_prefix}__next")
1142
+ if prev and st.session_state[page_state_key] > 0:
1143
+ st.session_state[page_state_key] -= 1
1144
+ if next_:
1145
+ st.session_state[page_state_key] += 1
1146
+
1147
+ return {
1148
+ "global_q": global_q,
1149
+ "field_filters": field_filters if len(field_filters) > 0 else {},
1150
+ "sort_field": sort_campo,
1151
+ "sort_dir": sort_dir,
1152
+ "page_size": int(page_size),
1153
+ "page_idx": int(st.session_state[page_state_key]),
1154
+ "page_state_key": page_state_key,
1155
+ "sort_labels": sort_labels
1156
+ }
1157
+
1158
+ def _apply_filters_build_query(db, fcfg):
1159
+ """Monta query SQLAlchemy com filtros, ordenação e retorna (query, total_count)."""
1160
+ q = db.query(RecebimentoRegistro)
1161
+
1162
+ # Tipos por coluna
1163
+ campo_to_sa = {c.name: c for c in RecebimentoRegistro.__table__.columns}
1164
+ campo_types = {name: _sa_col_python_type(col) for name, col in campo_to_sa.items()}
1165
+
1166
+ # Filtro global (aplica em campos textuais)
1167
+ if fcfg["global_q"]:
1168
+ g = f"%{fcfg['global_q']}%"
1169
+ ors = []
1170
+ for campo, sa_col in campo_to_sa.items():
1171
+ py_t = campo_types.get(campo, str)
1172
+ if py_t in (str,):
1173
+ try:
1174
+ ors.append(sa_col.ilike(g))
1175
+ except Exception:
1176
+ # alguns tipos não suportam ilike nativamente
1177
+ pass
1178
+ if ors:
1179
+ q = q.filter(or_(*ors))
1180
+
1181
+ # Filtros por campo
1182
+ for campo, spec in (fcfg["field_filters"] or {}).items():
1183
+ col = campo_to_sa.get(campo)
1184
+ if col is None:
1185
+ continue
1186
+ op = spec.get("op")
1187
+ if op == "between_date":
1188
+ d_ini = spec.get("ini")
1189
+ d_fim = spec.get("fim")
1190
+ ands = []
1191
+ if d_ini:
1192
+ ands.append(col >= d_ini)
1193
+ if d_fim:
1194
+ ands.append(col <= d_fim)
1195
+ if ands:
1196
+ q = q.filter(and_(*ands))
1197
+ elif op == "eq_bool":
1198
+ val = spec.get("value", None)
1199
+ if val is True or val is False:
1200
+ q = q.filter(col == val)
1201
+ elif op == "between_num":
1202
+ vmin = spec.get("min", None)
1203
+ vmax = spec.get("max", None)
1204
+ ands = []
1205
+ if vmin is not None:
1206
+ ands.append(col >= vmin)
1207
+ if vmax is not None:
1208
+ ands.append(col <= vmax)
1209
+ if ands:
1210
+ q = q.filter(and_(*ands))
1211
+ elif op == "contains":
1212
+ term = spec.get("value")
1213
+ if term:
1214
+ try:
1215
+ q = q.filter(col.ilike(f"%{term}%"))
1216
+ except Exception:
1217
+ pass
1218
+
1219
+ # Total filtrado
1220
+ total = q.with_entities(func.count(RecebimentoRegistro.id)).scalar() or 0
1221
+
1222
+ # Ordenação
1223
+ sort_field = fcfg.get("sort_field")
1224
+ sort_dir = fcfg.get("sort_dir", "asc")
1225
+ if sort_field and sort_field in campo_to_sa:
1226
+ col = campo_to_sa[sort_field]
1227
+ q = q.order_by(col.asc() if sort_dir == "asc" else col.desc())
1228
+ else:
1229
+ q = q.order_by(RecebimentoRegistro.created_at.desc())
1230
+
1231
+ return q, int(total)
1232
+
1233
+ def _query_paginated(q, page_idx: int, page_size: int):
1234
+ """Aplica paginação server-side."""
1235
+ if page_idx < 0:
1236
+ page_idx = 0
1237
+ return q.offset(page_idx * page_size).limit(page_size)
1238
+
1239
+ def _rows_to_dataframe(rows: List[RecebimentoRegistro]) -> pd.DataFrame:
1240
+ if not rows:
1241
+ return pd.DataFrame()
1242
+ return pd.DataFrame([{c.name: getattr(r, c.name) for c in r.__table__.columns} for r in rows])
1243
+
1244
+ def _format_display_df(df_base: pd.DataFrame) -> pd.DataFrame:
1245
+ """
1246
+ Constrói DF de exibição com rótulos oficiais, preservando ordem executiva.
1247
+ Adiciona extras ao final.
1248
+ """
1249
+ if df_base.empty:
1250
+ return df_base
1251
+
1252
+ # Rótulo -> campo e vice-versa
1253
+ label2campo = _label_to_campo()
1254
+
1255
+ # Monta DF de exibição com rótulos em ordem oficial
1256
+ df_disp = pd.DataFrame()
1257
+ # ID (opcional)
1258
+ if "id_planilha" in df_base.columns:
1259
+ df_disp["ID"] = df_base["id_planilha"]
1260
+
1261
+ # Oficiais
1262
+ for rot in OFFICIAL_COLUMNS:
1263
+ campo = label2campo.get(rot)
1264
+ if campo and campo in df_base.columns:
1265
+ df_disp[rot] = df_base[campo]
1266
+
1267
+ # Extras
1268
+ usados = {"id_planilha"}
1269
+ usados.update([label2campo.get(rot) for rot in OFFICIAL_COLUMNS if label2campo.get(rot)])
1270
+ for extra in df_base.columns:
1271
+ if extra not in usados:
1272
+ df_disp[extra] = df_base[extra]
1273
+
1274
+ # Normalizar datas na exibição
1275
+ if "DATA" in df_disp.columns:
1276
+ df_disp["DATA"] = pd.to_datetime(df_disp["DATA"], errors="coerce").dt.date
1277
+
1278
+ return df_disp
1279
+
1280
+
1281
  # ==========================================================
1282
  # Helpers de UI (preview + processamento)
1283
  # ==========================================================
 
1440
  return {
1441
  "id_planilha": (payload.get("id_planilha") or _next_id_planilha()),
1442
 
1443
+ "DATA": None, # apenas para referência visual no form (será filtrado no _filter_to_model)
1444
  "data": data,
1445
  "data_emissao": data_emissao,
1446
  "nota_fiscal": nf,
 
1901
  st.session_state["__idx_iguais_db__"] = []
1902
  st.info("Prévia descartada. Clique em **⚙️ Processar agora** para gerar novamente.")
1903
 
1904
+ # ------------------ REGISTROS (CONSULTA COMPLETA) ------------------
1905
  with aba_reg:
1906
+ st.header("Registros Consulta Completa")
1907
 
1908
  db = _get_db()
1909
  try:
1910
+ # Filtros e paginação (UI)
1911
+ fcfg = _make_filters_ui(db, key_prefix="reg")
1912
+
1913
+ # Montar query com filtros
1914
+ q, total = _apply_filters_build_query(db, fcfg)
1915
+
1916
+ # Cálculo de total de páginas
1917
+ total_pages = max(1, (total + fcfg["page_size"] - 1) // fcfg["page_size"])
1918
+
1919
+ # Navegação aprimorada: Primeira/Última/Ir para página
1920
+ st.markdown("### 🧭 Navegação")
1921
+ cnav1, cnav2, cnav3, cnav4 = st.columns([1, 1, 2, 1])
1922
+ first = cnav1.button("⏮️ Primeira", key="reg__first")
1923
+ last = cnav2.button("Última ⏭️", key="reg__last")
1924
+ goto_val = cnav3.number_input(
1925
+ "Ir para página",
1926
+ min_value=1, max_value=total_pages,
1927
+ value=min(fcfg['page_idx'] + 1, total_pages),
1928
+ step=1, key="reg__goto"
1929
  )
1930
+ go_btn = cnav4.button("Ir", key="reg__go")
1931
+
1932
+ # Ajustes de navegação
1933
+ if first:
1934
+ fcfg["page_idx"] = 0
1935
+ st.session_state[fcfg["page_state_key"]] = 0
1936
+ if last:
1937
+ fcfg["page_idx"] = total_pages - 1
1938
+ st.session_state[fcfg["page_state_key"]] = total_pages - 1
1939
+ if go_btn:
1940
+ new_idx = max(0, min(total_pages - 1, int(goto_val) - 1))
1941
+ fcfg["page_idx"] = new_idx
1942
+ st.session_state[fcfg["page_state_key"]] = new_idx
1943
+
1944
+ # Corrigir índice de página caso ultrapasse o total
1945
+ if fcfg["page_idx"] >= total_pages:
1946
+ fcfg["page_idx"] = max(0, total_pages - 1)
1947
+ st.session_state[fcfg["page_state_key"]] = fcfg["page_idx"]
1948
+
1949
+ # Buscar página
1950
+ qp = _query_paginated(q, fcfg["page_idx"], fcfg["page_size"])
1951
+ rows = qp.all()
1952
  finally:
1953
  db.close()
1954
 
1955
+ df_base = _rows_to_dataframe(rows)
1956
+ df_disp = _format_display_df(df_base)
 
 
 
 
 
 
 
 
 
 
1957
 
1958
+ # KPIs executivos
1959
+ cK1, cK2, cK3, cK4 = st.columns(4)
1960
+ cK1.metric("Total filtrado", value=f"{total:,}".replace(",", "."))
1961
+ cK2.metric("Página atual", value=f"{fcfg['page_idx']+1}/{total_pages}")
1962
+ if "DATA" in df_disp.columns:
1963
+ datas_validas = pd.to_datetime(df_disp["DATA"], errors="coerce").dt.date.dropna()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1964
  if not datas_validas.empty:
1965
+ cK3.metric("1ª Data", value=str(datas_validas.min()))
1966
+ cK4.metric("Últ. Data", value=str(datas_validas.max()))
 
 
1967
  else:
1968
+ cK3.metric(" Data", value="—")
1969
+ cK4.metric("Últ. Data", value="—")
1970
  else:
1971
+ cK3.metric(" Data", value="—")
1972
+ cK4.metric("Últ. Data", value="—")
1973
 
1974
+ st.divider()
 
1975
 
1976
+ # Seletor de colunas visíveis (labels)
1977
+ if df_disp.empty:
1978
+ st.info("Nenhum registro para os filtros aplicados.")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1979
  else:
1980
+ st.markdown("**Colunas visíveis**")
1981
+ all_labels = list(df_disp.columns)
1982
+ vis_key = "__cols_visiveis_labels__"
1983
+ if vis_key not in st.session_state:
1984
+ st.session_state[vis_key] = set(all_labels)
1985
+ else:
1986
+ # saneamento se schema mudar
1987
+ st.session_state[vis_key] = {lbl for lbl in st.session_state[vis_key] if lbl in all_labels}
1988
+ if not st.session_state[vis_key]:
1989
+ st.session_state[vis_key] = set(all_labels)
1990
+
1991
+ def render_columns_selector(title: str, labels: List[str], state_key: str):
1992
+ container_supported = hasattr(st, "popover")
1993
+ ctx_mgr = st.popover(title) if container_supported else st.expander(title, expanded=False)
1994
+ with ctx_mgr:
1995
+ st.write("Marque as colunas que deseja **exibir**:")
1996
+ ac1, ac2 = st.columns(2)
1997
+ if ac1.button("Selecionar tudo", key="__btn_sel_all_cols__"):
1998
+ st.session_state[state_key] = set(labels)
1999
+ if ac2.button("Limpar", key="__btn_clear_cols__"):
2000
+ st.session_state[state_key] = set()
2001
+
2002
+ left, right = st.columns(2)
2003
+ half = (len(labels) + 1) // 2
2004
+ for i, lbl in enumerate(labels):
2005
+ col = left if i < half else right
2006
+ checked = lbl in st.session_state[state_key]
2007
+ new_val = col.checkbox(lbl, value=checked, key=f"__chk_col_{state_key}_{lbl}")
2008
+ if new_val and lbl not in st.session_state[state_key]:
2009
+ st.session_state[state_key].add(lbl)
2010
+ if (not new_val) and (lbl in st.session_state[state_key]):
2011
+ st.session_state[state_key].discard(lbl)
2012
+
2013
+ render_columns_selector("⚙️ Definir colunas", all_labels, vis_key)
2014
+ visible_labels_sorted = [lbl for lbl in all_labels if lbl in st.session_state[vis_key]]
2015
+
2016
+ st.dataframe(
2017
+ df_disp[visible_labels_sorted],
2018
+ use_container_width=True,
2019
+ hide_index=True
2020
+ )
2021
+
2022
+ # Exportações — página e todos filtrados
2023
+ st.divider()
2024
+ cexp1, cexp2, cexp3 = st.columns([1, 1, 2])
2025
+
2026
+ # Exportar somente a página atual
2027
+ csv_page = df_disp[visible_labels_sorted].to_csv(index=False).encode("utf-8-sig")
2028
+ cexp1.download_button("⬇️ CSV (página)", data=csv_page, file_name="registros_pagina.csv")
2029
+
2030
+ xlsx_page = _df_to_excel_bytes(df_disp[visible_labels_sorted])
2031
+ if xlsx_page:
2032
+ cexp2.download_button("⬇️ Excel (página)", data=xlsx_page, file_name="registros_pagina.xlsx")
2033
+ else:
2034
+ cexp2.caption("Excel indisponível (openpyxl ausente).")
2035
+
2036
+ # Exportar TODOS os filtrados (sem paginação)
2037
+ with cexp3:
2038
+ st.caption("Exportar **todos** os registros filtrados")
2039
+ do_export_all = st.button("⬇️ Gerar arquivo completo", key="__btn_export_all__")
2040
+ if do_export_all:
2041
+ db = _get_db()
2042
+ try:
2043
+ q_all, _tot = _apply_filters_build_query(db, fcfg)
2044
+ # CUIDADO com bases muito grandes
2045
+ MAX_EXCEL = 150_000 # limite de segurança p/ Excel
2046
+ rows_all = q_all.all()
2047
+ df_all_base = _rows_to_dataframe(rows_all)
2048
+ df_all_disp = _format_display_df(df_all_base)
2049
+
2050
+ csv_all = df_all_disp.to_csv(index=False).encode("utf-8-sig")
2051
+ st.download_button("⬇️ Baixar CSV (completo)", data=csv_all, file_name="registros_filtrados_completo.csv", key="__dl_csv_all__")
2052
+
2053
+ if len(df_all_disp) <= MAX_EXCEL:
2054
+ xlsx_all = _df_to_excel_bytes(df_all_disp)
2055
+ if xlsx_all:
2056
+ st.download_button("⬇️ Baixar Excel (completo)", data=xlsx_all, file_name="registros_filtrados_completo.xlsx", key="__dl_xlsx_all__")
2057
+ else:
2058
+ st.caption("Excel indisponível (openpyxl ausente).")
2059
+ else:
2060
+ st.warning(f"A exportação completa em Excel foi limitada a {MAX_EXCEL:,} linhas. Use o CSV para volumes maiores.")
2061
+ finally:
2062
+ db.close()
2063
 
2064
  # ------------------ RELATÓRIOS ------------------
2065
  with aba_rel:
 
2094
  "Aprovado": "SIM" if getattr(r, "aprovado", None) is True else ("NÃO" if getattr(r, "aprovado", None) is False else "N/A"),
2095
  "SKU": getattr(r, "qtd_sku", None),
2096
  "Tipo de Operação": getattr(r, "tipo_operacao", None),
 
2097
  })
2098
  dr = pd.DataFrame(rows)
2099