From a4b1374fadebf8240807c393396c6c517897822a Mon Sep 17 00:00:00 2001
From: WanderMotta
Date: Fri, 4 Sep 2026 20:22:15 -0300
Subject: [PATCH] =?UTF-8?q?Linha=20de=20OS:=20tabela=20linhas-os.yaml=20co?=
=?UTF-8?q?mo=20fallback=20quando=20o=20SD.md=20n=C3=A3o=20declara?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Decisão de 2026-09-04, revendo a de 2026-09-03 (sem de-para): o .md continua
mandando, e a tabela contrato/linhas-os.yaml (OS Mãe × linha × item) só entra
quando o entregável não declara `linha_os`. Linha declarada vai como está e não
é conferida contra a tabela; existência/item/status seguem sendo do banco.
- caminhos.LINHAS_OS; regras_sd: Canonico(itens, linhas_os), carregar_linhas_os
(arquivo ausente = fallback desligado), preparar_linhas_os (valida: formato,
linha repetida, dois `padrao`, `padrao` não booleano) e linha_da_tabela.
- exporta_sd: ausente → padrão do item; 2+ candidatas sem padrão → pendência
nomeando-as; item fora da tabela → pendência. CLI informa quantas linhas
vieram da tabela.
- app: sessao.adotar_linhas_da_tabela grava a linha no frontmatter da sessão
com comentário de origem, para o Salvar SD.md deixar a SD autodeclarada;
a tela marca `tabela` e mostra a data da tabela.
- testes: SD18, SD27, SD31–36 passam a exportar de saída; SD10 só PO+objetivo;
casos sintéticos para tabela ausente, ambígua, sem o item e malformada.
- deploy: linhas-os.yaml vive em clientes/ (gitignored) — vai por scp ao volume.
Co-Authored-By: Claude Fable 5.1
---
.gitignore | 3 +-
Dockerfile | 6 +-
app/servidor.py | 6 ++
app/sessao.py | 43 ++++++++++-
app/templates/revisar.html | 10 ++-
docker-compose.yml | 3 +-
scripts/caminhos.py | 3 +
scripts/exporta_sd.py | 53 ++++++++++----
scripts/regras_sd.py | 106 ++++++++++++++++++++++++----
scripts/teste_e2e.py | 76 +++++++++-----------
scripts/teste_fluxo.py | 141 ++++++++++++++++++++++++-------------
11 files changed, 329 insertions(+), 121 deletions(-)
diff --git a/.gitignore b/.gitignore
index fb2a8c0..b073b68 100644
--- a/.gitignore
+++ b/.gitignore
@@ -8,7 +8,8 @@ scripts/backlog/
# Dados do cliente: contrato, projetos e SDs. Fora do git por decisão de
# 04/09/2026. No deploy, entram por volume montado em /app/clientes — o
-# app exige clientes/ses-mg/contrato/itens.yaml em tempo de execução.
+# app exige clientes/ses-mg/contrato/itens.yaml em tempo de execução, e usa
+# contrato/linhas-os.yaml (opcional) como fallback da linha de OS.
clientes/
# Documentos de trabalho na RAIZ (contrato, propostas, prompts, leiame).
diff --git a/Dockerfile b/Dockerfile
index afa9c04..0f161f6 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,8 +1,10 @@
# Imagem da aplicação de Exportação da SD — deploy em VPS via painel Coolify.
#
# O que NÃO entra na imagem, e por que: clientes/. O app lê
-# clientes/ses-mg/contrato/itens.yaml em tempo de execução (regras_sd.carregar_canonico)
-# e grava SD.md de volta em clientes/ses-mg/projetos/ (escrita_sd.gravar). Esses
+# clientes/ses-mg/contrato/itens.yaml em tempo de execução (regras_sd.carregar_canonico),
+# consulta contrato/linhas-os.yaml se existir (fallback da linha de OS; ausente,
+# `linha_os` é exigido no SD.md) e grava SD.md de volta em
+# clientes/ses-mg/projetos/ (escrita_sd.gravar). Esses
# são dados do cliente, versionados fora do git — entram por VOLUME montado em
# /app/clientes. Sem esse volume, a imagem sobe e a tela inicial responde, mas o
# primeiro upload falha com "Arquivo canônico ausente".
diff --git a/app/servidor.py b/app/servidor.py
index 1591516..0834dd2 100644
--- a/app/servidor.py
+++ b/app/servidor.py
@@ -362,12 +362,18 @@ def revisar():
sess = ATUAL["sessao"]
if sess is None:
return redirect(url_for("inicio"))
+ # A prévia lista os entregáveis pelo `n`, e a adoção da tabela é por índice
+ # no frontmatter: traduz para a tela marcar a origem na linha certa.
+ ents = sess.dados.get("entregaveis") or []
+ da_tabela = {ents[i].get("n"): linha for i, linha in sess.linhas_da_tabela.items()
+ if i < len(ents) and isinstance(ents[i], dict)}
return render_template(
"revisar.html",
s=sess,
rotulo=sess.rotulo_estado(),
formulario=montar_formulario(sess),
previa=sess.previa or {},
+ linhas_da_tabela=da_tabela,
aviso=ATUAL.pop("aviso", None),
)
diff --git a/app/sessao.py b/app/sessao.py
index 2ea4012..d43dcb1 100644
--- a/app/sessao.py
+++ b/app/sessao.py
@@ -26,7 +26,38 @@ from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parent.parent / "scripts"))
from exporta_sd import Pendencia, analisar # noqa: E402
-from regras_sd import Canonico # noqa: E402
+from regras_sd import Canonico, linha_da_tabela # noqa: E402
+
+
+def adotar_linhas_da_tabela(dados, canon: Canonico) -> dict[int, str]:
+ """Escreve NO OBJETO DA SESSÃO a linha padrão da tabela onde o SD.md não
+ declara `linha_os`. Devolve {índice do entregável: linha} do que adotou.
+
+ Por que mutar aqui, e não deixar só o exportador consultar a tabela: o
+ Salvar SD.md regrava este objeto. Com a linha adotada nele, a SD sai do
+ disco autodeclarada e a tabela deixa de importar para ela — o risco de
+ tabela envelhecida morde uma vez só. O comentário de linha diz de onde
+ veio, para quem abrir o .md depois. O exportador faz a mesma consulta
+ (mesmo helper, mesma tabela), então a CLI sobre o .md original e a app dão
+ os mesmos bytes. Idempotente: só toca em quem não declara.
+ """
+ tabela = canon.linhas_os
+ if not tabela:
+ return {}
+ adotadas: dict[int, str] = {}
+ for i, e in enumerate(dados.get("entregaveis") or []):
+ if not isinstance(e, dict) or e.get("linha_os") not in (None, ""):
+ continue
+ padrao, _ = linha_da_tabela(tabela, e.get("item"))
+ if not padrao:
+ continue
+ e["linha_os"] = padrao
+ if hasattr(e, "yaml_add_eol_comment"): # CommentedMap do ruamel
+ data = tabela.get("atualizado_em") or "sem data"
+ e.yaml_add_eol_comment(f"da tabela linhas-os.yaml ({data}) — o SD.md não declarava",
+ "linha_os")
+ adotadas[i] = padrao
+ return adotadas
# ---------------------------------------------------------------------------
# Estados — os que a tela precisa distinguir, e nenhum a mais
@@ -56,6 +87,10 @@ class Sessao:
caminho_origem: Path | None = None
ultimo_erro: str | None = None
exportada_em: str | None = None
+ # {índice do entregável: linha} adotado de linhas-os.yaml porque o SD.md
+ # não declarava — a tela marca a origem, e o Salvar SD.md grava.
+ linhas_da_tabela: dict = field(default_factory=dict)
+ tabela_atualizada_em: str | None = None
def reavaliar(self, canon: Canonico) -> None:
"""Roda o exportador em modo de análise sobre os dados COMO ESTÃO.
@@ -63,7 +98,13 @@ class Sessao:
É grátis e instantâneo; roda a cada edição. Uma edição depois da
exportação volta a sessão para pronta/pendente: o que foi exportado era
outra versão dos dados, e a tela não pode dizer "exportada" sobre esta.
+ Antes de analisar, adota da tabela a linha de OS que o .md não declara
+ (ver adotar_linhas_da_tabela) — e só então o exportador vê os dados.
"""
+ adotadas = adotar_linhas_da_tabela(self.dados, canon)
+ if adotadas:
+ self.linhas_da_tabela.update(adotadas)
+ self.tabela_atualizada_em = (canon.linhas_os or {}).get("atualizado_em")
self.previa, self.pendencias = analisar(self.dados, self.corpo, canon)
self.estado = PRONTA if not self.pendencias else COM_PENDENCIAS
self.exportada_em = None
diff --git a/app/templates/revisar.html b/app/templates/revisar.html
index 607615b..e2d2dc2 100644
--- a/app/templates/revisar.html
+++ b/app/templates/revisar.html
@@ -111,7 +111,7 @@
{{ e.titulo }}
{{ e.tipo_entrega }}
{{ e.item }}
-
{{ e.ordem_servico.linha or '—' }}
+
{{ e.ordem_servico.linha or '—' }}{% if e.n in linhas_da_tabela %} tabela{% endif %}
{{ e.data_inicio or '—' }}
{{ e.data_prevista_termino or '—' }}
{% if e.data_inicio and e.data_prevista_termino %}{{ dias(e.data_inicio, e.data_prevista_termino) }}{% else %}—{% endif %}
@@ -126,6 +126,14 @@
valor não saem daqui: o sistema os calcula na carga a partir de datas, item
e tipo, com as tabelas vigentes. Linha de OS é o linha_os declarado no SD.md;
quem confere se a OS e a linha existem é o sistema de gestão, na carga.
+ {% if linhas_da_tabela %}
+
tabela {{ linhas_da_tabela|length }} entregável(is) não
+ declaravam linha_os no SD.md: a linha veio de contrato/linhas-os.yaml
+ (atualizada em {{ s.tabela_atualizada_em or 'data não informada' }}), pelo padrão do item.
+ Salvar SD.md… grava essa linha no arquivo, com a origem em comentário —
+ a SD passa a declará-la e a tabela deixa de importar para ela. Se a OS certa for outra,
+ corrija no SD.md.
+ {% endif %}
{% endblock %}
diff --git a/docker-compose.yml b/docker-compose.yml
index 504809b..6e106ee 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -37,7 +37,8 @@ services:
PORT: "5000"
volumes:
# OBRIGATÓRIO. O app lê clientes/ses-mg/contrato/itens.yaml a cada
- # análise e grava SD.md em clientes/ses-mg/projetos/. O caminho no
+ # análise (e contrato/linhas-os.yaml, se existir, como fallback da linha
+ # de OS) e grava SD.md em clientes/ses-mg/projetos/. O caminho no
# container é fixo: caminhos.py resolve clientes/ a partir da raiz do
# código, que é o WORKDIR /app. Sem esta montagem, a tela inicial abre
# e o primeiro upload falha com "Arquivo canônico ausente".
diff --git a/scripts/caminhos.py b/scripts/caminhos.py
index a2c61da..3e1669b 100644
--- a/scripts/caminhos.py
+++ b/scripts/caminhos.py
@@ -21,6 +21,9 @@ METODOS = RAIZ / "metodos"
DERIVADOS = RAIZ / "_derivados"
ITENS = CONTRATO / "itens.yaml"
+# Fallback de linha de OS (OS Mãe × linha × item). OPCIONAL: ausente, o
+# exportador exige `linha_os` declarado no SD.md, como antes de 2026-09-04.
+LINHAS_OS = CONTRATO / "linhas-os.yaml"
# Visões GERADAS a partir das SDs (inversão da V0, 13/08/2026). Quem lê
# portfólio, carteira ou catálogo lê o resultado de `make derivar` — os
diff --git a/scripts/exporta_sd.py b/scripts/exporta_sd.py
index 20c4c1e..9dd3552 100644
--- a/scripts/exporta_sd.py
+++ b/scripts/exporta_sd.py
@@ -28,12 +28,15 @@ reajuste. As chaves continuam no JSON, na mesma posição — só o valor é `nu
`horas_semanais` e `memoria_calculo`, que só existiam para explicar a conta,
saem `null` pelo mesmo motivo.
-O que NÃO é derivado, por decisão da gestão (2026-09-03): `ordem_servico.linha`.
-OS e linha são cadastro do sistema de gestão e mudam durante o ano; um de-para
-local envelheceria em silêncio. Quem emite a SD declara `linha_os` no
-entregável, copiado da tela da OS; aqui só o formato `{OS}-L{n}` é conferido,
-e ausente é pendência. Existência, item e status quem confere é o banco, na
-carga — que recusa nomeando o que falta.
+O que NÃO é derivado: `ordem_servico.linha`. OS e linha são cadastro do sistema
+de gestão e mudam durante o ano. Quem emite a SD declara `linha_os` no
+entregável, copiado da tela da OS; aqui só o formato `{OS}-L{n}` é conferido, e
+existência, item e status quem confere é o banco, na carga — que recusa
+nomeando o que falta. AUSENTE, a tabela contrato/linhas-os.yaml (se existir)
+dá a linha padrão do item — fallback, decidido em 2026-09-04, para poupar a
+digitação; sem padrão é pendência, com as candidatas nomeadas quando a tabela
+tem mais de uma. A linha declarada nunca é conferida contra a tabela (decisão
+de 2026-09-03): o .md manda.
O que este script NÃO faz, por decisão da gestão (2026-09-03): não confere os
tetos de texto da §2/§3/§4 (nome ≤ 160, objetivo ≤ 4000 etc.) — o importador
@@ -85,6 +88,7 @@ from regras_sd import (
enquadramento_tr,
janela_dias,
ler_sd_arquivo,
+ linha_da_tabela,
perfil_do_cadastro,
secao,
semanas_por_datas,
@@ -421,15 +425,31 @@ def _montar(sd: dict, corpo: str, canon: Canonico, pend: list[Pendencia]) -> dic
"na carga (time-box × semanas das datas). Apague a declaração.")
# -- linha de OS -----------------------------------------------------------
- # Declarada no SD.md por quem emite a SD, e só o formato é conferido
- # aqui. OS e linha são cadastro do sistema de gestão e mudam durante o
- # ano; quem sabe se existem é o banco, na carga. Não há de-para local.
+ # Declarada no SD.md: vai como está, só o formato é conferido — OS e
+ # linha são cadastro do sistema de gestão, e quem sabe se existem é o
+ # banco, na carga. Ausente, a tabela contrato/linhas-os.yaml (se houver)
+ # dá a linha padrão do item; sem padrão é pendência, e com mais de uma
+ # candidata a mensagem as nomeia para a pessoa escolher.
linha = None
- queixa = conferir_formato_linha_os(e.get("linha_os"))
- if queixa:
- falta("linha_os", queixa)
+ declarada = e.get("linha_os")
+ if declarada in (None, ""):
+ padrao, candidatas = linha_da_tabela(canon.linhas_os, item) if item_ok else (None, [])
+ if padrao:
+ linha = padrao
+ elif len(candidatas) > 1:
+ falta("linha_os", f"`linha_os` ausente, e a tabela linhas-os.yaml tem {len(candidatas)} "
+ f"linhas para {item} sem `padrao` marcado: {', '.join(candidatas)}. "
+ "Escolha uma aqui, ou marque o padrão na tabela", opcoes=candidatas)
+ else:
+ sem_entrada = f" — a tabela linhas-os.yaml não tem linha para {item}" \
+ if canon.linhas_os and item_ok else ""
+ falta("linha_os", conferir_formato_linha_os(None) + sem_entrada)
else:
- linha = str(e.get("linha_os")).strip()
+ queixa = conferir_formato_linha_os(declarada)
+ if queixa:
+ falta("linha_os", queixa)
+ else:
+ linha = str(declarada).strip()
# -- listas filhas -----------------------------------------------------------
for campo in ("atividades", "criterios_aceite"):
@@ -695,8 +715,13 @@ def main() -> int:
print(f" ERRO {f}: {exc}", file=sys.stderr)
falhas.append(f)
continue
+ # Se montar() passou com `linha_os` ausente, a linha veio da tabela —
+ # dizer quantas: é a única pista de que o .md ainda não a declara.
+ da_tabela = sum(1 for e in (sd.get("entregaveis") or [])
+ if isinstance(e, dict) and e.get("linha_os") in (None, ""))
print(f" {rotulo} → {saida} {len(payload['entregaveis'])} entregável(is) "
- f"({', '.join(payload['sd']['itens_contratuais'])})")
+ f"({', '.join(payload['sd']['itens_contratuais'])})"
+ + (f" · {da_tabela} linha(s) de OS da tabela linhas-os.yaml" if da_tabela else ""))
if falhas:
# Isolar sem nomear é pior que o estouro que substitui: o lote
diff --git a/scripts/regras_sd.py b/scripts/regras_sd.py
index dcdc4b1..79817b8 100644
--- a/scripts/regras_sd.py
+++ b/scripts/regras_sd.py
@@ -30,6 +30,7 @@ except ImportError:
sys.exit("Falta pyyaml. pip install pyyaml")
from caminhos import ITENS as ITENS_YAML
+from caminhos import LINHAS_OS as LINHAS_OS_YAML
# ---------------------------------------------------------------------------
@@ -40,23 +41,44 @@ class Canonico(NamedTuple):
"""O YAML de contrato, carregado uma vez.
`itens` dá a lista de itens do contrato e a matriz de perfis do TR
- 4.1.1.7.1, que alimenta `alocacoes`. É o ÚNICO arquivo de dados de que o
- exportador depende. Time-box e tarifa saíram daqui em 2026-09-03: são
- tabelas do sistema de gestão, e é ele que deriva UST e valor na carga.
+ 4.1.1.7.1, que alimenta `alocacoes`. Time-box e tarifa saíram daqui em
+ 2026-09-03: são tabelas do sistema de gestão, e é ele que deriva UST e
+ valor na carga.
- O que NÃO está aqui, de propósito: OS e linhas de OS. Elas são cadastro do
- sistema de gestão e mudam durante o ano; qualquer cópia local envelhece em
- silêncio. O exportador exige `linha_os` declarado no SD.md e só confere o
- formato — existência, item e status quem confere é o banco, na carga.
+ `linhas_os` é a tabela OS Mãe × linha × item (contrato/linhas-os.yaml), já
+ preparada por preparar_linhas_os() — ou None quando o arquivo não existe.
+ É FALLBACK, não fonte: só entra quando o entregável NÃO declara `linha_os`
+ no SD.md. Linha declarada vai como está, e existência, item e status quem
+ confere é o banco, na carga. Decisão de 2026-09-04, revendo a de
+ 2026-09-03 (que não admitia de-para nenhum): o .md continua mandando; a
+ tabela poupa a digitação, e a aplicação grava no .md a linha que usou.
"""
itens: dict
+ linhas_os: dict | None = None
def carregar_canonico() -> Canonico:
if not ITENS_YAML.exists():
raise FileNotFoundError(f"Arquivo canônico ausente: {ITENS_YAML}")
itens = yaml.safe_load(ITENS_YAML.read_text(encoding="utf-8"))
- return Canonico(itens)
+ return Canonico(itens, carregar_linhas_os())
+
+
+def carregar_linhas_os(caminho: Path = LINHAS_OS_YAML) -> dict | None:
+ """A tabela de linhas de OS preparada, ou None quando o arquivo não existe.
+
+ Ausente NÃO é erro: o deploy monta contrato/ por volume, e um volume sem o
+ arquivo tem de se comportar como antes de 2026-09-04 — `linha_os` exigido
+ no SD.md. Presente e malformado É erro, e alto: quem edita é gente, e uma
+ entrada torta silenciada apontaria linha errada sem ninguém notar.
+ """
+ if not caminho.exists():
+ return None
+ bruto = yaml.safe_load(caminho.read_text(encoding="utf-8"))
+ try:
+ return preparar_linhas_os(bruto)
+ except ValueError as exc:
+ raise ValueError(f"{caminho.name}: {exc}") from exc
# ---------------------------------------------------------------------------
@@ -500,10 +522,10 @@ def conferir_formato_linha_os(linha) -> str | None:
Só FORMATO. Existência da OS, da linha, item e status são cadastro do
sistema de gestão e é ele que os confere na carga (resolução em dois passos
- no banco, §5) — a carga recusa nomeando o que falta e nada é gravado. Não
- há cópia local disso aqui de propósito: OS e linhas novas entram durante o
- ano, e um de-para em arquivo apontaria linha velha sem ninguém notar.
- Decisão da gestão em 2026-09-03.
+ no banco, §5) — a carga recusa nomeando o que falta e nada é gravado. A
+ tabela linhas-os.yaml NÃO é conferida contra o valor declarado: ela é
+ fallback para a ausência (ver linha_da_tabela), não régua do que a pessoa
+ copiou da tela da OS. Decisões da gestão em 2026-09-03 e 2026-09-04.
"""
if linha in (None, ""):
return ("`linha_os` ausente — ordem_servico.linha é obrigatório na carga e só quem "
@@ -514,6 +536,66 @@ def conferir_formato_linha_os(linha) -> str | None:
return None
+# ---------------------------------------------------------------------------
+# Tabela de linhas de OS — contrato/linhas-os.yaml (fallback)
+# ---------------------------------------------------------------------------
+
+def preparar_linhas_os(bruto) -> dict:
+ """Valida o YAML da tabela e a indexa por item.
+
+ Devolve {"atualizado_em", "linhas", "por_item": {item: [linha, ...]},
+ "padrao": {item: linha}}. Reprova, nomeando a entrada: sem `linha` ou
+ `item`, `linha` fora de {OS}-L{n}, linha repetida, e mais de um
+ `padrao: true` no mesmo item — a marca existe para desfazer ambiguidade,
+ e duas a recriam.
+ """
+ if not isinstance(bruto, dict) or not isinstance(bruto.get("linhas"), list):
+ raise ValueError("esperava um mapa com a lista `linhas`")
+ por_item: dict[str, list[str]] = {}
+ padrao: dict[str, str] = {}
+ vistas: set[str] = set()
+ for k, ent in enumerate(bruto["linhas"], 1):
+ if not isinstance(ent, dict):
+ raise ValueError(f"entrada {k} de `linhas` não é um mapa linha/item/padrao")
+ linha = " ".join(str(ent.get("linha") or "").split())
+ item = " ".join(str(ent.get("item") or "").split())
+ if not linha or not item:
+ raise ValueError(f"entrada {k} de `linhas` precisa de `linha` e `item`")
+ if not _LINHA_OS.match(linha):
+ raise ValueError(f"entrada {k}: linha {linha!r} fora do formato {{OS}}-L{{n}} (ex.: 1090-L1)")
+ if linha in vistas:
+ raise ValueError(f"linha {linha} aparece duas vezes")
+ vistas.add(linha)
+ por_item.setdefault(item, []).append(linha)
+ if "padrao" in ent and not isinstance(ent["padrao"], bool):
+ raise ValueError(f"entrada {k} ({linha}): `padrao` precisa ser true/false, "
+ f"não {ent['padrao']!r}")
+ if ent.get("padrao") is True:
+ if item in padrao:
+ raise ValueError(f"{item} tem duas linhas com `padrao: true` ({padrao[item]} e {linha}) "
+ "— só uma pode ser o padrão")
+ padrao[item] = linha
+ atualizado = bruto.get("atualizado_em")
+ return {"atualizado_em": str(atualizado) if atualizado is not None else None,
+ "linhas": bruto["linhas"], "por_item": por_item, "padrao": padrao}
+
+
+def linha_da_tabela(tabela: dict | None, item) -> tuple[str | None, list[str]]:
+ """(linha padrão do item, todas as candidatas) — (None, []) sem tabela ou sem o item.
+
+ Padrão é a linha marcada `padrao: true`; com UMA linha só para o item, ela
+ vale como padrão sem precisar da marca. Com duas ou mais e nenhuma marcada,
+ o padrão é None e o chamador pede para a pessoa escolher entre as candidatas.
+ """
+ if not tabela or item is None:
+ return None, []
+ candidatas = list(tabela["por_item"].get(str(item), []))
+ padrao = tabela["padrao"].get(str(item))
+ if padrao is None and len(candidatas) == 1:
+ padrao = candidatas[0]
+ return padrao, candidatas
+
+
def janela_dias(inicio: date, fim: date) -> int:
"""Dias entre as datas, exclusivo — a mesma subtração que a carga faz."""
return (fim - inicio).days
diff --git a/scripts/teste_e2e.py b/scripts/teste_e2e.py
index 9e09484..8048843 100644
--- a/scripts/teste_e2e.py
+++ b/scripts/teste_e2e.py
@@ -43,6 +43,9 @@ import servidor # noqa: E402
FALHAS: list[str] = []
CLIENTE = servidor.app.test_client()
+# Lida da tabela, não fixada: o cabeçalho de linhas-os.yaml manda atualizar
+# `atualizado_em` a cada edição, e um teste pinado ficaria vermelho à toa.
+DATA_TABELA = servidor.canonico().linhas_os["atualizado_em"]
SD8 = PROJETOS / "P2-vacinacao/sds/SD8-mvp-vacina-em-dia/SD.md"
SD10 = PROJETOS / "P2-vacinacao/sds/SD10-crie/SD.md"
@@ -119,56 +122,44 @@ def exportar_pela_cli(sd_md: Path) -> str:
# ---------------------------------------------------------------------------
-def preencher_linhas(pag: str, linha: str = "1090-L1") -> str:
- """Responde no formulário todas as pendências de `linha_os` com a mesma
- linha — o que a pessoa faria copiando da tela da OS. Devolve a página."""
- c = campos_do_form(pag)
- for k in c:
- if k.endswith("__linha_os"):
- c[k] = linha
- return texto(post("/revisar", c))
-
-
def sd_pronta():
- print("\n1. SD31: só falta a linha de OS; preenchida, o clique exporta — e nada sai antes dele")
+ print("\n1. SD31: o .md não declara linha de OS; a tabela preenche, o clique exporta — e nada sai antes dele")
reiniciar()
r = enviar(SD31)
ck(not baixou(r), "o upload NÃO devolve download — devolve a tela de revisão")
pag = texto(r)
- ck(estado_na_tela(pag) == "Com pendências" and "4 pendência(s)" in pag,
- "estado: com pendências — as 4 linhas de OS que o SD.md não declara")
- c = campos_do_form(pag)
- ck(set(c) == {f"e{i}__linha_os" for i in range(4)},
- f"o formulário traz SÓ os quatro campos de linha de OS, como texto ({sorted(c)})")
- ck("