zapamietanie akcji

This commit is contained in:
Mateusz Gruszczyński
2026-07-19 19:00:04 +02:00
parent e26d9e4ff3
commit 659f04f05f
9 changed files with 121 additions and 8 deletions
+2
View File
@@ -15,6 +15,7 @@ from ..extensions import db
from ..models import AppSetting, CompanySettings, FuelCard, FuelCardPolicy, FuelCardStationRule, FuelEntry, FuelStationCompany, FuelStationPoint, OrlenPrice, User, Vehicle from ..models import AppSetting, CompanySettings, FuelCard, FuelCardPolicy, FuelCardStationRule, FuelEntry, FuelStationCompany, FuelStationPoint, OrlenPrice, User, Vehicle
from ..services import POLISH_REGIONS, aggregate_ure_companies, calculate_costs, fetch_orlen_price, fetch_orlen_range, fetch_ure_stations, invoice_period from ..services import POLISH_REGIONS, aggregate_ure_companies, calculate_costs, fetch_orlen_price, fetch_orlen_range, fetch_ure_stations, invoice_period
from ..station_catalog import sync_station_catalog from ..station_catalog import sync_station_catalog
from ..vat import freeze_vat_action
FUEL_TYPES = ("PB95", "PB98", "DIESEL", "LPG") FUEL_TYPES = ("PB95", "PB98", "DIESEL", "LPG")
@@ -44,5 +45,6 @@ def create_fuel_entry():
rule=FuelCardStationRule.query.filter_by(fuel_card_id=card.id,station_company_id=station_company.id).first() rule=FuelCardStationRule.query.filter_by(fuel_card_id=card.id,station_company_id=station_company.id).first()
if rule and not rule.allowed:return fail('Ta stacja jest zablokowana dla wybranej karty paliwowej',403) if rule and not rule.allowed:return fail('Ta stacja jest zablokowana dla wybranej karty paliwowej',403)
e=FuelEntry(vehicle_id=vehicle.id,user_id=current_user.id,fueled_at=fueled_at,liters=Decimal(d['liters']),price_per_liter=Decimal(d['price_per_liter']),fuel_type=d['fuel_type'],odometer=odo,station=station_company.company_name if station_company else d.get('station'),station_company_id=station_company.id if station_company else None,fuel_card_id=card.id if used_card else None,invoice_number=d.get('invoice_number'),used_fuel_card=used_card,wholesale_price=wholesale,wholesale_source=source) e=FuelEntry(vehicle_id=vehicle.id,user_id=current_user.id,fueled_at=fueled_at,liters=Decimal(d['liters']),price_per_liter=Decimal(d['price_per_liter']),fuel_type=d['fuel_type'],odometer=odo,station=station_company.company_name if station_company else d.get('station'),station_company_id=station_company.id if station_company else None,fuel_card_id=card.id if used_card else None,invoice_number=d.get('invoice_number'),used_fuel_card=used_card,wholesale_price=wholesale,wholesale_source=source)
freeze_vat_action(e, settings.vat_rate)
vehicle.current_odometer=odo;db.session.add(e);db.session.commit();return response({'id':e.id,'redirect':url_for('main.dashboard')},'Zapisano tankowanie',201) vehicle.current_odometer=odo;db.session.add(e);db.session.commit();return response({'id':e.id,'redirect':url_for('main.dashboard')},'Zapisano tankowanie',201)
except Exception as exc:db.session.rollback();return fail(str(exc),400) except Exception as exc:db.session.rollback();return fail(str(exc),400)
+46
View File
@@ -21,8 +21,54 @@ def add_station_access_mode() -> None:
)) ))
def add_fuel_entry_vat_action_snapshot() -> None:
inspector = inspect(db.engine)
columns = {column["name"] for column in inspector.get_columns("fuel_entry")}
additions = {
"vat_action_frozen": "BOOLEAN NOT NULL DEFAULT 0",
"vat_action_applied": "BOOLEAN NOT NULL DEFAULT 0",
"vat_action_name": "VARCHAR(160)",
"vat_action_rate": "NUMERIC(6, 3)",
"vat_action_start": "DATE",
"vat_action_end": "DATE",
}
for name, definition in additions.items():
if name not in columns:
db.session.execute(text(f"ALTER TABLE fuel_entry ADD COLUMN {name} {definition}"))
settings = dict(db.session.execute(text(
"SELECT key, value FROM app_setting WHERE key IN "
"('global_vat_override_enabled','global_vat_override_name','global_vat_override_rate',"
"'global_vat_override_start','global_vat_override_end')"
)).all())
if settings.get("global_vat_override_enabled") == "1" and settings.get("global_vat_override_rate"):
conditions = ["vat_action_applied = 0"]
params = {
"name": settings.get("global_vat_override_name") or "Projekt rządowy",
"rate": settings["global_vat_override_rate"],
"start": settings.get("global_vat_override_start") or None,
"end": settings.get("global_vat_override_end") or None,
}
if params["start"]:
conditions.append("DATE(fueled_at) >= DATE(:start)")
if params["end"]:
conditions.append("DATE(fueled_at) <= DATE(:end)")
db.session.execute(text(f"""
UPDATE fuel_entry
SET vat_action_frozen = 1,
vat_action_applied = 1,
vat_action_name = :name,
vat_action_rate = :rate,
vat_action_start = :start,
vat_action_end = :end
WHERE {' AND '.join(conditions)}
"""), params)
db.session.execute(text("UPDATE fuel_entry SET vat_action_frozen = 1 WHERE vat_action_frozen = 0"))
MIGRATIONS: tuple[Migration, ...] = ( MIGRATIONS: tuple[Migration, ...] = (
Migration("20260713_company_station_access_mode", add_station_access_mode), Migration("20260713_company_station_access_mode", add_station_access_mode),
Migration("20260719_fuel_entry_vat_action_snapshot", add_fuel_entry_vat_action_snapshot),
) )
def ensure_migration_table() -> None: def ensure_migration_table() -> None:
+6 -2
View File
@@ -11,7 +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 .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 .station_catalog import sync_station_catalog
from . import THEMES from . import THEMES
from .vat import effective_vat_rate, global_vat_override, save_global_vat_override from .vat import entry_vat_rate, freeze_vat_action, global_vat_override, save_global_vat_override
from .domain.costs import calculate_net_price_costs, calculate_net_price_comparison from .domain.costs import calculate_net_price_costs, calculate_net_price_comparison
main = Blueprint("main", __name__) main = Blueprint("main", __name__)
@@ -70,6 +70,7 @@ def parse_fuel_entry_form(entry):
fuel_type = (request.form.get("fuel_type") or "").upper() fuel_type = (request.form.get("fuel_type") or "").upper()
if fuel_type not in FUEL_TYPES: if fuel_type not in FUEL_TYPES:
raise ValueError("Nieprawidłowy rodzaj paliwa") raise ValueError("Nieprawidłowy rodzaj paliwa")
previous_fueled_at = entry.fueled_at
fueled_at = datetime.fromisoformat(request.form.get("fueled_at", "")) fueled_at = datetime.fromisoformat(request.form.get("fueled_at", ""))
liters = Decimal(request.form.get("liters", "")) liters = Decimal(request.form.get("liters", ""))
price = Decimal(request.form.get("price_per_liter", "")) price = Decimal(request.form.get("price_per_liter", ""))
@@ -99,6 +100,8 @@ def parse_fuel_entry_form(entry):
wholesale_price = (request.form.get("wholesale_price") or "").strip() wholesale_price = (request.form.get("wholesale_price") or "").strip()
entry.wholesale_price = Decimal(wholesale_price) if wholesale_price else None entry.wholesale_price = Decimal(wholesale_price) if wholesale_price else None
entry.wholesale_source = (request.form.get("wholesale_source") or "").strip() or 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()
freeze_vat_action(entry, settings.vat_rate, previous_fueled_at)
return vehicle return vehicle
@@ -110,7 +113,7 @@ def calculate_entry_settlement(entry, fallback_settings=None):
separately and never counted as a Last Price saving. separately and never counted as a Last Price saving.
""" """
entry_settings = entry.vehicle.company or (entry.vehicle.owner.company if entry.vehicle and entry.vehicle.owner else None) or fallback_settings or CompanySettings.query.first() entry_settings = entry.vehicle.company or (entry.vehicle.owner.company if entry.vehicle and entry.vehicle.owner else None) or fallback_settings or CompanySettings.query.first()
vat_rate = effective_vat_rate(entry_settings.vat_rate, entry.fueled_at) vat_rate = entry_vat_rate(entry, entry_settings.vat_rate)
vat_deduction_percent = entry_settings.vat_deduction_percent vat_deduction_percent = entry_settings.vat_deduction_percent
retail_price = Decimal(entry.price_per_liter) retail_price = Decimal(entry.price_per_liter)
rule = FuelCardStationRule.query.filter_by( rule = FuelCardStationRule.query.filter_by(
@@ -318,6 +321,7 @@ def fuel():
except Exception: pass except Exception: pass
station_label = station_company.company_name if station_company else request.form.get("station") 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) 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)
freeze_vat_action(e, settings.vat_rate)
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")) 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) allowed=list(settings.allowed_stations)
company_favorites=list(settings.favorite_stations) company_favorites=list(settings.favorite_stations)
+6
View File
@@ -171,6 +171,12 @@ class FuelEntry(db.Model):
used_fuel_card = db.Column(db.Boolean, default=False, nullable=False) used_fuel_card = db.Column(db.Boolean, default=False, nullable=False)
wholesale_price = db.Column(db.Numeric(10, 4)) wholesale_price = db.Column(db.Numeric(10, 4))
wholesale_source = db.Column(db.String(120)) wholesale_source = db.Column(db.String(120))
vat_action_frozen = db.Column(db.Boolean, default=False, nullable=False)
vat_action_applied = db.Column(db.Boolean, default=False, nullable=False)
vat_action_name = db.Column(db.String(160))
vat_action_rate = db.Column(db.Numeric(6, 3))
vat_action_start = db.Column(db.Date)
vat_action_end = db.Column(db.Date)
created_at = db.Column(db.DateTime, default=datetime.utcnow, nullable=False) created_at = db.Column(db.DateTime, default=datetime.utcnow, nullable=False)
vehicle = db.relationship("Vehicle", back_populates="fuel_entries") vehicle = db.relationship("Vehicle", back_populates="fuel_entries")
user = db.relationship("User") user = db.relationship("User")
+7 -1
View File
@@ -16,6 +16,12 @@
[data-bs-theme="dark"] .table thead th{color:#adb5bd} [data-bs-theme="dark"] .table thead th{color:#adb5bd}
[data-bs-theme="dark"] .text-body-secondary,[data-bs-theme="dark"] .text-muted{color:#aeb4bb!important} [data-bs-theme="dark"] .text-body-secondary,[data-bs-theme="dark"] .text-muted{color:#aeb4bb!important}
[data-bs-theme="dark"] .bg-body-tertiary{background-color:#181b1f!important} [data-bs-theme="dark"] .bg-body-tertiary{background-color:#181b1f!important}
[data-bs-theme="dark"] .dropdown-menu{background:#202327;border-color:#3a3f45;box-shadow:0 .75rem 2rem rgba(0,0,0,.35)} [data-bs-theme="dark"] .dropdown-menu{background:#202327;border-color:#3a3f45;box-shadow:0 .4rem 1.1rem rgba(0,0,0,.22)}
[data-bs-theme="dark"] .dropdown-item{color:#e9ecef} [data-bs-theme="dark"] .dropdown-item{color:#e9ecef}
[data-bs-theme="dark"] .dropdown-item:hover,[data-bs-theme="dark"] .dropdown-item:focus{background:#2b3035;color:#fff} [data-bs-theme="dark"] .dropdown-item:hover,[data-bs-theme="dark"] .dropdown-item:focus{background:#2b3035;color:#fff}
.vat-action-row>td{background-color:rgba(var(--bs-warning-rgb),.075)}
.vat-action-row:hover>td{background-color:rgba(var(--bs-warning-rgb),.13)!important}
.vat-action-badge{background:rgba(var(--bs-warning-rgb),.2);color:var(--bs-body-color);border:1px solid rgba(var(--bs-warning-rgb),.45);margin-top:.25rem}
[data-bs-theme="dark"] .vat-action-row>td{background-color:rgba(var(--bs-warning-rgb),.09)}
[data-bs-theme="dark"] .vat-action-row:hover>td{background-color:rgba(var(--bs-warning-rgb),.15)!important}
+5 -3
View File
@@ -1,5 +1,5 @@
:root{--app-radius:1rem;--app-shadow:0 .4rem 1.5rem rgba(15,23,42,.07);--app-shadow-hover:0 .75rem 2rem rgba(15,23,42,.11)} :root{--app-radius:1rem;--app-shadow:0 .4rem 1.5rem rgba(15,23,42,.07);--app-shadow-hover:0 .75rem 2rem rgba(15,23,42,.11)}
html,body{min-height:100%}body{min-height:100vh;display:flex;flex-direction:column;background:var(--bs-tertiary-bg)} html,body{min-height:100%}body{min-height:100vh;min-height:100dvh;display:flex;flex-direction:column;background:var(--bs-tertiary-bg);padding-left:env(safe-area-inset-left);padding-right:env(safe-area-inset-right)}
.app-navbar{background:color-mix(in srgb,var(--bs-body-bg) 92%,transparent);border-bottom:1px solid var(--bs-border-color-translucent);backdrop-filter:blur(14px);box-shadow:0 .15rem .75rem rgba(15,23,42,.04)} .app-navbar{background:color-mix(in srgb,var(--bs-body-bg) 92%,transparent);border-bottom:1px solid var(--bs-border-color-translucent);backdrop-filter:blur(14px);box-shadow:0 .15rem .75rem rgba(15,23,42,.04)}
.navbar-brand{font-weight:800;letter-spacing:-.04em;white-space:nowrap}.app-brand,.auth-brand{font-size:1.4rem;line-height:1;text-decoration:none}.brand-fuel{color:var(--bs-emphasis-color)}.brand-track{color:var(--bs-primary)} .navbar-brand{font-weight:800;letter-spacing:-.04em;white-space:nowrap}.app-brand,.auth-brand{font-size:1.4rem;line-height:1;text-decoration:none}.brand-fuel{color:var(--bs-emphasis-color)}.brand-track{color:var(--bs-primary)}
.app-brand::after,.auth-brand::after{content:"";display:inline-block;width:.38rem;height:.38rem;margin-left:.18rem;border-radius:50%;background:var(--bs-warning);box-shadow:0 0 0 .2rem color-mix(in srgb,var(--bs-warning) 18%,transparent);vertical-align:.15em} .app-brand::after,.auth-brand::after{content:"";display:inline-block;width:.38rem;height:.38rem;margin-left:.18rem;border-radius:50%;background:var(--bs-warning);box-shadow:0 0 0 .2rem color-mix(in srgb,var(--bs-warning) 18%,transparent);vertical-align:.15em}
@@ -17,11 +17,13 @@ html,body{min-height:100%}body{min-height:100vh;display:flex;flex-direction:colu
.app-brand{font-family:inherit;font-size:1.4rem!important;font-weight:800!important;line-height:1!important;letter-spacing:-.04em!important} .app-brand{font-family:inherit;font-size:1.4rem!important;font-weight:800!important;line-height:1!important;letter-spacing:-.04em!important}
.app-navbar .nav-link{font-size:1rem;line-height:1.5} .app-navbar .nav-link{font-size:1rem;line-height:1.5}
[data-bs-theme="dark"]{--app-shadow:0 .45rem 1.6rem rgba(0,0,0,.28);--app-shadow-hover:0 .8rem 2.2rem rgba(0,0,0,.38)} [data-bs-theme="dark"]{--app-shadow:0 .25rem .9rem rgba(0,0,0,.16);--app-shadow-hover:0 .45rem 1.25rem rgba(0,0,0,.22)}
[data-bs-theme="dark"] body{background:#17191c;color:#f1f3f5} [data-bs-theme="dark"] body{background:#17191c;color:#f1f3f5}
[data-bs-theme="dark"] .app-navbar{background:rgba(24,26,29,.94);border-bottom-color:#34383d;box-shadow:0 .2rem 1rem rgba(0,0,0,.32)} [data-bs-theme="dark"] .app-navbar{background:rgba(24,26,29,.94);border-bottom-color:#34383d;box-shadow:0 .1rem .45rem rgba(0,0,0,.18)}
[data-bs-theme="dark"] .app-footer{background:#1b1d20;border-top-color:#34383d} [data-bs-theme="dark"] .app-footer{background:#1b1d20;border-top-color:#34383d}
[data-bs-theme="dark"] .brand-fuel{color:#f8f9fa} [data-bs-theme="dark"] .brand-fuel{color:#f8f9fa}
[data-bs-theme="dark"] .auth-page{background:radial-gradient(circle at 20% 10%,rgba(55,139,230,.16),transparent 36%),radial-gradient(circle at 85% 85%,rgba(255,193,7,.08),transparent 32%),#141619} [data-bs-theme="dark"] .auth-page{background:radial-gradient(circle at 20% 10%,rgba(55,139,230,.16),transparent 36%),radial-gradient(circle at 85% 85%,rgba(255,193,7,.08),transparent 32%),#141619}
@media(max-width:991.98px){.app-main{padding-top:1.35rem}.app-navbar .navbar-collapse{border-top:1px solid var(--bs-border-color-translucent);margin-top:.75rem}.app-navbar .nav-link{padding:.7rem .8rem}.page-header{align-items:stretch;flex-direction:column}} @media(max-width:991.98px){.app-main{padding-top:1.35rem}.app-navbar .navbar-collapse{border-top:1px solid var(--bs-border-color-translucent);margin-top:.75rem}.app-navbar .nav-link{padding:.7rem .8rem}.page-header{align-items:stretch;flex-direction:column}}
@media(max-width:575.98px){.app-main{padding-left:1rem;padding-right:1rem}.page-header h1{font-size:1.65rem}.auth-main{padding-top:1.25rem;padding-bottom:1.25rem}} @media(max-width:575.98px){.app-main{padding-left:1rem;padding-right:1rem}.page-header h1{font-size:1.65rem}.auth-main{padding-top:1.25rem;padding-bottom:1.25rem}}
@supports(padding:max(0px)){.app-navbar{padding-top:max(.5rem,env(safe-area-inset-top))}.app-footer{padding-bottom:max(1.25rem,env(safe-area-inset-bottom))}}
+3 -1
View File
@@ -2,7 +2,9 @@
<html lang="pl" data-bs-theme="{{ app_theme.mode }}"> <html lang="pl" data-bs-theme="{{ app_theme.mode }}">
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1"> <meta name="viewport" content="width=device-width,initial-scale=1,viewport-fit=cover">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
<meta name="theme-color" content="#0d6efd"> <meta name="theme-color" content="#0d6efd">
<title>{% block title %}FuelTrack{% endblock %}</title> <title>{% block title %}FuelTrack{% endblock %}</title>
<link id="bootstrap-theme" rel="stylesheet" href="{{ app_theme.url }}"> <link id="bootstrap-theme" rel="stylesheet" href="{{ app_theme.url }}">
+1 -1
View File
@@ -10,5 +10,5 @@
</div> </div>
<div class="alert alert-info small"><strong>Wykres brutto:</strong> porównuje wartość po cenie detalicznej z ostateczną kwotą faktury brutto. Cena rozliczeniowa uwzględnia Last Price albo cenę detaliczną netto, następnie rabat procentowy i dopłatę przypisaną do karty/stacji. Oszczędność handlowa Last Price nadal jest liczona netto.</div> <div class="alert alert-info small"><strong>Wykres brutto:</strong> porównuje wartość po cenie detalicznej z ostateczną kwotą faktury brutto. Cena rozliczeniowa uwzględnia Last Price albo cenę detaliczną netto, następnie rabat procentowy i dopłatę przypisaną do karty/stacji. Oszczędność handlowa Last Price nadal jest liczona netto.</div>
<div class="row g-4"><div class="col-lg-7"><div class="card"><div class="card-body"><h2 class="h5">Kwoty brutto według pojazdu</h2><p class="small text-body-secondary">Detal brutto, ostateczna kwota do zapłaty brutto oraz różnica. Rabaty i dopłaty użytkownika są uwzględniane.</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 ustawień firmy{% else %}Podział dzienny wyłączony{% endif %}</small><table class="table mt-2"><thead><tr><th>Okres</th><th>Pozycje</th><th>Faktura*</th><th>Koszt po VAT</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 class="small text-body-secondary">* Kwota brutto: cena rozliczeniowa netto + VAT właściwy dla firmy.</div></div></div></div> <div class="row g-4"><div class="col-lg-7"><div class="card"><div class="card-body"><h2 class="h5">Kwoty brutto według pojazdu</h2><p class="small text-body-secondary">Detal brutto, ostateczna kwota do zapłaty brutto oraz różnica. Rabaty i dopłaty użytkownika są uwzględniane.</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 ustawień firmy{% else %}Podział dzienny wyłączony{% endif %}</small><table class="table mt-2"><thead><tr><th>Okres</th><th>Pozycje</th><th>Faktura*</th><th>Koszt po VAT</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 class="small text-body-secondary">* Kwota brutto: cena rozliczeniowa netto + VAT właściwy dla firmy.</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>Litry</th><th>Detal brutto</th><th>Detal netto</th><th>Last Price netto</th><th>Oszczędność netto</th><th>Faktura brutto</th><th>Koszt po VAT</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.liters}}</td><td>{{'%.4f'|format(s.retail_price)}} zł/l<br><small>{{'%.2f'|format(s.retail_gross)}} zł</small></td><td>{{'%.4f'|format(s.retail_net_price)}} zł/l<br><small>{{'%.2f'|format(s.retail_net_total)}} zł</small></td><td>{% if s.uses_last_price %}<span class="badge text-bg-info">Last Price</span><br>{{'%.4f'|format(s.settlement_net)}} zł/l{% else %}{{'%.4f'|format(s.settlement_net)}} zł/l{% endif %}<br><small>{{'%.2f'|format(s.settlement_net_total)}} zł</small></td><td>{% if s.uses_last_price %}<strong>{{'%.2f'|format(s.saving_net)}} zł</strong><br><small>{{'%.4f'|format(s.saving_net_per_liter)}} zł/l</small>{% else %}—{% endif %}</td><td>{{'%.4f'|format(s.invoice_price)}} zł/l<br><strong>{{'%.2f'|format(s.invoice_gross)}} zł</strong><br><small>VAT {{'%.2f'|format(s.vat_rate)}}%</small></td><td>{{'%.4f'|format(s.effective_price)}} zł/l<br><strong>{{'%.2f'|format(s.effective_cost)}} zł</strong><br><small>odliczenie VAT {{'%.0f'|format(s.vat_deduction_percent)}}%</small></td></tr>{% else %}<tr><td colspan="10">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>Litry</th><th>Detal brutto</th><th>Detal netto</th><th>Last Price netto</th><th>Oszczędność netto</th><th>Faktura brutto</th><th>Koszt po VAT</th></tr></thead><tbody>{% for e in entries %}{% set s=settlements[e.id] %}<tr class="{% if e.vat_action_applied %}vat-action-row{% endif %}"{% if e.vat_action_applied %} title="Akcja: {{ e.vat_action_name }} · VAT {{ '%.2f'|format(e.vat_action_rate|float) }}%{% if e.vat_action_start or e.vat_action_end %} · {{ e.vat_action_start or 'bez daty początkowej' }}{{ e.vat_action_end or 'bez daty końcowej' }}{% endif %}"{% endif %}><td>{{e.fueled_at.strftime('%d.%m.%Y')}}{% if e.vat_action_applied %}<br><span class="badge vat-action-badge">{{e.vat_action_name or 'Akcja rządowa'}}</span>{% endif %}</td><td>{{e.vehicle.name}}</td><td>{{e.station or '—'}}</td><td>{{e.liters}}</td><td>{{'%.4f'|format(s.retail_price)}} zł/l<br><small>{{'%.2f'|format(s.retail_gross)}} zł</small></td><td>{{'%.4f'|format(s.retail_net_price)}} zł/l<br><small>{{'%.2f'|format(s.retail_net_total)}} zł</small></td><td>{% if s.uses_last_price %}<span class="badge text-bg-info">Last Price</span><br>{{'%.4f'|format(s.settlement_net)}} zł/l{% else %}{{'%.4f'|format(s.settlement_net)}} zł/l{% endif %}<br><small>{{'%.2f'|format(s.settlement_net_total)}} zł</small></td><td>{% if s.uses_last_price %}<strong>{{'%.2f'|format(s.saving_net)}} zł</strong><br><small>{{'%.4f'|format(s.saving_net_per_liter)}} zł/l</small>{% else %}—{% endif %}</td><td>{{'%.4f'|format(s.invoice_price)}} zł/l<br><strong>{{'%.2f'|format(s.invoice_gross)}} zł</strong><br><small>VAT {{'%.2f'|format(s.vat_rate)}}%</small></td><td>{{'%.4f'|format(s.effective_price)}} zł/l<br><strong>{{'%.2f'|format(s.effective_cost)}} zł</strong><br><small>odliczenie VAT {{'%.0f'|format(s.vat_deduction_percent)}}%</small></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 %} {% endblock %}{% block scripts %}<script>FuelTrack.renderBarChart('cost-chart', {{by_vehicle|tojson}});</script>{% endblock %}
+45
View File
@@ -55,6 +55,51 @@ def effective_vat_rate(nominal_rate, on_date=None):
return override["rate"] return override["rate"]
def vat_override_for_date(nominal_rate, on_date=None):
"""Return a complete, immutable VAT-action snapshot for a date."""
override = global_vat_override()
if isinstance(on_date, datetime):
on_date = on_date.date()
on_date = on_date or date.today()
applies = bool(
override["enabled"]
and override["rate"] is not None
and (not override["start"] or on_date >= override["start"])
and (not override["end"] or on_date <= override["end"])
)
return {
"applied": applies,
"name": override["name"] if applies else None,
"rate": override["rate"] if applies else Decimal(str(nominal_rate)),
"start": override["start"] if applies else None,
"end": override["end"] if applies else None,
}
def freeze_vat_action(entry, nominal_rate, previous_fueled_at=None):
"""Attach VAT action conditions to an entry without rewriting valid history."""
fueled_date = entry.fueled_at.date() if isinstance(entry.fueled_at, datetime) else entry.fueled_at
previous_date = previous_fueled_at.date() if isinstance(previous_fueled_at, datetime) else previous_fueled_at
if entry.vat_action_frozen and (previous_date is None or previous_date == fueled_date):
return
snapshot = vat_override_for_date(nominal_rate, fueled_date)
entry.vat_action_frozen = True
entry.vat_action_applied = snapshot["applied"]
entry.vat_action_name = snapshot["name"]
entry.vat_action_rate = snapshot["rate"] if snapshot["applied"] else None
entry.vat_action_start = snapshot["start"]
entry.vat_action_end = snapshot["end"]
def entry_vat_rate(entry, nominal_rate):
if entry.vat_action_frozen:
if entry.vat_action_applied and entry.vat_action_rate is not None:
return Decimal(str(entry.vat_action_rate))
return Decimal(str(nominal_rate))
return effective_vat_rate(nominal_rate, entry.fueled_at)
def save_global_vat_override(form): def save_global_vat_override(form):
enabled = "enabled" in form enabled = "enabled" in form
name = (form.get("name") or "Projekt rządowy").strip() name = (form.get("name") or "Projekt rządowy").strip()