ArtStones commited on
Commit
31c6dcb
·
verified ·
1 Parent(s): 0747a14

Update src/streamlit_app.py

Browse files
Files changed (1) hide show
  1. src/streamlit_app.py +138 -260
src/streamlit_app.py CHANGED
@@ -1,268 +1,146 @@
1
  import re
2
- from io import BytesIO
3
- from datetime import date
4
 
5
- import streamlit as st
6
- from docx import Document
7
- from docx.shared import Inches, Pt
8
- from docx.enum.text import WD_ALIGN_PARAGRAPH
9
- from docx.oxml import OxmlElement
10
- from docx.oxml.ns import qn
11
 
12
- # -----------------------------
13
- # Config
14
- # -----------------------------
15
- st.set_page_config(page_title="Checklist de Remessa", layout="centered")
16
 
17
- CLIENTE_FIXO = "ArtStones"
18
- CHAVE_RE = re.compile(r"\b(\d{44})\b")
19
 
 
20
 
21
- def extrair_chaves(texto: str) -> list[str]:
22
- if not texto:
23
- return []
24
- return CHAVE_RE.findall(texto)
25
 
26
-
27
- def listar_duplicadas(lista: list[str]) -> list[str]:
 
28
  seen = set()
29
- dups = []
30
- for x in lista:
31
- if x in seen and x not in dups:
32
- dups.append(x)
33
- seen.add(x)
34
- return dups
35
-
36
-
37
- def chunk_3cols(items: list[str]) -> tuple[list[str], list[str], list[str]]:
38
- """Divide em 3 colunas o mais equilibrado possível."""
39
- n = len(items)
40
- if n == 0:
41
- return [], [], []
42
- a = (n + 2) // 3 # ceil(n/3)
43
- b = (n + 1) // 3
44
- col1 = items[:a]
45
- col2 = items[a : a + b]
46
- col3 = items[a + b :]
47
- return col1, col2, col3
48
-
49
-
50
- def set_cell(cell, title: str, value: str = "", font_size=11, bold_title=True, align="center"):
51
- """
52
- Escreve título + valor na célula e centraliza (bonitinho).
53
- """
54
- cell.text = ""
55
- p = cell.paragraphs[0]
56
- p.alignment = {
57
- "left": WD_ALIGN_PARAGRAPH.LEFT,
58
- "center": WD_ALIGN_PARAGRAPH.CENTER,
59
- "right": WD_ALIGN_PARAGRAPH.RIGHT,
60
- }[align]
61
-
62
- r1 = p.add_run(title + ("\n" if value else ""))
63
- r1.bold = bold_title
64
- r1.font.size = Pt(font_size)
65
- r1.font.name = "Calibri"
66
- r1._element.rPr.rFonts.set(qn("w:eastAsia"), "Calibri")
67
-
68
- if value:
69
- r2 = p.add_run(value)
70
- r2.bold = False
71
- r2.font.size = Pt(font_size)
72
- r2.font.name = "Calibri"
73
- r2._element.rPr.rFonts.set(qn("w:eastAsia"), "Calibri")
74
-
75
-
76
- def set_cell_lines(cell, header: str, lines: list[str], font_size=9, align="center"):
77
- """
78
- Cabeçalho + lista de chaves em linhas. Alinhamento central para ficar como o modelo.
79
- """
80
- cell.text = ""
81
- p = cell.paragraphs[0]
82
- p.alignment = {
83
- "left": WD_ALIGN_PARAGRAPH.LEFT,
84
- "center": WD_ALIGN_PARAGRAPH.CENTER,
85
- "right": WD_ALIGN_PARAGRAPH.RIGHT,
86
- }[align]
87
-
88
- rh = p.add_run(header + ("\n" if lines else ""))
89
- rh.bold = True
90
- rh.font.size = Pt(10)
91
- rh.font.name = "Calibri"
92
- rh._element.rPr.rFonts.set(qn("w:eastAsia"), "Calibri")
93
-
94
- if lines:
95
- rl = p.add_run("\n".join(lines))
96
- rl.bold = False
97
- rl.font.size = Pt(font_size)
98
- rl.font.name = "Calibri"
99
- rl._element.rPr.rFonts.set(qn("w:eastAsia"), "Calibri")
100
-
101
-
102
- def remove_table_borders(table):
103
- tbl = table._tbl
104
- tblPr = tbl.tblPr
105
- borders = OxmlElement("w:tblBorders")
106
- for edge in ("top", "left", "bottom", "right", "insideH", "insideV"):
107
- elem = OxmlElement(f"w:{edge}")
108
- elem.set(qn("w:val"), "nil")
109
- borders.append(elem)
110
- tblPr.append(borders)
111
-
112
-
113
- def build_docx_layout_modelo(cliente: str, data_coleta: str, hora: str, eco: list[str], rap: list[str]) -> bytes:
114
- doc = Document()
115
-
116
- # Margens
117
- section = doc.sections[0]
118
- section.top_margin = Inches(0.55)
119
- section.bottom_margin = Inches(0.55)
120
- section.left_margin = Inches(0.55)
121
- section.right_margin = Inches(0.55)
122
-
123
- # Tabela principal (igual ao modelo)
124
- table = doc.add_table(rows=4, cols=5)
125
- table.style = "Table Grid"
126
-
127
- # --- Linha 0 (cabeçalho) ---
128
- # CLIENTE (col 0), DATA (mescla col 1-3), HORA (col 4)
129
- cell_cliente = table.cell(0, 0)
130
- cell_data = table.cell(0, 1).merge(table.cell(0, 2)).merge(table.cell(0, 3))
131
- cell_hora = table.cell(0, 4)
132
-
133
- set_cell(cell_cliente, "CLIENTE:", cliente, font_size=11, bold_title=True, align="center")
134
- set_cell(cell_data, "DATA DA COLETA:", data_coleta, font_size=11, bold_title=True, align="center")
135
- set_cell(cell_hora, "HORA DA COLETA:", hora if hora else "_____ : _____", font_size=11, bold_title=True, align="center")
136
-
137
- # --- Linha 1 (ECONÔMICO) ---
138
- eco_left = table.cell(1, 0).merge(table.cell(1, 1))
139
- eco_mid = table.cell(1, 2)
140
- eco_right = table.cell(1, 3).merge(table.cell(1, 4))
141
-
142
- e1, e2, e3 = chunk_3cols(eco)
143
- set_cell_lines(eco_left, f"ECONÔMICO = {len(eco)}", e1, font_size=9, align="center")
144
- set_cell_lines(eco_mid, "", e2, font_size=9, align="center")
145
- set_cell_lines(eco_right, "", e3, font_size=9, align="center")
146
-
147
- # --- Linha 2 (RÁPIDO) ---
148
- rap_left = table.cell(2, 0).merge(table.cell(2, 1))
149
- rap_mid = table.cell(2, 2)
150
- rap_right = table.cell(2, 3).merge(table.cell(2, 4))
151
-
152
- r1, r2, r3 = chunk_3cols(rap)
153
- set_cell_lines(rap_left, f"RÁPIDO = {len(rap)}", r1, font_size=9, align="center")
154
- set_cell_lines(rap_mid, "", r2, font_size=9, align="center")
155
- set_cell_lines(rap_right, "", r3, font_size=9, align="center")
156
-
157
- # --- Linha 3 (TOTAL) ---
158
- total_cell = table.cell(3, 0)
159
- for c in range(1, 5):
160
- total_cell = total_cell.merge(table.cell(3, c))
161
- set_cell(total_cell, "TOTAL DA REMESSA:", f"{len(eco) + len(rap)} VOLUMES", font_size=11, bold_title=True, align="center")
162
-
163
- # Espaço + assinaturas (centralizadas)
164
- doc.add_paragraph("\n")
165
-
166
- sign = doc.add_table(rows=2, cols=2)
167
- remove_table_borders(sign)
168
-
169
- set_cell(sign.cell(0, 0), "", "______________________________", font_size=11, bold_title=False, align="center")
170
- set_cell(sign.cell(0, 1), "", "______________________________", font_size=11, bold_title=False, align="center")
171
- set_cell(sign.cell(1, 0), "ASSINATURA DO REPRESENTANTE", "", font_size=10, bold_title=True, align="center")
172
- set_cell(sign.cell(1, 1), "ASSINATURA DO MOTORISTA", "", font_size=10, bold_title=True, align="center")
173
-
174
- bio = BytesIO()
175
- doc.save(bio)
176
- return bio.getvalue()
177
-
178
-
179
- # -----------------------------
180
- # Estado
181
- # -----------------------------
182
- if "eco" not in st.session_state:
183
- st.session_state.eco = []
184
- if "rap" not in st.session_state:
185
- st.session_state.rap = []
186
-
187
- # -----------------------------
188
- # UI
189
- # -----------------------------
190
- st.title("📋📦 Checklist de Remessa (Econômico + Rápido)")
191
- st.caption(f"Cliente: {CLIENTE_FIXO}")
192
-
193
- data_coleta = st.text_input("Data da coleta:", value=date.today().strftime("%d/%m/%Y"))
194
- hora = st.text_input("Hora da coleta:", value="_____ : _____")
195
-
196
- modo = st.radio("Tipo de frete atual:", ["ECONÔMICO", "RÁPIDO"], horizontal=True)
197
-
198
-
199
- def adicionar_scan():
200
- raw = st.session_state.scan_input.strip()
201
- st.session_state.scan_input = ""
202
-
203
- chaves = extrair_chaves(raw)
204
- if not chaves:
205
- st.warning("Nenhuma chave (44 dígitos) detectada nesse scan.")
206
- return
207
-
208
- destino = st.session_state.eco if modo == "ECONÔMICO" else st.session_state.rap
209
- destino.extend(chaves)
210
-
211
-
212
- st.text_input(
213
- "Bipe aqui (o scanner já quebra linha):",
214
- key="scan_input",
215
- on_change=adicionar_scan,
216
- placeholder="Bipe/cole a chave de 44 dígitos…",
217
- )
218
-
219
- eco = st.session_state.eco
220
- rap = st.session_state.rap
221
- total = len(eco) + len(rap)
222
-
223
- c1, c2, c3 = st.columns(3)
224
- c1.success(f"🟩 Econ.: {len(eco)}")
225
- c2.info(f"🟦 Ráp.: {len(rap)}")
226
- c3.warning(f"🚚 Total: {total}")
227
-
228
- dups_eco = listar_duplicadas(eco)
229
- dups_rap = listar_duplicadas(rap)
230
- if dups_eco or dups_rap:
231
- with st.expander("⚠️ Ver duplicadas"):
232
- if dups_eco:
233
- st.write(f"Econômico ({len(dups_eco)}):")
234
- st.code("\n".join(dups_eco[:200]))
235
- if dups_rap:
236
- st.write(f"Rápido ({len(dups_rap)}):")
237
- st.code("\n".join(dups_rap[:200]))
238
-
239
- st.divider()
240
-
241
- doc_bytes = build_docx_layout_modelo(CLIENTE_FIXO, data_coleta, hora, eco, rap)
242
-
243
- # Nome padronizado: "Xml Mandae - DD.MM.AAAA.docx"
244
- data_nome = data_coleta.replace("/", ".")
245
- nome_arquivo = f"Xml Mandae - {data_nome}.docx"
246
-
247
- st.download_button(
248
- "⬇️ Baixar checklist (.docx) para assinatura",
249
- data=doc_bytes,
250
- file_name=nome_arquivo,
251
- mime="application/vnd.openxmlformats-officedocument.wordprocessingml.document",
252
- )
253
-
254
- colR1, colR2 = st.columns(2)
255
- with colR1:
256
- if st.button("🧹 Zerar tudo"):
257
- st.session_state.eco = []
258
- st.session_state.rap = []
259
- st.rerun()
260
-
261
- with colR2:
262
- if st.button("↩️ Remover última chave do modo atual"):
263
- if modo == "ECONÔMICO" and st.session_state.eco:
264
- st.session_state.eco.pop()
265
- st.rerun()
266
- if modo == "RÁPIDO" and st.session_state.rap:
267
- st.session_state.rap.pop()
268
- st.rerun()
 
