"""Documentary framing equivalent to MESA modelFoundation/globalFoundation. No valuation is recalculated. Missing evidence remains pending. Equivalence is checked against MESA's JavaScript, including the per-item minima. """ from decimal import Decimal, InvalidOperation from .mesa_content import content_for POINTS = {'Grau III': 3, 'Grau II': 2, 'Grau I': 1, 'Sem enquadramento': 0} def number(value): try: if value is None or value == '' or isinstance(value, bool): return None text = str(value) if ',' in text: text = text.replace('.', '').replace(',', '.') result = Decimal(text) return result if result.is_finite() else None except (InvalidOperation, ValueError): return None def grade(points): return {3: 'Grau III', 2: 'Grau II', 1: 'Grau I', 0: 'Sem enquadramento'}.get(points, 'Pendente') def threshold(value, limits): value = number(value) if value is None: return None return next((3-i for i, limit in enumerate(limits) if value <= Decimal(str(limit))), 0) def model_foundation(source): terrain = source.get('terreno') or {} state = terrain.get('fundamentacaoModelo') or {} context = terrain.get('contextoModelo') or {} evidence = context.get('normativeEvidence') or {} evaluation = terrain.get('avaliacao') or {} content = content_for(source) items = [] def add(key, label, points, detail=''): items.append({'key': key, 'label': label, 'points': points, 'grade': grade(points), 'detail': detail}) for key, label in [('caracterizacao', 'Caracterização do imóvel avaliando'), ('identificacaoDados', 'Identificação dos dados de mercado')]: saved = state.get(key) or {} selected = saved.get('grau') desc = content.get('descricoes_modelo', {}).get(key, {}).get(selected, '') detail = ' '.join(filter(None, [desc, saved.get('justificativa')])) add(key, label, POINTS.get(selected), detail or 'Enquadramento não informado pelo avaliador.') n, k = number(evidence.get('n')), number(evidence.get('k')) qty = next((points for mult, points in [(6, 3), (4, 2), (3, 1)] if n >= mult*(k+1)), 0) if n is not None and k is not None and k >= 0 else None add('quantidadeDados', 'Quantidade de dados de mercado', qty, f'n = {n}; k = {k}.' if qty is not None else 'n e k indisponíveis no contexto salvo.') extrap = POINTS.get(evaluation.get('fundamentacao')) if extrap is None and 'qtd_extrapolacoes' in evaluation: count = max(Decimal(0),number(evaluation['qtd_extrapolacoes']) or Decimal(0)) impact = number(evaluation.get('perc_ext')) or Decimal(0) severe = any(isinstance(item, dict) and item.get('status') == 'grave' for item in (evaluation.get('extrapolacoes') or {}).values()) extrap = 0 if severe or (impact is not None and impact > 20) else 3 if count == 0 else 2 if count == 1 and impact is not None and impact <= 15 else 1 count_text = evaluation.get('qtd_extrapolacoes') impact_text = evaluation.get('perc_ext') detail = 'Nenhuma variável extrapolada.' if count_text == 0 else f"Variáveis extrapoladas: {count_text if count_text is not None else 'não informado'}." + (f' Impacto: {impact_text}%.' if impact_text is not None else '') add('extrapolacao', 'Extrapolação', extrap, detail) pvalues = [(item.get('name', ''), number(item.get('pValue'))) for item in evidence.get('regressorPValues', []) if isinstance(item, dict)] worst = max(((name, p) for name, p in pvalues if p is not None), key=lambda item: item[1], default=('', None)) add('significanciaRegressores', 'Significância dos regressores', threshold(worst[1], [.1,.2,.3]), f'Maior p-valor: {worst[1]} ({worst[0]}).' if worst[1] is not None else 'p-valores indisponíveis.') add('significanciaModelo', 'Teste F', threshold(evidence.get('fPValue'), [.01,.02,.05]), f"p-valor: {evidence.get('fPValue', 'não informado')}.") by_key = {item['key']: item['points'] for item in items} p = [by_key[key] for key in ['caracterizacao','quantidadeDados','identificacaoDados','extrapolacao','significanciaRegressores','significanciaModelo']] total = sum(value for value in p if value is not None) result = 'Pendente' requirements = state.get('requisitosGrauIII') or {} req_keys = [item['key'] for item in content.get('requisitos_modelo', [])] candidate = all(value is not None for value in p) and total >= 16 and all(value >= minimum for value, minimum in zip(p,[2,3,2,3,3,3])) missing_req = candidate and any(requirements.get(key) not in ('sim','nao',True,False) for key in req_keys) if all(value is not None for value in p) and not missing_req: if candidate and all(requirements.get(key) in ('sim',True) for key in req_keys): result = 'Grau III' elif total >= 10 and all(value >= minimum for value, minimum in zip(p,[1,2,1,2,2,2])): result = 'Grau II' elif all(value >= 1 for value in p): result = 'Grau I' else: result = 'Sem enquadramento' return {'items': items, 'totalPoints': total, 'grade': result, 'requirements': requirements, 'missingRequirements': bool(missing_req)} def construction_foundation(source): construction = source.get('construcao') or {} saved = construction.get('fundamentacaoMqc') or (construction.get('avaliacao') or {}).get('fundamentacaoMqc') or {} defs = saved.get('definitions') or {} # Labels are extracted from the very function that assigns the MESA scores. import re rule = content_for(source)['funcoes_mqc']['calcularEnquadramento'] options = re.findall(r"definitions\.(\w+) === '([^']+)' \? (\d)", rule) items = [] for key, label in [('formaCub','Custo direto'),('tipoBdi','BDI'),('depreciacao','Depreciação física')]: value = defs.get(key) points = next((int(score) for field, text, score in options if field == key and value == text), 1 if value else None) items.append({'label':label,'points':points,'grade':grade(points),'detail':value or 'Critério não informado.'}) p = [item['points'] for item in items] total = sum(value for value in p if value is not None) result = 'Pendente' if all(value is not None for value in p): if total >= 7 and p[0] == 3 and min(p[1:]) >= 2: result = 'Grau III' elif total >= 5 and p[0] >= 2 and p[1] >= 2: result = 'Grau II' elif total >= 3: result = 'Grau I' else: result = 'Fora dos critérios' return {'items':items,'totalPoints':total,'grade':result} def global_foundation(source): terrain = model_foundation(source) construction = construction_foundation(source) result = source.get('resultadoComposicao') or {} vt = number((source.get('terreno') or {}).get('valorTotal')) vc = number((source.get('construcao') or {}).get('valorTotal')) # calculate(year).beforeFc in MESA is the sum of these two saved components. total_value = vt+vc if vt is not None and vc is not None else None items = [] for label, value, foundation in [('Terreno',vt,terrain),('Construções',vc,construction)]: share = value/total_value if value is not None and total_value is not None and total_value > 0 else None reduced = value is not None and value > 0 and share is not None and share < Decimal('.15') points = 2 if reduced else POINTS.get(foundation['grade']) # Global MESA only recognises the three positive grades. if not reduced and points == 0: points = None items.append({'label':label,'grade':grade(points),'points':points,'detail':'Participação inferior a 15%; atribuídos dois pontos ao item.' if reduced else f"Fundamentação do componente: {foundation['grade']}.", 'share':share}) fc = str((source.get('fundamentacaoGlobal') or {}).get('fatorComercializacao') or '') option = next((item for item in content_for(source)['criterios_fc'] if item['value'] == fc), {}) items.append({'label':'Fator de comercialização','grade':option.get('grade','Pendente'),'points':option.get('points'),'detail':option.get('label','Critério do Fc não informado.')}) p = [item['points'] for item in items] total = sum(value for value in p if value is not None) result_grade = 'Pendente' if all(value is not None for value in p): if total >= 8 and p[0] == p[1] == 3 and p[2] >= 2: result_grade = 'Grau III' elif total >= 5 and p[0] >= 2 and p[1] >= 2: result_grade = 'Grau II' elif total >= 3 and min(p) >= 1: result_grade = 'Grau I' else: result_grade = 'Sem enquadramento' return {'items':items,'totalPoints':total,'grade':result_grade,'terrain':terrain,'construction':construction}