pyflutter-app 0.3.0__tar.gz → 0.3.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {pyflutter_app-0.3.0 → pyflutter_app-0.3.1}/PKG-INFO +1 -1
- {pyflutter_app-0.3.0 → pyflutter_app-0.3.1}/cartas/common.py +7 -0
- {pyflutter_app-0.3.0 → pyflutter_app-0.3.1}/converter/page_forms/config.py +6 -6
- {pyflutter_app-0.3.0 → pyflutter_app-0.3.1}/converter/page_forms/controller_forms.py +143 -9
- pyflutter_app-0.3.1/converter/page_forms/master_forms.py +113 -0
- {pyflutter_app-0.3.0 → pyflutter_app-0.3.1}/converter/page_forms/model_forms.py +124 -10
- pyflutter_app-0.3.1/converter/page_forms/view_forms.py +164 -0
- {pyflutter_app-0.3.0 → pyflutter_app-0.3.1}/ocrlib/relatorio_ocr.py +11 -0
- {pyflutter_app-0.3.0 → pyflutter_app-0.3.1}/ocrlib/table_ocr.py +12 -2
- {pyflutter_app-0.3.0 → pyflutter_app-0.3.1}/pyproject.toml +1 -1
- {pyflutter_app-0.3.0 → pyflutter_app-0.3.1}/test.py +1 -0
- pyflutter_app-0.3.1/update.sh +5 -0
- {pyflutter_app-0.3.0 → pyflutter_app-0.3.1}/uv.lock +1 -1
- pyflutter_app-0.3.0/converter/page_forms/view_forms.py +0 -104
- {pyflutter_app-0.3.0 → pyflutter_app-0.3.1}/.gitignore +0 -0
- {pyflutter_app-0.3.0 → pyflutter_app-0.3.1}/.idea/.gitignore +0 -0
- {pyflutter_app-0.3.0 → pyflutter_app-0.3.1}/.idea/inspectionProfiles/Project_Default.xml +0 -0
- {pyflutter_app-0.3.0 → pyflutter_app-0.3.1}/.idea/inspectionProfiles/profiles_settings.xml +0 -0
- {pyflutter_app-0.3.0 → pyflutter_app-0.3.1}/.idea/misc.xml +0 -0
- {pyflutter_app-0.3.0 → pyflutter_app-0.3.1}/.idea/modules.xml +0 -0
- {pyflutter_app-0.3.0 → pyflutter_app-0.3.1}/.idea/pyflutter_app.iml +0 -0
- {pyflutter_app-0.3.0 → pyflutter_app-0.3.1}/.idea/vcs.xml +0 -0
- {pyflutter_app-0.3.0 → pyflutter_app-0.3.1}/README.md +0 -0
- {pyflutter_app-0.3.0 → pyflutter_app-0.3.1}/cartas/__init__.py +0 -0
- {pyflutter_app-0.3.0 → pyflutter_app-0.3.1}/cartas/erros.py +0 -0
- {pyflutter_app-0.3.0 → pyflutter_app-0.3.1}/cartas/find_toi.py +0 -0
- {pyflutter_app-0.3.0 → pyflutter_app-0.3.1}/cartas/relatorio.py +0 -0
- {pyflutter_app-0.3.0 → pyflutter_app-0.3.1}/cartas/table_toi.py +0 -0
- {pyflutter_app-0.3.0 → pyflutter_app-0.3.1}/cartas/util.py +0 -0
- {pyflutter_app-0.3.0 → pyflutter_app-0.3.1}/converter/__init__.py +0 -0
- {pyflutter_app-0.3.0 → pyflutter_app-0.3.1}/converter/__main__.py +0 -0
- {pyflutter_app-0.3.0 → pyflutter_app-0.3.1}/converter/assets/forms_config.json +0 -0
- {pyflutter_app-0.3.0 → pyflutter_app-0.3.1}/converter/config_app.py +0 -0
- {pyflutter_app-0.3.0 → pyflutter_app-0.3.1}/converter/home_page/__init__.py +0 -0
- {pyflutter_app-0.3.0 → pyflutter_app-0.3.1}/converter/home_page/_page_edit.py +0 -0
- {pyflutter_app-0.3.0 → pyflutter_app-0.3.1}/converter/home_page/controller.py +0 -0
- {pyflutter_app-0.3.0 → pyflutter_app-0.3.1}/converter/home_page/model.py +0 -0
- {pyflutter_app-0.3.0 → pyflutter_app-0.3.1}/converter/home_page/view.py +0 -0
- {pyflutter_app-0.3.0 → pyflutter_app-0.3.1}/converter/lib_toi.py +0 -0
- {pyflutter_app-0.3.0 → pyflutter_app-0.3.1}/converter/page_forms/Diagnorstico.txt +0 -0
- {pyflutter_app-0.3.0 → pyflutter_app-0.3.1}/converter/page_forms/__init__.py +0 -0
- {pyflutter_app-0.3.0 → pyflutter_app-0.3.1}/converter/page_forms/common.py +0 -0
- {pyflutter_app-0.3.0 → pyflutter_app-0.3.1}/converter/page_forms/diagnose_forms.py +0 -0
- {pyflutter_app-0.3.0 → pyflutter_app-0.3.1}/converter/page_forms/service.py +0 -0
- {pyflutter_app-0.3.0 → pyflutter_app-0.3.1}/converter/run_app_convert.py +0 -0
- {pyflutter_app-0.3.0 → pyflutter_app-0.3.1}/converter/table_widget.py +0 -0
- {pyflutter_app-0.3.0 → pyflutter_app-0.3.1}/converter/util.py +0 -0
- {pyflutter_app-0.3.0 → pyflutter_app-0.3.1}/converter/whatsapp/__init__.py +0 -0
- {pyflutter_app-0.3.0 → pyflutter_app-0.3.1}/converter/whatsapp/model.py +0 -0
- {pyflutter_app-0.3.0 → pyflutter_app-0.3.1}/export_app.sh +0 -0
- {pyflutter_app-0.3.0 → pyflutter_app-0.3.1}/local_build/linux.sh +0 -0
- {pyflutter_app-0.3.0 → pyflutter_app-0.3.1}/local_build/send.sh +0 -0
- {pyflutter_app-0.3.0 → pyflutter_app-0.3.1}/main.py +0 -0
- {pyflutter_app-0.3.0 → pyflutter_app-0.3.1}/mediator.py +0 -0
- {pyflutter_app-0.3.0 → pyflutter_app-0.3.1}/mvc/controller.py +0 -0
- {pyflutter_app-0.3.0 → pyflutter_app-0.3.1}/mvc/model.py +0 -0
- {pyflutter_app-0.3.0 → pyflutter_app-0.3.1}/mvc/view.py +0 -0
- {pyflutter_app-0.3.0 → pyflutter_app-0.3.1}/new-export_app.sh +0 -0
- {pyflutter_app-0.3.0 → pyflutter_app-0.3.1}/ocrlib/__init__.py +0 -0
- {pyflutter_app-0.3.0 → pyflutter_app-0.3.1}/ocrlib/_tesseract.py +0 -0
- {pyflutter_app-0.3.0 → pyflutter_app-0.3.1}/ocrlib/common.py +0 -0
- {pyflutter_app-0.3.0 → pyflutter_app-0.3.1}/ocrlib/erros.py +0 -0
- {pyflutter_app-0.3.0 → pyflutter_app-0.3.1}/ocrlib/export_ocr.sh +0 -0
- {pyflutter_app-0.3.0 → pyflutter_app-0.3.1}/ocrlib/util.py +0 -0
- {pyflutter_app-0.3.0 → pyflutter_app-0.3.1}/pdflib/__init__.py +0 -0
- {pyflutter_app-0.3.0 → pyflutter_app-0.3.1}/pdflib/common.py +0 -0
- {pyflutter_app-0.3.0 → pyflutter_app-0.3.1}/pdflib/imagelib.py +0 -0
- {pyflutter_app-0.3.0 → pyflutter_app-0.3.1}/pdflib/pdf_convert.py +0 -0
- {pyflutter_app-0.3.0 → pyflutter_app-0.3.1}/pdflib/pdf_document.py +0 -0
- {pyflutter_app-0.3.0 → pyflutter_app-0.3.1}/pyflutter_ui/__init__.py +0 -0
- {pyflutter_app-0.3.0 → pyflutter_app-0.3.1}/pyflutter_ui/app.py +0 -0
- {pyflutter_app-0.3.0 → pyflutter_app-0.3.1}/pyflutter_ui/file_picker.py +0 -0
- {pyflutter_app-0.3.0 → pyflutter_app-0.3.1}/pyflutter_ui/navigator.py +0 -0
- {pyflutter_app-0.3.0 → pyflutter_app-0.3.1}/pyflutter_ui/scheduler.py +0 -0
- {pyflutter_app-0.3.0 → pyflutter_app-0.3.1}/pyflutter_ui/theme.py +0 -0
- {pyflutter_app-0.3.0 → pyflutter_app-0.3.1}/pyflutter_ui/ui/__init__.py +0 -0
- {pyflutter_app-0.3.0 → pyflutter_app-0.3.1}/pyflutter_ui/ui/buttons.py +0 -0
- {pyflutter_app-0.3.0 → pyflutter_app-0.3.1}/pyflutter_ui/ui/container.py +0 -0
- {pyflutter_app-0.3.0 → pyflutter_app-0.3.1}/pyflutter_ui/ui/core.py +0 -0
- {pyflutter_app-0.3.0 → pyflutter_app-0.3.1}/pyflutter_ui/ui/list_builder.py +0 -0
- {pyflutter_app-0.3.0 → pyflutter_app-0.3.1}/pyflutter_ui/ui/progress.py +0 -0
- {pyflutter_app-0.3.0 → pyflutter_app-0.3.1}/pyflutter_ui/ui/tables.py +0 -0
- {pyflutter_app-0.3.0 → pyflutter_app-0.3.1}/pyflutter_ui/ui/widgets.py +0 -0
- {pyflutter_app-0.3.0 → pyflutter_app-0.3.1}/table_stream/__init__.py +0 -0
- {pyflutter_app-0.3.0 → pyflutter_app-0.3.1}/table_stream/array.py +0 -0
- {pyflutter_app-0.3.0 → pyflutter_app-0.3.1}/table_stream/erros.py +0 -0
- {pyflutter_app-0.3.0 → pyflutter_app-0.3.1}/table_stream/hash_map.py +0 -0
- {pyflutter_app-0.3.0 → pyflutter_app-0.3.1}/table_stream/notifier.py +0 -0
- {pyflutter_app-0.3.0 → pyflutter_app-0.3.1}/table_stream/pattner/__init__.py +0 -0
- {pyflutter_app-0.3.0 → pyflutter_app-0.3.1}/table_stream/pattner/adapter.py +0 -0
- {pyflutter_app-0.3.0 → pyflutter_app-0.3.1}/table_stream/pattner/observer.py +0 -0
- {pyflutter_app-0.3.0 → pyflutter_app-0.3.1}/table_stream/sheet/__init__.py +0 -0
- {pyflutter_app-0.3.0 → pyflutter_app-0.3.1}/table_stream/sheet/check_mod.py +0 -0
- {pyflutter_app-0.3.0 → pyflutter_app-0.3.1}/table_stream/sheet/data_storage.py +0 -0
- {pyflutter_app-0.3.0 → pyflutter_app-0.3.1}/table_stream/sheet/options.py +0 -0
- {pyflutter_app-0.3.0 → pyflutter_app-0.3.1}/table_stream/state.py +0 -0
- {pyflutter_app-0.3.0 → pyflutter_app-0.3.1}/table_stream/sysfiles.py +0 -0
- {pyflutter_app-0.3.0 → pyflutter_app-0.3.1}/table_stream/table.py +0 -0
- {pyflutter_app-0.3.0 → pyflutter_app-0.3.1}/table_stream/thread.py +0 -0
- {pyflutter_app-0.3.0 → pyflutter_app-0.3.1}/table_stream/workbook.py +0 -0
- {pyflutter_app-0.3.0 → pyflutter_app-0.3.1}/toi.py +0 -0
- {pyflutter_app-0.3.0 → pyflutter_app-0.3.1}/whatsapp_files/__init__.py +0 -0
- {pyflutter_app-0.3.0 → pyflutter_app-0.3.1}/whatsapp_files/_table_whatsapp.py +0 -0
- {pyflutter_app-0.3.0 → pyflutter_app-0.3.1}/whatsapp_files/common.py +0 -0
- {pyflutter_app-0.3.0 → pyflutter_app-0.3.1}/whatsapp_files/erros.py +0 -0
- {pyflutter_app-0.3.0 → pyflutter_app-0.3.1}/whatsapp_files/stream_files.py +0 -0
- {pyflutter_app-0.3.0 → pyflutter_app-0.3.1}/whatsapp_files/util.py +0 -0
|
@@ -10,6 +10,7 @@ class ConfigTOI:
|
|
|
10
10
|
file_relatorio_toi: File
|
|
11
11
|
file_relatorio_forms: File
|
|
12
12
|
file_table_toi: File
|
|
13
|
+
file_master_forms: File # NOVO
|
|
13
14
|
dir_base_toi: Directory
|
|
14
15
|
dir_log_excel: Directory
|
|
15
16
|
dir_relatorios: Directory
|
|
@@ -66,6 +67,11 @@ class BuilderConfigTOI(object):
|
|
|
66
67
|
d = self._builder_ocr.builder().dir_table
|
|
67
68
|
return d.join_file("cartas-toi-lancadas.xlsx")
|
|
68
69
|
|
|
70
|
+
@property
|
|
71
|
+
def file_master_forms(self) -> File:
|
|
72
|
+
d = self._builder_ocr.builder().dir_table
|
|
73
|
+
return d.join_file("master-forms-toi.xlsx")
|
|
74
|
+
|
|
69
75
|
@property
|
|
70
76
|
def file_table_toi(self) -> File:
|
|
71
77
|
d = self._builder_ocr.builder().dir_table
|
|
@@ -80,6 +86,7 @@ class BuilderConfigTOI(object):
|
|
|
80
86
|
file_relatorio_forms=self.file_relatorio_forms,
|
|
81
87
|
dir_log_excel=self.dir_log_excel,
|
|
82
88
|
dir_relatorios=self.dir_relatorios,
|
|
89
|
+
file_master_forms=self.file_master_forms,
|
|
83
90
|
)
|
|
84
91
|
|
|
85
92
|
def builder(self) -> ConfigTOI:
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
from dataclasses import dataclass, field
|
|
3
|
-
|
|
4
3
|
from cartas import LocalEntregaCarta, TipoCarta
|
|
5
4
|
|
|
6
5
|
FORMS_URL = (
|
|
@@ -31,11 +30,12 @@ class FormsSubmission:
|
|
|
31
30
|
Contrato entre Model e Service: UMA resposta do formulário, já validada
|
|
32
31
|
e com os textos finais das opções. O Service não conhece pandas nem PyFlutter.
|
|
33
32
|
"""
|
|
33
|
+
|
|
34
34
|
uc: str
|
|
35
35
|
toi: str
|
|
36
36
|
local_label: str
|
|
37
37
|
tipo_label: str
|
|
38
|
-
date: str
|
|
38
|
+
date: str # dd/MM/yyyy
|
|
39
39
|
files: list[str] = field(default_factory=list)
|
|
40
40
|
|
|
41
41
|
|
|
@@ -47,15 +47,15 @@ class FormsConfig:
|
|
|
47
47
|
user_data_dir: str = ""
|
|
48
48
|
screenshot_dir: str = ""
|
|
49
49
|
headless: bool = False
|
|
50
|
-
slow_mo_ms: int = 150
|
|
51
|
-
pause_between_rows_s: float = 1.5
|
|
52
|
-
browser_channel: str | None = None
|
|
50
|
+
slow_mo_ms: int = 150 # pausa automática entre CADA ação do Playwright
|
|
51
|
+
pause_between_rows_s: float = 1.5 # pausa entre uma carta e outra
|
|
52
|
+
browser_channel: str | None = None # ex.: "chrome" para usar o Chrome instalado
|
|
53
53
|
|
|
54
54
|
login_timeout_s: int = 900
|
|
55
55
|
action_timeout_ms: int = 15000
|
|
56
56
|
upload_timeout_ms: int = 120000
|
|
57
57
|
wait_after_submit_ms: int = 1500
|
|
58
|
-
send_receipt: bool = True
|
|
58
|
+
send_receipt: bool = True # marcar "Enviar-me um email de confirmação"
|
|
59
59
|
|
|
60
60
|
# Regex (case-insensitive) que identifica o TÍTULO de cada pergunta.
|
|
61
61
|
# O texto do bloco vem colado ("1.UCTexto de linha única."), por isso NÃO
|
|
@@ -9,8 +9,15 @@ from pyflutter_ui.ui import ChangeNotifier, Provider
|
|
|
9
9
|
from pyflutter_ui.scheduler import MainThreadScheduler
|
|
10
10
|
|
|
11
11
|
from converter.config_app import AppConfigNotifier
|
|
12
|
-
from converter.page_forms.common import MapRows, StatusForms
|
|
13
|
-
|
|
12
|
+
from converter.page_forms.common import MapRows, StatusForms, HeaderForms
|
|
13
|
+
|
|
14
|
+
from converter.page_forms.master_forms import ControllerMasterForms, make_key
|
|
15
|
+
from converter.page_forms.model_forms import (
|
|
16
|
+
ModelForms,
|
|
17
|
+
FormsCallbacks,
|
|
18
|
+
ResultGenerateTable,
|
|
19
|
+
)
|
|
20
|
+
from table_stream import File, Directory, TableData, ArrayList
|
|
14
21
|
|
|
15
22
|
from cartas import HeaderFilesTOI, BuilderCartas
|
|
16
23
|
from table_stream import File, Directory, TableData
|
|
@@ -37,6 +44,9 @@ class ControllerForms(ChangeNotifier):
|
|
|
37
44
|
self.progress: tuple[int, int] = (0, 0)
|
|
38
45
|
self.send_receipt: bool = True
|
|
39
46
|
|
|
47
|
+
self.master = ControllerMasterForms()
|
|
48
|
+
self.generating: bool = False
|
|
49
|
+
|
|
40
50
|
# Log em Excel gravado durante o lançamento (1 arquivo por execução).
|
|
41
51
|
self.save_every: int = 2 # grava a cada N linhas concluídas (sucesso OU falha)
|
|
42
52
|
self.result_file: File | None = None
|
|
@@ -66,16 +76,119 @@ class ControllerForms(ChangeNotifier):
|
|
|
66
76
|
if self.fn_show_error is not None:
|
|
67
77
|
_ui(lambda: self.fn_show_error(e))
|
|
68
78
|
|
|
79
|
+
# ------------------------------------------------------------------ master
|
|
80
|
+
|
|
81
|
+
def _apply_master(self, mp: MapRows) -> int:
|
|
82
|
+
"""Marca como SKIPPED o que já está no master (key UC+TOI) ou repetido na tabela."""
|
|
83
|
+
keys = self.master.load_keys()
|
|
84
|
+
seen: set[str] = set()
|
|
85
|
+
skipped = 0
|
|
86
|
+
|
|
87
|
+
for idx in mp.header():
|
|
88
|
+
row = mp[idx]
|
|
89
|
+
if mp.get_status(idx) == StatusForms.SUCCESS:
|
|
90
|
+
seen.add(make_key(row.get(HeaderForms.UC), row.get(HeaderForms.TOI)))
|
|
91
|
+
continue
|
|
92
|
+
|
|
93
|
+
key = make_key(row.get(HeaderForms.UC), row.get(HeaderForms.TOI))
|
|
94
|
+
if key in keys:
|
|
95
|
+
mp.update_status(idx, StatusForms.SKIPPED, "Já lançada (master)")
|
|
96
|
+
skipped += 1
|
|
97
|
+
elif key in seen:
|
|
98
|
+
mp.update_status(idx, StatusForms.SKIPPED, "Duplicada na tabela")
|
|
99
|
+
skipped += 1
|
|
100
|
+
seen.add(key)
|
|
101
|
+
return skipped
|
|
102
|
+
|
|
69
103
|
# ------------------------------------------------------------------ importação
|
|
70
104
|
def set_map_rows(self, mp: MapRows) -> None:
|
|
71
105
|
if mp.is_empty():
|
|
72
106
|
self.show_error("Nenhuma linha TOI encontrada na tabela.")
|
|
73
107
|
return
|
|
108
|
+
|
|
109
|
+
try:
|
|
110
|
+
skipped = self._apply_master(mp)
|
|
111
|
+
except Exception as e:
|
|
112
|
+
self.show_error(f"Falha ao ler o arquivo master:\n{e}")
|
|
113
|
+
return
|
|
114
|
+
|
|
74
115
|
self.map_rows = mp
|
|
75
116
|
self.progress = (0, 0)
|
|
76
|
-
|
|
117
|
+
pending = mp.size_header() - skipped
|
|
118
|
+
self.status_text = (
|
|
119
|
+
f"{mp.size_header()} cartas importadas | "
|
|
120
|
+
f"{skipped} já lançadas/ignoradas | {pending} a lançar."
|
|
121
|
+
)
|
|
122
|
+
self.notify_listeners()
|
|
123
|
+
|
|
124
|
+
# ------------------------------------------------------------------ gerar tabela (cache)
|
|
125
|
+
def generate_table(self, files: ArrayList[File]) -> None:
|
|
126
|
+
if self.running or self.generating:
|
|
127
|
+
self.show_info("Existe outra operação em andamento.")
|
|
128
|
+
return
|
|
129
|
+
if files.empty():
|
|
130
|
+
return
|
|
131
|
+
|
|
132
|
+
self.generating = True
|
|
133
|
+
self.progress = (0, 0)
|
|
134
|
+
self.status_text = (
|
|
135
|
+
f"Gerando tabela a partir de {files.size()} arquivos (somente cache)..."
|
|
136
|
+
)
|
|
137
|
+
self.notify_listeners()
|
|
138
|
+
|
|
139
|
+
try:
|
|
140
|
+
self.model.start_generate(
|
|
141
|
+
files,
|
|
142
|
+
on_progress=lambda d, t: _ui(lambda: self._on_gen_progress(d, t)),
|
|
143
|
+
on_done=lambda r, e: _ui(lambda: self._on_gen_done(r, e)),
|
|
144
|
+
)
|
|
145
|
+
except Exception as e:
|
|
146
|
+
self.generating = False
|
|
147
|
+
self.show_error(str(e))
|
|
148
|
+
|
|
149
|
+
def _on_gen_progress(self, done: int, total: int) -> None:
|
|
150
|
+
self.progress = (done, total)
|
|
151
|
+
self.status_text = f"Calculando hashes {done}/{total}"
|
|
152
|
+
if done == total or done % 25 == 0: # evita piscar a tela
|
|
153
|
+
self.notify_listeners()
|
|
154
|
+
|
|
155
|
+
def _on_gen_done(
|
|
156
|
+
self, res: ResultGenerateTable | None, err: Exception | None
|
|
157
|
+
) -> None:
|
|
158
|
+
self.generating = False
|
|
159
|
+
self.progress = (0, 0)
|
|
160
|
+
|
|
161
|
+
if err is not None:
|
|
162
|
+
self.status_text = f"Erro ao gerar tabela: {err}"
|
|
163
|
+
self.notify_listeners()
|
|
164
|
+
self.show_error(str(err))
|
|
165
|
+
return
|
|
166
|
+
|
|
167
|
+
if res is None or res.table is None:
|
|
168
|
+
self.status_text = "Nenhum arquivo encontrado no cache."
|
|
169
|
+
self.notify_listeners()
|
|
170
|
+
self.show_info(
|
|
171
|
+
"Nenhum dos arquivos está na tabela TOI em cache.\n"
|
|
172
|
+
"Aplique o OCR/relatório TOI antes de gerar a tabela."
|
|
173
|
+
)
|
|
174
|
+
return
|
|
175
|
+
|
|
176
|
+
self.status_text = f"Tabela gerada: {res.found}/{res.total} arquivos."
|
|
77
177
|
self.notify_listeners()
|
|
78
178
|
|
|
179
|
+
msg = f"Tabela gerada com {res.found} de {res.total} arquivos.\n\n{res.file.absolute()}"
|
|
180
|
+
if res.not_found:
|
|
181
|
+
sample = "\n".join(res.not_found[:10])
|
|
182
|
+
more = (
|
|
183
|
+
f"\n... e mais {len(res.not_found) - 10}"
|
|
184
|
+
if len(res.not_found) > 10
|
|
185
|
+
else ""
|
|
186
|
+
)
|
|
187
|
+
msg += f"\n\nSem dados no cache ({len(res.not_found)}):\n{sample}{more}"
|
|
188
|
+
if res.invalid:
|
|
189
|
+
msg += f"\n\nIgnorados (não são PDF/imagem): {res.invalid}"
|
|
190
|
+
self.show_info(msg)
|
|
191
|
+
|
|
79
192
|
def set_table_toi(self, tb: TableData[HeaderFilesTOI]) -> None:
|
|
80
193
|
if tb.is_empty():
|
|
81
194
|
self.show_error("Tabela TOI vazia")
|
|
@@ -122,17 +235,27 @@ class ControllerForms(ChangeNotifier):
|
|
|
122
235
|
|
|
123
236
|
# ------------------------------------------------------------------ execução
|
|
124
237
|
def start_forms(self) -> None:
|
|
125
|
-
|
|
126
|
-
|
|
238
|
+
|
|
239
|
+
if self.running or self.generating:
|
|
240
|
+
self.show_info("Já existe uma operação em andamento.")
|
|
127
241
|
return
|
|
128
242
|
if self.map_rows.is_empty():
|
|
129
243
|
self.show_error("Importe uma tabela TOI antes de iniciar.")
|
|
130
244
|
return
|
|
131
245
|
|
|
246
|
+
try:
|
|
247
|
+
self._apply_master(self.map_rows) # o master pode ter mudado
|
|
248
|
+
except Exception as e:
|
|
249
|
+
self.show_error(f"Falha ao ler o arquivo master:\n{e}")
|
|
250
|
+
return
|
|
251
|
+
|
|
132
252
|
jobs = {}
|
|
133
253
|
for idx in self.map_rows.header():
|
|
134
|
-
if self.map_rows.get_status(idx)
|
|
135
|
-
|
|
254
|
+
if self.map_rows.get_status(idx) in (
|
|
255
|
+
StatusForms.SUCCESS,
|
|
256
|
+
StatusForms.SKIPPED,
|
|
257
|
+
):
|
|
258
|
+
continue
|
|
136
259
|
try:
|
|
137
260
|
jobs[idx] = self.model.build_submission(self.map_rows[idx])
|
|
138
261
|
self.map_rows.update_status(idx, StatusForms.PENDING)
|
|
@@ -140,10 +263,13 @@ class ControllerForms(ChangeNotifier):
|
|
|
140
263
|
self.map_rows.update_status(idx, StatusForms.FAILED, str(e))
|
|
141
264
|
|
|
142
265
|
if not jobs:
|
|
143
|
-
self.status_text =
|
|
266
|
+
self.status_text = (
|
|
267
|
+
"Nenhuma carta nova para lançar (todas já lançadas ou inválidas)."
|
|
268
|
+
)
|
|
144
269
|
self.notify_listeners()
|
|
145
270
|
return
|
|
146
271
|
|
|
272
|
+
# ... resto idêntico ao atual
|
|
147
273
|
config = self.model.create_config(
|
|
148
274
|
self.get_config().value.work_dir, self.send_receipt
|
|
149
275
|
)
|
|
@@ -184,6 +310,12 @@ class ControllerForms(ChangeNotifier):
|
|
|
184
310
|
def _on_status(self, idx: int, status: StatusForms, msg: str) -> None:
|
|
185
311
|
self.map_rows.update_status(idx, status, msg)
|
|
186
312
|
|
|
313
|
+
if status == StatusForms.SUCCESS:
|
|
314
|
+
try:
|
|
315
|
+
self.master.register(self.map_rows[idx])
|
|
316
|
+
except Exception as e:
|
|
317
|
+
self.show_error(f"Carta lançada, mas falhou ao gravar no master:\n{e}")
|
|
318
|
+
|
|
187
319
|
if status in (StatusForms.SUCCESS, StatusForms.FAILED):
|
|
188
320
|
self._since_save += 1
|
|
189
321
|
if self._since_save >= self.save_every:
|
|
@@ -203,7 +335,9 @@ class ControllerForms(ChangeNotifier):
|
|
|
203
335
|
def _on_done(self, err: Exception | None, stopped: bool) -> None:
|
|
204
336
|
ok = self.map_rows.count_status(StatusForms.SUCCESS)
|
|
205
337
|
bad = self.map_rows.count_status(StatusForms.FAILED)
|
|
206
|
-
|
|
338
|
+
skip = self.map_rows.count_status(StatusForms.SKIPPED)
|
|
339
|
+
summary = f"sucesso: {ok} | falha: {bad} | ignoradas: {skip}"
|
|
340
|
+
# summary = f"sucesso: {ok} | falha: {bad}"
|
|
207
341
|
|
|
208
342
|
if err is not None:
|
|
209
343
|
self.status_text = f"Erro: {err}"
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
import os
|
|
3
|
+
import threading
|
|
4
|
+
from datetime import datetime
|
|
5
|
+
from enum import StrEnum
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
import pandas as pd
|
|
9
|
+
|
|
10
|
+
from table_stream import RowH, TableData
|
|
11
|
+
from cartas import BuilderConfigTOI
|
|
12
|
+
|
|
13
|
+
from converter.page_forms.common import HeaderForms, clean_number, clean_text
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class HeaderMasterForms(StrEnum):
|
|
17
|
+
KEY = "key_uc_toi"
|
|
18
|
+
UC = HeaderForms.UC.value
|
|
19
|
+
TOI = HeaderForms.TOI.value
|
|
20
|
+
DATA_ENTREGA = HeaderForms.DATA_ENTREGA.value
|
|
21
|
+
LOCAL_ENTREGA = HeaderForms.LOCAL_ENTREGA.value
|
|
22
|
+
TIPO = HeaderForms.TIPO.value
|
|
23
|
+
HASH_FILE = HeaderForms.HASH_FILE.value
|
|
24
|
+
FULL_PATH = HeaderForms.FULL_PATH.value
|
|
25
|
+
DATA_LANCAMENTO = "data_lancamento"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def make_key(uc: Any, toi: Any) -> str:
|
|
29
|
+
"""Chave de uma carta: UC sozinha não identifica, usa UC+TOI."""
|
|
30
|
+
return f"{clean_number(uc)}_{clean_number(toi)}"
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class ControllerMasterForms(object):
|
|
34
|
+
"""
|
|
35
|
+
Arquivo master do Forms: tudo que já foi lançado com sucesso.
|
|
36
|
+
Equivalente ao ControllerTOI().load_table_toi(), mas exclusivo do Forms.
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
COLUMNS: list[str] = [h.value for h in HeaderMasterForms]
|
|
40
|
+
|
|
41
|
+
def __init__(self) -> None:
|
|
42
|
+
self._lock = threading.Lock()
|
|
43
|
+
|
|
44
|
+
@property
|
|
45
|
+
def file_master(self):
|
|
46
|
+
return BuilderConfigTOI().builder().file_master_forms
|
|
47
|
+
|
|
48
|
+
# ---------------------------------------------------------------- leitura
|
|
49
|
+
def load_dataframe(self) -> pd.DataFrame:
|
|
50
|
+
f = self.file_master
|
|
51
|
+
if not f.exists():
|
|
52
|
+
return pd.DataFrame(columns=self.COLUMNS)
|
|
53
|
+
|
|
54
|
+
df = pd.read_excel(f.absolute(), dtype=str).fillna("")
|
|
55
|
+
for c in self.COLUMNS:
|
|
56
|
+
if c not in df.columns:
|
|
57
|
+
df[c] = ""
|
|
58
|
+
return df[self.COLUMNS]
|
|
59
|
+
|
|
60
|
+
def load_table_master(self) -> TableData[HeaderMasterForms]:
|
|
61
|
+
return TableData(self.load_dataframe())
|
|
62
|
+
|
|
63
|
+
def load_keys(self) -> set[str]:
|
|
64
|
+
df = self.load_dataframe()
|
|
65
|
+
return set(df[HeaderMasterForms.KEY.value].astype(str).tolist())
|
|
66
|
+
|
|
67
|
+
def contains(self, uc: Any, toi: Any) -> bool:
|
|
68
|
+
return make_key(uc, toi) in self.load_keys()
|
|
69
|
+
|
|
70
|
+
# ---------------------------------------------------------------- escrita
|
|
71
|
+
def register(self, row: RowH[HeaderForms, Any]) -> bool:
|
|
72
|
+
"""
|
|
73
|
+
Grava uma carta lançada com sucesso. Retorna False se a chave já existia.
|
|
74
|
+
Escrita atômica (tmp + replace): nunca deixa .xlsx corrompido.
|
|
75
|
+
"""
|
|
76
|
+
uc = clean_number(row.get(HeaderForms.UC))
|
|
77
|
+
toi = clean_number(row.get(HeaderForms.TOI))
|
|
78
|
+
key = make_key(uc, toi)
|
|
79
|
+
|
|
80
|
+
with self._lock:
|
|
81
|
+
df = self.load_dataframe()
|
|
82
|
+
if key in set(df[HeaderMasterForms.KEY.value].tolist()):
|
|
83
|
+
return False
|
|
84
|
+
|
|
85
|
+
record = {
|
|
86
|
+
HeaderMasterForms.KEY.value: key,
|
|
87
|
+
HeaderMasterForms.UC.value: uc,
|
|
88
|
+
HeaderMasterForms.TOI.value: toi,
|
|
89
|
+
HeaderMasterForms.DATA_ENTREGA.value: clean_text(
|
|
90
|
+
row.get(HeaderForms.DATA_ENTREGA)
|
|
91
|
+
),
|
|
92
|
+
HeaderMasterForms.LOCAL_ENTREGA.value: clean_text(
|
|
93
|
+
row.get(HeaderForms.LOCAL_ENTREGA)
|
|
94
|
+
),
|
|
95
|
+
HeaderMasterForms.TIPO.value: clean_text(row.get(HeaderForms.TIPO)),
|
|
96
|
+
HeaderMasterForms.HASH_FILE.value: clean_text(
|
|
97
|
+
row.get(HeaderForms.HASH_FILE)
|
|
98
|
+
),
|
|
99
|
+
HeaderMasterForms.FULL_PATH.value: clean_text(
|
|
100
|
+
row.get(HeaderForms.FULL_PATH)
|
|
101
|
+
),
|
|
102
|
+
HeaderMasterForms.DATA_LANCAMENTO.value: datetime.now().strftime(
|
|
103
|
+
"%d/%m/%Y %H:%M:%S"
|
|
104
|
+
),
|
|
105
|
+
}
|
|
106
|
+
new = pd.DataFrame([record], columns=self.COLUMNS)
|
|
107
|
+
final = new if df.empty else pd.concat([df, new], ignore_index=True)
|
|
108
|
+
|
|
109
|
+
path = self.file_master.absolute()
|
|
110
|
+
tmp = f"{path}-tmp.xlsx"
|
|
111
|
+
final.to_excel(tmp, index=False)
|
|
112
|
+
os.replace(tmp, path)
|
|
113
|
+
return True
|
|
@@ -1,13 +1,29 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
import os
|
|
3
3
|
import threading
|
|
4
|
-
from dataclasses import dataclass
|
|
4
|
+
from dataclasses import dataclass, field
|
|
5
|
+
from datetime import datetime
|
|
6
|
+
from cartas import BuilderConfigTOI
|
|
5
7
|
from typing import Any, Callable
|
|
6
8
|
|
|
7
9
|
import pandas as pd
|
|
8
10
|
|
|
9
|
-
from table_stream import
|
|
10
|
-
|
|
11
|
+
from table_stream import (
|
|
12
|
+
ArrayList,
|
|
13
|
+
File,
|
|
14
|
+
Directory,
|
|
15
|
+
HashMapDict,
|
|
16
|
+
RowH,
|
|
17
|
+
TableData,
|
|
18
|
+
load_file_hash,
|
|
19
|
+
)
|
|
20
|
+
from cartas import (
|
|
21
|
+
TableTOI,
|
|
22
|
+
ControllerTOI,
|
|
23
|
+
HeaderEntregaTOI,
|
|
24
|
+
HeaderFilesTOI,
|
|
25
|
+
BuilderCartas,
|
|
26
|
+
)
|
|
11
27
|
|
|
12
28
|
from converter.page_forms.common import (
|
|
13
29
|
HeaderForms,
|
|
@@ -43,10 +59,21 @@ class FormsCallbacks:
|
|
|
43
59
|
on_done: Callable[[Exception | None, bool], None] = _noop # (erro, interrompido)
|
|
44
60
|
|
|
45
61
|
|
|
62
|
+
@dataclass
|
|
63
|
+
class ResultGenerateTable:
|
|
64
|
+
table: TableData[HeaderFilesTOI] | None = None
|
|
65
|
+
file: File | None = None
|
|
66
|
+
total: int = 0
|
|
67
|
+
found: int = 0
|
|
68
|
+
invalid: int = 0
|
|
69
|
+
not_found: list[str] = field(default_factory=list)
|
|
70
|
+
|
|
71
|
+
|
|
46
72
|
class ModelForms(object):
|
|
47
73
|
|
|
48
74
|
def __init__(self) -> None:
|
|
49
75
|
self._relatorio_toi: ControllerTOI | None = None
|
|
76
|
+
self.builder_carta = BuilderCartas()
|
|
50
77
|
self._thread: threading.Thread | None = None
|
|
51
78
|
self._stop = threading.Event()
|
|
52
79
|
|
|
@@ -66,21 +93,23 @@ class ModelForms(object):
|
|
|
66
93
|
self._relatorio_toi = ControllerTOI()
|
|
67
94
|
return self._relatorio_toi
|
|
68
95
|
|
|
69
|
-
def create_relatorio_toi(self, files: ArrayList[File]) ->
|
|
96
|
+
def create_relatorio_toi(self, files: ArrayList[File]) -> TableData[HeaderFilesTOI]:
|
|
70
97
|
if files.empty():
|
|
71
|
-
return
|
|
98
|
+
return self.builder_carta.create_void()
|
|
72
99
|
|
|
73
100
|
mp = HashMapDict()
|
|
74
101
|
files.for_each(lambda f: mp.set_value(load_file_hash(f), f))
|
|
75
102
|
|
|
76
|
-
tb = self.relatorio_toi.load_from_map(mp)
|
|
103
|
+
tb: TableData[HeaderFilesTOI] | None = self.relatorio_toi.load_from_map(mp)
|
|
77
104
|
if tb is None:
|
|
78
|
-
return
|
|
105
|
+
return self.builder_carta.create_void()
|
|
79
106
|
|
|
80
107
|
df = tb.to_dataframe().astype(str)
|
|
81
|
-
|
|
82
|
-
df = df[df[
|
|
83
|
-
|
|
108
|
+
|
|
109
|
+
df = df[df[HeaderFilesTOI.UC] != TableTOI.NULL_STRING]
|
|
110
|
+
df = df[df[HeaderFilesTOI.TOI] != TableTOI.NULL_STRING]
|
|
111
|
+
df = df[df[HeaderFilesTOI.DATE] != TableTOI.NULL_STRING]
|
|
112
|
+
return self.builder_carta.from_data(df)
|
|
84
113
|
|
|
85
114
|
# ------------------------------------------------------------------ validação/conversão
|
|
86
115
|
@staticmethod
|
|
@@ -213,3 +242,88 @@ class ModelForms(object):
|
|
|
213
242
|
finally:
|
|
214
243
|
service.stop()
|
|
215
244
|
cb.on_done(error, self._stop.is_set())
|
|
245
|
+
|
|
246
|
+
# ------------------------------------------------------------------
|
|
247
|
+
# gerar tabela (SOMENTE cache, sem OCR)
|
|
248
|
+
# ------------------------------------------------------------------
|
|
249
|
+
def is_generating(self) -> bool:
|
|
250
|
+
t = getattr(self, "_gen_thread", None)
|
|
251
|
+
return t is not None and t.is_alive()
|
|
252
|
+
|
|
253
|
+
def generate_table(
|
|
254
|
+
self,
|
|
255
|
+
files: ArrayList[File],
|
|
256
|
+
on_progress: Callable[[int, int], None] = _noop,
|
|
257
|
+
) -> ResultGenerateTable:
|
|
258
|
+
"""Calcula o hash, consulta ControllerTOI.find_on_cache() e NÃO aplica OCR."""
|
|
259
|
+
res = ResultGenerateTable()
|
|
260
|
+
valid = ArrayList[File]()
|
|
261
|
+
|
|
262
|
+
for f in files:
|
|
263
|
+
if f.is_image() or f.is_pdf():
|
|
264
|
+
valid.add(f)
|
|
265
|
+
else:
|
|
266
|
+
res.invalid += 1
|
|
267
|
+
|
|
268
|
+
mp = HashMapDict()
|
|
269
|
+
total = valid.size()
|
|
270
|
+
for n, f in enumerate(valid):
|
|
271
|
+
mp.set_value(load_file_hash(f), f)
|
|
272
|
+
on_progress(n + 1, total)
|
|
273
|
+
|
|
274
|
+
res.total = mp.size_header()
|
|
275
|
+
if mp.is_empty():
|
|
276
|
+
return res
|
|
277
|
+
|
|
278
|
+
tb: TableData[HeaderFilesTOI] = self.relatorio_toi.find_on_cache(mp)
|
|
279
|
+
found_hashes: set[str] = set()
|
|
280
|
+
|
|
281
|
+
if not tb.is_empty():
|
|
282
|
+
df = tb.to_dataframe().astype(str)
|
|
283
|
+
for h in (HeaderFilesTOI.UC, HeaderFilesTOI.TOI, HeaderFilesTOI.DATE):
|
|
284
|
+
df = df[df[h] != TableTOI.NULL_STRING]
|
|
285
|
+
|
|
286
|
+
if not df.empty:
|
|
287
|
+
found_hashes = set(df[HeaderFilesTOI.HASH_FILE].tolist())
|
|
288
|
+
res.table = self.builder_carta.from_data(df)
|
|
289
|
+
|
|
290
|
+
res.found = len(found_hashes)
|
|
291
|
+
res.not_found = [
|
|
292
|
+
mp.get_value(h).basename() for h in mp.header() if h not in found_hashes
|
|
293
|
+
]
|
|
294
|
+
return res
|
|
295
|
+
|
|
296
|
+
def save_generated_table(self, tb: TableData[HeaderFilesTOI]) -> File:
|
|
297
|
+
"""Grava em ConfigTOI.dir_relatorios (padrão do projeto)."""
|
|
298
|
+
cfg = BuilderConfigTOI().builder()
|
|
299
|
+
out = cfg.dir_relatorios.join_file(
|
|
300
|
+
f"tabela-toi-{datetime.now():%Y%m%d-%H%M%S}.xlsx"
|
|
301
|
+
)
|
|
302
|
+
tmp = f"{out.absolute()}-tmp.xlsx"
|
|
303
|
+
tb.to_dataframe().to_excel(tmp, index=False)
|
|
304
|
+
os.replace(tmp, out.absolute())
|
|
305
|
+
return out
|
|
306
|
+
|
|
307
|
+
def start_generate(
|
|
308
|
+
self,
|
|
309
|
+
files: ArrayList[File],
|
|
310
|
+
on_progress: Callable[[int, int], None],
|
|
311
|
+
on_done: Callable[[ResultGenerateTable | None, Exception | None], None],
|
|
312
|
+
) -> None:
|
|
313
|
+
if self.is_generating():
|
|
314
|
+
raise RuntimeError("Já existe uma geração de tabela em andamento")
|
|
315
|
+
|
|
316
|
+
def work() -> None:
|
|
317
|
+
res: ResultGenerateTable | None = None
|
|
318
|
+
err: Exception | None = None
|
|
319
|
+
try:
|
|
320
|
+
res = self.generate_table(files, on_progress)
|
|
321
|
+
if res.table is not None:
|
|
322
|
+
res.file = self.save_generated_table(res.table)
|
|
323
|
+
except Exception as e:
|
|
324
|
+
err = e
|
|
325
|
+
finally:
|
|
326
|
+
on_done(res, err)
|
|
327
|
+
|
|
328
|
+
self._gen_thread = threading.Thread(target=work, daemon=True, name="gen-table")
|
|
329
|
+
self._gen_thread.start()
|