Spaces:
Sleeping
Sleeping
Download document/territorial_narrative.py from gui-sparim/teste_docs: direct link, hf CLI and curl.
- Browser
- Download file 16.2 kB
-
https://huggingface.co/spaces/gui-sparim/teste_docs/resolve/main/document/territorial_narrative.py
- Command line
-
hf download hf://spaces/gui-sparim/teste_docs/document/territorial_narrative.py
-
curl -L -o territorial_narrative.py https://huggingface.co/spaces/gui-sparim/teste_docs/resolve/main/document/territorial_narrative.py
16.2 kB
| """Narrative from the recorded property conditions to the adopted land value. | |
| Percentages and effects come from the saved MESA assessment. Legal references | |
| identify the condition without replacing the historical percentages or results. | |
| """ | |
| from collections import OrderedDict | |
| import re | |
| import unicodedata | |
| from .evo_adjustments import REDUCERS, applied_reducers | |
| from .fundamentacao import number | |
| from .evolutivo import (source, terrain, result, ref, fmt, pct, complement, | |
| heading, add_body_text) | |
| # Original arts. 56, 60 and 61 and the 2019 wording of art. 88 checked on | |
| # 21/10/2026. Percentages remain those recorded in the assessment. | |
| DECREE_SOURCE = 'https://legislacao.portoalegre.rs.gov.br/media/sapl/public/normajuridica/sirel/Norma_16500_20091110_1.html' | |
| DECREE = 'Decreto Municipal nº 16.500, de 10 de novembro de 2009' | |
| AMENDMENT_SOURCE = 'https://dopaonlineupload.procempa.com.br/dopaonlineupload/3123_ce_272713_1.pdf' | |
| ARTICLE_88_DECREE = DECREE+', na redação dada pelo Decreto Municipal nº 20.396, de 11 de novembro de 2019' | |
| ARTICLE_88_LETTERS = { | |
| 'redutor88I': {'app': 'a', 'diretriz_logradouro': 'b', 'faixa_redes': 'c', | |
| 'alta_tensao': 'd', 'ocupacoes_irregulares': 'e'}, | |
| 'redutor88II': {'sem_agua': 'a', 'sem_luz': 'b', 'sem_esgoto': 'c', 'sem_pavimentacao': 'd'}, | |
| } | |
| ALIASES = { | |
| 'app': ['APP', 'área de preservação permanente'], | |
| 'diretriz_logradouro': ['diretriz de logradouro', 'diretriz para implantação de logradouro'], | |
| 'faixa_redes': ['faixa não edificável', 'passagem de redes'], | |
| 'alta_tensao': ['alta tensão'], | |
| 'ocupacoes_irregulares': ['ocupação irregular', 'ocupações irregulares'], | |
| 'vila': ['imóvel em vila'], 'encravado': ['terreno encravado'], | |
| 'triangular_ate_1000': ['terreno triangular', 'terreno interno triangular'], | |
| 'topografia': ['topografia desfavorável', 'condições topográficas', 'declive', 'aclive', 'desnível'], | |
| 'agua': ['inundação', 'alagamento', 'enchente'], | |
| 'apan': ['APAN', 'área de proteção do ambiente natural', 'área de proteção ao ambiente natural'], | |
| } | |
| def normalized(text): | |
| text = ''.join(c for c in unicodedata.normalize('NFKD',str(text)) if not unicodedata.combining(c)) | |
| return ' '.join(re.findall(r'[a-z0-9]+',text.casefold())) | |
| def condition_refs(labels, characteristics): | |
| """Only refer to actual numbered headings; no guessed positional mapping.""" | |
| phrases = [normalized(label) for label in labels if normalized(label)] | |
| matches = [] | |
| for item in characteristics: | |
| title = ' '+normalized(item.get('titulo',''))+' ' | |
| if any(' '+phrase+' ' in title for phrase in phrases): | |
| reference = item.get('referencia') | |
| if reference and reference not in matches: matches.append(reference) | |
| return matches | |
| def reference_sentence(labels, characteristics): | |
| refs = condition_refs(labels,characteristics) | |
| if not refs: | |
| return 'Não há remissão específica a uma característica individualizada do capítulo 2.2 para esta condição.' | |
| if len(refs)==1: | |
| return f'A condição é examinada no item {refs[0]}, ao qual se remete sua caracterização.' | |
| return 'As condições são examinadas nos itens '+join_words(refs)+', aos quais se remete sua caracterização.' | |
| def join_words(items): | |
| return items[0] if len(items)==1 else ', '.join(items[:-1])+' e '+items[-1] | |
| def config_for(row): | |
| return ((terrain(row).get('avaliacao') or {}).get('ajuste_territorial') or {}).get('configuracao') or {} | |
| def weighted_conditions(row): | |
| catalog = (source(row).get('catalogoAjustes') or {}).get('art_88_i') or {} | |
| config = config_for(row) | |
| minimum = number(catalog.get('limite_minimo_area')) | |
| if minimum is None: return [] | |
| conditions = [] | |
| for item in catalog.get('condicoes') or []: | |
| code = item.get('codigo') | |
| area = number(((config.get('art_88_i') or {}).get('condicoes',{}).get(code) or {}).get('percentual_area')) | |
| rate = number(item.get('redutor',item.get('percentual_reducao'))) | |
| if area is not None and area>minimum and rate is not None and rate>0: | |
| conditions.append((code,item.get('rotulo') or code,area,rate,minimum)) | |
| return conditions | |
| def scope(rows, selected): | |
| if len(selected)==len(rows): return 'Para todas as referências avaliadas' | |
| return 'Para '+join_words([ref(row) for row in selected]) | |
| def grouped_prose(doc, rows, entries): | |
| variants = OrderedDict() | |
| for row,text in entries: variants.setdefault(text,[]).append(row) | |
| for text,selected in variants.items(): | |
| add_body_text(doc,scope(rows,selected)+', '+text).paragraph_format.keep_together = True | |
| def legal_reference(key, codes=()): | |
| if key in ARTICLE_88_LETTERS: | |
| clause = 'I' if key == 'redutor88I' else 'II' | |
| reference = f'art. 88, inciso {clause}' | |
| mapping = ARTICLE_88_LETTERS[key] | |
| # A missing/unknown condition must not inherit another condition's letter. | |
| if codes and all(code in mapping for code in codes): | |
| letters = list(dict.fromkeys('“'+mapping[code]+'”' for code in codes)) | |
| reference += (', alínea ' if len(letters)==1 else ', alíneas ')+join_words(letters) | |
| return reference | |
| if key in ('redutorTopografia', 'redutorAgua', 'apan'): | |
| return 'art. 88, § 5º' | |
| if key=='redutor6061': | |
| articles = [] | |
| if 'vila' in codes: articles.append('art. 60, caput') | |
| if 'encravado' in codes: articles.append('art. 61, inciso I') | |
| if 'triangular_ate_1000' in codes: articles.append('art. 61, inciso II') | |
| return join_words(articles) if articles else '' | |
| return '' | |
| def legal_basis(key, codes=()): | |
| reference = legal_reference(key, codes) | |
| if not reference: | |
| return 'A memória não identifica a hipótese legal deste ajuste; o fundamento específico não pode ser atribuído apenas a partir do resultado numérico.' | |
| if key=='redutor6061': | |
| return f'O enquadramento da configuração do terreno fundamenta-se no {reference} do {DECREE}. O art. 56, parágrafo único, admite que a correção seja efetuada no preço do metro quadrado, pelos mesmos coeficientes.' | |
| text = f'O tratamento desta condição fundamenta-se no {reference} do {ARTICLE_88_DECREE}.' | |
| if key in ('redutorTopografia', 'redutorAgua', 'apan'): | |
| text += ' O dispositivo admite a identificação particularizada de outras hipóteses de desvalorização e a avaliação de seu impacto, desde que fundamentadas tecnicamente.' | |
| return text | |
| def reducer_parts(row, key, label, rate): | |
| """Split only documented, effective conditions, without revaluing the land.""" | |
| saved = source(row).get('catalogoAjustes') or {} | |
| config = config_for(row) | |
| if key=='redutor88I': | |
| parts = [] | |
| for code,name,area,reference_rate,minimum in weighted_conditions(row): | |
| text = f'a condição {name} foi considerada sobre {pct(area,True)} da área territorial. O percentual de referência registrado é de {pct(reference_rate,True)} sobre a parcela atingida; sua contribuição ponderada corresponde a {pct(area*reference_rate,True)} do valor unitário. O atingimento supera o limiar de {pct(minimum,True)} registrado para a incidência do ajuste.' | |
| parts.append((key+':'+code,name,[name]+ALIASES.get(code,[]),text,[code])) | |
| if parts: return parts | |
| elif key=='redutor6061': | |
| codes = (config.get('art_60_61') or {}).get('selecionados') or [] | |
| choices = [item for item in (saved.get('art_60_61') or {}).get('condicoes',[]) | |
| if item.get('codigo') in codes and number(item.get('redutor'))==rate] | |
| if choices: | |
| names = [item.get('rotulo') or item['codigo'] for item in choices] | |
| labels = names+[alias for item in choices for alias in ALIASES.get(item['codigo'],[])] | |
| return [(key,'Configuração do terreno',labels,f'considerou-se {join_words(names)}, aplicando-se a redução de {pct(rate,True)} ao valor unitário territorial. Entre as condições selecionadas nos arts. 60 e 61, este é o maior percentual efetivamente utilizado; os percentuais não são somados.',[item['codigo'] for item in choices])] | |
| elif key=='redutorTopografia': | |
| descriptions,labels = [],[] | |
| for part,rate_key in [('relevo','redutorRelevo'),('nivel_rua','redutorNivel')]: | |
| code = (config.get('topografia') or {}).get(part) | |
| item = next((item for item in (saved.get('topografia') or {}).get(part,[]) if item.get('codigo')==code),{}) | |
| part_rate = number(result(row).get(rate_key)) if rate_key in result(row) else number(item.get('redutor')) | |
| if item and part_rate is not None and part_rate>0: | |
| name = item.get('rotulo') or code | |
| labels.append(name);descriptions.append(f'{name} ({pct(part_rate,True)})') | |
| if descriptions: | |
| return [(key,'Condições topográficas',labels+ALIASES['topografia'],f'foram consideradas as condições {join_words(descriptions)}. Suas parcelas integram o ajuste topográfico, cuja redução efetiva registrada é de {pct(rate,True)} do valor unitário territorial.',[])] | |
| elif key=='redutorAgua': | |
| code = (config.get('agua') or {}).get('situacao') | |
| item = next((item for item in (saved.get('agua') or {}).get('condicoes',[]) if item.get('codigo')==code),{}) | |
| if item: | |
| name = item.get('rotulo') or code | |
| return [(key,'Inundação e ação da água',[name]+ALIASES['agua'],f'a situação considerada foi {name}, com redução efetiva de {pct(rate,True)} do valor unitário territorial.',[])] | |
| # A missing catalog or breakdown must not be replaced with today's rates. | |
| text = f'o resultado da avaliação registra redução de {pct(rate,True)} do valor unitário territorial.' if rate is not None else 'o arquivo registra esta etapa de ajuste, mas não preserva seu percentual efetivo.' | |
| return [(key,label,[label],text+' A condição específica e os parâmetros de incidência não foram preservados em detalhe.',[])] | |
| def summarize_restrictions(doc,rows): | |
| entries = [] | |
| for row in rows: | |
| active = {key:rate for key,label,rate in applied_reducers(row,True)} | |
| if 'redutor88I' not in active: continue | |
| rate = active['redutor88I'] | |
| text = f'consideradas em conjunto as restrições ponderadas pela área, a redução efetiva registrada é de {pct(rate,True)} do valor unitário territorial.' | |
| conditions = weighted_conditions(row) | |
| weighted = sum(area*reference for code,name,area,reference,minimum in conditions) | |
| if conditions and rate is not None and abs(weighted-rate)>number('0.000000001'): | |
| text += ' A soma das contribuições ponderadas não coincide com esse percentual salvo; a divergência requer conferência, preservando-se o resultado de origem.' | |
| entries.append((row,text)) | |
| grouped_prose(doc,rows,entries) | |
| def gerar_ajustes_territoriais(doc, rows, num, characteristics): | |
| groups = OrderedDict() | |
| for row in rows: | |
| for key,label,rate in applied_reducers(row,True): | |
| for identity,title,labels,text,codes in reducer_parts(row,key,label,rate): | |
| groups.setdefault(identity,[]).append((row,key,title,labels,text,codes)) | |
| if groups: | |
| add_body_text(doc,'A passagem da estimativa do modelo ao valor territorial adotado considera as condições particulares do imóvel examinadas no capítulo 2.2. Cada ajuste efetivamente aplicado é fundamentado nos itens seguintes, com indicação da condição de incidência e do percentual utilizado em cada referência.') | |
| if any(identity.startswith('redutor88I:') for identity in groups): | |
| add_body_text(doc,'As contribuições das restrições ponderadas pela área são somadas para formar um único redutor. Esse fator participa da composição multiplicativa com os demais ajustes territoriais, antes da escolha do valor no campo de arbítrio.') | |
| order = {key:i for i,key in enumerate(REDUCERS)} | |
| ordered = sorted(groups.items(),key=lambda pair:order.get(pair[0].split(':',1)[0],len(order))) | |
| restrictions = [identity for identity in groups if identity.startswith('redutor88I:')] | |
| for identity,entries in ordered: | |
| first = entries[0] | |
| heading(doc,num,first[2],5) | |
| # Group the full narrative so historical labels or legal hypotheses | |
| # are never silently borrowed from another exercise. | |
| variants = OrderedDict() | |
| for row,key,title,labels,text,codes in entries: | |
| opening = reference_sentence(labels,characteristics)+' '+legal_basis(key,codes) | |
| variants.setdefault(opening,[]).append((row,text)) | |
| for opening,details in variants.items(): | |
| add_body_text(doc,opening).paragraph_format.keep_with_next = True | |
| grouped_prose(doc,rows,details) | |
| if restrictions and identity==restrictions[-1]: | |
| summarize_restrictions(doc,rows) | |
| else: | |
| add_body_text(doc,'O resultado salvo não registra redutores territoriais com efeito identificado. A passagem ao valor adotado considera a escolha no campo de arbítrio e a área de aplicação, demonstradas a seguir.') | |
| campo_arbitrio(doc,rows,num,characteristics) | |
| def campo_arbitrio(doc, rows, num, characteristics): | |
| active = [row for row in rows if number(result(row).get('fatorCaFinal')) not in (None,1)] | |
| # A public choice justification can matter even when the final factor is 1. | |
| justified = any((row.get('complementos') or {}).get('justificativa_vu') for row in rows) | |
| if not active and not justified: return | |
| heading(doc,num,'Escolha do valor no campo de arbítrio',5) | |
| add_body_text(doc,'Após a incidência dos ajustes territoriais, a escolha do valor unitário é tratada no campo de arbítrio da estimativa, no âmbito da metodologia da ABNT NBR 14653-2. O fator adotado expressa essa decisão técnica e deve ser lido em conjunto com sua justificativa.') | |
| apan = [row for row in active if (number(result(row).get('redutorApan')) or 0)>0] | |
| if apan: | |
| add_body_text(doc,reference_sentence(ALIASES['apan'],characteristics)+' '+legal_basis('apan')+' A influência da APAN participa desta decisão, sem constituir um redutor autônomo a ser descontado novamente.') | |
| grouped_prose(doc,rows,[(row,f'a área atingida por APAN é de {pct((config_for(row).get("apan") or {}).get("percentual_area"),True)}, com referência de influência de {pct(result(row).get("redutorApan"),True)}. O fator final escolhido para o campo de arbítrio é {fmt(result(row).get("fatorCaFinal"),4)}.') for row in apan]) | |
| entries = [] | |
| for row in rows: | |
| factor = number(result(row).get('fatorCaFinal')) | |
| reason = complement(row,'justificativa_vu','') | |
| if factor in (None,1) and not reason: continue | |
| text = f'o fator do campo de arbítrio adotado foi {fmt(factor,4)}. ' if factor is not None else 'o fator do campo de arbítrio não foi preservado no arquivo. ' | |
| text += 'A justificativa técnica registrada para essa escolha é: '+reason if reason else 'A justificativa técnica dessa escolha não foi informada no arquivo.' | |
| entries.append((row,text)) | |
| grouped_prose(doc,rows,entries) | |
| def justificar_area(doc,rows): | |
| entries = [] | |
| for row in rows: | |
| model_area = number(terrain(row).get('areaModelo',row['avaliacao'].get('valor_area'))) | |
| area = number(row.get('area')) | |
| if model_area is None or area is None or model_area==area: continue | |
| reason = complement(row,'justificativa_area','') | |
| text = f'o modelo utilizou a área de {fmt(model_area)} m² para estimar o valor unitário, enquanto sua aplicação ao imóvel considerou {fmt(area)} m². ' | |
| text += 'A distinção de áreas foi justificada nos seguintes termos: '+reason if reason else 'A justificativa para a distinção de áreas não foi informada no arquivo.' | |
| entries.append((row,text)) | |
| grouped_prose(doc,rows,entries) | |