Spaces:
Sleeping
Sleeping
Integra conservação e coeficiente H às tabelas de construção
Browse filesTransfere 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 +47 -22
- tests/test_conservation_notes.py +79 -0
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','
|
|
|
|
| 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 |
-
|
| 347 |
-
|
| 348 |
-
|
|
|
|
|
|
|
| 349 |
records.append([entry_scope(subset,groups,show_base=False)]+list(values))
|
| 350 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 458 |
from backend.app.evo_documentation import public_reason
|
| 459 |
-
notes =
|
| 460 |
-
|
| 461 |
-
|
| 462 |
-
|
| 463 |
-
|
| 464 |
-
|
| 465 |
-
|
| 466 |
-
|
| 467 |
-
|
| 468 |
-
|
| 469 |
-
|
| 470 |
-
|
| 471 |
-
|
| 472 |
-
|
|
|
|
| 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()
|