gui-sparim commited on
Commit
bb18f65
·
verified ·
1 Parent(s): 6bc379e

Integra conservação e coeficiente H às tabelas de construção

Browse files

Transfere as descrições das classes, referências de vida útil e ajustes manuais para notas nativas de rodapé, sem repetição. Preserva coeficientes salvos e aplicabilidade por exercício. Validação: 117 testes e conferência visual com três construções e cinco IPTUs.

document/evolutivo.py CHANGED
@@ -252,11 +252,15 @@ def gerar_corpo_evolutivo(doc, imported, num, caracteristicas_refs=None):
252
  add_body_text(doc,'A depreciação física considera a vida útil consumida e o estado de conservação, pelo método de Ross-Heidecke. O valor residual integra o fator remanescente aplicado ao custo de reedição.').paragraph_format.keep_with_next = True
253
  depreciation_equations(doc)
254
  add_body_text(doc,'Os resultados apresentados são os registrados na avaliação, sem recomposição a partir dos coeficientes arredondados para exibição.')
255
- construction_depreciation_notes(doc,groups)
256
  heading(doc,num,'Cálculo por construção e exercício',4)
257
  for entries in groups.values():
258
  add_body_text(doc,construction_name(entries[0][1],entries[0][2]))
259
  by_id = {row['id']: c for row,c,i in entries}
 
 
 
 
 
260
  # One comparative calculation table per effective construction, with all
261
  # annual inputs together; characteristics above are grouped by changes.
262
  compared(doc,[row for row,c,i in entries],[
@@ -268,10 +272,12 @@ def gerar_corpo_evolutivo(doc, imported, num, caracteristicas_refs=None):
268
  ('BDI (%)',lambda row:pct(by_id[row['id']].get('bdi'))),
269
  ('Fator BDI',lambda row:fmt(1+number(by_id[row['id']]['bdi'])/100,4) if number(by_id[row['id']].get('bdi')) is not None else 'Não informado'),
270
  ('Vida útil consumida (%)',lambda row:pct(by_id[row['id']].get('percentualVidaUtil'))),
 
 
271
  ('Depreciação K',lambda row:fmt(by_id[row['id']].get('depreciacao'),4)),
272
  ('Kd',lambda row:fmt(by_id[row['id']].get('kd'),3)),
273
  ('Valor da construção (R$)',lambda row:currency(by_id[row['id']].get('vcalcConstSeq'))),
274
- ])
275
  refs['composicao'] = heading(doc,num,'AVALIAÇÃO DO IMÓVEL OBJETO',2)
276
  add_body_text(doc,'Na ausência de outros ajustes, a composição corresponde a VI = (VT + VC) × Fc, em que VT é o valor territorial e VC é o custo de reedição das construções. Havendo ajustes, a sequência efetivamente salva, apresentada abaixo, prevalece sobre essa expressão simplificada.').paragraph_format.keep_with_next = True
277
  gerar_ajustes(doc,rows,territorial=False)
@@ -336,18 +342,30 @@ def construction_notes(doc, notes, groups):
336
 
337
 
338
  def construction_characteristics(doc, groups):
339
- records = [['Construção / referência','Tipo / ano','Área real / de cálculo (m²)','Vida útil / residual R','Conservação / H']]
 
340
  for entries in groups.values():
341
  variants = OrderedDict()
342
  for row,c,i in entries:
343
  values = (f"{c.get('tipoConstrucao') or 'Não informado'} / {c.get('anoConstrucao') or 'Não informado'}",
344
  f"{fmt(c.get('areaTotal'))} / {fmt(c.get('areaCalculada'))}",
345
  f"{fmt(c.get('vidaUtil'),0)} anos / {pct(c.get('valorResidual'),True)}",
346
- f"{c.get('estadoConservacao') or 'Não informado'} / {fmt(c.get('coefHeidecke'),4)}")
347
- variants.setdefault(values,[]).append((row,c,i))
348
- for values,subset in variants.items():
 
 
349
  records.append([entry_scope(subset,groups,show_base=False)]+list(values))
350
- if len(records)>1: table(doc,records,[1.6,1.6,1.2,1.1,1.5],font_size=7)
 
 
 
 
 
 
 
 
 
351
  area_records = [['Construção / referência','Tipo de área','Área real (m²)','Coeficiente','Parcela equivalente (m²)']]
352
  for entries in groups.values():
353
  areas = OrderedDict()
@@ -454,22 +472,29 @@ def construction_bdi_notes(doc, groups):
454
  table(doc,records,[2.3,3.2,1],font_size=7)
455
 
456
 
