"""专线 — 签约主体 normalisation + 续费日期 free-text → recurring MM-DD. Revision ID: 311 Revises: 310 Create Date: 2026-06-21 Two idempotent data fixes so the 专线管理 page can switch its 签约主体 field to a Select sourced from the company legal-entity register (公司主体信息) and its 续费日期 field to a date picker: 1. **Seed missing legal entities** — every distinct ``contracting_entity`` on ``cmdb_dedicated_lines`` that is not yet a row in ``asset_legal_entities`` is created (CN / 中国 / CNY / active). Branch companies (分公司) and the Shanghai entity are real contracting parties but were never registered as legal entities. Match is by exact name, so the dropdown shows each line's current value as already-selected. 2. **Normalise renewal_date** — free text like "每年4月份" / "每年9月份" becomes the recurring "MM-DD" form ("04-01" / "09-01"). The circuit renews on that month/day every year, so no year is stored. Unparseable / already-normalised values are left untouched. Pure data migration — no schema change (``renewal_date`` stays ``String``, now holding "MM-DD"). Re-runnable: entity inserts skip existing names, the renewal rewrite is a no-op once values are MM-DD. """ from __future__ import annotations import re import uuid import sqlalchemy as sa from alembic import op revision = "311" down_revision = "310" branch_labels = None depends_on = None _RENEWAL_RE = re.compile(r"^\d{2}-\d{2}$") # "每年4月份" / "每年4月" / "4月" / "4月份" → month number. _MONTH_RE = re.compile(r"(?:每年)?\s*(\d{1,2})\s*月(?:份)?\s*$") def _normalize_renewal(value: str | None) -> str | None: """Map a free-text renewal string to recurring "MM-DD", or None if it cannot be parsed (caller then leaves the row untouched).""" if not value: return None v = value.strip() if _RENEWAL_RE.match(v): return v # already normalised m = _MONTH_RE.match(v) if m: month = int(m.group(1)) if 1 <= month <= 12: return f"{month:02d}-01" return None def upgrade() -> None: bind = op.get_bind() # 1. Seed any contracting entity that isn't a registered legal entity. names = [ r[0] for r in bind.execute( sa.text( "SELECT DISTINCT contracting_entity FROM cmdb_dedicated_lines " "WHERE deleted_at IS NULL AND contracting_entity IS NOT NULL " "AND contracting_entity <> ''" ) ) ] existing = { r[0] for r in bind.execute(sa.text("SELECT name FROM asset_legal_entities")) } for name in names: if name in existing: continue bind.execute( sa.text( "INSERT INTO asset_legal_entities " "(id, name, code, country_or_region, currency, is_active, " " created_at, updated_at) " "VALUES (:id, :name, 'CN', '中国', 'CNY', 1, NOW(), NOW())" ), {"id": str(uuid.uuid4()), "name": name}, ) existing.add(name) # 2. Normalise renewal_date free text → recurring "MM-DD". rows = bind.execute( sa.text( "SELECT id, renewal_date FROM cmdb_dedicated_lines " "WHERE renewal_date IS NOT NULL AND renewal_date <> ''" ) ).fetchall() for rid, raw in rows: norm = _normalize_renewal(raw) if norm and norm != raw: bind.execute( sa.text( "UPDATE cmdb_dedicated_lines SET renewal_date = :v " "WHERE id = :id" ), {"v": norm, "id": rid}, ) def downgrade() -> None: # Data normalisation — not reversible (the original free-text is lost). pass