- 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).
170 lines
5.6 KiB
Python
Executable File
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()
|