Files
Autoparts-DB/pos/migrations/runner.py
consultoria-as f42910f4f6
Some checks failed
CI / lint-and-test (3.11) (push) Has been cancelled
CI / lint-and-test (3.13) (push) Has been cancelled
feat: cashier/counter reports, service-order & remission flows, Rached migration utils
- Add "Mis cortes de caja" report for cashiers/counters with sales detail.
- Cash register history scoped to own cuts for non-admin roles; new /register/<id>/sales endpoint.
- Remove dashboard from cashier menu; add Reports to cashier/counter.
- Service orders: assign mechanic, budget field, invoice flag, counter/cashier can add items/remissions, convert to remission.
- Remission notes module (UI, CSS, courier, counter remissions).
- Customer hard-delete and vehicle/customer linkage in workshop.
- POS: always show search results, compact payment grid, credit validation, tier pricing (5%/10%), ticket with customer/folio.
- Inventory: CSV template with sku_secondary, alias import.
- Rached migration scripts and DB migrations.
- Version-bump cached JS/CSS query strings.

Excludes local Rached session tokens/captures (rached_*.json / rached_*.txt).
2026-07-02 12:51:56 +00:00

170 lines
5.6 KiB
Python
Executable File

#!/usr/bin/env python3
# /home/Autopartes/pos/migrations/runner.py
"""Apply schema migrations to all tenant databases."""
import os
import sys
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from tenant_db import get_master_conn, get_tenant_conn_by_dbname
MIGRATIONS_DIR = os.path.dirname(os.path.abspath(__file__))
# Migration registry: version -> filename
MIGRATIONS = {
"v1.0": "v1.0_initial.sql",
"v1.1": "v1.1_pos_tables.sql",
"v1.2": "v1.2_subdomain.sql",
"v1.3": "v1.3_fleet.sql",
"v1.4": "v1.4_whatsapp.sql",
"v1.5": "v1.5_returns.sql",
"v1.6": "v1.6_marketplace.sql",
"v1.7": "v1.7_plates.sql",
"v1.8": "v1.8_performance_indexes.sql",
"v1.9": "v1.9_redis_cache.sql",
"v2.0": "v2.0_multi_currency.sql",
"v2.1": "v2.1_suppliers.sql",
"v2.2": "v2.2_alerts_warranty.sql",
"v2.3": "v2.3_metabase.sql",
"v2.4": "v2.4_crm_enhanced.sql",
"v2.5": "v2.5_service_orders.sql",
"v2.6": "v2.6_bnpl_erp.sql",
"v2.7": "v2.7_notifications.sql",
"v2.8": "v2.8_savings.sql",
"v2.9": "v2.9_logistics.sql",
"v3.0": "v3.0_public_api.sql",
"v3.1": "v3.1_inventory_vehicle_compat.sql",
"v3.2": "v3.2_db_performance.sql",
"v3.2.1": "v3.2_qwen_vehicle_compat.sql",
"v3.3": "v3.3_marketplace_any_part.sql",
"v3.3.1": "v3.3_materialized_view.sql",
"v3.4": "v3.4_meli_integration.sql",
"v3.5": "v3.5_meli_questions.sql",
"v3.5.1": "v3.5_whatsapp_state_machine.sql",
"v3.6": "v3.6_dropshipping.sql",
"v3.7": "v3.7_sku_aliases.sql",
"v3.8": "v3.8_supplier_catalog.sql",
"v3.9": "v3.9_supplier_catalog_prices.sql",
"v4.0": "v4.0_multi_branch.sql",
"v4.1": "v4.1_global_invoice.sql",
"v4.2": "v4.2_meli_sync_queue.sql",
"v4.3": "v4.3_facturapi.sql",
"v4.4": "v4.4_workshop.sql",
"v4.5": "v4.5_customer_max_discount.sql",
"v4.6": "v4.6_inventory_support.sql",
"v4.7": "v4.7_workshop_business.sql",
"v4.8": "v4.8_workshop_permissions.sql",
"v4.9": "v4.9_workshop_customers_view.sql",
"v4.10": "v4.10_fleet_permissions.sql",
"v4.11": "v4.11_clean_workshop_mechanic_permissions.sql",
"v4.12": "v4.12_service_order_soft_delete.sql",
"v4.13": "v4.13_fleet_vehicle_customer.sql",
"v4.14": "v4.14_service_order_delivery_cleanup.sql",
"v4.15": "v4.15_counter_remission.sql",
"v4.16": "v4.16_remission_courier.sql",
"v4.17": "v4.17_service_order_invoice.sql",
"v4.18": "v4.18_rached_workshop.sql",
"v4.19": "v4.19_counter_inventory_create.sql",
"v4.20": "v4.20_cashier_sell_permissions.sql",
"v4.21": "v4.21_cashier_workshop_permissions.sql",
"v4.22": "v4.22_cashier_invoicing_permissions.sql",
"v4.23": "v4.23_service_order_mechanic_name.sql",
}
def get_all_tenants():
"""Get all tenants with their current schema version."""
conn = get_master_conn()
cur = conn.cursor()
cur.execute("""
SELECT t.id, t.db_name, t.name, COALESCE(v.version, 'v0.0') as version
FROM tenants t
LEFT JOIN tenant_schema_version v ON t.id = v.tenant_id
WHERE t.is_active = true
""")
tenants = cur.fetchall()
cur.close()
conn.close()
return tenants
def apply_migration(db_name, version):
"""Apply a single migration to a tenant DB."""
filename = MIGRATIONS[version]
filepath = os.path.join(MIGRATIONS_DIR, filename)
if not os.path.exists(filepath):
print(f" ERROR: Migration file not found: {filepath}")
return False
with open(filepath) as f:
sql = f.read()
# Skip migrations marked for manual/non-tenant execution
first_line = sql.splitlines()[0].strip() if sql.strip() else ""
if first_line.startswith(": SKIP") or first_line.startswith("-- : SKIP"):
print(" SKIP (manual/non-tenant migration)")
return True
conn = get_tenant_conn_by_dbname(db_name)
cur = conn.cursor()
try:
cur.execute(sql)
conn.commit()
return True
except Exception as e:
conn.rollback()
print(f" ERROR: {e}")
return False
finally:
cur.close()
conn.close()
def run_migrations():
"""Apply pending migrations to all tenants."""
tenants = get_all_tenants()
def _version_key(v):
return tuple(int(x) for x in v.lstrip('v').split('.'))
sorted_versions = sorted(MIGRATIONS.keys(), key=_version_key)
print(f"Found {len(tenants)} active tenants")
print(f"Available migrations: {sorted_versions}")
for tenant_id, db_name, name, current_version in tenants:
print(f"\n[{name}] (db={db_name}, current={current_version})")
current_key = _version_key(current_version)
for version in sorted_versions:
if _version_key(version) <= current_key:
continue
print(f" Applying {version}...", end=" ")
if apply_migration(db_name, version):
# Update version in master
master_conn = get_master_conn()
master_cur = master_conn.cursor()
master_cur.execute(
"""
INSERT INTO tenant_schema_version (tenant_id, version)
VALUES (%s, %s)
ON CONFLICT (tenant_id) DO UPDATE SET version = %s, updated_at = NOW()
""",
(tenant_id, version, version),
)
master_conn.commit()
master_cur.close()
master_conn.close()
print("OK")
else:
print("FAILED — stopping migrations for this tenant")
break
print("\nDone.")
if __name__ == "__main__":
run_migrations()