857 lines
49 KiB
Python
857 lines
49 KiB
Python
from collections import defaultdict
|
||
from datetime import datetime, date
|
||
from decimal import Decimal
|
||
from functools import wraps
|
||
from flask import Blueprint, render_template, request, redirect, url_for, flash, jsonify, abort
|
||
from flask_login import login_required, current_user
|
||
from sqlalchemy import extract, or_, and_
|
||
from sqlalchemy.exc import IntegrityError
|
||
from .extensions import db
|
||
from .models import User, Vehicle, FuelEntry, CompanySettings, AppSetting, OrlenPrice, FuelCardPolicy, FuelStationCompany, FuelStationPoint, FuelCard, FuelCardStationRule
|
||
from .services import calculate_costs, fetch_orlen_price, fetch_orlen_range, invoice_period, fetch_ure_stations, aggregate_ure_companies, POLISH_REGIONS
|
||
from .station_catalog import sync_station_catalog
|
||
from . import THEMES
|
||
from .vat import entry_vat_rate, freeze_vat_action, global_vat_override, save_global_vat_override
|
||
from .settlement import calculate as calculate_snapshot_settlement, freeze as freeze_settlement
|
||
|
||
main = Blueprint("main", __name__)
|
||
FUEL_TYPES = ("PB95", "PB98", "DIESEL", "LPG")
|
||
|
||
|
||
def wants_json():
|
||
return request.headers.get("X-Requested-With") == "XMLHttpRequest" or request.is_json
|
||
|
||
def ok_response(message, **extra):
|
||
if wants_json():
|
||
return jsonify({"ok": True, "message": message, **extra})
|
||
flash(message, "success")
|
||
return None
|
||
|
||
def error_response(message, status=400):
|
||
if wants_json():
|
||
return jsonify({"ok": False, "error": message}), status
|
||
flash(message, "danger")
|
||
return None
|
||
|
||
def roles(*allowed):
|
||
def deco(fn):
|
||
@wraps(fn)
|
||
def wrapped(*a, **kw):
|
||
if current_user.role not in allowed:
|
||
flash("Brak uprawnień", "danger")
|
||
return redirect(url_for("main.dashboard"))
|
||
return fn(*a, **kw)
|
||
return wrapped
|
||
return deco
|
||
|
||
|
||
|
||
def fuel_entry_accessible(entry):
|
||
if current_user.role == "admin":
|
||
return True
|
||
if current_user.role != "boss" or not current_user.company_id:
|
||
return False
|
||
company_id = entry.vehicle.company_id or (entry.vehicle.owner.company_id if entry.vehicle and entry.vehicle.owner else None)
|
||
return company_id == current_user.company_id
|
||
|
||
def recalculate_vehicle_odometer(vehicle):
|
||
latest = (db.session.query(db.func.max(FuelEntry.odometer))
|
||
.filter(FuelEntry.vehicle_id == vehicle.id)
|
||
.scalar())
|
||
vehicle.current_odometer = int(latest or 0)
|
||
|
||
def parse_fuel_entry_form(entry):
|
||
vehicle_id = request.form.get("vehicle_id", type=int)
|
||
vehicle = db.session.get(Vehicle, vehicle_id)
|
||
if not vehicle:
|
||
raise ValueError("Nie znaleziono pojazdu")
|
||
if current_user.role == "boss" and (vehicle.company_id or vehicle.owner.company_id) != current_user.company_id:
|
||
abort(403)
|
||
fuel_type = (request.form.get("fuel_type") or "").upper()
|
||
if fuel_type not in FUEL_TYPES:
|
||
raise ValueError("Nieprawidłowy rodzaj paliwa")
|
||
previous_fueled_at = entry.fueled_at
|
||
fueled_at = datetime.fromisoformat(request.form.get("fueled_at", ""))
|
||
liters = Decimal(request.form.get("liters", ""))
|
||
price = Decimal(request.form.get("price_per_liter", ""))
|
||
odometer = int(request.form.get("odometer", ""))
|
||
if liters <= 0:
|
||
raise ValueError("Liczba litrów musi być większa od zera")
|
||
if price < 0 or odometer < 0:
|
||
raise ValueError("Cena i stan licznika nie mogą być ujemne")
|
||
station_company_id = request.form.get("station_company_id", type=int)
|
||
station_company = db.session.get(FuelStationCompany, station_company_id) if station_company_id else None
|
||
fuel_card_id = request.form.get("fuel_card_id", type=int)
|
||
fuel_card = db.session.get(FuelCard, fuel_card_id) if fuel_card_id else None
|
||
company_id = vehicle.company_id or vehicle.owner.company_id
|
||
if fuel_card and fuel_card.company_id and fuel_card.company_id != company_id:
|
||
raise ValueError("Wybrana karta nie należy do firmy pojazdu")
|
||
entry.vehicle = vehicle
|
||
entry.fueled_at = fueled_at
|
||
entry.liters = liters
|
||
entry.price_per_liter = price
|
||
entry.fuel_type = fuel_type
|
||
entry.odometer = odometer
|
||
entry.station_company = station_company
|
||
entry.station = station_company.company_name if station_company else (request.form.get("station") or "").strip() or None
|
||
entry.invoice_number = (request.form.get("invoice_number") or "").strip() or None
|
||
entry.used_fuel_card = "used_fuel_card" in request.form
|
||
entry.fuel_card = fuel_card if entry.used_fuel_card else None
|
||
wholesale_price = (request.form.get("wholesale_price") or "").strip()
|
||
entry.wholesale_price = Decimal(wholesale_price) if wholesale_price else None
|
||
entry.wholesale_source = (request.form.get("wholesale_source") or "").strip() or None
|
||
settings = vehicle.company or (vehicle.owner.company if vehicle.owner else None) or CompanySettings.query.first()
|
||
|
||
# During manual editing the action snapshot is authoritative. This prevents
|
||
# later global actions from changing historical settlements.
|
||
if "vat_action_applied" in request.form or any(
|
||
key in request.form for key in ("vat_action_name", "vat_action_rate", "vat_action_start", "vat_action_end")
|
||
):
|
||
applied = "vat_action_applied" in request.form
|
||
entry.vat_action_frozen = True
|
||
entry.vat_action_applied = applied
|
||
if applied:
|
||
action_name = (request.form.get("vat_action_name") or "").strip()
|
||
action_rate_raw = (request.form.get("vat_action_rate") or "").strip()
|
||
if not action_name:
|
||
raise ValueError("Podaj nazwę akcji")
|
||
if not action_rate_raw:
|
||
raise ValueError("Podaj stawkę VAT akcji")
|
||
action_rate = Decimal(action_rate_raw)
|
||
if action_rate < 0 or action_rate > 100:
|
||
raise ValueError("Stawka VAT akcji musi mieścić się w zakresie 0–100%")
|
||
start_raw = (request.form.get("vat_action_start") or "").strip()
|
||
end_raw = (request.form.get("vat_action_end") or "").strip()
|
||
action_start = date.fromisoformat(start_raw) if start_raw else None
|
||
action_end = date.fromisoformat(end_raw) if end_raw else None
|
||
if action_start and action_end and action_start > action_end:
|
||
raise ValueError("Data zakończenia akcji nie może być wcześniejsza niż data rozpoczęcia")
|
||
fuel_day = fueled_at.date()
|
||
if action_start and fuel_day < action_start:
|
||
raise ValueError("Data tankowania jest wcześniejsza niż początek akcji")
|
||
if action_end and fuel_day > action_end:
|
||
raise ValueError("Data tankowania jest późniejsza niż koniec akcji")
|
||
entry.vat_action_name = action_name
|
||
entry.vat_action_rate = action_rate
|
||
entry.vat_action_start = action_start
|
||
entry.vat_action_end = action_end
|
||
else:
|
||
entry.vat_action_name = None
|
||
entry.vat_action_rate = None
|
||
entry.vat_action_start = None
|
||
entry.vat_action_end = None
|
||
else:
|
||
freeze_vat_action(entry, settings.vat_rate, previous_fueled_at)
|
||
|
||
manual_keys = ("snapshot_vat_rate", "snapshot_vat_deduction_percent", "snapshot_discount_percent", "snapshot_surcharge_per_liter", "snapshot_uses_last_price")
|
||
if any(key in request.form for key in manual_keys):
|
||
vat_rate = Decimal(request.form.get("snapshot_vat_rate", ""))
|
||
deduction = Decimal(request.form.get("snapshot_vat_deduction_percent", ""))
|
||
discount = Decimal(request.form.get("snapshot_discount_percent", "0") or 0)
|
||
surcharge = Decimal(request.form.get("snapshot_surcharge_per_liter", "0") or 0)
|
||
if not (0 <= vat_rate <= 100 and 0 <= deduction <= 100 and 0 <= discount <= 100):
|
||
raise ValueError("Stawki VAT, odliczenia i rabatu muszą mieścić się w zakresie 0–100%")
|
||
if entry.vat_action_applied and entry.vat_action_rate is not None and vat_rate != Decimal(entry.vat_action_rate):
|
||
raise ValueError("Stawka VAT rozliczenia musi być zgodna ze stawką zapisanej akcji")
|
||
uses_last = "snapshot_uses_last_price" in request.form
|
||
if uses_last and entry.wholesale_price is None:
|
||
raise ValueError("Last Price wymaga zapisanej ceny hurtowej")
|
||
conditions = {"vat_rate": vat_rate, "vat_deduction_percent": deduction, "uses_last_price": uses_last,
|
||
"discount_percent": discount, "surcharge_per_liter": surcharge,
|
||
"rule_source": (request.form.get("snapshot_rule_source") or "Ręczna korekta").strip()}
|
||
freeze_settlement(entry, settings, conditions, source="Ręczna korekta w edycji")
|
||
else:
|
||
freeze_settlement(entry, settings, source="Automatycznie przy zapisie/edycji")
|
||
return vehicle
|
||
|
||
|
||
def calculate_entry_settlement(entry, fallback_settings=None):
|
||
# Historyczne wyniki po zamrożeniu korzystają wyłącznie z migawki wpisu.
|
||
if not entry.settlement_frozen:
|
||
freeze_settlement(entry, fallback_settings, source="Migracja istniejącego wpisu", migration_note="Warunki odtworzono z ustawień dostępnych podczas migracji; wymagają weryfikacji.")
|
||
return calculate_snapshot_settlement(entry, fallback_settings)
|
||
|
||
def accessible_vehicles():
|
||
if current_user.role == "admin":
|
||
return Vehicle.query.order_by(Vehicle.name).all()
|
||
if current_user.role == "boss" and current_user.company_id:
|
||
return Vehicle.query.filter_by(company_id=current_user.company_id).order_by(Vehicle.name).all()
|
||
ids = {v.id for v in current_user.owned_vehicles + current_user.shared_vehicles}
|
||
return Vehicle.query.filter(Vehicle.id.in_(ids)).order_by(Vehicle.name).all() if ids else []
|
||
|
||
@main.route("/")
|
||
@login_required
|
||
def dashboard():
|
||
settings = CompanySettings.query.first(); vehicles = accessible_vehicles(); ids = [v.id for v in vehicles]
|
||
split_enabled = (db.session.get(AppSetting, "invoice_split_enabled") or AppSetting(key="invoice_split_enabled", value="1")).value == "1"
|
||
month = request.args.get("month", datetime.now().strftime("%Y-%m"))
|
||
try: year, mon = map(int, month.split("-"))
|
||
except ValueError: year, mon = datetime.now().year, datetime.now().month; month=f"{year:04d}-{mon:02d}"
|
||
entries = FuelEntry.query.filter(FuelEntry.vehicle_id.in_(ids), extract("year", FuelEntry.fueled_at)==year, extract("month", FuelEntry.fueled_at)==mon).order_by(FuelEntry.fueled_at.desc()).all() if ids else []
|
||
zero = Decimal("0")
|
||
totals = {"retail_gross": zero, "retail_net": zero, "settlement_net": zero, "invoice_gross": zero, "gross_saving": zero, "deductible_vat": zero, "final_cost": zero, "liters": zero, "saving_net": zero, "saving_base_net": zero}
|
||
by_vehicle = defaultdict(lambda: {"retail_gross": 0, "invoice_gross": 0, "gross_saving": 0, "retail_net": 0, "settlement_net": 0, "saving_net": 0, "effective": 0, "liters": 0})
|
||
invoices = defaultdict(lambda: {"gross": 0, "payable": 0, "count": 0}); settlements = {}
|
||
for e in entries:
|
||
row = calculate_entry_settlement(e, settings)
|
||
totals["retail_gross"] += row["retail_gross"]
|
||
totals["retail_net"] += row["retail_net_total"]
|
||
totals["settlement_net"] += row["settlement_net_total"]
|
||
totals["invoice_gross"] += row["invoice_gross"]
|
||
totals["gross_saving"] += row["gross_saving"]
|
||
totals["deductible_vat"] += row["deductible_vat"]
|
||
totals["final_cost"] += row["effective_cost"]
|
||
totals["liters"] += row["liters"]
|
||
totals["saving_net"] += row["saving_net"]
|
||
if row["uses_last_price"]:
|
||
totals["saving_base_net"] += row["retail_net_total"]
|
||
settlements[e.id] = {k: float(v) if isinstance(v, Decimal) else v for k, v in row.items()}
|
||
bucket = by_vehicle[e.vehicle.name]
|
||
bucket["retail_gross"] += float(row["retail_gross"])
|
||
bucket["retail_net"] += float(row["retail_net_total"])
|
||
bucket["settlement_net"] += float(row["settlement_net_total"])
|
||
bucket["saving_net"] += float(row["saving_net"])
|
||
bucket["invoice_gross"] += float(row["invoice_gross"])
|
||
bucket["gross_saving"] += float(row["gross_saving"])
|
||
bucket["effective"] += float(row["effective_cost"])
|
||
bucket["liters"] += float(row["liters"])
|
||
entry_settings = e.vehicle.company or settings
|
||
if e.used_fuel_card:
|
||
key = invoice_period(e.fueled_at, entry_settings.invoice_split_day) if split_enabled else "Cały miesiąc"
|
||
else:
|
||
key = "Poza kartą"
|
||
invoices[key]["gross"] += float(row["invoice_gross"]); invoices[key]["payable"] += float(row["effective_cost"]); invoices[key]["count"] += 1
|
||
totals["saving_percent"] = (totals["saving_net"] / totals["saving_base_net"] * Decimal("100")) if totals["saving_base_net"] else zero
|
||
return render_template("dashboard.html", vehicles=vehicles, entries=entries, totals=totals, month=month, by_vehicle=dict(by_vehicle), invoices=dict(invoices), settings=settings, split_enabled=split_enabled, settlements=settlements, vat_override=global_vat_override())
|
||
|
||
@main.route("/vehicles", methods=["GET", "POST"])
|
||
@login_required
|
||
def vehicles():
|
||
if request.method == "POST":
|
||
v = Vehicle(name=request.form["name"], make=request.form["make"], model=request.form["model"], registration=request.form["registration"].upper(), fuel_type=request.form["fuel_type"], owner_id=current_user.id, has_fuel_card="has_fuel_card" in request.form, card_provider=request.form.get("card_provider"), current_odometer=int(request.form.get("current_odometer") or 0))
|
||
db.session.add(v); db.session.commit(); response = ok_response("Dodano pojazd", vehicle_id=v.id); return response or redirect(url_for("main.vehicles"))
|
||
company_id=current_user.company_id
|
||
users_q=User.query.filter_by(active=True)
|
||
cards_q=FuelCard.query.filter_by(active=True)
|
||
if current_user.role=='boss' and company_id:
|
||
users_q=users_q.filter_by(company_id=company_id);cards_q=cards_q.filter_by(company_id=company_id)
|
||
return render_template("vehicles.html", vehicles=accessible_vehicles(), fuel_cards=cards_q.order_by(FuelCard.name).all(), assignable_users=users_q.order_by(User.name).all())
|
||
|
||
@main.route("/vehicles/<int:vehicle_id>/share", methods=["POST"])
|
||
@login_required
|
||
def share_vehicle(vehicle_id):
|
||
vehicle = Vehicle.query.get_or_404(vehicle_id)
|
||
if current_user.role != "admin" and vehicle.owner_id != current_user.id:
|
||
response = error_response("Tylko właściciel lub admin może udostępniać auto", 403); return response or redirect(url_for("main.vehicles"))
|
||
user = User.query.filter_by(email=request.form["email"].strip().lower()).first()
|
||
if not user:
|
||
response = error_response("Nie znaleziono użytkownika", 404); return response or redirect(url_for("main.vehicles"))
|
||
if user not in vehicle.drivers: vehicle.drivers.append(user); db.session.commit()
|
||
response = ok_response("Udostępniono pojazd"); return response or redirect(url_for("main.vehicles"))
|
||
|
||
|
||
@main.post("/vehicles/<int:vehicle_id>/unshare/<int:user_id>")
|
||
@login_required
|
||
def unshare_vehicle(vehicle_id, user_id):
|
||
vehicle = Vehicle.query.get_or_404(vehicle_id)
|
||
if current_user.role != "admin" and vehicle.owner_id != current_user.id:
|
||
return error_response("Tylko właściciel lub admin może odebrać dostęp", 403)
|
||
user = User.query.get_or_404(user_id)
|
||
if user in vehicle.drivers:
|
||
vehicle.drivers.remove(user); db.session.commit()
|
||
response = ok_response("Odebrano dostęp do pojazdu")
|
||
return response or redirect(url_for("main.vehicles"))
|
||
|
||
@main.post("/vehicles/<int:vehicle_id>/card-policy")
|
||
@login_required
|
||
def update_card_policy(vehicle_id):
|
||
vehicle = Vehicle.query.get_or_404(vehicle_id)
|
||
if current_user.role != "admin" and vehicle.owner_id != current_user.id:
|
||
return error_response("Tylko właściciel lub admin może zmienić zasady rozliczeń", 403)
|
||
policy = vehicle.fuel_card_policy or FuelCardPolicy(vehicle=vehicle)
|
||
policy.use_orlen_last_price = "use_orlen_last_price" in request.form
|
||
policy.discount_percent = Decimal(request.form.get("discount_percent") or 0)
|
||
policy.surcharge_per_liter = Decimal(request.form.get("surcharge_per_liter") or 0)
|
||
policy.description = request.form.get("description", "").strip() or None
|
||
db.session.add(policy); db.session.commit()
|
||
response = ok_response("Zapisano zasady rozliczeń karty")
|
||
return response or redirect(url_for("main.vehicles"))
|
||
|
||
@main.route("/fuel", methods=["GET", "POST"])
|
||
@login_required
|
||
def fuel():
|
||
vehicles = accessible_vehicles()
|
||
settings = current_user.company or CompanySettings.query.first()
|
||
if settings is None:
|
||
flash("Najpierw utwórz firmę i przypisz do niej użytkownika, aby korzystać z tankowań.", "warning")
|
||
if current_user.role in ("admin", "boss"):
|
||
return redirect(url_for("main.admin_companies"))
|
||
return redirect(url_for("main.dashboard"))
|
||
if request.method == "POST":
|
||
vehicle = next((v for v in vehicles if v.id == int(request.form["vehicle_id"])), None)
|
||
if not vehicle:
|
||
response = error_response("Brak dostępu do pojazdu", 403); return response or redirect(url_for("main.fuel"))
|
||
odometer = int(request.form["odometer"])
|
||
if odometer < vehicle.current_odometer:
|
||
response = error_response("Licznik nie może być niższy od poprzedniego"); return response or redirect(url_for("main.fuel"))
|
||
fueled_at = datetime.fromisoformat(request.form["fueled_at"]); wholesale = None; source = None
|
||
station_company_id = request.form.get("station_company_id", type=int)
|
||
station_company = db.session.get(FuelStationCompany, station_company_id) if station_company_id else None
|
||
settings = vehicle.company or settings
|
||
access_mode = settings.station_access_mode or "all_prefer_favorites"
|
||
if station_company and access_mode == "allowed_only" and station_company not in settings.allowed_stations:
|
||
response = error_response("Ta stacja nie jest dozwolona przez politykę firmy", 403); return response or redirect(url_for("main.fuel"))
|
||
if station_company and access_mode == "favorites_allowed_only" and station_company not in settings.allowed_stations and station_company not in settings.favorite_stations:
|
||
response = error_response("Ta stacja nie jest ulubiona ani dozwolona przez politykę firmy", 403); return response or redirect(url_for("main.fuel"))
|
||
price_region = station_company.selected_region if station_company and station_company.selected_region else settings.region
|
||
try:
|
||
p = fetch_orlen_price(request.form["fuel_type"], fueled_at.date(), price_region); wholesale=p["price_per_liter"]; source=p["source"]
|
||
except Exception: pass
|
||
station_label = station_company.company_name if station_company else request.form.get("station")
|
||
e = FuelEntry(vehicle=vehicle, user_id=current_user.id, fueled_at=fueled_at, liters=Decimal(request.form["liters"]), price_per_liter=Decimal(request.form["price_per_liter"]), fuel_type=request.form["fuel_type"], odometer=odometer, station=station_label, station_company_id=station_company.id if station_company else None, invoice_number=request.form.get("invoice_number"), used_fuel_card="used_fuel_card" in request.form, wholesale_price=wholesale, wholesale_source=source)
|
||
freeze_vat_action(e, settings.vat_rate)
|
||
freeze_settlement(e, settings, source="Automatycznie przy dodaniu")
|
||
vehicle.current_odometer = odometer; db.session.add(e); db.session.commit(); response = ok_response("Zapisano tankowanie", redirect=url_for("main.dashboard")); return response or redirect(url_for("main.dashboard"))
|
||
allowed=list(settings.allowed_stations)
|
||
company_favorites=list(settings.favorite_stations)
|
||
access_mode=settings.station_access_mode or "all_prefer_favorites"
|
||
base_query=FuelStationCompany.query.filter_by(active=True)
|
||
if access_mode == "allowed_only":
|
||
allowed_ids=[x.id for x in allowed]
|
||
stations=base_query.filter(FuelStationCompany.id.in_(allowed_ids)).all() if allowed_ids else []
|
||
elif access_mode == "favorites_allowed_only":
|
||
scope_ids={x.id for x in allowed}|{x.id for x in company_favorites}
|
||
stations=base_query.filter(FuelStationCompany.id.in_(scope_ids)).all() if scope_ids else []
|
||
else:
|
||
stations=base_query.all()
|
||
stations=sorted(stations,key=lambda x:(-x.station_count,x.display_name.lower()))
|
||
user_favorites=[x for x in current_user.favorite_stations if x in stations]
|
||
user_favorite_ids={x.id for x in current_user.favorite_stations}
|
||
favorites=list(user_favorites)
|
||
favorite_scope="user"
|
||
if not favorites and access_mode in ("all_prefer_favorites", "favorites_allowed_only") and company_favorites:
|
||
favorites=[x for x in company_favorites if x in stations];favorite_scope="company"
|
||
if not favorites and access_mode == "all_prefer_favorites": favorites=stations[:10];favorite_scope="default"
|
||
favorites=sorted(favorites,key=lambda x:(-x.station_count,x.display_name.lower()))
|
||
favorite_ids={x.id for x in favorites}
|
||
top_stations=sorted((x for x in stations if x.id not in favorite_ids),key=lambda x:(-x.station_count,x.display_name.lower()))[:10]
|
||
proposed_ids=favorite_ids|{x.id for x in top_stations}
|
||
favorite_catalog=sorted(stations,key=lambda x:(0 if x.id in user_favorite_ids else 1,-x.station_count,x.display_name.lower()))
|
||
other_stations=sorted((x for x in stations if x.id not in proposed_ids),key=lambda x:x.display_name.lower())
|
||
return render_template("fuel_form.html", vehicles=vehicles, stations=stations, favorite_stations=favorites, top_stations=top_stations, favorite_catalog=favorite_catalog, other_stations=other_stations, now=datetime.now().strftime("%Y-%m-%dT%H:%M"), restricted=access_mode in ("allowed_only", "favorites_allowed_only"), access_mode=access_mode, user_favorite_ids=user_favorite_ids, favorite_scope=favorite_scope, company=settings, fuel_cards=FuelCard.query.filter_by(active=True,company_id=settings.id).order_by(FuelCard.name).all())
|
||
|
||
@main.route("/api/orlen-price")
|
||
@login_required
|
||
def orlen_price():
|
||
try: return jsonify(fetch_orlen_price(request.args["fuel_type"], datetime.fromisoformat(request.args["date"]).date(), CompanySettings.query.first().region))
|
||
except Exception as exc: return jsonify({"error": str(exc)}), 502
|
||
|
||
@main.route("/orlen")
|
||
@login_required
|
||
def orlen_data():
|
||
settings = CompanySettings.query.first()
|
||
today = date.today()
|
||
mode = request.args.get("mode", "history")
|
||
if mode not in {"history", "compare"}:
|
||
mode = "history"
|
||
|
||
include_vat = request.args.get("gross") == "1"
|
||
available_years = [
|
||
year for (year,) in db.session.query(extract("year", OrlenPrice.effective_date))
|
||
.distinct().order_by(extract("year", OrlenPrice.effective_date).desc()).all()
|
||
if year is not None
|
||
]
|
||
available_years = [int(year) for year in available_years]
|
||
if today.year not in available_years:
|
||
available_years.insert(0, today.year)
|
||
|
||
selected_year = request.args.get("year", type=int) or today.year
|
||
selected_fuels = [f.upper() for f in request.args.getlist("fuel") if f.upper() in FUEL_TYPES] or ["PB95"]
|
||
selected_regions = [r.lower() for r in request.args.getlist("region") if r.lower() in POLISH_REGIONS]
|
||
if not selected_regions:
|
||
selected_regions = [settings.region.lower()]
|
||
|
||
rows = []
|
||
chart = {}
|
||
comparison_summary = []
|
||
comparison_type = request.args.get("comparison_type", "years")
|
||
compare_fuel = request.args.get("compare_fuel", "PB95").upper()
|
||
if compare_fuel not in FUEL_TYPES:
|
||
compare_fuel = "PB95"
|
||
compare_region = request.args.get("compare_region", settings.region).lower()
|
||
if compare_region not in POLISH_REGIONS:
|
||
compare_region = settings.region.lower()
|
||
compare_year = request.args.get("compare_year", type=int) or (selected_year - 1)
|
||
|
||
def priced_value(row):
|
||
multiplier = 1 + float(effective_vat_rate(settings.vat_rate, row.effective_date)) / 100 if include_vat else 1
|
||
return round(float(row.price_per_liter) * multiplier, 4)
|
||
|
||
def summary(label, source_rows):
|
||
values = [priced_value(row) for row in source_rows]
|
||
return {
|
||
"label": label,
|
||
"count": len(values),
|
||
"average": round(sum(values) / len(values), 4) if values else None,
|
||
"minimum": min(values) if values else None,
|
||
"maximum": max(values) if values else None,
|
||
"first": values[0] if values else None,
|
||
"last": values[-1] if values else None,
|
||
"change": round(values[-1] - values[0], 4) if len(values) > 1 else None,
|
||
}
|
||
|
||
if mode == "compare":
|
||
if comparison_type not in {"years", "periods"}:
|
||
comparison_type = "years"
|
||
base_query = OrlenPrice.query.filter_by(fuel_type=compare_fuel)
|
||
if compare_fuel == "LPG":
|
||
base_query = base_query.filter_by(region=compare_region)
|
||
|
||
if comparison_type == "years":
|
||
year_a, year_b = selected_year, compare_year
|
||
all_rows = base_query.filter(extract("year", OrlenPrice.effective_date).in_([year_a, year_b])).order_by(OrlenPrice.effective_date).all()
|
||
for year in (year_a, year_b):
|
||
period_rows = [row for row in all_rows if row.effective_date.year == year]
|
||
chart[str(year)] = [{"date": row.effective_date.strftime("%m-%d"), "value": priced_value(row)} for row in period_rows]
|
||
comparison_summary.append(summary(str(year), period_rows))
|
||
rows = all_rows
|
||
period_a_from = date(year_a, 1, 1)
|
||
period_a_to = date(year_a, 12, 31)
|
||
period_b_from = date(year_b, 1, 1)
|
||
period_b_to = date(year_b, 12, 31)
|
||
else:
|
||
def parse_date_arg(name, fallback):
|
||
try:
|
||
return date.fromisoformat(request.args.get(name, ""))
|
||
except ValueError:
|
||
return fallback
|
||
|
||
period_a_from = parse_date_arg("period_a_from", today.replace(month=1, day=1))
|
||
period_a_to = parse_date_arg("period_a_to", today)
|
||
previous_year = today.year - 1
|
||
period_b_from = parse_date_arg("period_b_from", date(previous_year, period_a_from.month, min(period_a_from.day, 28)))
|
||
period_b_to = parse_date_arg("period_b_to", date(previous_year, period_a_to.month, min(period_a_to.day, 28)))
|
||
if period_a_from > period_a_to:
|
||
period_a_from, period_a_to = period_a_to, period_a_from
|
||
if period_b_from > period_b_to:
|
||
period_b_from, period_b_to = period_b_to, period_b_from
|
||
|
||
ranges = [
|
||
("Okres A", period_a_from, period_a_to),
|
||
("Okres B", period_b_from, period_b_to),
|
||
]
|
||
all_rows = base_query.filter(
|
||
or_(
|
||
OrlenPrice.effective_date.between(period_a_from, period_a_to),
|
||
OrlenPrice.effective_date.between(period_b_from, period_b_to),
|
||
)
|
||
).order_by(OrlenPrice.effective_date).all()
|
||
for label, start, end in ranges:
|
||
period_rows = [row for row in all_rows if start <= row.effective_date <= end]
|
||
chart[label] = [{"date": f"Dzień {(row.effective_date - start).days + 1:03d}", "value": priced_value(row)} for row in period_rows]
|
||
comparison_summary.append(summary(f"{label}: {start.isoformat()} – {end.isoformat()}", period_rows))
|
||
rows = all_rows
|
||
else:
|
||
query = OrlenPrice.query.filter(OrlenPrice.fuel_type.in_(selected_fuels), extract("year", OrlenPrice.effective_date) == selected_year)
|
||
rows = query.order_by(OrlenPrice.effective_date, OrlenPrice.fuel_type).all()
|
||
for fuel in selected_fuels:
|
||
fuel_rows = [row for row in rows if row.fuel_type == fuel and (fuel != "LPG" or row.region in selected_regions)]
|
||
if fuel == "LPG":
|
||
for region in selected_regions:
|
||
chart[f"LPG · {region}"] = [{"date": row.effective_date.isoformat(), "value": priced_value(row)} for row in fuel_rows if row.region == region]
|
||
else:
|
||
chart[fuel] = [{"date": row.effective_date.isoformat(), "value": priced_value(row)} for row in fuel_rows]
|
||
period_a_from = period_a_to = period_b_from = period_b_to = None
|
||
|
||
summary_difference = None
|
||
if len(comparison_summary) == 2 and comparison_summary[0]["average"] is not None and comparison_summary[1]["average"] is not None:
|
||
first_average = comparison_summary[0]["average"]
|
||
second_average = comparison_summary[1]["average"]
|
||
summary_difference = {
|
||
"value": round(first_average - second_average, 4),
|
||
"percent": round((first_average - second_average) / second_average * 100, 2) if second_average else None,
|
||
}
|
||
|
||
return render_template(
|
||
"orlen.html", rows=rows, chart=chart, mode=mode,
|
||
selected_fuels=selected_fuels, selected_year=selected_year,
|
||
compare_year=compare_year, compare_fuel=compare_fuel,
|
||
comparison_type=comparison_type, comparison_summary=comparison_summary,
|
||
summary_difference=summary_difference, compare_region=compare_region,
|
||
period_a_from=period_a_from, period_a_to=period_a_to,
|
||
period_b_from=period_b_from, period_b_to=period_b_to,
|
||
settings=settings, fuel_types=FUEL_TYPES, include_vat=include_vat,
|
||
regions=POLISH_REGIONS, selected_regions=selected_regions,
|
||
available_years=available_years,
|
||
)
|
||
|
||
@main.post("/orlen/sync")
|
||
@login_required
|
||
@roles("boss", "admin")
|
||
def orlen_sync():
|
||
payload = request.get_json(silent=True) or request.form
|
||
fuels = payload.get("fuels", [])
|
||
if isinstance(fuels, str): fuels = [fuels]
|
||
fuels = [f.upper() for f in fuels if f.upper() in FUEL_TYPES]
|
||
year = int(payload.get("year", date.today().year))
|
||
if not fuels: return jsonify({"ok": False, "error": "Wybierz co najmniej jedno paliwo."}), 400
|
||
settings = CompanySettings.query.first(); start = date(year,1,1); end = min(date(year,12,31), date.today())
|
||
result = {}; total = 0
|
||
try:
|
||
for fuel in fuels:
|
||
imported = 0; updated = 0
|
||
for row in fetch_orlen_range(fuel, start, end, "all" if fuel == "LPG" else settings.region):
|
||
existing = OrlenPrice.query.filter_by(fuel_type=row["fuel_type"], effective_date=row["effective_date"], region=row["region"]).first()
|
||
if existing:
|
||
existing.price_per_liter=row["price_per_liter"]; existing.raw_value=row["raw_value"]; existing.product_name=row["product_name"]; existing.source=row["source"]; existing.fetched_at=datetime.utcnow(); updated += 1
|
||
else:
|
||
db.session.add(OrlenPrice(**row)); imported += 1
|
||
result[fuel] = {"added": imported, "updated": updated}; total += imported + updated
|
||
db.session.commit()
|
||
return jsonify({"ok": True, "message": f"Przetworzono {total} rekordów.", "result": result})
|
||
except Exception as exc:
|
||
db.session.rollback(); return jsonify({"ok": False, "error": str(exc)}), 502
|
||
|
||
|
||
|
||
|
||
@main.get("/admin/reports")
|
||
@login_required
|
||
@roles("boss", "admin")
|
||
def admin_reports():
|
||
today = date.today()
|
||
period = request.args.get("period", "month")
|
||
company_id = request.args.get("company_id", type=int)
|
||
month = request.args.get("month", today.strftime("%Y-%m"))
|
||
year = request.args.get("year", today.year, type=int)
|
||
date_from_raw = request.args.get("date_from", "")
|
||
date_to_raw = request.args.get("date_to", "")
|
||
|
||
if current_user.role == "boss":
|
||
company_id = current_user.company_id
|
||
companies = CompanySettings.query.order_by(CompanySettings.name).all() if current_user.role == "admin" else [current_user.company]
|
||
|
||
try:
|
||
if period == "year":
|
||
start_date, end_date = date(year, 1, 1), date(year, 12, 31)
|
||
elif period == "custom":
|
||
start_date = date.fromisoformat(date_from_raw)
|
||
end_date = date.fromisoformat(date_to_raw)
|
||
if end_date < start_date:
|
||
raise ValueError
|
||
else:
|
||
period = "month"
|
||
selected_year, selected_month = map(int, month.split("-"))
|
||
start_date = date(selected_year, selected_month, 1)
|
||
if selected_month == 12:
|
||
end_date = date(selected_year, 12, 31)
|
||
else:
|
||
end_date = date(selected_year, selected_month + 1, 1)
|
||
end_date = date.fromordinal(end_date.toordinal() - 1)
|
||
except (TypeError, ValueError):
|
||
period = "month"
|
||
month = today.strftime("%Y-%m")
|
||
start_date = date(today.year, today.month, 1)
|
||
next_month = date(today.year + (today.month == 12), 1 if today.month == 12 else today.month + 1, 1)
|
||
end_date = date.fromordinal(next_month.toordinal() - 1)
|
||
|
||
query = FuelEntry.query.join(Vehicle).join(User, Vehicle.owner_id == User.id).filter(
|
||
FuelEntry.fueled_at >= datetime.combine(start_date, datetime.min.time()),
|
||
FuelEntry.fueled_at < datetime.combine(date.fromordinal(end_date.toordinal() + 1), datetime.min.time()),
|
||
)
|
||
if company_id:
|
||
query = query.filter(or_(Vehicle.company_id == company_id, and_(Vehicle.company_id.is_(None), User.company_id == company_id)))
|
||
entries = query.order_by(FuelEntry.fueled_at.asc()).all()
|
||
|
||
zero = Decimal("0")
|
||
totals = {"retail_gross": zero, "retail_net": zero, "settlement_net": zero, "invoice": zero, "gross_saving": zero, "effective": zero, "vat_deducted": zero, "saving_net": zero, "saving_base_net": zero, "liters": zero, "count": 0, "last_price_count": 0}
|
||
by_month = defaultdict(lambda: {"retail_gross": 0.0, "invoice": 0.0, "gross_saving": 0.0, "retail_net": 0.0, "settlement_net": 0.0, "effective": 0.0, "saving_net": 0.0, "liters": 0.0, "count": 0})
|
||
by_vehicle = defaultdict(lambda: {"retail_gross": 0.0, "invoice": 0.0, "gross_saving": 0.0, "retail_net": 0.0, "settlement_net": 0.0, "effective": 0.0, "saving_net": 0.0, "liters": 0.0, "count": 0})
|
||
by_company = defaultdict(lambda: {"retail_gross": 0.0, "invoice": 0.0, "gross_saving": 0.0, "retail_net": 0.0, "settlement_net": 0.0, "effective": 0.0, "saving_net": 0.0, "liters": 0.0, "count": 0})
|
||
|
||
for entry in entries:
|
||
row = calculate_entry_settlement(entry)
|
||
totals["retail_gross"] += row["retail_gross"]
|
||
totals["retail_net"] += row["retail_net_total"]
|
||
totals["settlement_net"] += row["settlement_net_total"]
|
||
totals["invoice"] += row["invoice_gross"]
|
||
totals["gross_saving"] += row["gross_saving"]
|
||
totals["effective"] += row["effective_cost"]
|
||
totals["vat_deducted"] += row["deductible_vat"]
|
||
totals["saving_net"] += row["saving_net"]
|
||
if row["uses_last_price"]:
|
||
totals["saving_base_net"] += row["retail_net_total"]
|
||
totals["liters"] += row["liters"]
|
||
totals["count"] += 1
|
||
totals["last_price_count"] += int(row["uses_last_price"])
|
||
company = entry.vehicle.company or entry.vehicle.owner.company
|
||
keys = ((by_month, entry.fueled_at.strftime("%Y-%m")), (by_vehicle, entry.vehicle.name), (by_company, company.name if company else "Bez firmy"))
|
||
for bucket, key in keys:
|
||
bucket[key]["retail_gross"] += float(row["retail_gross"])
|
||
bucket[key]["retail_net"] += float(row["retail_net_total"])
|
||
bucket[key]["settlement_net"] += float(row["settlement_net_total"])
|
||
bucket[key]["invoice"] += float(row["invoice_gross"])
|
||
bucket[key]["gross_saving"] += float(row["gross_saving"])
|
||
bucket[key]["effective"] += float(row["effective_cost"])
|
||
bucket[key]["saving_net"] += float(row["saving_net"])
|
||
bucket[key]["liters"] += float(row["liters"])
|
||
bucket[key]["count"] += 1
|
||
|
||
totals["saving_percent"] = (totals["saving_net"] / totals["saving_base_net"] * Decimal("100")) if totals["saving_base_net"] else zero
|
||
return render_template(
|
||
"admin_reports.html", companies=[c for c in companies if c], company_id=company_id, period=period, month=month, year=year,
|
||
date_from=date_from_raw, date_to=date_to_raw, start_date=start_date, end_date=end_date, totals=totals,
|
||
by_month=dict(sorted(by_month.items())), by_vehicle=dict(sorted(by_vehicle.items())), by_company=dict(sorted(by_company.items())),
|
||
)
|
||
|
||
@main.get("/admin/fuel-entries")
|
||
@login_required
|
||
@roles("boss", "admin")
|
||
def admin_fuel_entries():
|
||
q = request.args.get("q", "").strip()
|
||
month = request.args.get("month", "").strip()
|
||
company_id = request.args.get("company_id", type=int)
|
||
page = request.args.get("page", 1, type=int)
|
||
query = FuelEntry.query.join(Vehicle).join(User, Vehicle.owner_id == User.id)
|
||
if current_user.role == "boss":
|
||
query = query.filter(or_(Vehicle.company_id == current_user.company_id, and_(Vehicle.company_id.is_(None), User.company_id == current_user.company_id)))
|
||
company_id = current_user.company_id
|
||
elif company_id:
|
||
query = query.filter(or_(Vehicle.company_id == company_id, and_(Vehicle.company_id.is_(None), User.company_id == company_id)))
|
||
if month:
|
||
try:
|
||
year, mon = map(int, month.split("-"))
|
||
query = query.filter(extract("year", FuelEntry.fueled_at) == year, extract("month", FuelEntry.fueled_at) == mon)
|
||
except ValueError:
|
||
month = ""
|
||
if q:
|
||
like = f"%{q}%"
|
||
query = query.outerjoin(FuelStationCompany, FuelEntry.station_company_id == FuelStationCompany.id).filter(or_(
|
||
Vehicle.name.ilike(like), Vehicle.registration.ilike(like), FuelEntry.station.ilike(like),
|
||
FuelEntry.invoice_number.ilike(like), User.name.ilike(like), User.email.ilike(like),
|
||
FuelStationCompany.company_name.ilike(like), FuelStationCompany.brand_name.ilike(like)
|
||
))
|
||
entries_page = query.order_by(FuelEntry.fueled_at.desc(), FuelEntry.id.desc()).paginate(page=page, per_page=25, error_out=False)
|
||
companies = CompanySettings.query.order_by(CompanySettings.name).all() if current_user.role == "admin" else [current_user.company]
|
||
vehicles_q = Vehicle.query.join(User, Vehicle.owner_id == User.id)
|
||
cards_q = FuelCard.query.filter_by(active=True)
|
||
if current_user.role == "boss":
|
||
vehicles_q = vehicles_q.filter(or_(Vehicle.company_id == current_user.company_id, and_(Vehicle.company_id.is_(None), User.company_id == current_user.company_id)))
|
||
cards_q = cards_q.filter_by(company_id=current_user.company_id)
|
||
elif company_id:
|
||
vehicles_q = vehicles_q.filter(or_(Vehicle.company_id == company_id, and_(Vehicle.company_id.is_(None), User.company_id == company_id)))
|
||
cards_q = cards_q.filter_by(company_id=company_id)
|
||
return render_template("admin_fuel_entries.html", entries_page=entries_page, q=q, month=month, company_id=company_id, companies=companies, vehicles=vehicles_q.order_by(Vehicle.name).all(), stations=FuelStationCompany.query.filter_by(active=True).order_by(FuelStationCompany.brand_name, FuelStationCompany.company_name).all(), fuel_cards=cards_q.order_by(FuelCard.name).all())
|
||
|
||
@main.put("/api/fuel-entries/<int:entry_id>")
|
||
@login_required
|
||
@roles("boss", "admin")
|
||
def update_fuel_entry(entry_id):
|
||
entry = FuelEntry.query.get_or_404(entry_id)
|
||
if not fuel_entry_accessible(entry):
|
||
abort(403)
|
||
old_vehicle = entry.vehicle
|
||
try:
|
||
new_vehicle = parse_fuel_entry_form(entry)
|
||
db.session.flush()
|
||
recalculate_vehicle_odometer(old_vehicle)
|
||
if new_vehicle.id != old_vehicle.id:
|
||
recalculate_vehicle_odometer(new_vehicle)
|
||
db.session.commit()
|
||
except (ValueError, ArithmeticError) as exc:
|
||
db.session.rollback()
|
||
return jsonify({"ok": False, "error": str(exc)}), 400
|
||
return jsonify({"ok": True, "message": "Zaktualizowano tankowanie"})
|
||
|
||
@main.delete("/api/fuel-entries/<int:entry_id>")
|
||
@login_required
|
||
@roles("boss", "admin")
|
||
def delete_fuel_entry(entry_id):
|
||
entry = FuelEntry.query.get_or_404(entry_id)
|
||
if not fuel_entry_accessible(entry):
|
||
abort(403)
|
||
vehicle = entry.vehicle
|
||
db.session.delete(entry)
|
||
db.session.flush()
|
||
recalculate_vehicle_odometer(vehicle)
|
||
db.session.commit()
|
||
return jsonify({"ok": True, "message": "Usunięto tankowanie"})
|
||
|
||
|
||
@main.route("/admin", methods=["GET","POST"])
|
||
@login_required
|
||
@roles("boss", "admin")
|
||
def admin():
|
||
if request.method=="POST":
|
||
settings=current_user.company or CompanySettings.query.first()
|
||
settings.name=request.form.get("name",settings.name);settings.entity_type=request.form.get("entity_type",settings.entity_type);settings.vat_rate=Decimal(request.form.get("vat_rate",settings.vat_rate));settings.vat_deduction_percent=Decimal(request.form.get("vat_deduction_percent",settings.vat_deduction_percent));settings.region=request.form.get("region",settings.region);settings.invoice_split_day=int(request.form.get("invoice_split_day",settings.invoice_split_day))
|
||
theme=request.form.get("theme")
|
||
if theme in THEMES:db.session.merge(AppSetting(key="theme",value=theme))
|
||
db.session.commit();return ok_response("Zapisano") or redirect(url_for("main.admin_companies"))
|
||
return redirect(url_for("main.admin_companies"))
|
||
|
||
@main.get("/admin/companies")
|
||
@login_required
|
||
@roles("boss", "admin")
|
||
def admin_companies():
|
||
q = request.args.get("q", "").strip()
|
||
page = request.args.get("page", 1, type=int)
|
||
selected_id = request.args.get("company_id", type=int)
|
||
companies_query = CompanySettings.query
|
||
if current_user.role == "boss":
|
||
companies_query = companies_query.filter_by(id=current_user.company_id)
|
||
elif q:
|
||
companies_query = companies_query.filter(CompanySettings.name.ilike(f"%{q}%"))
|
||
companies_page = companies_query.order_by(CompanySettings.name.asc()).paginate(
|
||
page=page, per_page=20, error_out=False
|
||
)
|
||
visible_companies = companies_page.items
|
||
if current_user.role == "boss":
|
||
selected_company = current_user.company
|
||
else:
|
||
selected_company = db.session.get(CompanySettings, selected_id) if selected_id else None
|
||
if selected_company is None:
|
||
selected_company = visible_companies[0] if visible_companies else CompanySettings.query.order_by(CompanySettings.name.asc()).first()
|
||
if selected_company and current_user.role == "boss" and selected_company.id != current_user.company_id:
|
||
abort(403)
|
||
fuel_cards = FuelCard.query.filter_by(company_id=selected_company.id).order_by(FuelCard.name.asc()).all() if selected_company else []
|
||
stations = FuelStationCompany.query.filter_by(active=True).order_by(
|
||
FuelStationCompany.station_count.desc(), FuelStationCompany.brand_name.asc()
|
||
).all()
|
||
return render_template(
|
||
"admin_companies.html",
|
||
companies_page=companies_page,
|
||
selected_company=selected_company,
|
||
fuel_cards=fuel_cards,
|
||
q=q,
|
||
regions=POLISH_REGIONS,
|
||
stations=stations,
|
||
vat_override=global_vat_override(),
|
||
)
|
||
|
||
|
||
@main.post("/admin/global-vat")
|
||
@login_required
|
||
@roles("admin")
|
||
def update_global_vat():
|
||
try:
|
||
save_global_vat_override(request.form)
|
||
db.session.commit()
|
||
response = ok_response("Zapisano globalne ustawienia VAT")
|
||
return response or redirect(url_for("main.admin_companies"))
|
||
except ValueError as exc:
|
||
db.session.rollback()
|
||
response = error_response(str(exc))
|
||
return response or redirect(url_for("main.admin_companies"))
|
||
|
||
@main.get("/admin/application")
|
||
@login_required
|
||
@roles("admin")
|
||
def admin_application():
|
||
return render_template("admin_application.html")
|
||
|
||
@main.get("/admin/users")
|
||
@login_required
|
||
@roles("boss", "admin")
|
||
def admin_users():
|
||
page=request.args.get("page",1,type=int);q=request.args.get("q","").strip();users_q=User.query
|
||
if current_user.role=="boss": users_q=users_q.filter_by(company_id=current_user.company_id)
|
||
if q: users_q=users_q.filter(db.or_(User.name.ilike(f"%{q}%"),User.email.ilike(f"%{q}%")))
|
||
users_page=users_q.order_by(User.name).paginate(page=page,per_page=25,error_out=False)
|
||
companies=CompanySettings.query.order_by(CompanySettings.name).all() if current_user.role=="admin" else [current_user.company]
|
||
cards=FuelCard.query.order_by(FuelCard.name).all()
|
||
if current_user.role=="boss": cards=cards.filter_by(company_id=current_user.company_id).all()
|
||
return render_template("admin_users.html",users_page=users_page,q=q,companies=[c for c in companies if c],fuel_cards=cards)
|
||
|
||
@main.post("/admin/users/<int:user_id>")
|
||
@login_required
|
||
@roles("admin")
|
||
def edit_user(user_id):
|
||
user = User.query.get_or_404(user_id)
|
||
try:
|
||
user.name = request.form["name"].strip(); user.email = request.form["email"].strip().lower(); user.role = request.form["role"]
|
||
user.active = "active" in request.form
|
||
password = request.form.get("password", "")
|
||
if password:
|
||
if len(password) < 8: raise ValueError("Nowe hasło musi mieć co najmniej 8 znaków.")
|
||
user.set_password(password)
|
||
db.session.commit(); response = ok_response(f"Zaktualizowano użytkownika {user.email}"); return response or redirect(url_for("main.admin"))
|
||
except (ValueError, IntegrityError) as exc:
|
||
db.session.rollback(); message = str(exc.orig) if isinstance(exc, IntegrityError) else str(exc); response = error_response(message); return response or redirect(url_for("main.admin", _anchor=f"user-{user_id}"))
|
||
|
||
|
||
@main.route("/stations")
|
||
@login_required
|
||
@roles("boss", "admin")
|
||
def stations():
|
||
q = request.args.get("q", "").strip()
|
||
page = request.args.get("page", 1, type=int)
|
||
sort = request.args.get("sort", "company_name")
|
||
direction = request.args.get("direction", "asc")
|
||
companies_q = CompanySettings.query.filter_by(active=True).order_by(CompanySettings.name.asc())
|
||
if current_user.role == "boss":
|
||
companies_q = companies_q.filter_by(id=current_user.company_id)
|
||
companies = companies_q.all()
|
||
requested_company_id = request.args.get("company_id", type=int)
|
||
if current_user.role == "boss":
|
||
company = current_user.company
|
||
elif requested_company_id is None:
|
||
return render_template("stations_company_select.html", companies=companies)
|
||
else:
|
||
company = db.session.get(CompanySettings, requested_company_id)
|
||
if company not in companies:
|
||
abort(404, description="Nie znaleziono aktywnej firmy")
|
||
if company is None:
|
||
abort(404, description="Brak aktywnej firmy do konfiguracji")
|
||
|
||
query = FuelStationCompany.query.filter_by(active=True)
|
||
if q:
|
||
terms = [term for term in q.split() if term]
|
||
for term in terms:
|
||
pattern = f"%{term}%"
|
||
query = query.filter(db.or_(FuelStationCompany.brand_name.ilike(pattern), FuelStationCompany.company_name.ilike(pattern), FuelStationCompany.nip.ilike(pattern), FuelStationCompany.regon.ilike(pattern), FuelStationCompany.station_names.ilike(pattern)))
|
||
columns = {"company_name": FuelStationCompany.brand_name, "station_count": FuelStationCompany.station_count, "nip": FuelStationCompany.nip, "regon": FuelStationCompany.regon, "selected_region": FuelStationCompany.selected_region}
|
||
column = columns.get(sort, FuelStationCompany.company_name)
|
||
query = query.order_by(column.desc() if direction == "desc" else column.asc(), FuelStationCompany.brand_name.asc())
|
||
station_page = query.paginate(page=page, per_page=30, error_out=False)
|
||
cards_q=FuelCard.query.filter_by(active=True, company_id=company.id)
|
||
cards=cards_q.order_by(FuelCard.name).all()
|
||
rules={(r.fuel_card_id,r.station_company_id):r for r in FuelCardStationRule.query.filter(FuelCardStationRule.fuel_card_id.in_([c.id for c in cards] or [-1])).all()}
|
||
return render_template("stations.html", station_page=station_page, q=q, regions=POLISH_REGIONS, sort=sort, direction=direction, allowed_ids={x.id for x in company.allowed_stations}, favorite_ids={x.id for x in company.favorite_stations}, company=company, companies=companies, fuel_cards=cards, card_rules=rules)
|
||
|
||
@main.post("/stations/sync")
|
||
@login_required
|
||
@roles("boss", "admin")
|
||
def stations_sync():
|
||
try:
|
||
companies = aggregate_ure_companies(fetch_ure_stations())
|
||
added, updated = sync_station_catalog(companies)
|
||
db.session.commit()
|
||
response = ok_response(f"Katalog URE: dodano {added}, zaktualizowano {updated}.")
|
||
return response or redirect(url_for("main.stations"))
|
||
except Exception as exc:
|
||
db.session.rollback(); response = error_response(f"Nie udało się pobrać danych URE: {exc}", 502); return response or redirect(url_for("main.stations"))
|
||
|
||
@main.post("/stations/<int:station_id>")
|
||
@login_required
|
||
@roles("boss", "admin")
|
||
def update_station(station_id):
|
||
station = FuelStationCompany.query.get_or_404(station_id)
|
||
region = request.form.get("selected_region", "").strip().lower()
|
||
if region and region not in station.region_list:
|
||
return error_response("Wybrane województwo nie występuje w danych tej firmy.")
|
||
station.selected_region = region
|
||
station.active = "active" in request.form
|
||
db.session.commit(); response = ok_response(f"Zapisano ustawienia firmy {station.company_name}"); return response or redirect(url_for("main.stations"))
|
||
|
||
@main.get("/api/stations")
|
||
@login_required
|
||
def station_suggestions():
|
||
q = request.args.get("q", "").strip()
|
||
query = FuelStationCompany.query.filter_by(active=True)
|
||
if q:
|
||
pattern = f"%{q}%"
|
||
query = query.filter(db.or_(FuelStationCompany.brand_name.ilike(pattern), FuelStationCompany.company_name.ilike(pattern), FuelStationCompany.station_names.ilike(pattern), FuelStationCompany.nip.ilike(pattern)))
|
||
rows = query.order_by(FuelStationCompany.company_name).limit(30).all()
|
||
return jsonify([{"id": x.id, "name": x.company_name, "brands": x.station_name_list[:5], "region": x.selected_region, "last_price": x.use_orlen_last_price} for x in rows])
|