Guilherme Silberfarb Costa commited on
Commit
9ffa258
·
1 Parent(s): d24d3f0

Alert when SIAT PDF skips venal value years

Browse files
backend/app/schemas.py CHANGED
@@ -78,6 +78,7 @@ class PdfExtractResponse(BaseModel):
78
  area_territorial: str = ""
79
  construcoes_imovel: str = ""
80
  valores_venais: str = ""
 
81
 
82
 
83
  class LaudoGenerateResponse(BaseModel):
 
78
  area_territorial: str = ""
79
  construcoes_imovel: str = ""
80
  valores_venais: str = ""
81
+ anos_venais_ausentes: List[int] = Field(default_factory=list)
82
 
83
 
84
  class LaudoGenerateResponse(BaseModel):
backend/app/services.py CHANGED
@@ -176,7 +176,7 @@ def generate_market_value_years(ano_inicial: int, ano_final: int) -> Tuple[List[
176
  return anos_visiveis, referencias
177
 
178
 
179
- def extract_pdf_data(upload: UploadFile) -> Dict[str, str]:
180
  upload.file.seek(0)
181
  dados = extrair_dados_pdf(upload.file) or {}
182
 
@@ -194,6 +194,7 @@ def extract_pdf_data(upload: UploadFile) -> Dict[str, str]:
194
  "area_territorial": (dados.get("area_territorial") or "").strip(),
195
  "construcoes_imovel": (dados.get("construcoes_imovel") or "").strip(),
196
  "valores_venais": (dados.get("valores_venais") or "").strip(),
 
197
  }
198
 
199
 
 
176
  return anos_visiveis, referencias
177
 
178
 
179
+ def extract_pdf_data(upload: UploadFile) -> Dict[str, Any]:
180
  upload.file.seek(0)
181
  dados = extrair_dados_pdf(upload.file) or {}
182
 
 
194
  "area_territorial": (dados.get("area_territorial") or "").strip(),
195
  "construcoes_imovel": (dados.get("construcoes_imovel") or "").strip(),
196
  "valores_venais": (dados.get("valores_venais") or "").strip(),
197
+ "anos_venais_ausentes": dados.get("anos_venais_ausentes") or [],
198
  }
199
 
200
 
extractors/pdf_extractor.py CHANGED
@@ -11,6 +11,16 @@ def _valores_unicos_em_ordem(valores):
11
  return list(dict.fromkeys(valor for valor in valores if valor))
12
 
13
 
 
 
 
 
 
 
 
 
 
 
14
  def _extrair_inscricao(textos_paginas):
15
  """Obtém a inscrição da unidade, sem confundi-la com a inscrição anterior."""
16
  # Conforme a versão do relatório, o pypdf pode devolver a inscrição antes
@@ -81,7 +91,8 @@ def extrair_dados_pdf(pdf_file) -> Optional[Dict]:
81
  "finalidade_imovel": None,
82
  "area_territorial": None,
83
  "construcoes_imovel": "Sem construções cadastradas",
84
- "valores_venais": None
 
85
  }
86
 
87
  # INSCRIÇÃO
@@ -143,16 +154,19 @@ def extrair_dados_pdf(pdf_file) -> Optional[Dict]:
143
  pattern_valor = re.compile(r'Tributação(.*?)Planta', re.DOTALL)
144
  matches_valor = pattern_valor.findall(text)
145
  valores_ano = []
 
146
  for m in matches_valor:
147
  try:
148
  ano = re.search(r'(\d{4})', m).group(1)
149
  valores = re.findall(r'(\d{1,3}(?:\.\d{3})*,\d{2})', m)
150
  valor_venal = valores[2]
151
  valores_ano.append(f"{ano}: R$ {valor_venal}")
 
152
  except:
153
  continue
154
  if valores_ano:
155
  dados["valores_venais"] = " | ".join(valores_ano)
 
156
 
157
  # ÁREA TERRITORIAL
158
  match_area_real = re.search(r"Área real total\s+(\d{1,5},\d{2})\s*m²", text, re.IGNORECASE)
 
11
  return list(dict.fromkeys(valor for valor in valores if valor))
12
 
13
 
14
+ def anos_venais_ausentes(anos):
15
+ """Lista os anos não encontrados entre o primeiro e o último exercício extraído."""
16
+ anos_validos = sorted({int(ano) for ano in anos if str(ano).isdigit()})
17
+ if len(anos_validos) < 2:
18
+ return []
19
+
20
+ anos_encontrados = set(anos_validos)
21
+ return [ano for ano in range(anos_validos[0], anos_validos[-1] + 1) if ano not in anos_encontrados]
22
+
23
+
24
  def _extrair_inscricao(textos_paginas):
25
  """Obtém a inscrição da unidade, sem confundi-la com a inscrição anterior."""
26
  # Conforme a versão do relatório, o pypdf pode devolver a inscrição antes
 
91
  "finalidade_imovel": None,
92
  "area_territorial": None,
93
  "construcoes_imovel": "Sem construções cadastradas",
94
+ "valores_venais": None,
95
+ "anos_venais_ausentes": [],
96
  }
97
 
98
  # INSCRIÇÃO
 
154
  pattern_valor = re.compile(r'Tributação(.*?)Planta', re.DOTALL)
155
  matches_valor = pattern_valor.findall(text)
156
  valores_ano = []
157
+ anos_venais = []
158
  for m in matches_valor:
159
  try:
160
  ano = re.search(r'(\d{4})', m).group(1)
