zapamietanie akcji

This commit is contained in:
Mateusz Gruszczyński
2026-07-19 19:15:21 +02:00
parent 659f04f05f
commit 697ac6da30
11 changed files with 221 additions and 86 deletions
+68 -79
View File
@@ -12,7 +12,7 @@ from .services import calculate_costs, fetch_orlen_price, fetch_orlen_range, inv
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 .domain.costs import calculate_net_price_costs, calculate_net_price_comparison
from .settlement import calculate as calculate_snapshot_settlement, freeze as freeze_settlement
main = Blueprint("main", __name__)
FUEL_TYPES = ("PB95", "PB98", "DIESEL", "LPG")
@@ -101,87 +101,75 @@ def parse_fuel_entry_form(entry):
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()
freeze_vat_action(entry, settings.vat_rate, previous_fueled_at)
# 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 0100%")
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 0100%")
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):
"""Calculate retail, settlement and company costs for one fuel entry.
Commercial saving is always calculated net-to-net: pump gross price is first
converted to net and then compared with Last Price net. VAT is reported
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()
vat_rate = entry_vat_rate(entry, entry_settings.vat_rate)
vat_deduction_percent = entry_settings.vat_deduction_percent
retail_price = Decimal(entry.price_per_liter)
rule = FuelCardStationRule.query.filter_by(
fuel_card_id=entry.fuel_card_id,
station_company_id=entry.station_company_id,
).first() if entry.used_fuel_card and entry.fuel_card_id and entry.station_company_id else None
policy = entry.vehicle.fuel_card_policy if entry.used_fuel_card else None
uses_last_price = bool(
entry.wholesale_price is not None and (
(rule and rule.use_orlen_last_price) or (not rule and policy and policy.use_orlen_last_price)
)
)
if uses_last_price:
settlement_net = Decimal(entry.wholesale_price)
else:
settlement_net = retail_price / (Decimal("1") + Decimal(str(vat_rate)) / Decimal("100"))
# A station-specific card rule has priority. The vehicle policy is a safe
# fallback for cards/stations without a dedicated rule.
if rule:
discount_percent = Decimal(rule.discount_net_percent or 0)
surcharge_per_liter = Decimal(rule.surcharge_net_per_liter or 0)
elif policy:
discount_percent = Decimal(policy.discount_percent or 0)
surcharge_per_liter = Decimal(policy.surcharge_per_liter or 0)
else:
discount_percent = Decimal("0")
surcharge_per_liter = Decimal("0")
discount_percent = min(max(discount_percent, Decimal("0")), Decimal("100"))
settlement_net *= Decimal("1") - discount_percent / Decimal("100")
settlement_net += surcharge_per_liter
settlement_net = max(settlement_net, Decimal("0"))
price_costs = calculate_net_price_costs(settlement_net, vat_rate, vat_deduction_percent)
liters = Decimal(entry.liters)
comparison = calculate_net_price_comparison(retail_price, settlement_net, liters, vat_rate)
retail_gross = retail_price * liters
invoice_gross = price_costs["invoice_gross"] * liters
deductible_vat = price_costs["deductible_vat"] * liters
effective_cost = price_costs["effective_cost"] * liters
saving_net = comparison["saving_net_total"] if uses_last_price else Decimal("0")
saving_net_per_liter = comparison["saving_net_per_liter"] if uses_last_price else Decimal("0")
gross_saving = max(retail_gross - invoice_gross, Decimal("0"))
gross_saving_per_liter = max(retail_price - price_costs["invoice_gross"], Decimal("0"))
return {
"liters": liters,
"retail_price": retail_price,
"retail_net_price": comparison["retail_net_price"],
"settlement_net": settlement_net,
"invoice_price": price_costs["invoice_gross"],
"effective_price": price_costs["effective_cost"],
"retail_gross": retail_gross,
"retail_net_total": comparison["retail_net_total"],
"settlement_net_total": comparison["settlement_net_total"],
"invoice_gross": invoice_gross,
"deductible_vat": deductible_vat,
"effective_cost": effective_cost,
"saving_net": saving_net,
"saving_net_per_liter": saving_net_per_liter,
"gross_saving": gross_saving,
"gross_saving_per_liter": gross_saving_per_liter,
"discount_percent": discount_percent,
"surcharge_per_liter": surcharge_per_liter,
"uses_last_price": uses_last_price,
"vat_rate": Decimal(str(vat_rate)),
"vat_deduction_percent": Decimal(str(vat_deduction_percent)),
}
# 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":
@@ -320,8 +308,9 @@ def fuel():
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)
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)