457
- def construction_depreciation_notes(doc,groups):
 
 
 
 
 
 
458
  from backend.app.evo_documentation import public_reason
459
- notes = OrderedDict()
460
- for entries in groups.values():
461
- for row,c,i in entries:
462
- saved = ((source(row).get('construcao') or {}).get('avaliacao') or {}).get('referenciasCalculo') or content_for(source(row))['referencias']
463
- life = next((item for item in saved.get('vidaUtilTipos',[]) if item.get('tipo')==c.get('tipoConstrucao')),None)
464
- if life:
465
- notes.setdefault(f"Referência de vida útil para {life['tipo']}: {life.get('vidaUtil','não informada')} anos. A vida consumida considera a idade no ano de referência, limitada a 100%, ou o ajuste manual registrado.",[]).append((row,c,i))
466
- state = next((item for item in saved.get('estadosConservacao',[]) if item.get('estado')==c.get('estadoConservacao')),None)
467
- if state and state.get('descricao'): notes.setdefault(f"{c['estadoConservacao']}: {state['descricao']}",[]).append((row,c,i))
468
- if c.get('percentualVidaUtilManualAtivo') or c.get('percentualVidaUtilAjusteManual'):
469
- reason = public_reason(c,'percentualVidaUtilJustificativa')
470
- text = f"Ajuste manual de vida consumida: padrão {pct(c.get('percentualVidaUtilPadrao'))}; adotado {pct(c.get('percentualVidaUtil'))}. " + (reason or 'A justificativa pública não está disponível no arquivo.')
471
- notes.setdefault(text,[]).append((row,c,i))
472
- construction_notes(doc,notes,groups)
 
473
 
474
 
475
  def reducer_metrics(rows,territorial):
 
252
  add_body_text(doc,'A depreciação física considera a vida útil consumida e o estado de conservação, pelo método de Ross-Heidecke. O valor residual integra o fator remanescente aplicado ao custo de reedição.').paragraph_format.keep_with_next = True
253
  depreciation_equations(doc)
254
  add_body_text(doc,'Os resultados apresentados são os registrados na avaliação, sem recomposição a partir dos coeficientes arredondados para exibição.')
 
255
  heading(doc,num,'Cálculo por construção e exercício',4)
256
  for entries in groups.values():
257
  add_body_text(doc,construction_name(entries[0][1],entries[0][2]))
258
  by_id = {row['id']: c for row,c,i in entries}
259
+ manual_notes = OrderedDict()
260
+ for row,c,i in entries:
261
+ note = construction_depreciation_notes(row,c).get('manual')
262
+ if note: manual_notes.setdefault(note,[]).append(ref(row,show_base=False))
263
+ manual_text = ' '.join(f"{'; '.join(references)}: {text}" for text,references in manual_notes.items())
264
  # One comparative calculation table per effective construction, with all
265
  # annual inputs together; characteristics above are grouped by changes.
266
  compared(doc,[row for row,c,i in entries],[
 
272
  ('BDI (%)',lambda row:pct(by_id[row['id']].get('bdi'))),
273
  ('Fator BDI',lambda row:fmt(1+number(by_id[row['id']]['bdi'])/100,4) if number(by_id[row['id']].get('bdi')) is not None else 'Não informado'),
274
  ('Vida útil consumida (%)',lambda row:pct(by_id[row['id']].get('percentualVidaUtil'))),
275
+ ('Estado de conservação',lambda row:conservation_code(by_id[row['id']].get('estadoConservacao'))),
276
+ ('Coeficiente H',lambda row:fmt(by_id[row['id']].get('coefHeidecke'),4)),
277
  ('Depreciação K',lambda row:fmt(by_id[row['id']].get('depreciacao'),4)),
278
  ('Kd',lambda row:fmt(by_id[row['id']].get('kd'),3)),
279
  ('Valor da construção (R$)',lambda row:currency(by_id[row['id']].get('vcalcConstSeq'))),
280
+ ],notes={'Vida útil consumida (%)':manual_text} if manual_text else None)
281
  refs['composicao'] = heading(doc,num,'AVALIAÇÃO DO IMÓVEL OBJETO',2)
282
  add_body_text(doc,'Na ausência de outros ajustes, a composição corresponde a VI = (VT + VC) × Fc, em que VT é o valor territorial e VC é o custo de reedição das construções. Havendo ajustes, a sequência efetivamente salva, apresentada abaixo, prevalece sobre essa expressão simplificada.').paragraph_format.keep_with_next = True
283
  gerar_ajustes(doc,rows,territorial=False)
 
342
 
343
 
344
  def construction_characteristics(doc, groups):
