From 6f46508da47f091c8dc0bf4d21637189875015ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Gruszczy=C5=84ski?= Date: Tue, 14 Jul 2026 13:43:27 +0200 Subject: [PATCH] fix w kalkulacji last price --- app/domain/costs.py | 22 ++++++++++++++++ app/main.py | 50 ++++++++++++++++++++++++++---------- app/templates/dashboard.html | 4 +-- tests/test_app.py | 13 ++++++++++ 4 files changed, 73 insertions(+), 16 deletions(-) diff --git a/app/domain/costs.py b/app/domain/costs.py index 93c2b43..554d10b 100644 --- a/app/domain/costs.py +++ b/app/domain/costs.py @@ -12,3 +12,25 @@ def calculate_costs(gross, vat_rate=23, vat_deduction_percent=50): deductible_vat = vat * deduction return {"gross": money(gross), "net": money(net), "vat": money(vat), "deductible_vat": money(deductible_vat), "final_cost": money(gross - deductible_vat)} + + +def calculate_net_price_costs(net_price, vat_rate=23, vat_deduction_percent=50): + """Return per-unit invoice and effective costs for a net fuel price. + + A 50% VAT deduction means the company bears half of the VAT, not a 50% VAT rate. + """ + net = Decimal(str(net_price)) + rate = Decimal(str(vat_rate)) / Decimal("100") + deduction = Decimal(str(vat_deduction_percent)) / Decimal("100") + deduction = min(max(deduction, Decimal("0")), Decimal("1")) + vat = net * rate + invoice_gross = net + vat + deductible_vat = vat * deduction + effective_cost = invoice_gross - deductible_vat + return { + "net": net, + "vat": vat, + "invoice_gross": invoice_gross, + "deductible_vat": deductible_vat, + "effective_cost": effective_cost, + } diff --git a/app/main.py b/app/main.py index d669cce..ad8726a 100644 --- a/app/main.py +++ b/app/main.py @@ -120,29 +120,51 @@ 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: - 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 + 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) - payable_price = normal_price 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: - payable_price = float(e.wholesale_price) * (1 + float(vat_rate) / 100) + settlement_net = Decimal(e.wholesale_price) + else: + settlement_net = Decimal(str(normal_price)) / (Decimal("1") + Decimal(str(vat_rate)) / Decimal("100")) if rule: - 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(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, "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) + 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(c["gross"]); invoices[key]["payable"] += payable_gross; invoices[key]["count"] += 1 + 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"]) diff --git a/app/templates/dashboard.html b/app/templates/dashboard.html index 07811c5..fd0066b 100644 --- a/app/templates/dashboard.html +++ b/app/templates/dashboard.html @@ -2,6 +2,6 @@

Podsumowanie kosztów

{{ settings.name }} · VAT do odliczenia {{ settings.vat_deduction_percent }}%
{% if vat_override.enabled %}
{{ vat_override.name }}: 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.
{% endif %}
Brutto{{ '%.2f'|format(totals.gross) }} zł
VAT odliczony{{ '%.2f'|format(totals.deductible_vat) }} zł
Koszt po VAT{{ '%.2f'|format(totals.final_cost) }} zł
Litry{{ '%.2f'|format(totals.liters) }} l
-

Koszt według pojazdu

Cena na dystrybutorze a kwota do zapłaty według cennika karty.

Brak danych dla wybranego miesiąca.

Okresy faktur

{% if split_enabled %}Podział według dnia {{settings.invoice_split_day}}{% else %}Podział dzienny wyłączony{% endif %}{% for key,row in invoices.items() %}{% else %}{% endfor %}
OkresPozycjeDetalDo zapłaty
{{key}}{{row.count}}{{'%.2f'|format(row.gross)}} zł{{'%.2f'|format(row.payable)}} zł
Brak danych
-

Tankowania

{% for e in entries %}{% set s=settlements[e.id] %}{% else %}{% endfor %}
DataAutoStacjaPaliwoLitryCena detal.Orlen lastDo zapłatyVATRazem
{{e.fueled_at.strftime('%d.%m.%Y')}}{{e.vehicle.name}}{{e.station or '—'}}{{e.fuel_type}}{{e.liters}}{{'%.4f'|format(s.normal_price)}} zł/l{% if e.wholesale_price %}{{'%.4f'|format(e.wholesale_price|float)}} zł/l{% else %}—{% endif %}{% if s.uses_last_price %}last price {% endif %}{{'%.4f'|format(s.payable_price)}} zł/l{{'%.2f'|format(s.vat_rate)}}%{{'%.2f'|format(s.normal_gross)}} zł
{{'%.2f'|format(s.payable_gross)}} zł
Brak tankowań w tym miesiącu.
+

Koszt według pojazdu

Cena fakturowa a rzeczywisty koszt po odliczeniu VAT.

Brak danych dla wybranego miesiąca.

Okresy faktur

{% if split_enabled %}Podział według dnia {{settings.invoice_split_day}}{% else %}Podział dzienny wyłączony{% endif %}{% for key,row in invoices.items() %}{% else %}{% endfor %}
OkresPozycjeDetalKoszt po VAT
{{key}}{{row.count}}{{'%.2f'|format(row.gross)}} zł{{'%.2f'|format(row.payable)}} zł
Brak danych
+

Tankowania

{% for e in entries %}{% set s=settlements[e.id] %}{% else %}{% endfor %}
DataAutoStacjaPaliwoLitryCena detal.Last price nettoKoszt po VATVATRazem
{{e.fueled_at.strftime('%d.%m.%Y')}}{{e.vehicle.name}}{{e.station or '—'}}{{e.fuel_type}}{{e.liters}}{{'%.4f'|format(s.normal_price)}} zł/l{% if s.uses_last_price %}{{'%.4f'|format(s.net_price)}} zł/l{% elif e.wholesale_price %}{{'%.4f'|format(e.wholesale_price|float)}} zł/l{% else %}—{% endif %}{% if s.uses_last_price %}last price {% endif %}{{'%.4f'|format(s.payable_price)}} zł/l
faktura {{'%.4f'|format(s.invoice_price)}} zł/l
{{'%.2f'|format(s.vat_rate)}}%
odliczenie {{'%.0f'|format(s.vat_deduction_percent)}}%
{{'%.2f'|format(s.normal_gross)}} zł
{{'%.2f'|format(s.payable_gross)}} zł
faktura {{'%.2f'|format(s.invoice_gross)}} zł
Brak tankowań w tym miesiącu.
{% endblock %}{% block scripts %}{% endblock %} diff --git a/tests/test_app.py b/tests/test_app.py index db1d8f3..71aef01 100644 --- a/tests/test_app.py +++ b/tests/test_app.py @@ -349,3 +349,16 @@ def test_orlen_comparison_filters_to_one_fuel(tmp_path): assert '4.5000' in html assert '4.1000' in html assert '>DIESEL' not in html + + +def test_last_price_uses_only_non_deductible_vat(): + from app.domain.costs import calculate_net_price_costs + result = calculate_net_price_costs(5.484, 23, 50) + assert round(float(result["invoice_gross"]), 4) == 6.7453 + assert round(float(result["effective_cost"]), 4) == 6.1147 + + +def test_last_price_zero_and_full_vat_deduction(): + from app.domain.costs import calculate_net_price_costs + assert round(float(calculate_net_price_costs(10, 23, 0)["effective_cost"]), 2) == 12.30 + assert round(float(calculate_net_price_costs(10, 23, 100)["effective_cost"]), 2) == 10.00