161
  valores = re.findall(r'(\d{1,3}(?:\.\d{3})*,\d{2})', m)
162
  valor_venal = valores[2]
163
  valores_ano.append(f"{ano}: R$ {valor_venal}")
164
+ anos_venais.append(ano)
165
  except:
166
  continue
167
  if valores_ano:
168
  dados["valores_venais"] = " | ".join(valores_ano)
169
+ dados["anos_venais_ausentes"] = anos_venais_ausentes(anos_venais)
170
 
171
  # ÁREA TERRITORIAL
172
  match_area_real = re.search(r"Área real total\s+(\d{1,5},\d{2})\s*m²", text, re.IGNORECASE)
frontend/src/App.jsx CHANGED
@@ -182,6 +182,7 @@ function App() {
182
  const [databaseColumns, setDatabaseColumns] = useState([]);
183
 
184
  const [error, setError] = useState("");
 
185
  const [result, setResult] = useState(null);
186
 
187
  useEffect(() => {
@@ -405,6 +406,7 @@ function App() {
405
 
406
  setLoadingPdf(true);
407
  setError("");
 
408
 
409
  try {
410
  const formData = new FormData();
@@ -421,7 +423,15 @@ function App() {
421
  }
422
 
423
  const extracted = await response.json();
424
- setForm((prev) => ({ ...prev, ...extracted }));
 
 
 
 
 
 
 
 
425
  } catch (err) {
426
  setError(String(err));
427
  } finally {
@@ -454,6 +464,7 @@ function App() {
454
  setForm(buildEmptyForm(options));
455
  setDatabaseColumns([]);
456
  setError("");
 
457
  setResult(null);
458
  };
459
 
@@ -762,6 +773,8 @@ function App() {
762
  </button>
763
  </div>
764
 
 
 
765
  <label>
766
  Inscrição
767
  <input
 
182
  const [databaseColumns, setDatabaseColumns] = useState([]);
183
 
184
  const [error, setError] = useState("");
185
+ const [pdfWarning, setPdfWarning] = useState("");
186
  const [result, setResult] = useState(null);
187
 
188
  useEffect(() => {
 
406
 
407
  setLoadingPdf(true);
408
  setError("");
409
+ setPdfWarning("");
410
 
411
  try {
412
  const formData = new FormData();
 
423
  }
424
 
425
  const extracted = await response.json();
426
+ const { anos_venais_ausentes: anosAusentes = [], ...dadosImovel } = extracted;
427
+ setForm((prev) => ({ ...prev, ...dadosImovel }));
428
+ if (anosAusentes.length) {
429
+ const anos = anosAusentes.join(", ");
430
+ setPdfWarning(
431
+ `O PDF não contém valor venal para ${anosAusentes.length === 1 ? "o ano" : "os anos"} ${anos}. ` +
432
+ 'Complete manualmente o campo “Valores Venais”.',
433
+ );
434
+ }
435
  } catch (err) {
436
  setError(String(err));
437
  } finally {
 
464
  setForm(buildEmptyForm(options));
465
  setDatabaseColumns([]);
466
  setError("");
467
+ setPdfWarning("");
468
  setResult(null);
469
  };
470
 
 
773
  </button>
774
  </div>
775
 
776
+ {pdfWarning && <div className="warning-box">{pdfWarning}</div>}
777
+
778
  <label>
779
  Inscrição
780
  <input
frontend/src/styles.css CHANGED
@@ -182,6 +182,16 @@ h1 {
182
  padding: 10px 12px;
183
  }
184
 
 
 
 
 
 
 
 
 
 
 
185
  .tab-panel {
186
  background: var(--panel);
187
  border: 1px solid var(--line);
 
182
  padding: 10px 12px;
183
  }
184
 
185
+ .warning-box {
186
+ border: 1px solid #d99b2b;
187
+ border-radius: 10px;
188
+ background: #fff8e8;
189
+ color: #704600;
190
+ font-weight: 600;
191
+ line-height: 1.45;
192
+ padding: 12px 14px;
193
+ }
194
+
195
  .tab-panel {
196
  background: var(--panel);
197
  border: 1px solid var(--line);
tests/test_pdf_extractor.py CHANGED
@@ -1,6 +1,6 @@
1
  import unittest
2
 
3
- from extractors.pdf_extractor import _extrair_construcoes, _extrair_inscricao
4
 
5
 
6
  class PdfExtractorTests(unittest.TestCase):
@@ -32,6 +32,10 @@ Documento de Origem
32
  ["76,46 m² / 2012 / 63-ALVENARIA (D) - EDIFICIOS DE 6 A 10 PAVIMENTOS"],
33
  )
34
 
 
 
 
 
35
 
36
  if __name__ == "__main__":
37
  unittest.main()
 
1
  import unittest
2
 
3
+ from extractors.pdf_extractor import _extrair_construcoes, _extrair_inscricao, anos_venais_ausentes
4
 
5
 
6
  class PdfExtractorTests(unittest.TestCase):
 
32
  ["76,46 m² / 2012 / 63-ALVENARIA (D) - EDIFICIOS DE 6 A 10 PAVIMENTOS"],
33
  )
34
 
35
+ def test_identifica_anos_venais_ausentes_no_intervalo(self):
36
+ self.assertEqual(anos_venais_ausentes(["2021", "2023", "2025"]), [2022, 2024])
37
+ self.assertEqual(anos_venais_ausentes(["2024", "2025", "2026"]), [])
38
+
39
 
40
  if __name__ == "__main__":
41
  unittest.main()