Files
fuel_track/app/database_migrations.py
Mateusz Gruszczyński 697ac6da30 zapamietanie akcji
2026-07-19 19:15:21 +02:00

156 lines
5.8 KiB
Python

from __future__ import annotations
from collections.abc import Callable
from dataclasses import dataclass
from sqlalchemy import inspect, text
from .extensions import db
@dataclass(frozen=True)
class Migration:
version: str
upgrade: Callable[[], None]
def add_station_access_mode() -> None:
inspector = inspect(db.engine)
columns = {column["name"] for column in inspector.get_columns("company_settings")}
if "station_access_mode" not in columns:
db.session.execute(text(
"ALTER TABLE company_settings ADD COLUMN station_access_mode VARCHAR(40) "
"NOT NULL DEFAULT 'all_prefer_favorites'"
))
def add_fuel_entry_vat_action_snapshot() -> None:
inspector = inspect(db.engine)
columns = {column["name"] for column in inspector.get_columns("fuel_entry")}
additions = {
"vat_action_frozen": "BOOLEAN NOT NULL DEFAULT 0",
"vat_action_applied": "BOOLEAN NOT NULL DEFAULT 0",
"vat_action_name": "VARCHAR(160)",
"vat_action_rate": "NUMERIC(6, 3)",
"vat_action_start": "DATE",
"vat_action_end": "DATE",
}
for name, definition in additions.items():
if name not in columns:
db.session.execute(text(f"ALTER TABLE fuel_entry ADD COLUMN {name} {definition}"))
settings = dict(db.session.execute(text(
"SELECT key, value FROM app_setting WHERE key IN "
"('global_vat_override_enabled','global_vat_override_name','global_vat_override_rate',"
"'global_vat_override_start','global_vat_override_end')"
)).all())
if settings.get("global_vat_override_enabled") == "1" and settings.get("global_vat_override_rate"):
conditions = ["vat_action_applied = 0"]
params = {
"name": settings.get("global_vat_override_name") or "Projekt rządowy",
"rate": settings["global_vat_override_rate"],
"start": settings.get("global_vat_override_start") or None,
"end": settings.get("global_vat_override_end") or None,
}
if params["start"]:
conditions.append("DATE(fueled_at) >= DATE(:start)")
if params["end"]:
conditions.append("DATE(fueled_at) <= DATE(:end)")
db.session.execute(text(f"""
UPDATE fuel_entry
SET vat_action_frozen = 1,
vat_action_applied = 1,
vat_action_name = :name,
vat_action_rate = :rate,
vat_action_start = :start,
vat_action_end = :end
WHERE {' AND '.join(conditions)}
"""), params)
db.session.execute(text("UPDATE fuel_entry SET vat_action_frozen = 1 WHERE vat_action_frozen = 0"))
def add_fuel_entry_full_settlement_snapshot() -> None:
inspector = inspect(db.engine)
columns = {column["name"] for column in inspector.get_columns("fuel_entry")}
additions = {
"settlement_frozen": "BOOLEAN NOT NULL DEFAULT 0",
"snapshot_vat_rate": "NUMERIC(6, 3)", "snapshot_vat_deduction_percent": "NUMERIC(6, 3)",
"snapshot_uses_last_price": "BOOLEAN NOT NULL DEFAULT 0", "snapshot_discount_percent": "NUMERIC(6, 3)",
"snapshot_surcharge_per_liter": "NUMERIC(10, 4)", "snapshot_rule_source": "VARCHAR(160)",
"snapshot_base_net_price": "NUMERIC(12, 6)", "snapshot_settlement_net_price": "NUMERIC(12, 6)",
"result_retail_net_price": "NUMERIC(12, 6)", "result_invoice_price": "NUMERIC(12, 6)",
"result_effective_price": "NUMERIC(12, 6)", "result_retail_gross": "NUMERIC(14, 4)",
"result_retail_net_total": "NUMERIC(14, 4)", "result_settlement_net_total": "NUMERIC(14, 4)",
"result_invoice_gross": "NUMERIC(14, 4)", "result_deductible_vat": "NUMERIC(14, 4)",
"result_effective_cost": "NUMERIC(14, 4)", "result_saving_net": "NUMERIC(14, 4)",
"result_gross_saving": "NUMERIC(14, 4)", "settlement_snapshot_source": "VARCHAR(120)",
"settlement_migration_note": "VARCHAR(255)"
}
for name, definition in additions.items():
if name not in columns:
db.session.execute(text(f"ALTER TABLE fuel_entry ADD COLUMN {name} {definition}"))
MIGRATIONS: tuple[Migration, ...] = (
Migration("20260713_company_station_access_mode", add_station_access_mode),
Migration("20260719_fuel_entry_vat_action_snapshot", add_fuel_entry_vat_action_snapshot),
Migration("20260720_fuel_entry_full_settlement_snapshot", add_fuel_entry_full_settlement_snapshot),
)
def ensure_migration_table() -> None:
inspector = inspect(db.engine)
if inspector.has_table("schema_migration"):
return
db.session.execute(text("""
CREATE TABLE schema_migration (
version VARCHAR(100) PRIMARY KEY,
applied_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
)
"""))
db.session.commit()
def migrate_database() -> list[str]:
"""Uruchamia wyłącznie niewykonane migracje."""
ensure_migration_table()
applied = {
row[0]
for row in db.session.execute(text("SELECT version FROM schema_migration"))
}
completed: list[str] = []
for migration in MIGRATIONS:
if migration.version in applied:
continue
try:
migration.upgrade()
db.session.execute(
text("INSERT INTO schema_migration (version) VALUES (:version)"),
{"version": migration.version},
)
db.session.commit()
completed.append(migration.version)
except Exception:
db.session.rollback()
raise
return completed
def main() -> None:
from . import create_app
app = create_app()
with app.app_context():
applied = migrate_database()
if applied:
print("Wykonane migracje: " + ", ".join(applied))
else:
print("Brak nowych migracji.")
if __name__ == "__main__":
main()