"""Import the captured MESA MQC tables through CARLA's catalog service. Run explicitly with --author . Existing entries are preserved. """ import argparse import json import sys from datetime import datetime from html import escape from pathlib import Path ROOT = Path(__file__).resolve().parents[1] sys.path.insert(0, str(ROOT / "backend")) def table(headers, rows): head = "".join(f"{escape(str(value))}" for value in headers) body = "".join( "" + "".join(f"{escape(str(value))}" for value in row) + "" for row in rows ) return f"{head}{body}
" def number(value, decimals=0): return ( f"{value:,.{decimals}f}".replace(",", "_").replace(".", ",").replace("_", ".") ) def examples(tables): return [ { "code": "MESA-MQC-RH", "title": "Ross-Heidecke — estados de conservação e coeficientes", "summary": "Os nove estados de conservação da tabela R&H do MQC, com coeficientes de Heidecke e descrições usadas pela MESA.", "subjects": ["MQC", "Depreciação", "Benfeitorias"], "body": "

Estados de conservação

Na aba R&H, a MESA apresenta os coeficientes em percentual. Esta tabela mantém essa apresentação.

" + table( ["Estado", "Coeficiente de Heidecke (%)", "Descrição"], [ [ row["estado"], number(row["coeficiente"] * 100, 2) + "%", row.get("descricao", "—"), ] for row in tables["estadosConservacao"] ], ), }, { "code": "MESA-MQC-VIDA-UTIL", "title": "Vida útil — referências por tipo de construção", "summary": "Tabela de vida útil da MESA para 14 tipos de construção, incluindo apartamentos, casas, lojas, armazéns e galpões.", "subjects": ["MQC", "Depreciação", "Benfeitorias"], "body": "

Vida útil por tipo construtivo

" + table( ["Tipo de construção", "Vida útil (anos)"], [[row["tipo"], row["vidaUtil"]] for row in tables["vidaUtilTipos"]], ), }, { "code": "MESA-MQC-AREA-EQUIVALENTE", "title": "Áreas equivalentes — coeficientes de referência do MQC", "summary": "Quinze coeficientes e intervalos para áreas equivalentes, como garagens, varandas, terraços e áreas sem acabamento. O MQC cita o item 5 da NBR 12.721/2006.", "subjects": ["MQC", "Benfeitorias", "Normas técnicas"], "body": "

Coeficientes para áreas equivalentes

A nota exibida na MESA é: “Critérios para determinação e cálculo de áreas — item 5 da NBR 12.721/2006.” Esta entrada reproduz a tabela auxiliar do MQC e não contém o texto integral da norma.

" + table( ["Item", "Área", "Coeficiente médio"], [ [row["item"], row["descricao"], row["coeficiente"]] for row in tables["areaEquivalenteCoeficientes"] ], ), }, { "code": "MESA-MQC-BDI", "title": "BDI — adicionais e faixas por tipo de construção", "summary": "As duas tabelas de BDI do MQC: seis componentes adicionais e 17 referências por tipo construtivo, preservando as faixas exibidas pela MESA.", "subjects": ["MQC", "Benfeitorias", "Fundamentação"], "body": "

Adicionais de BDI

" + table( [ "Item", "Descrição", "Percentual 1", "Percentual 2", "Percentual 3", "Escolha", ], [ [row["item"], row["descricao"]] + [ number(row[key] * 100, 1) + "%" if row[key] is not None else "—" for key in ["perc1", "perc2", "perc3", "escolha"] ] for row in tables["bdiAdicionais"] ], ) + "

BDI por tipo de construção

" + table( ["Tipo de construção", "BDI"], [[row["tipo"], row["bdi"]] for row in tables["bdiTiposConstrucao"]], ), }, { "code": "MESA-MQC-VALOR-RESIDUAL", "title": "Valor residual — referências por classe, tipo e padrão", "summary": "Tabela com 29 referências de valor residual e vida referencial para construções residenciais e comerciais, conforme a aba Valor Residual do MQC.", "subjects": ["MQC", "Depreciação", "Benfeitorias"], "body": "

Vida referencial e valor residual

A coluna de vida referencial desta tabela é preservada separadamente da tabela Vida útil do MQC.

" + table( [ "Classe", "Tipo", "Padrão", "Vida referencial (anos)", "Valor residual (%)", ], [ [ row["classe"], row["tipo"], row["padrao"], row["vidaReferencialIr"], str(row["valorResidual"]) + "%", ] for row in tables["valorResidualReferencias"] ], ), }, { "code": "MESA-MQC-SIGLAS-CUB", "title": "CUB — siglas e características dos projetos padrão", "summary": "Dez siglas da tabela SIGLAS do MQC, com descrição, dormitórios, área real e área equivalente: R1-B, R1-N, R1-A, RP1Q, PIS, PP4-B, PP4-N, R8-N, CAL8-N e GI.", "subjects": ["MQC", "Benfeitorias"], "body": "

