"""Canonicalize the legacy ``asset_items.status='in_stock'`` token to ``idle``. ``in_stock`` is a pre-Helix import-only synonym that entered the table with the 2026-04 Excel intake. It is NOT in ``schemas.ALLOWED_STATUSES`` and no code path writes it, but it is also not recognised by any lifecycle gate: ``claim_asset`` / ``assign_asset`` / ``recycle_to_office`` / ``mark_held_by_leaver`` all whitelist ``('idle', 'available', ...)``, so a row carrying it is **frozen** — the admin "变更持有" modal renders 「当前状态不支持变更持有」 and there is no way to dispatch the device to anyone. Migration 188 already treated ``{'idle','available','in_stock'}`` as one class for the ``holder_kind`` backfill; this migration retires the synonym from the data instead of teaching every future gate about it. 2026-05-09 chose to add an ``asset.status.in_stock`` i18n key rather than migrate the data (so the list cell stopped rendering the raw token). That made the row *legible* but still *unusable* — reported 2026-08-07 by IT for ``P-JP-NB2302001`` (MacBook Air, 日本), the single surviving row in prod. Idempotent: re-running is a no-op once no ``in_stock`` rows remain. Rows are matched regardless of ``deleted_at`` so soft-deleted stragglers converge too. Revision ID: 454 Revises: 453 """ from __future__ import annotations import logging import sqlalchemy as sa from alembic import op revision = "454" down_revision = "453" branch_labels = None depends_on = None logger = logging.getLogger("alembic.runtime.migration") LEGACY_STATUS = "in_stock" CANONICAL_STATUS = "idle" def upgrade() -> None: bind = op.get_bind() result = bind.execute( sa.text("UPDATE asset_items SET status = :new WHERE status = :old"), {"new": CANONICAL_STATUS, "old": LEGACY_STATUS}, ) logger.info( "migration 454: canonicalized %s asset_items row(s) from %r to %r", result.rowcount, LEGACY_STATUS, CANONICAL_STATUS, ) def downgrade() -> None: # One-way: the pre-migration rows are indistinguishable from genuine # ``idle`` rows afterwards, and restoring a status no gate accepts # would only re-freeze the assets. No-op by design. pass