Spaces:
Sleeping
Sleeping
Simplifica o quadro de CUB e move explicações para notas de rodapé
Browse filesUma linha por construção com CUB final por exercício; projetos simples mostram 100%. Composições e definições técnicas da MESA ficam em notas nativas junto da primeira ocorrência. Preserva alterações de pesos entre exercícios e ordena notas conforme a leitura. Validação: 110 testes e renderização conferida.
- document/evolutivo.py +60 -22
- document/footnotes.py +63 -0
- tests/test_cub_notes.py +73 -0
- tests/test_evo_adjustments.py +10 -4
- tests/test_evolutivo_documento.py +2 -2
document/evolutivo.py
CHANGED
|
@@ -14,7 +14,7 @@ from .avaliacoes_importadas import currency
|
|
| 14 |
from .mesa_content import content_for
|
| 15 |
from .evo_adjustments import REDUCERS, applied_reducers
|
| 16 |
from .fundamentacao import number, global_foundation
|
| 17 |
-
from .footnotes import add_footnote
|
| 18 |
from .formatters.table import set_table_font_size
|
| 19 |
from .formatters.table_fit import fit_table_to_page
|
| 20 |
from .anexos.metodologia import (_gerar_variaveis, _adicionar_pesquisa,
|
|
@@ -244,7 +244,6 @@ def gerar_corpo_evolutivo(doc, imported, num, caracteristicas_refs=None):
|
|
| 244 |
if not groups: add_body_text(doc,'Não há sequências construtivas detalhadas preservadas; o total importado permanece identificado na composição.')
|
| 245 |
construction_characteristics(doc,groups)
|
| 246 |
heading(doc,num,'CUB histórico, referência temporal e adequações',4)
|
| 247 |
-
definitions_for(doc,rows,['cubGroup','cubType','cubValue','cubPercent'])
|
| 248 |
construction_cub_notes(doc,groups)
|
| 249 |
heading(doc,num,'BDI e justificativa',4)
|
| 250 |
definitions_for(doc,rows,['bdi'])
|
|
@@ -373,28 +372,67 @@ def construction_characteristics(doc, groups):
|
|
| 373 |
|
| 374 |
|
| 375 |
def construction_cub_notes(doc, groups):
|
| 376 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 377 |
for entries in groups.values():
|
|
|
|
|
|
|
| 378 |
for row,c,i in entries:
|
| 379 |
-
|
| 380 |
-
|
| 381 |
-
|
| 382 |
-
|
| 383 |
-
|
| 384 |
-
|
| 385 |
-
|
| 386 |
-
|
| 387 |
-
|
| 388 |
-
|
| 389 |
-
|
| 390 |
-
|
| 391 |
-
|
| 392 |
-
|
| 393 |
-
|
| 394 |
-
|
| 395 |
-
|
| 396 |
-
|
| 397 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 398 |
|
| 399 |
|
| 400 |
def construction_bdi_notes(doc, groups):
|
|
|
|
| 14 |
from .mesa_content import content_for
|
| 15 |
from .evo_adjustments import REDUCERS, applied_reducers
|
| 16 |
from .fundamentacao import number, global_foundation
|
| 17 |
+
from .footnotes import add_footnote, add_first_footnote, order_footnotes
|
| 18 |
from .formatters.table import set_table_font_size
|
| 19 |
from .formatters.table_fit import fit_table_to_page
|
| 20 |
from .anexos.metodologia import (_gerar_variaveis, _adicionar_pesquisa,
|
|
|
|
| 244 |
if not groups: add_body_text(doc,'Não há sequências construtivas detalhadas preservadas; o total importado permanece identificado na composição.')
|
| 245 |
construction_characteristics(doc,groups)
|
| 246 |
heading(doc,num,'CUB histórico, referência temporal e adequações',4)
|
|
|
|
| 247 |
construction_cub_notes(doc,groups)
|
| 248 |
heading(doc,num,'BDI e justificativa',4)
|
| 249 |
definitions_for(doc,rows,['bdi'])
|
|
|
|
| 372 |
|
| 373 |
|
| 374 |
def construction_cub_notes(doc, groups):
|
| 375 |
+
entries_all = [entry for entries in groups.values() for entry in entries]
|
| 376 |
+
if not entries_all:
|
| 377 |
+
return
|
| 378 |
+
rows = list({row['id']:row for row,c,i in entries_all}.values())
|
| 379 |
+
labels = [ref(row,show_base=False) for row in rows]
|
| 380 |
+
records = [['Construção','Projeto CUB'] + labels]
|
| 381 |
+
composition_notes, siglas = [], OrderedDict()
|
| 382 |
for entries in groups.values():
|
| 383 |
+
by_id = {row['id']:c for row,c,i in entries}
|
| 384 |
+
variants = OrderedDict()
|
| 385 |
for row,c,i in entries:
|
| 386 |
+
if c.get('cubCompostoAtivo'):
|
| 387 |
+
composition = c.get('cubComposicaoCalculada') or c.get('cubComposicao') or [
|
| 388 |
+
{'cubTipo':c.get('cubTipo'),'peso':c.get('cubPesoPrincipalCalculado')},
|
| 389 |
+
{'cubTipo':c.get('cubTipoSecundario'),'peso':c.get('cubPesoSecundarioCalculado')}]
|
| 390 |
+
parts = [f"{item.get('cubTipo') or 'Projeto não informado'}: {pct(item['peso']) if number(item.get('peso')) is not None else 'peso não informado'}"
|
| 391 |
+
for item in composition if isinstance(item,dict)]
|
| 392 |
+
description = '; '.join(parts) or 'Composição não preservada no arquivo.'
|
| 393 |
+
project = 'CUB composto'
|
| 394 |
+
else:
|
| 395 |
+
project = str(c.get('cubTipo') or 'Não informado').split(' (',1)[0] + ' (100%)'
|
| 396 |
+
description = project
|
| 397 |
+
short = str(c.get('cubTipo') or '').split(' (',1)[0]
|
| 398 |
+
catalog = content_for(source(row))['referencias'].get('siglasCub',[])
|
| 399 |
+
found = next((item for item in catalog if item.get('sigla')==short),{})
|
| 400 |
+
if found:
|
| 401 |
+
siglas.setdefault(short,found['descricao'])
|
| 402 |
+
variants.setdefault((project,description),[]).append((row,c,i))
|
| 403 |
+
project_labels = list(dict.fromkeys(key[0] for key in variants))
|
| 404 |
+
label = project_labels[0] if len(project_labels)==1 else 'Varia por exercício'
|
| 405 |
+
records.append([construction_name(entries[0][1],entries[0][2]),label] + [currency(by_id[row['id']].get('valorCub')) if row['id'] in by_id else '—' for row in rows])
|
| 406 |
+
if any(key[0]=='CUB composto' for key in variants) or len(variants)>1:
|
| 407 |
+
details = []
|
| 408 |
+
for (project,description),subset in variants.items():
|
| 409 |
+
scope = '; '.join(ref(row,show_base=False) for row,c,i in subset) if len(variants)>1 else 'Todos os exercícios apresentados'
|
| 410 |
+
details.append(f'{scope}: {description}.')
|
| 411 |
+
composition_notes.append((len(records)-1,' '.join(details),any(key[0]=='CUB composto' for key in variants)))
|
| 412 |
+
obj = table(doc,records,[2.5,1.5]+[1.15]*len(rows),auto_fit=False)
|
| 413 |
+
for key,pattern in [('cubGroup',r'\bCUB\b'),('cubType',r'Projeto CUB'),
|
| 414 |
+
('cubValue',r'CUB histórico'),('cubPercent',r'%\s+(?:do\s+)?CUB')]:
|
| 415 |
+
variants = OrderedDict()
|
| 416 |
+
for row,c,i in entries_all:
|
| 417 |
+
text = content_for(source(row)).get('notas',{}).get(key)
|
| 418 |
+
if text:
|
| 419 |
+
variants.setdefault(text,[]).append(ref(row,show_base=False))
|
| 420 |
+
if variants:
|
| 421 |
+
text = ' '.join(text + (f" Aplicável a {'; '.join(dict.fromkeys(scope))}." if len(variants)>1 else '') for text,scope in variants.items())
|
| 422 |
+
if add_first_footnote(doc,pattern,text) is None:
|
| 423 |
+
add_footnote(doc,obj.cell(0,1).paragraphs[0],text)
|
| 424 |
+
used_notes = []
|
| 425 |
+
explained_composite = False
|
| 426 |
+
for index,text,has_composite in composition_notes:
|
| 427 |
+
if has_composite and not explained_composite:
|
| 428 |
+
text = 'O CUB composto corresponde à média ponderada dos projetos: CUB = Σ(CUBᵢ × pesoᵢ) / Σ(pesoᵢ). ' + text
|
| 429 |
+
explained_composite = True
|
| 430 |
+
add_footnote(doc,obj.cell(index,1).paragraphs[0],text)
|
| 431 |
+
used_notes.append(text)
|
| 432 |
+
for short,description in siglas.items():
|
| 433 |
+
add_first_footnote(doc,r'\b'+re.escape(short)+r'\b',f'{short}: {description}.')
|
| 434 |
+
order_footnotes(doc)
|
| 435 |
+
fit_table_to_page(doc,obj,notes=used_notes)
|
| 436 |
|
| 437 |
|
| 438 |
def construction_bdi_notes(doc, groups):
|
document/footnotes.py
CHANGED
|
@@ -6,6 +6,69 @@ from docx.opc.constants import CONTENT_TYPE as CT, RELATIONSHIP_TYPE as RT
|
|
| 6 |
from docx.opc.packuri import PackURI
|
| 7 |
from docx.opc.part import Part, XmlPart
|
| 8 |
from docx.shared import Pt
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 9 |
|
| 10 |
|
| 11 |
def add_footnote(doc, paragraph, text):
|
|
|
|
| 6 |
from docx.opc.packuri import PackURI
|
| 7 |
from docx.opc.part import Part, XmlPart
|
| 8 |
from docx.shared import Pt
|
| 9 |
+
from docx.text.paragraph import Paragraph
|
| 10 |
+
from copy import deepcopy
|
| 11 |
+
import re
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
def add_first_footnote(doc, pattern, text):
|
| 15 |
+
"""Attach a note immediately after the first body/table occurrence of a term."""
|
| 16 |
+
for element in doc.element.body.iter(qn('w:p')):
|
| 17 |
+
paragraph = Paragraph(element, doc._body)
|
| 18 |
+
match = re.search(pattern, paragraph.text)
|
| 19 |
+
if not match:
|
| 20 |
+
continue
|
| 21 |
+
offset = 0
|
| 22 |
+
for run in paragraph.runs:
|
| 23 |
+
end = offset + len(run.text)
|
| 24 |
+
if offset < match.end() <= end:
|
| 25 |
+
cut = match.end() - offset
|
| 26 |
+
tail = run.text[cut:]
|
| 27 |
+
run.text = run.text[:cut]
|
| 28 |
+
identifier = add_footnote(doc, paragraph, text)
|
| 29 |
+
marker = paragraph.runs[-1]._r
|
| 30 |
+
run._r.addnext(marker)
|
| 31 |
+
if tail:
|
| 32 |
+
remainder = OxmlElement('w:r')
|
| 33 |
+
if run._r.rPr is not None:
|
| 34 |
+
remainder.append(deepcopy(run._r.rPr))
|
| 35 |
+
content = OxmlElement('w:t')
|
| 36 |
+
content.text = tail
|
| 37 |
+
content.set(qn('xml:space'), 'preserve')
|
| 38 |
+
remainder.append(content)
|
| 39 |
+
marker.addnext(remainder)
|
| 40 |
+
return identifier
|
| 41 |
+
offset = end
|
| 42 |
+
return add_footnote(doc, paragraph, text)
|
| 43 |
+
return None
|
| 44 |
+
|
| 45 |
+
|
| 46 |
+
def order_footnotes(doc):
|
| 47 |
+
"""Keep IDs and stored note order aligned with their first body occurrence.
|
| 48 |
+
|
| 49 |
+
Notes can be attached retroactively to earlier terms. Align both orders so
|
| 50 |
+
Word and LibreOffice associate every visible number with the right text.
|
| 51 |
+
"""
|
| 52 |
+
try:
|
| 53 |
+
part = doc.part.part_related_by(RT.FOOTNOTES)
|
| 54 |
+
except KeyError:
|
| 55 |
+
return
|
| 56 |
+
root = parse_xml(part.blob)
|
| 57 |
+
notes = {n.get(qn('w:id')): n for n in root if int(n.get(qn('w:id'))) > 0}
|
| 58 |
+
refs = list(doc.element.body.iter(qn('w:footnoteReference')))
|
| 59 |
+
ordered_ids = list(dict.fromkeys(r.get(qn('w:id')) for r in refs))
|
| 60 |
+
ordered_ids += [key for key in notes if key not in ordered_ids]
|
| 61 |
+
mapping = {key: str(i+1) for i,key in enumerate(ordered_ids)}
|
| 62 |
+
for reference in refs:
|
| 63 |
+
reference.set(qn('w:id'),mapping[reference.get(qn('w:id'))])
|
| 64 |
+
for old_id in ordered_ids:
|
| 65 |
+
note = notes[old_id]
|
| 66 |
+
note.set(qn('w:id'),mapping[old_id])
|
| 67 |
+
root.append(note)
|
| 68 |
+
if isinstance(part,XmlPart):
|
| 69 |
+
part._element = root
|
| 70 |
+
else:
|
| 71 |
+
part._blob = etree.tostring(root,xml_declaration=True,encoding='UTF-8',standalone=True)
|
| 72 |
|
| 73 |
|
| 74 |
def add_footnote(doc, paragraph, text):
|
tests/test_cub_notes.py
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import copy
|
| 2 |
+
import io
|
| 3 |
+
import unittest
|
| 4 |
+
import zipfile
|
| 5 |
+
|
| 6 |
+
from docx import Document
|
| 7 |
+
from docx.oxml import parse_xml
|
| 8 |
+
from docx.oxml.ns import qn
|
| 9 |
+
|
| 10 |
+
from document.evolutivo import construction_cub_notes, construction_groups
|
| 11 |
+
from document.footnotes import add_first_footnote
|
| 12 |
+
from test_evolutivo_documento import fixture, resolved
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
class CubNotesTests(unittest.TestCase):
|
| 16 |
+
def notes(self, doc):
|
| 17 |
+
stream=io.BytesIO();doc.save(stream)
|
| 18 |
+
with zipfile.ZipFile(stream) as archive:
|
| 19 |
+
root=parse_xml(archive.read('word/footnotes.xml'))
|
| 20 |
+
return [''.join(t.text or '' for t in n.iter(qn('w:t'))).strip() for n in root if int(n.get(qn('w:id'))) > 0]
|
| 21 |
+
|
| 22 |
+
def test_one_row_per_construction_saved_values_and_composition_in_footnotes(self):
|
| 23 |
+
data=fixture((2021,2022,2023,2024,2025))
|
| 24 |
+
for year in data['anos']:
|
| 25 |
+
simple=copy.deepcopy(year['construcao']['construcoes'][0])
|
| 26 |
+
simple.update(id='simples',seq=2,nome='Galpão simples',cubTipo='GI (Galpao Industrial)',cubCompostoAtivo=False,valorCub=1000)
|
| 27 |
+
year['construcao']['construcoes'].append(simple)
|
| 28 |
+
imported=resolved(data)
|
| 29 |
+
doc=Document()
|
| 30 |
+
intro=doc.add_paragraph('O custo unitário básico (CUB) é empregado no cálculo.')
|
| 31 |
+
percent=doc.add_paragraph('CUNIT = CUB × (% CUB / 100)')
|
| 32 |
+
doc.add_paragraph('CUB histórico, referência temporal e adequações')
|
| 33 |
+
construction_cub_notes(doc,construction_groups(imported['avaliacoes']))
|
| 34 |
+
self.assertEqual(len(doc.tables),1)
|
| 35 |
+
table=doc.tables[0]
|
| 36 |
+
self.assertEqual(len(table.rows),3)
|
| 37 |
+
self.assertEqual(len(table.columns),7)
|
| 38 |
+
self.assertEqual(table.cell(1,1).text,'CUB composto')
|
| 39 |
+
self.assertEqual(table.cell(2,1).text,'GI (100%)')
|
| 40 |
+
self.assertEqual([c.text for c in table.rows[1].cells][2:],['R$ 1.400,00']*5)
|
| 41 |
+
self.assertEqual([c.text for c in table.rows[2].cells][2:],['R$ 1.000,00']*5)
|
| 42 |
+
notes=self.notes(doc)
|
| 43 |
+
self.assertEqual(len(notes),6)
|
| 44 |
+
self.assertIn('% do CUB',notes[1])
|
| 45 |
+
self.assertIn('valor do CUB por metro quadrado',notes[2])
|
| 46 |
+
self.assertIn('tipo ou composição de CUB',notes[3])
|
| 47 |
+
composite=next(n for n in notes if 'média ponderada' in n)
|
| 48 |
+
self.assertIn('GI: 60,00%; R1-N: 40,00%',composite)
|
| 49 |
+
self.assertIn('Todos os exercícios',composite)
|
| 50 |
+
self.assertTrue(any('GI: Galpao industrial' in n for n in notes))
|
| 51 |
+
self.assertIsNotNone(intro._p.find('.//'+qn('w:footnoteReference')))
|
| 52 |
+
self.assertIsNotNone(percent._p.find('.//'+qn('w:footnoteReference')))
|
| 53 |
+
self.assertFalse(any('grupo dos dados' in p.text or 'média ponderada' in p.text for p in doc.paragraphs))
|
| 54 |
+
self.assertEqual(intro.text,'O custo unitário básico (CUB) é empregado no cálculo.')
|
| 55 |
+
|
| 56 |
+
def test_first_occurrence_marker_preserves_text_and_formatting(self):
|
| 57 |
+
doc=Document()
|
| 58 |
+
first=doc.add_paragraph()
|
| 59 |
+
first.add_run('Projeto ')
|
| 60 |
+
first.add_run('CUB composto e CUB simples.').italic=True
|
| 61 |
+
later=doc.add_paragraph('Outro CUB composto.')
|
| 62 |
+
add_first_footnote(doc,r'CUB composto','Explicação técnica.')
|
| 63 |
+
self.assertEqual(first.text,'Projeto CUB composto e CUB simples.')
|
| 64 |
+
self.assertEqual(first.runs[1].text,'CUB composto')
|
| 65 |
+
self.assertTrue(first.runs[1].italic)
|
| 66 |
+
self.assertIsNotNone(first.runs[2]._r.find(qn('w:footnoteReference')))
|
| 67 |
+
self.assertTrue(first.runs[3].italic)
|
| 68 |
+
self.assertIsNone(later._p.find('.//'+qn('w:footnoteReference')))
|
| 69 |
+
self.assertEqual(self.notes(doc),['Explicação técnica.'])
|
| 70 |
+
|
| 71 |
+
|
| 72 |
+
if __name__=='__main__':
|
| 73 |
+
unittest.main()
|
tests/test_evo_adjustments.py
CHANGED
|
@@ -121,8 +121,10 @@ class AppliedAdjustmentsTests(unittest.TestCase):
|
|
| 121 |
c=copy.deepcopy(y['construcao']['construcoes'][0]);c.update(id='segundo',seq=2,nome='Galpão B')
|
| 122 |
y['construcao']['construcoes'].append(c)
|
| 123 |
doc=body(resolved(data))
|
| 124 |
-
weighted=[t for t in doc.tables if t.rows[0].cells[1].text=='Projeto
|
| 125 |
self.assertEqual(len(weighted),1);self.assertEqual(len(weighted[0].rows),3)
|
|
|
|
|
|
|
| 126 |
calculations=[t for t in doc.tables if any(r.cells[0].text=='Valor da construção (R$)' for r in t.rows)]
|
| 127 |
self.assertEqual(len(calculations),2)
|
| 128 |
for t in calculations:
|
|
@@ -136,6 +138,10 @@ class AppliedAdjustmentsTests(unittest.TestCase):
|
|
| 136 |
t=next(t for t in doc.tables if t.rows[0].cells[1].text=='Tipo / ano')
|
| 137 |
self.assertEqual(len(t.rows),3)
|
| 138 |
self.assertIn('IPTU 2023',t.rows[2].cells[0].text)
|
| 139 |
-
t=next(t for t in doc.tables if t.rows[0].cells[1].text=='Projeto
|
| 140 |
-
self.assertEqual(len(t.rows),
|
| 141 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 121 |
c=copy.deepcopy(y['construcao']['construcoes'][0]);c.update(id='segundo',seq=2,nome='Galpão B')
|
| 122 |
y['construcao']['construcoes'].append(c)
|
| 123 |
doc=body(resolved(data))
|
| 124 |
+
weighted=[t for t in doc.tables if t.rows[0].cells[1].text=='Projeto CUB']
|
| 125 |
self.assertEqual(len(weighted),1);self.assertEqual(len(weighted[0].rows),3)
|
| 126 |
+
self.assertEqual(weighted[0].cell(1,1).text,'CUB composto')
|
| 127 |
+
self.assertEqual([c.text for c in weighted[0].rows[0].cells][2:],['IPTU 2022','IPTU 2023'])
|
| 128 |
calculations=[t for t in doc.tables if any(r.cells[0].text=='Valor da construção (R$)' for r in t.rows)]
|
| 129 |
self.assertEqual(len(calculations),2)
|
| 130 |
for t in calculations:
|
|
|
|
| 138 |
t=next(t for t in doc.tables if t.rows[0].cells[1].text=='Tipo / ano')
|
| 139 |
self.assertEqual(len(t.rows),3)
|
| 140 |
self.assertIn('IPTU 2023',t.rows[2].cells[0].text)
|
| 141 |
+
t=next(t for t in doc.tables if t.rows[0].cells[1].text=='Projeto CUB')
|
| 142 |
+
self.assertEqual(len(t.rows),2)
|
| 143 |
+
from docx.opc.constants import RELATIONSHIP_TYPE as RT
|
| 144 |
+
from docx.oxml import parse_xml
|
| 145 |
+
text=' '.join(parse_xml(doc.part.part_related_by(RT.FOOTNOTES).blob).itertext())
|
| 146 |
+
for expected in ['IPTU 2022','IPTU 2023','60,00%','50,00%']:
|
| 147 |
+
self.assertIn(expected,text)
|
tests/test_evolutivo_documento.py
CHANGED
|
@@ -127,8 +127,8 @@ class EvolutivoDocumentTests(unittest.TestCase):
|
|
| 127 |
body = etree.fromstring(archive.read('word/document.xml'))
|
| 128 |
refs = body.findall('.//'+qn('w:footnoteReference'))
|
| 129 |
notes = {n.get(qn('w:id')): ''.join(n.itertext()) for n in root if int(n.get(qn('w:id'))) > 0}
|
| 130 |
-
self.
|
| 131 |
-
self.assertEqual(len(
|
| 132 |
self.assertEqual({r.get(qn('w:id')) for r in refs}, set(notes))
|
| 133 |
self.assertIn('CA: campo de arbítrio', notes['2'])
|
| 134 |
self.assertIn('intervalo de confiança de 80%', notes['3'])
|
|
|
|
| 127 |
body = etree.fromstring(archive.read('word/document.xml'))
|
| 128 |
refs = body.findall('.//'+qn('w:footnoteReference'))
|
| 129 |
notes = {n.get(qn('w:id')): ''.join(n.itertext()) for n in root if int(n.get(qn('w:id'))) > 0}
|
| 130 |
+
self.assertGreaterEqual(len(refs), 4)
|
| 131 |
+
self.assertEqual(len(refs), len(notes))
|
| 132 |
self.assertEqual({r.get(qn('w:id')) for r in refs}, set(notes))
|
| 133 |
self.assertIn('CA: campo de arbítrio', notes['2'])
|
| 134 |
self.assertIn('intervalo de confiança de 80%', notes['3'])
|