345
+ records = [['Construção / referência','Tipo / ano','Área real / de cálculo (m²)','Vida útil / residual R','Estado de conservação','H']]
346
+ record_notes = []
347
  for entries in groups.values():
348
  variants = OrderedDict()
349
  for row,c,i in entries:
350
  values = (f"{c.get('tipoConstrucao') or 'Não informado'} / {c.get('anoConstrucao') or 'Não informado'}",
351
  f"{fmt(c.get('areaTotal'))} / {fmt(c.get('areaCalculada'))}",
352
  f"{fmt(c.get('vidaUtil'),0)} anos / {pct(c.get('valorResidual'),True)}",
353
+ conservation_code(c.get('estadoConservacao')),fmt(c.get('coefHeidecke'),4))
354
+ notes = construction_depreciation_notes(row,c)
355
+ details = (notes.get('life',''),notes.get('state',''))
356
+ variants.setdefault((values,details),[]).append((row,c,i))
357
+ for (values,details),subset in variants.items():
358
  records.append([entry_scope(subset,groups,show_base=False)]+list(values))
359
+ record_notes.append((len(records)-1,details))
360
+ if len(records)>1:
361
+ obj = table(doc,records,[1.8,1.6,1.2,1.1,.9,.65],auto_fit=False)
362
+ seen = set()
363
+ for index,details in record_notes:
364
+ for column,text in zip((3,4),details):
365
+ if text and text not in seen:
366
+ add_footnote(doc,obj.cell(index,column).paragraphs[0],text)
367
+ seen.add(text)
368
+ fit_table_to_page(doc,obj,notes=seen)
369
  area_records = [['Construção / referência','Tipo de área','Área real (m²)','Coeficiente','Parcela equivalente (m²)']]
370
  for entries in groups.values():
371
  areas = OrderedDict()
 
472
  table(doc,records,[2.3,3.2,1],font_size=7)
473
 
474
 
475
+ def conservation_code(value):
476
+ text = str(value or '').strip()
477
+ match = re.match(r'^([A-I])(?:\s*[-–—]|$)',text,re.I)
478
+ return match[1].upper() if match else text or 'Não informado'
479
+
480
+
481
+ def construction_depreciation_notes(row,c):
482
  from backend.app.evo_documentation import public_reason
483
+ notes = {}
484
+ saved = ((source(row).get('construcao') or {}).get('avaliacao') or {}).get('referenciasCalculo') or {}
485
+ fallback = content_for(source(row))['referencias']
486
+ life = next((item for item in saved.get('vidaUtilTipos',fallback.get('vidaUtilTipos',[])) if item.get('tipo')==c.get('tipoConstrucao')),None)
487
+ if life:
488
+ notes['life'] = f"Referência de vida útil para {life['tipo']}: {life.get('vidaUtil','não informada')} anos. A vida consumida considera a idade no ano de referência, limitada a 100%, ou o ajuste manual registrado. R é o valor residual, expresso em percentual no quadro."
489
+ raw = str(c.get('estadoConservacao') or '').strip()
490
+ state = next((item for item in saved.get('estadosConservacao',fallback.get('estadosConservacao',[])) if str(item.get('estado','')).strip().casefold()==raw.casefold()),None)
491
+ if raw:
492
+ notes['state'] = f"{raw}: {state['descricao']}" if state and state.get('descricao') else f"Estado de conservação registrado: {raw}. A descrição técnica desta classe não foi preservada no arquivo."
493
+ notes['state'] += ' H é o coeficiente de conservação adotado no cálculo de Ross-Heidecke; o valor registrado consta no quadro.'
494
+ if c.get('percentualVidaUtilManualAtivo') or c.get('percentualVidaUtilAjusteManual'):
495
+ reason = public_reason(c,'percentualVidaUtilJustificativa')
496
+ notes['manual'] = f"Ajuste manual de vida consumida: padrão {pct(c.get('percentualVidaUtilPadrao'))}; adotado {pct(c.get('percentualVidaUtil'))}. " + (reason or 'A justificativa pública não está disponível no arquivo.')
497
+ return notes
498
 
499
 
500
  def reducer_metrics(rows,territorial):
