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 effective_vat_rate, global_vat_override, save_global_vat_override 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") 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 return vehicle 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 [] totals = {"gross": Decimal("0"), "deductible_vat": Decimal("0"), "final_cost": Decimal("0"), "liters": Decimal("0")} by_vehicle = defaultdict(lambda: {"gross": 0, "payable": 0, "liters": 0}); invoices = defaultdict(lambda: {"gross": 0, "payable": 0, "count": 0}); settlements = {} for e in entries: entry_settings = e.vehicle.company or settings vat_rate = effective_vat_rate(entry_settings.vat_rate, e.fueled_at) vat_deduction_percent = entry_settings.vat_deduction_percent normal_price = float(e.price_per_liter) rule = FuelCardStationRule.query.filter_by(fuel_card_id=e.fuel_card_id, station_company_id=e.station_company_id).first() if e.used_fuel_card and e.fuel_card_id and e.station_company_id else None uses_station_last_price = bool(rule and rule.use_orlen_last_price and e.wholesale_price is not None) # Last price is a NET station price. Discounts and surcharges are also net. if uses_station_last_price: settlement_net = Decimal(e.wholesale_price) else: settlement_net = Decimal(str(normal_price)) / (Decimal("1") + Decimal(str(vat_rate)) / Decimal("100")) if rule: settlement_net = settlement_net * (Decimal("1") - Decimal(rule.discount_net_percent or 0) / Decimal("100")) settlement_net += Decimal(rule.surcharge_net_per_liter or 0) settlement_net = max(settlement_net, Decimal("0")) price_costs = calculate_net_price_costs(settlement_net, vat_rate, vat_deduction_percent) liters = Decimal(e.liters) invoice_gross = price_costs["invoice_gross"] * liters deductible_vat = price_costs["deductible_vat"] * liters effective_gross = price_costs["effective_cost"] * liters totals["gross"] += invoice_gross totals["deductible_vat"] += deductible_vat totals["final_cost"] += effective_gross totals["liters"] += liters settlements[e.id] = { "normal_price": normal_price, "net_price": float(settlement_net), "invoice_price": float(price_costs["invoice_gross"]), "payable_price": float(price_costs["effective_cost"]), "normal_gross": e.gross, "invoice_gross": float(invoice_gross), "payable_gross": float(effective_gross), "uses_last_price": uses_station_last_price, "vat_rate": float(vat_rate), "vat_deduction_percent": float(vat_deduction_percent), } by_vehicle[e.vehicle.name]["gross"] += float(invoice_gross); by_vehicle[e.vehicle.name]["payable"] += float(effective_gross); by_vehicle[e.vehicle.name]["liters"] += float(liters) if e.used_fuel_card: key = invoice_period(e.fueled_at, settings.invoice_split_day) if split_enabled else "Cały miesiąc" else: key = "Poza kartą" invoices[key]["gross"] += float(invoice_gross); invoices[key]["payable"] += float(effective_gross); invoices[key]["count"] += 1 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//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//unshare/") @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//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_id=vehicle.id, 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) 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/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/") @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/") @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/") @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/") @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])