edycja tankowań
This commit is contained in:
+134
-1
@@ -4,7 +4,7 @@ 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_
|
||||
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
|
||||
@@ -43,6 +43,63 @@ def roles(*allowed):
|
||||
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()
|
||||
@@ -374,6 +431,82 @@ def orlen_sync():
|
||||
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/<int:entry_id>")
|
||||
@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/<int:entry_id>")
|
||||
@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")
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
(()=>{const F=FuelTrack;F.bindFuelEntryModal=()=>{const form=F.qs('#fuel-entry-edit-form'),del=F.qs('#fuel-entry-delete-btn');if(!form)return;F.qsa('.fuel-entry-edit-btn').forEach(btn=>{if(btn.dataset.bound)return;btn.dataset.bound='1';btn.addEventListener('click',()=>{const d=JSON.parse(btn.dataset.entry);form.action=`/api/fuel-entries/${d.id}`;form.dataset.entryId=d.id;['vehicle_id','fueled_at','fuel_type','liters','price_per_liter','odometer','station_company_id','station','invoice_number','fuel_card_id','wholesale_price','wholesale_source'].forEach(k=>{if(form.elements[k])form.elements[k].value=d[k]??''});form.elements.used_fuel_card.checked=Boolean(d.used_fuel_card)})});if(del&&!del.dataset.bound){del.dataset.bound='1';del.addEventListener('click',async()=>{const id=form.dataset.entryId;if(!id||!confirm('Usunąć to tankowanie? Tej operacji nie można cofnąć.'))return;const old=del.innerHTML;del.disabled=true;del.innerHTML='Usuwanie…';try{const r=await fetch(`/api/fuel-entries/${id}`,{method:'DELETE',headers:{'X-Requested-With':'XMLHttpRequest','Accept':'application/json'}}),d=await r.json();if(!r.ok||!d.ok)throw new Error(d.error||d.message||'Nie udało się usunąć tankowania.');F.notify(d.message);bootstrap.Modal.getInstance(form.closest('.modal'))?.hide();await F.replaceMain(location.href)}catch(e){F.notify(e.message,'danger')}finally{del.disabled=false;del.innerHTML=old}})}}})();
|
||||
@@ -1 +1 @@
|
||||
FuelTrack.initPage=()=>{FuelTrack.bindAjaxForms();FuelTrack.bindFuelPreview();FuelTrack.bindOrlenSync();FuelTrack.bindUserModal();FuelTrack.bindAuth();FuelTrack.bindStationLiveSearch();FuelTrack.bindStationFavoriteToggles();FuelTrack.bindStationInlineCompany();FuelTrack.bindLpgRegionPicker();FuelTrack.bindFavoriteStations();FuelTrack.bindStationProposalSelect();FuelTrack.bindStationPoints()};window.addEventListener('popstate',()=>FuelTrack.replaceMain(location.href));document.addEventListener('DOMContentLoaded',()=>FuelTrack.initPage());
|
||||
FuelTrack.initPage=()=>{FuelTrack.bindAjaxForms();FuelTrack.bindFuelPreview();FuelTrack.bindOrlenSync();FuelTrack.bindUserModal();FuelTrack.bindFuelEntryModal();FuelTrack.bindAuth();FuelTrack.bindStationLiveSearch();FuelTrack.bindStationFavoriteToggles();FuelTrack.bindStationInlineCompany();FuelTrack.bindLpgRegionPicker();FuelTrack.bindFavoriteStations();FuelTrack.bindStationProposalSelect();FuelTrack.bindStationPoints()};window.addEventListener('popstate',()=>FuelTrack.replaceMain(location.href));document.addEventListener('DOMContentLoaded',()=>FuelTrack.initPage());
|
||||
|
||||
@@ -2,4 +2,5 @@
|
||||
<li class="nav-item"><a class="nav-link {% if request.endpoint=='main.admin_companies' %}active{% endif %}" href="{{url_for('main.admin_companies')}}">Firmy i karty</a></li>
|
||||
{% if current_user.role=='admin' %}<li class="nav-item"><a class="nav-link {% if request.endpoint=='main.admin_application' %}active{% endif %}" href="{{url_for('main.admin_application')}}">Aplikacja</a></li>{% endif %}
|
||||
<li class="nav-item"><a class="nav-link {% if request.endpoint=='main.admin_users' %}active{% endif %}" href="{{url_for('main.admin_users')}}">Użytkownicy</a></li>
|
||||
<li class="nav-item"><a class="nav-link {% if request.endpoint=='main.admin_fuel_entries' %}active{% endif %}" href="{{url_for('main.admin_fuel_entries')}}">Tankowania</a></li>
|
||||
</ul>
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
{% extends 'base.html' %}{% block content %}
|
||||
<div class="mb-4"><h1 class="h2 mb-1">Administracja</h1><p class="text-body-secondary mb-0">Korekta i usuwanie zapisanych tankowań.</p></div>
|
||||
{% include '_admin_nav.html' %}
|
||||
<div class="card"><div class="card-body">
|
||||
<form method="get" class="row g-2 mb-4 ajax-nav-form" action="{{url_for('main.admin_fuel_entries')}}">
|
||||
{% if current_user.role=='admin' %}<div class="col-lg-3"><label class="form-label">Firma</label><select class="form-select" name="company_id"><option value="">Wszystkie firmy</option>{% for c in companies %}<option value="{{c.id}}" {% if company_id==c.id %}selected{% endif %}>{{c.name}}</option>{% endfor %}</select></div>{% endif %}
|
||||
<div class="col-lg-3"><label class="form-label">Miesiąc</label><input class="form-control" type="month" name="month" value="{{month}}"></div>
|
||||
<div class="col-lg-4"><label class="form-label">Szukaj</label><input class="form-control" name="q" value="{{q}}" placeholder="Auto, rejestracja, stacja, faktura, użytkownik"></div>
|
||||
<div class="col-lg-2 d-flex align-items-end"><button class="btn btn-primary w-100">Filtruj</button></div>
|
||||
</form>
|
||||
<div class="table-responsive"><table class="table table-hover align-middle"><thead><tr><th>Data</th><th>Pojazd</th><th>Użytkownik</th><th>Stacja</th><th>Litry</th><th>Cena</th><th>Licznik</th><th class="text-end">Akcja</th></tr></thead><tbody>
|
||||
{% for e in entries_page.items %}<tr><td>{{e.fueled_at.strftime('%d.%m.%Y %H:%M')}}</td><td><strong>{{e.vehicle.name}}</strong><div class="small text-body-secondary">{{e.vehicle.registration}}</div></td><td>{{e.user.name}}</td><td>{{e.station or '—'}}</td><td>{{e.liters}}</td><td>{{e.price_per_liter}} zł/l</td><td>{{e.odometer}} km</td><td class="text-end"><button type="button" class="btn btn-sm btn-primary fuel-entry-edit-btn" data-bs-toggle="modal" data-bs-target="#fuelEntryEditModal" data-entry='{{ {"id":e.id,"vehicle_id":e.vehicle_id,"fueled_at":e.fueled_at.strftime("%Y-%m-%dT%H:%M"),"fuel_type":e.fuel_type,"liters":e.liters|string,"price_per_liter":e.price_per_liter|string,"odometer":e.odometer,"station_company_id":e.station_company_id or "","station":e.station or "","invoice_number":e.invoice_number or "","used_fuel_card":e.used_fuel_card,"fuel_card_id":e.fuel_card_id or "","wholesale_price":e.wholesale_price|string if e.wholesale_price is not none else "","wholesale_source":e.wholesale_source or ""}|tojson|forceescape }}'>Edytuj</button></td></tr>
|
||||
{% else %}<tr><td colspan="8" class="text-center text-body-secondary py-4">Brak tankowań spełniających kryteria.</td></tr>{% endfor %}</tbody></table></div>
|
||||
{% if entries_page.pages > 1 %}<nav><ul class="pagination mb-0">{% for p in entries_page.iter_pages() %}{% if p %}<li class="page-item {% if p==entries_page.page %}active{% endif %}"><a class="page-link ajax-nav-link" href="{{url_for('main.admin_fuel_entries',page=p,q=q,month=month,company_id=company_id)}}">{{p}}</a></li>{% else %}<li class="page-item disabled"><span class="page-link">…</span></li>{% endif %}{% endfor %}</ul></nav>{% endif %}
|
||||
</div></div>
|
||||
<div class="modal fade" id="fuelEntryEditModal" tabindex="-1"><div class="modal-dialog modal-lg modal-dialog-scrollable"><div class="modal-content"><form id="fuel-entry-edit-form" class="ajax-form" data-method="PUT"><div class="modal-header"><h2 class="modal-title fs-5">Edytuj tankowanie</h2><button type="button" class="btn-close" data-bs-dismiss="modal"></button></div><div class="modal-body"><div class="row g-3">
|
||||
<div class="col-md-6"><label class="form-label">Pojazd</label><select class="form-select" name="vehicle_id" required>{% for v in vehicles %}<option value="{{v.id}}">{{v.name}} · {{v.registration}}</option>{% endfor %}</select></div>
|
||||
<div class="col-md-6"><label class="form-label">Data i godzina</label><input class="form-control" type="datetime-local" name="fueled_at" required></div>
|
||||
<div class="col-md-4"><label class="form-label">Paliwo</label><select class="form-select" name="fuel_type">{% for f in ['PB95','PB98','DIESEL','LPG'] %}<option>{{f}}</option>{% endfor %}</select></div>
|
||||
<div class="col-md-4"><label class="form-label">Litry</label><input class="form-control" type="number" step="0.001" min="0.001" name="liters" required></div>
|
||||
<div class="col-md-4"><label class="form-label">Cena za litr</label><input class="form-control" type="number" step="0.0001" min="0" name="price_per_liter" required></div>
|
||||
<div class="col-md-4"><label class="form-label">Stan licznika</label><input class="form-control" type="number" min="0" name="odometer" required></div>
|
||||
<div class="col-md-8"><label class="form-label">Sieć stacji</label><select class="form-select" name="station_company_id"><option value="">Inna / własna nazwa</option>{% for s in stations %}<option value="{{s.id}}">{{s.display_name}}{% if s.company_name != s.display_name %} — {{s.company_name}}{% endif %}</option>{% endfor %}</select></div>
|
||||
<div class="col-md-6"><label class="form-label">Nazwa stacji</label><input class="form-control" name="station"></div><div class="col-md-6"><label class="form-label">Numer faktury</label><input class="form-control" name="invoice_number"></div>
|
||||
<div class="col-md-6"><label class="form-label">Karta paliwowa</label><select class="form-select" name="fuel_card_id"><option value="">Brak</option>{% for c in fuel_cards %}<option value="{{c.id}}">{{c.name}} · {{c.provider}}</option>{% endfor %}</select><div class="form-check mt-2"><input class="form-check-input" type="checkbox" name="used_fuel_card" id="edit-used-card"><label class="form-check-label" for="edit-used-card">Użyto karty paliwowej</label></div></div>
|
||||
<div class="col-md-3"><label class="form-label">Cena hurtowa</label><input class="form-control" type="number" step="0.0001" min="0" name="wholesale_price"></div><div class="col-md-3"><label class="form-label">Źródło ceny</label><input class="form-control" name="wholesale_source"></div>
|
||||
</div></div><div class="modal-footer"><button type="button" class="btn btn-danger me-auto" id="fuel-entry-delete-btn">Usuń tankowanie</button><button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Anuluj</button><button class="btn btn-primary">Zapisz zmiany</button></div></form></div></div></div>
|
||||
{% endblock %}
|
||||
@@ -55,6 +55,7 @@
|
||||
<script src="{{ static_url('js/fuel.js') }}"></script>
|
||||
<script src="{{ static_url('js/auth.js') }}"></script>
|
||||
<script src="{{ static_url('js/users.js') }}"></script>
|
||||
<script src="{{ static_url('js/fuel_entries.js') }}"></script>
|
||||
<script src="{{ static_url('js/init.js') }}"></script>
|
||||
{% block scripts %}{% endblock %}
|
||||
</body></html>
|
||||
|
||||
Reference in New Issue
Block a user