tests/test_conservation_notes.py ADDED
@@ -0,0 +1,79 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import copy
2
+ import unittest
3
+
4
+ from docx import Document
5
+ from docx.opc.constants import RELATIONSHIP_TYPE as RT
6
+ from docx.oxml import parse_xml
7
+ from docx.oxml.ns import qn
8
+
9
+ from document.evolutivo import construction_characteristics, construction_groups
10
+ from test_evo_adjustments import body
11
+ from test_evolutivo_documento import fixture, resolved, all_text
12
+
13
+
14
+ def notes(doc):
15
+ root = parse_xml(doc.part.part_related_by(RT.FOOTNOTES).blob)
16
+ return [''.join(n.itertext(qn('w:t'))) for n in root if int(n.get(qn('w:id'))) > 0]
17
+
18
+
19
+ class ConservationNotesTests(unittest.TestCase):
20
+ def data(self):
21
+ data = fixture((2021, 2022))
22
+ for year in data['anos']:
23
+ year['construcao']['avaliacao']['referenciasCalculo'] = {
24
+ 'vidaUtilTipos': [{'tipo': 'GALPOES (DEPOSITOS)', 'vidaUtil': 70}],
25
+ 'estadosConservacao': [{'estado': 'C - regular', 'coeficiente': .0252,
26
+ 'descricao': 'Descrição histórica da classe regular.'}]}
27
+ year['construcao']['construcoes'][0].update(estadoConservacao='C - regular', coefHeidecke=.0317)
28
+ return data
29
+
30
+ def test_codes_and_saved_coefficients_in_tables_descriptions_only_in_notes(self):
31
+ imported = resolved(self.data())
32
+ before = copy.deepcopy(imported)
33
+ doc = body(imported)
34
+ characteristics = next(t for t in doc.tables if t.cell(0, 1).text == 'Tipo / ano')
35
+ self.assertEqual([c.text for c in characteristics.rows[0].cells][-2:], ['Estado de conservação', 'H'])
36
+ self.assertEqual([c.text for c in characteristics.rows[1].cells][-2:], ['C', '0,0317'])
37
+ calculation = next(t for t in doc.tables if any(r.cells[0].text == 'Coeficiente H' for r in t.rows))
38
+ for label, expected in [('Estado de conservação', 'C'), ('Coeficiente H', '0,0317')]:
39
+ row = next(r for r in calculation.rows if r.cells[0].text == label)
40
+ self.assertEqual([c.text for c in row.cells[1:]], [expected, expected])
41
+ self.assertNotIn('Descrição histórica', all_text(doc))
42
+ self.assertNotIn('Referência de vida útil para', all_text(doc))
43
+ self.assertNotIn('Ajuste manual de vida consumida', all_text(doc))
44
+ footnotes = notes(doc)
45
+ self.assertEqual(sum('Descrição histórica da classe regular.' in text for text in footnotes), 1)
46
+ self.assertTrue(any('70 anos' in text and 'R é o valor residual' in text for text in footnotes))
47
+ self.assertTrue(any('IPTU 2022; IPTU 2023' in text and 'padrão 50,00%; adotado 40,00%' in text for text in footnotes))
48
+ self.assertNotIn('SEGREDO INTERNO', ' '.join(footnotes))
49
+ self.assertEqual(imported, before)
50
+
51
+ def test_shared_class_is_explained_once_and_historical_changes_keep_their_scope(self):
52
+ data = self.data()
53
+ for year in data['anos']:
54
+ second = copy.deepcopy(year['construcao']['construcoes'][0])
55
+ second.update(id='segundo', seq=2, nome='Galpão B')
56
+ year['construcao']['construcoes'].append(second)
57
+ data['anos'][1]['construcao']['avaliacao']['referenciasCalculo']['estadosConservacao'][0]['descricao'] = 'Caracterização revista no segundo exercício.'
58
+ doc = Document()
59
+ construction_characteristics(doc, construction_groups(resolved(data)['avaliacoes']))
60
+ self.assertEqual(len(doc.tables[0].rows), 5)
61
+ for index in (1, 3):
62
+ self.assertIn('IPTU 2022', doc.tables[0].cell(index, 0).text)
63
+ self.assertIn('IPTU 2023', doc.tables[0].cell(index + 1, 0).text)
64
+ self.assertEqual(sum('Descrição histórica' in text for text in notes(doc)), 1)
65
+ self.assertEqual(sum('Caracterização revista' in text for text in notes(doc)), 1)
66
+
67
+ def test_unknown_state_keeps_original_label_without_borrowing_catalog_description(self):
68
+ data = self.data()
69
+ for year in data['anos']:
70
+ year['construcao']['construcoes'][0]['estadoConservacao'] = 'C - classificação especial'
71
+ doc = Document()
72
+ construction_characteristics(doc, construction_groups(resolved(data)['avaliacoes']))
73
+ self.assertIn('C - classificação especial', ' '.join(notes(doc)))
74
+ self.assertNotIn('Descrição histórica', ' '.join(notes(doc)))
75
+ self.assertIn('não foi preservada', ' '.join(notes(doc)))
76
+
77
+
78
+ if __name__ == '__main__':
79
+ unittest.main()