Projetos padrão do CUB

" + table( [ "Sigla", "Descrição", "Dormitórios", "Área real (m²)", "Área equivalente (m²)", ], [ [ row["sigla"], row["descricao"], row["dormitorios"], number(row["areaReal"], 2), number(row["areaEquivalente"], 2), ] for row in tables["siglasCub"] ], ), }, ] def origin(capture): captured = datetime.fromisoformat(capture["captured_at"]) return ( "

Origem

Importado das tabelas auxiliares da aba MQC da MESA em " + captured.strftime("%d/%m/%Y") + ". Os valores e descrições registram o conteúdo disponível na MESA nessa data.

" ) def example_content(record, capture, subject_ids): return { "title": record["title"], "type": "reference", "summary": record["summary"], "content_html": record["body"] + origin(capture), "subject_ids": subject_ids, } def simplify_snapshot(snapshot, capture): # Only captured examples are passed here; never discard later attachments. if any(snapshot.get(key) for key in ("file_ids", "links", "relations")): raise ValueError("Esta referência recebeu anexos ou conexões; revise antes de simplificar.") content = {key: snapshot[key] for key in ("title", "type", "summary", "content_html", "subject_ids")} if "Importado das tabelas auxiliares da aba MQC da MESA em " not in content["content_html"]: content["content_html"] += origin(capture) return content def known_examples(db): from sqlalchemy import select from app.models import Audit, Revision, Entry # Old immutable revisions retain the import code even after current fields change. known = { rev.snapshot["metadata"]["code"]: rev.entry_id for rev in db.scalars(select(Revision).order_by(Revision.number)) if rev.snapshot.get("metadata", {}).get("code", "").startswith("MESA-MQC-") } for event in db.scalars(select(Audit).where(Audit.action == "mqc_examples_imported")): known.update(event.details.get("references", {})) capture = json.loads((ROOT / "scripts/data/mqc_reference_examples.json").read_text()) titles = {record["title"]: record["code"] for record in examples(capture["tables"])} for entry in db.scalars(select(Entry)): revision = db.scalar( select(Revision).where(Revision.entry_id == entry.id, Revision.number == entry.latest) ) if revision and revision.snapshot["title"] in titles: known.setdefault(titles[revision.snapshot["title"]], entry.id) return known def run(author, simplify_existing=False): from sqlalchemy import select from app import catalog, schemas from app.database import SessionLocal from app.models import Entry, Subject, User from app.security import audit capture = json.loads((ROOT / "scripts/data/mqc_reference_examples.json").read_text()) records = examples(capture["tables"]) with SessionLocal.begin() as db: user = db.scalar(select(User).where(User.username == author, User.active == True)) if not user or not catalog.curator(user): raise ValueError("Informe um usuário existente e ativo com perfil de administrador ou curador.") subjects = { s.name: s.id for s in db.scalars( select(Subject).where(Subject.active == True, Subject.approved == True) ) } missing = {name for record in records for name in record["subjects"] if name not in subjects} if missing: raise ValueError(f"Assuntos aprovados ausentes: {', '.join(sorted(missing))}") existing = known_examples(db) imported, updated, skipped, references = [], [], [], {} for record in records: code = record["code"] entry = db.get(Entry, existing[code]) if code in existing else None if entry: references[code] = entry.id snapshot = catalog.revision(db, entry, entry.latest).snapshot extras = any(snapshot.get(key) for key in ("source_author", "source_name", "usage", "metadata")) or snapshot.get("source_state", "unchecked") != "unchecked" if not simplify_existing or not extras: skipped.append(entry.id) continue content = simplify_snapshot(snapshot, capture) reason = "Adequação do exemplo aos cinco campos do cadastro simplificado." else: content = example_content(record, capture, [subjects[name] for name in record["subjects"]]) reason = "Importação das referências do MQC da MESA, por solicitação de Guilherme." payload = schemas.SaveInput( content=schemas.Content(**content), expected_version=entry.lock_version if entry else 0, reason=reason, action="draft" if entry else "publish", ) result = catalog.save(db, user, payload, entry.id if entry else None) references[code] = result["id"] (updated if entry else imported).append(result["id"]) if imported or updated: audit( db, user, "mqc_examples_imported", entry_ids=imported, updated_entry_ids=updated, references=references, source_file=capture["source_file"], source_sha256=capture["source_sha256"], captured_at=capture["captured_at"], ) result = {"created": imported, "updated": updated, "preserved": skipped} print(json.dumps(result, ensure_ascii=False)) return result if __name__ == "__main__": parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("--author", required=True, help="Usuário existente responsável pelo cadastro.") parser.add_argument("--simplify-existing", action="store_true", help="Revisar exemplos antigos preservando tabelas, autoria e histórico.") args = parser.parse_args() run(args.author, args.simplify_existing)