1
  import re
2
+ from datetime import datetime
3
+ from typing import List
4
 
5
+ import gradio as gr
6
+ from pypdf import PdfReader
 
 
 
 
7
 
8
+ from reportlab.lib.pagesizes import A4
9
+ from reportlab.pdfgen import canvas
10
+ from reportlab.lib.units import mm
 
11
 
 
 
12
 
13
+ KEY_RE = re.compile(r"\b\d{44}\b")
14
 
 
 
 
 
15
 
16
+ def extract_keys_from_pdf(pdf_path: str) -> List[str]:
17
+ reader = PdfReader(pdf_path)
18
+ found: List[str] = []
19
  seen = set()
20
+
21
+ for page in reader.pages:
22
+ text = page.extract_text() or ""
23
+ for k in KEY_RE.findall(text):
24
+ if k not in seen:
25
+ seen.add(k)
26
+ found.append(k)
27
+ return found
28
+
29
+
30
+ def safe_date_default() -> str:
31
+ return datetime.now().strftime("%d/%m/%Y")
32
+
33
+
34
+ def render_print_pdf(out_path: str, data_coleta: str, hora_coleta: str, keys: List[str]):
35
+ c = canvas.Canvas(out_path, pagesize=A4)
36
+ w, h = A4
37
+
38
+ left = 20 * mm
39
+ top = h - 20 * mm
40
+ line_h = 6.2 * mm
41
+
42
+ def draw(text: str, y: float, bold=False, size=11) -> float:
43
+ c.setFont("Helvetica-Bold" if bold else "Helvetica", size)
44
+ c.drawString(left, y, text)
45
+ return y - line_h
46
+
47
+ y = top
48
+
49
+ # Cabeçalho
50
+ y = draw("CLIENTE:", y)
51
+ y -= line_h * 1.3
52
+
53
+ y = draw("DATA DA COLETA:", y)
54
+ y = draw(data_coleta, y)
55
+ y -= line_h * 0.6
56
+
57
+ y = draw("HORA DA COLETA:", y)
58
+ y = draw("_____ : _____", y)
59
+ y -= line_h * 0.9
60
+
61
+ # ===== ALTERAÇÃO AQUI =====
62
+ y = draw("CHAVES DE ACESSO:", y)
63
+ y -= line_h * 0.2
64
+ # =========================
65
+
66
+ c.setFont("Helvetica", 10.8)
67
+ for k in keys:
68
+ if y < 40 * mm:
69
+ c.showPage()
70
+ y = top
71
+ c.setFont("Helvetica", 10.8)
72
+ c.drawString(left, y, k)
73
+ y -= line_h * 0.85
74
+
75
+ y -= line_h * 0.8
76
+ y = draw(f"TOTAL DA REMESSA: {len(keys)} VOLUMES", y)
77
+
78
+ # Assinaturas
79
+ if y < 55 * mm:
80
+ c.showPage()
81
+
82
+ y_sig = 25 * mm
83
+ c.setFont("Helvetica", 9.5)
84
+ c.drawString(left + 70 * mm, y_sig + 12, "ASSINATURA DO REPRESENTANTE")
85
+ c.drawString(left + 140 * mm, y_sig + 12, "ASSINATURA DO MOTORISTA")
86
+ c.line(left + 55 * mm, y_sig + 10, left + 118 * mm, y_sig + 10)
87
+ c.line(left + 130 * mm, y_sig + 10, left + 193 * mm, y_sig + 10)
88
+
89
+ c.save()
90
+
91
+
92
+ def build_txt(out_path: str, keys: List[str]):
93
+ with open(out_path, "w", encoding="utf-8") as f:
94
+ f.write("\n".join(keys))
95
+ f.write("\n")
96
+
97
+
98
+ def run(pdf_file, data_coleta, hora_coleta):
99
+ if pdf_file is None:
100
+ raise gr.Error("Envie um PDF.")
101
+
102
+ keys = extract_keys_from_pdf(pdf_file)
103
+ if not keys:
104
+ raise gr.Error("Não encontrei chaves de 44 dígitos no PDF.")
105
+
106
+ ts = datetime.now().strftime("%Y%m%d-%H%M%S")
107
+ out_pdf = f"chaves_prontas_{ts}.pdf"
108
+ out_txt = f"chaves_{ts}.txt"
109
+
110
+ render_print_pdf(
111
+ out_pdf,
112
+ data_coleta=data_coleta or safe_date_default(),
113
+ hora_coleta=hora_coleta or "_____ : _____",
114
+ keys=keys,
115
+ )
116
+ build_txt(out_txt, keys)
117
+
118
+ preview = (
119
+ f"Total extraído: {len(keys)}\n\n"
120
+ "Primeiras chaves:\n" + "\n".join(keys[:10])
121
+ )
122
+ return preview, out_pdf, out_txt
123
+
124
+
125
+ with gr.Blocks(title="Extrator de Chaves NF-e") as demo:
126
+ gr.Markdown("### Envie um PDF e gere um arquivo pronto para imprimir.")
127
+
128
+ with gr.Row():
129
+ pdf = gr.File(label="PDF", file_types=[".pdf"])
130
+ preview = gr.Textbox(label="Prévia", lines=12)
131
+
132
+ with gr.Row():
133
+ data_coleta = gr.Textbox(label="Data da coleta", value=safe_date_default())
134
+ hora_coleta = gr.Textbox(label="Hora da coleta", value="_____ : _____")
135
+
136
+ btn = gr.Button("Gerar arquivos")
137
+ out_pdf = gr.File(label="PDF pronto para imprimir")
138
+ out_txt = gr.File(label="TXT (opcional)")
139
+
140
+ btn.click(
141
+ fn=run,
142
+ inputs=[pdf, data_coleta, hora_coleta],
143
+ outputs=[preview, out_pdf, out_txt],
144
+ )
145
+
146
+ demo.launch()