add projekt cpn support

This commit is contained in:
Mateusz Gruszczyński
2026-07-13 14:47:07 +02:00
parent 47927f3ac3
commit 951b732db6
5 changed files with 162 additions and 10 deletions
+26 -9
View File
@@ -11,6 +11,7 @@ from .models import User, Vehicle, FuelEntry, CompanySettings, AppSetting, Orlen
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")
@@ -63,7 +64,8 @@ def dashboard():
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:
c = calculate_costs(e.gross, settings.vat_rate, settings.vat_deduction_percent)
vat_rate = effective_vat_rate(settings.vat_rate, e.fueled_at)
c = calculate_costs(e.gross, vat_rate, settings.vat_deduction_percent)
totals["gross"] += c["gross"]; totals["deductible_vat"] += c["deductible_vat"]; totals["final_cost"] += c["final_cost"]; totals["liters"] += Decimal(e.liters)
station_policy = e.station_company
normal_price = float(e.price_per_liter)
@@ -71,21 +73,21 @@ def dashboard():
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)
if uses_station_last_price:
payable_price = float(e.wholesale_price) * (1 + float(settings.vat_rate) / 100)
payable_price = float(e.wholesale_price) * (1 + float(vat_rate) / 100)
if rule:
net = payable_price / (1 + float(settings.vat_rate) / 100)
net = payable_price / (1 + float(vat_rate) / 100)
net = net * (1 - float(rule.discount_net_percent or 0) / 100) + float(rule.surcharge_net_per_liter or 0)
payable_price = net * (1 + float(settings.vat_rate) / 100)
payable_price = net * (1 + float(vat_rate) / 100)
payable_price = max(payable_price, 0)
payable_gross = float(e.liters) * payable_price
settlements[e.id] = {"normal_price": normal_price, "payable_price": payable_price, "normal_gross": e.gross, "payable_gross": payable_gross, "uses_last_price": uses_station_last_price}
settlements[e.id] = {"normal_price": normal_price, "payable_price": payable_price, "normal_gross": e.gross, "payable_gross": payable_gross, "uses_last_price": uses_station_last_price, "vat_rate": float(vat_rate)}
by_vehicle[e.vehicle.name]["gross"] += float(c["gross"]); by_vehicle[e.vehicle.name]["payable"] += payable_gross; by_vehicle[e.vehicle.name]["liters"] += float(e.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(c["gross"]); invoices[key]["payable"] += payable_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)
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
@@ -200,13 +202,12 @@ def orlen_data():
chart = {}
for fuel in selected_fuels:
fuel_rows = [r for r in rows if r.fuel_type == fuel and (fuel != "LPG" or r.region in selected_regions)]
multiplier = 1 + float(settings.vat_rate) / 100 if include_vat else 1
chart_key = fuel if fuel != "LPG" else None
if fuel == "LPG":
for region in selected_regions:
chart[f"LPG · {region}"] = [{"date": r.effective_date.isoformat(), "value": round(float(r.price_per_liter) * multiplier, 4)} for r in fuel_rows if r.region == region]
chart[f"LPG · {region}"] = [{"date": r.effective_date.isoformat(), "value": round(float(r.price_per_liter) * (1 + float(effective_vat_rate(settings.vat_rate, r.effective_date)) / 100 if include_vat else 1), 4)} for r in fuel_rows if r.region == region]
else:
chart[fuel] = [{"date": r.effective_date.isoformat(), "value": round(float(r.price_per_liter) * multiplier, 4)} for r in fuel_rows]
chart[fuel] = [{"date": r.effective_date.isoformat(), "value": round(float(r.price_per_liter) * (1 + float(effective_vat_rate(settings.vat_rate, r.effective_date)) / 100 if include_vat else 1), 4)} for r in fuel_rows]
return render_template("orlen.html", rows=rows, chart=chart, selected_fuels=selected_fuels, selected_year=selected_year, settings=settings, fuel_types=FUEL_TYPES, include_vat=include_vat, regions=POLISH_REGIONS, selected_regions=selected_regions, show_lpg_regions=show_lpg_regions)
@main.post("/orlen/sync")
@@ -284,8 +285,24 @@ def admin_companies():
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")
+11
View File
@@ -7,6 +7,17 @@
</div>
{% include '_admin_nav.html' %}
{% if current_user.role == 'admin' %}
<div class="card mb-4 border-warning"><div class="card-header"><strong>Globalny, czasowy VAT</strong></div><div class="card-body">
<form action="{{ url_for('main.update_global_vat') }}" method="post" class="ajax-form row g-3">
<div class="col-12"><div class="form-check form-switch"><input class="form-check-input" type="checkbox" name="enabled" id="global-vat-enabled" {% if vat_override.enabled %}checked{% endif %}><label class="form-check-label" for="global-vat-enabled">Nadpisz VAT we wszystkich firmach</label></div></div>
<div class="col-md-4"><label class="form-label">Nazwa projektu</label><input class="form-control" name="name" value="{{ vat_override.name }}" placeholder="Np. CPN"></div>
<div class="col-md-2"><label class="form-label">VAT (%)</label><input class="form-control" type="number" min="0" max="100" step="0.01" name="rate" value="{{ vat_override.rate if vat_override.rate is not none else '' }}" placeholder="8"></div>
<div class="col-md-3"><label class="form-label">Obowiązuje od</label><input class="form-control" type="date" name="start" value="{{ vat_override.start.isoformat() if vat_override.start else '' }}"></div>
<div class="col-md-3"><label class="form-label">Obowiązuje do</label><input class="form-control" type="date" name="end" value="{{ vat_override.end.isoformat() if vat_override.end else '' }}"></div>
<div class="col-12 d-flex justify-content-between align-items-center"><small class="text-body-secondary">Stawka zastępuje VAT każdej firmy wyłącznie w podanym okresie. Odliczenie 50% przy VAT 8% oznacza odliczenie połowy podatku, czyli 4% podstawy netto.</small><button class="btn btn-warning">Zapisz globalny VAT</button></div>
</form></div></div>
{% endif %}
<div class="row g-4">
<div class="col-xl-4 col-xxl-3">
<div class="card h-100">
+2 -1
View File
@@ -1,6 +1,7 @@
{% extends 'base.html' %}{% block content %}
<div class="d-flex justify-content-between align-items-center mb-3"><div><h1 class="h2 mb-0">Podsumowanie kosztów</h1><small class="text-body-secondary">{{ settings.name }} · VAT do odliczenia {{ settings.vat_deduction_percent }}%</small></div><form action="{{url_for('main.dashboard')}}" class="ajax-nav-form"><input class="form-control" type="month" name="month" value="{{month}}"></form></div>
{% if vat_override.enabled %}<div class="alert alert-warning"><strong>{{ vat_override.name }}:</strong> globalny VAT {{ vat_override.rate }}%{% if vat_override.start %} od {{ vat_override.start.strftime('%d.%m.%Y') }}{% endif %}{% if vat_override.end %} do {{ vat_override.end.strftime('%d.%m.%Y') }}{% endif %}. Rozliczenia są liczone według daty tankowania.</div>{% endif %}
<div class="row g-3 mb-4"><div class="col-6 col-lg-3"><div class="card metric"><div class="card-body"><small>Brutto</small><strong>{{ '%.2f'|format(totals.gross) }} zł</strong></div></div></div><div class="col-6 col-lg-3"><div class="card metric"><div class="card-body"><small>VAT odliczony</small><strong>{{ '%.2f'|format(totals.deductible_vat) }} zł</strong></div></div></div><div class="col-6 col-lg-3"><div class="card metric"><div class="card-body"><small>Koszt po VAT</small><strong>{{ '%.2f'|format(totals.final_cost) }} zł</strong></div></div></div><div class="col-6 col-lg-3"><div class="card metric"><div class="card-body"><small>Litry</small><strong>{{ '%.2f'|format(totals.liters) }} l</strong></div></div></div></div>
<div class="row g-4"><div class="col-lg-7"><div class="card"><div class="card-body"><h2 class="h5">Koszt według pojazdu</h2><p class="small text-body-secondary">Cena na dystrybutorze a kwota do zapłaty według cennika karty.</p><div class="chart-box"><canvas id="cost-chart"></canvas><div id="cost-chart-empty" class="chart-empty d-none">Brak danych dla wybranego miesiąca.</div></div></div></div></div><div class="col-lg-5"><div class="card h-100"><div class="card-body"><h2 class="h5">Okresy faktur</h2><small class="text-body-secondary">{% if split_enabled %}Podział według dnia {{settings.invoice_split_day}}{% else %}Podział dzienny wyłączony{% endif %}</small><table class="table mt-2"><thead><tr><th>Okres</th><th>Pozycje</th><th>Detal</th><th>Do zapłaty</th></tr></thead><tbody>{% for key,row in invoices.items() %}<tr><td>{{key}}</td><td>{{row.count}}</td><td>{{'%.2f'|format(row.gross)}} zł</td><td>{{'%.2f'|format(row.payable)}} zł</td></tr>{% else %}<tr><td colspan="4">Brak danych</td></tr>{% endfor %}</tbody></table></div></div></div></div>
<div class="card mt-4"><div class="card-body"><h2 class="h5">Tankowania</h2><div class="table-responsive"><table class="table table-hover align-middle"><thead><tr><th>Data</th><th>Auto</th><th>Stacja</th><th>Paliwo</th><th>Litry</th><th>Cena detal.</th><th>Orlen last</th><th>Do zapłaty</th><th>Razem</th></tr></thead><tbody>{% for e in entries %}{% set s=settlements[e.id] %}<tr><td>{{e.fueled_at.strftime('%d.%m.%Y')}}</td><td>{{e.vehicle.name}}</td><td>{{e.station or '—'}}</td><td>{{e.fuel_type}}</td><td>{{e.liters}}</td><td>{{'%.4f'|format(s.normal_price)}} zł/l</td><td>{% if e.wholesale_price %}{{'%.4f'|format(e.wholesale_price|float)}} zł/l{% else %}—{% endif %}</td><td>{% if s.uses_last_price %}<span class="badge text-bg-info">last price</span> {% endif %}{{'%.4f'|format(s.payable_price)}} zł/l</td><td><span class="text-body-secondary text-decoration-line-through">{{'%.2f'|format(s.normal_gross)}} zł</span><br><strong>{{'%.2f'|format(s.payable_gross)}} zł</strong></td></tr>{% else %}<tr><td colspan="9">Brak tankowań w tym miesiącu.</td></tr>{% endfor %}</tbody></table></div></div></div>
<div class="card mt-4"><div class="card-body"><h2 class="h5">Tankowania</h2><div class="table-responsive"><table class="table table-hover align-middle"><thead><tr><th>Data</th><th>Auto</th><th>Stacja</th><th>Paliwo</th><th>Litry</th><th>Cena detal.</th><th>Orlen last</th><th>Do zapłaty</th><th>VAT</th><th>Razem</th></tr></thead><tbody>{% for e in entries %}{% set s=settlements[e.id] %}<tr><td>{{e.fueled_at.strftime('%d.%m.%Y')}}</td><td>{{e.vehicle.name}}</td><td>{{e.station or '—'}}</td><td>{{e.fuel_type}}</td><td>{{e.liters}}</td><td>{{'%.4f'|format(s.normal_price)}} zł/l</td><td>{% if e.wholesale_price %}{{'%.4f'|format(e.wholesale_price|float)}} zł/l{% else %}—{% endif %}</td><td>{% if s.uses_last_price %}<span class="badge text-bg-info">last price</span> {% endif %}{{'%.4f'|format(s.payable_price)}} zł/l</td><td>{{'%.2f'|format(s.vat_rate)}}%</td><td><span class="text-body-secondary text-decoration-line-through">{{'%.2f'|format(s.normal_gross)}} zł</span><br><strong>{{'%.2f'|format(s.payable_gross)}} zł</strong></td></tr>{% else %}<tr><td colspan="10">Brak tankowań w tym miesiącu.</td></tr>{% endfor %}</tbody></table></div></div></div>
{% endblock %}{% block scripts %}<script>FuelTrack.renderBarChart('cost-chart', {{by_vehicle|tojson}});</script>{% endblock %}
+88
View File
@@ -0,0 +1,88 @@
from datetime import date, datetime
from decimal import Decimal, InvalidOperation
from .extensions import db
from .models import AppSetting
KEYS = {
"enabled": "global_vat_override_enabled",
"name": "global_vat_override_name",
"rate": "global_vat_override_rate",
"start": "global_vat_override_start",
"end": "global_vat_override_end",
}
def _value(key, default=""):
setting = db.session.get(AppSetting, KEYS[key])
return setting.value if setting else default
def global_vat_override():
rate_raw = _value("rate")
try:
rate = Decimal(rate_raw) if rate_raw != "" else None
except InvalidOperation:
rate = None
return {
"enabled": _value("enabled", "0") == "1",
"name": _value("name", "Projekt rządowy"),
"rate": rate,
"start": _parse_date(_value("start")),
"end": _parse_date(_value("end")),
}
def _parse_date(value):
if not value:
return None
try:
return date.fromisoformat(value)
except ValueError:
return None
def effective_vat_rate(nominal_rate, on_date=None):
override = global_vat_override()
if not override["enabled"] or override["rate"] is None:
return Decimal(str(nominal_rate))
if isinstance(on_date, datetime):
on_date = on_date.date()
on_date = on_date or date.today()
if override["start"] and on_date < override["start"]:
return Decimal(str(nominal_rate))
if override["end"] and on_date > override["end"]:
return Decimal(str(nominal_rate))
return override["rate"]
def save_global_vat_override(form):
enabled = "enabled" in form
name = (form.get("name") or "Projekt rządowy").strip()
rate_raw = (form.get("rate") or "").strip()
start_raw = (form.get("start") or "").strip()
end_raw = (form.get("end") or "").strip()
if enabled:
try:
rate = Decimal(rate_raw)
except InvalidOperation as exc:
raise ValueError("Podaj prawidłową globalną stawkę VAT.") from exc
if rate < 0 or rate > 100:
raise ValueError("Globalna stawka VAT musi mieścić się w zakresie 0100%.")
if start_raw and not _parse_date(start_raw):
raise ValueError("Nieprawidłowa data rozpoczęcia.")
if end_raw and not _parse_date(end_raw):
raise ValueError("Nieprawidłowa data zakończenia.")
if start_raw and end_raw and start_raw > end_raw:
raise ValueError("Data zakończenia nie może być wcześniejsza niż rozpoczęcia.")
values = {
KEYS["enabled"]: "1" if enabled else "0",
KEYS["name"]: name,
KEYS["rate"]: rate_raw,
KEYS["start"]: start_raw,
KEYS["end"]: end_raw,
}
for key, value in values.items():
db.session.merge(AppSetting(key=key, value=value))