uprawnienia do stacji i usuwanie firm

This commit is contained in:
Mateusz Gruszczyński
2026-07-13 15:55:37 +02:00
parent 31f10fc8d9
commit 6cfbfa658c
7 changed files with 61 additions and 14 deletions
+18 -2
View File
@@ -148,8 +148,11 @@ def create_fuel_entry():
try: try:
odo=int(d['odometer']); fueled_at=datetime.fromisoformat(d['fueled_at']); station_company=db.session.get(FuelStationCompany,int(d['station_company_id'])) if d.get('station_company_id') else None odo=int(d['odometer']); fueled_at=datetime.fromisoformat(d['fueled_at']); station_company=db.session.get(FuelStationCompany,int(d['station_company_id'])) if d.get('station_company_id') else None
settings=vehicle.company or current_user.company or CompanySettings.query.first() settings=vehicle.company or current_user.company or CompanySettings.query.first()
if station_company and settings.allowed_stations and station_company not in settings.allowed_stations: access_mode=settings.station_access_mode or 'all_prefer_favorites'
if station_company and access_mode=='allowed_only' and station_company not in settings.allowed_stations:
return fail('Ta stacja nie jest dozwolona przez politykę firmy',403) return fail('Ta stacja nie jest dozwolona przez politykę firmy',403)
if station_company and access_mode=='favorites_allowed_only' and station_company not in settings.allowed_stations and station_company not in settings.favorite_stations:
return fail('Ta stacja nie jest ulubiona ani dozwolona przez politykę firmy',403)
wholesale=source=None; region=(station_company.selected_region if station_company else None) or settings.region wholesale=source=None; region=(station_company.selected_region if station_company else None) or settings.region
try: try:
p=fetch_orlen_price(d['fuel_type'],fueled_at.date(),region);wholesale=p['price_per_liter'];source=p['source'] p=fetch_orlen_price(d['fuel_type'],fueled_at.date(),region);wholesale=p['price_per_liter'];source=p['source']
@@ -391,7 +394,7 @@ def companies_list():
def companies_create(): def companies_create():
d=payload() d=payload()
try: try:
c=CompanySettings(name=(d.get('name') or '').strip(),entity_type=d.get('entity_type','JDG'),vat_rate=Decimal(d.get('vat_rate') or 23),vat_deduction_percent=Decimal(d.get('vat_deduction_percent') or 50),region=(d.get('region') or 'mazowieckie').lower(),invoice_split_day=int(d.get('invoice_split_day') or 15),active=True) c=CompanySettings(name=(d.get('name') or '').strip(),entity_type=d.get('entity_type','JDG'),vat_rate=Decimal(d.get('vat_rate') or 23),vat_deduction_percent=Decimal(d.get('vat_deduction_percent') or 50),region=(d.get('region') or 'mazowieckie').lower(),invoice_split_day=int(d.get('invoice_split_day') or 15),active=True,station_access_mode='all_prefer_favorites')
if not c.name: raise ValueError('Nazwa firmy jest wymagana') if not c.name: raise ValueError('Nazwa firmy jest wymagana')
db.session.add(c);db.session.commit();return response({'id':c.id,'redirect':url_for('main.admin_companies',company_id=c.id)},'Dodano firmę',201) db.session.add(c);db.session.commit();return response({'id':c.id,'redirect':url_for('main.admin_companies',company_id=c.id)},'Dodano firmę',201)
except Exception as exc:db.session.rollback();return fail(str(exc)) except Exception as exc:db.session.rollback();return fail(str(exc))
@@ -445,6 +448,19 @@ def company_favorites(company_id):
c.favorite_stations=FuelStationCompany.query.filter(FuelStationCompany.id.in_(ids),FuelStationCompany.active.is_(True)).all() if ids else [] c.favorite_stations=FuelStationCompany.query.filter(FuelStationCompany.id.in_(ids),FuelStationCompany.active.is_(True)).all() if ids else []
db.session.commit();socketio.emit('app_changed',{'resource':'company_favorites','company_id':c.id});return response(message='Zapisano ulubione stacje firmy') db.session.commit();socketio.emit('app_changed',{'resource':'company_favorites','company_id':c.id});return response(message='Zapisano ulubione stacje firmy')
@api.put('/companies/<int:company_id>/station-access')
@role_required('boss','admin')
def company_station_access(company_id):
company=CompanySettings.query.get_or_404(company_id)
if current_user.role=='boss' and current_user.company_id!=company.id:return fail('Brak uprawnień',403)
d=payload();mode=(d.get('station_access_mode') or '').strip()
modes={'all_prefer_favorites','all','allowed_only','favorites_allowed_only'}
if mode not in modes:return fail('Nieprawidłowy tryb dostępu do stacji')
company.station_access_mode=mode
db.session.commit();socketio.emit('app_changed',{'resource':'station_access','company_id':company.id})
return response({'station_access_mode':mode},'Zapisano politykę dostępu do stacji')
@api.put('/companies/<int:company_id>/allowed-stations') @api.put('/companies/<int:company_id>/allowed-stations')
@role_required('boss','admin') @role_required('boss','admin')
def company_allowed_stations(company_id): def company_allowed_stations(company_id):
+13 -1
View File
@@ -14,7 +14,19 @@ class Migration:
upgrade: Callable[[], None] upgrade: Callable[[], None]
MIGRATIONS: tuple[Migration, ...] = () def add_station_access_mode() -> None:
inspector = inspect(db.engine)
columns = {column["name"] for column in inspector.get_columns("company_settings")}
if "station_access_mode" not in columns:
db.session.execute(text(
"ALTER TABLE company_settings ADD COLUMN station_access_mode VARCHAR(40) "
"NOT NULL DEFAULT 'all_prefer_favorites'"
))
MIGRATIONS: tuple[Migration, ...] = (
Migration("20260713_company_station_access_mode", add_station_access_mode),
)
def ensure_migration_table() -> None: def ensure_migration_table() -> None:
inspector = inspect(db.engine) inspector = inspect(db.engine)
+26 -8
View File
@@ -156,7 +156,13 @@ def fuel():
fueled_at = datetime.fromisoformat(request.form["fueled_at"]); wholesale = None; source = None fueled_at = datetime.fromisoformat(request.form["fueled_at"]); wholesale = None; source = None
station_company_id = request.form.get("station_company_id", type=int) 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 station_company = db.session.get(FuelStationCompany, station_company_id) if station_company_id else None
price_region = station_company.selected_region if station_company and station_company.selected_region else CompanySettings.query.first().region settings = vehicle.company or current_user.company or CompanySettings.query.first()
access_mode = settings.station_access_mode or "all_prefer_favorites"
if station_company and access_mode == "allowed_only" and station_company not in settings.allowed_stations:
response = error_response("Ta stacja nie jest dozwolona przez politykę firmy", 403); return response or redirect(url_for("main.fuel"))
if station_company and access_mode == "favorites_allowed_only" and station_company not in settings.allowed_stations and station_company not in settings.favorite_stations:
response = error_response("Ta stacja nie jest ulubiona ani dozwolona przez politykę firmy", 403); return response or redirect(url_for("main.fuel"))
price_region = station_company.selected_region if station_company and station_company.selected_region else settings.region
try: try:
p = fetch_orlen_price(request.form["fuel_type"], fueled_at.date(), price_region); wholesale=p["price_per_liter"]; source=p["source"] p = fetch_orlen_price(request.form["fuel_type"], fueled_at.date(), price_region); wholesale=p["price_per_liter"]; source=p["source"]
except Exception: pass except Exception: pass
@@ -165,20 +171,32 @@ def fuel():
vehicle.current_odometer = odometer; db.session.add(e); db.session.commit(); socketio.emit("fuel_added", {"vehicle": vehicle.name, "gross": round(e.gross,2)}); 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(); socketio.emit("fuel_added", {"vehicle": vehicle.name, "gross": round(e.gross,2)}); response = ok_response("Zapisano tankowanie", redirect=url_for("main.dashboard")); return response or redirect(url_for("main.dashboard"))
settings=current_user.company or CompanySettings.query.first() settings=current_user.company or CompanySettings.query.first()
allowed=list(settings.allowed_stations) allowed=list(settings.allowed_stations)
company_favorites=list(settings.favorite_stations)
access_mode=settings.station_access_mode or "all_prefer_favorites"
base_query=FuelStationCompany.query.filter_by(active=True) base_query=FuelStationCompany.query.filter_by(active=True)
stations=(base_query.filter(FuelStationCompany.id.in_([x.id for x in allowed])) if allowed else base_query).order_by(FuelStationCompany.station_count.desc(), FuelStationCompany.brand_name.asc()).all() if access_mode == "allowed_only":
favorites=[x for x in current_user.favorite_stations if x in stations] allowed_ids=[x.id for x in allowed]
stations=base_query.filter(FuelStationCompany.id.in_(allowed_ids)).all() if allowed_ids else []
elif access_mode == "favorites_allowed_only":
scope_ids={x.id for x in allowed}|{x.id for x in company_favorites}
stations=base_query.filter(FuelStationCompany.id.in_(scope_ids)).all() if scope_ids else []
else:
stations=base_query.all()
stations=sorted(stations,key=lambda x:(-x.station_count,x.display_name.lower()))
user_favorites=[x for x in current_user.favorite_stations if x in stations]
user_favorite_ids={x.id for x in current_user.favorite_stations}
favorites=list(user_favorites)
favorite_scope="user" favorite_scope="user"
if not favorites and settings.favorite_stations: if not favorites and access_mode in ("all_prefer_favorites", "favorites_allowed_only") and company_favorites:
favorites=[x for x in settings.favorite_stations if x in stations];favorite_scope="company" favorites=[x for x in company_favorites if x in stations];favorite_scope="company"
if not favorites: favorites=sorted(stations,key=lambda x:(-x.station_count,x.company_name.lower()))[:10];favorite_scope="default" if not favorites and access_mode == "all_prefer_favorites": favorites=stations[:10];favorite_scope="default"
favorites=sorted(favorites,key=lambda x:(-x.station_count,x.display_name.lower())) favorites=sorted(favorites,key=lambda x:(-x.station_count,x.display_name.lower()))
favorite_ids={x.id for x in favorites} favorite_ids={x.id for x in favorites}
top_stations=sorted((x for x in stations if x.id not in favorite_ids),key=lambda x:(-x.station_count,x.display_name.lower()))[:10] top_stations=sorted((x for x in stations if x.id not in favorite_ids),key=lambda x:(-x.station_count,x.display_name.lower()))[:10]
proposed_ids=favorite_ids|{x.id for x in top_stations} proposed_ids=favorite_ids|{x.id for x in top_stations}
favorite_catalog=sorted(stations,key=lambda x:(-x.station_count,x.display_name.lower())) favorite_catalog=sorted(stations,key=lambda x:(0 if x.id in user_favorite_ids else 1,-x.station_count,x.display_name.lower()))
other_stations=sorted((x for x in stations if x.id not in proposed_ids),key=lambda x:x.display_name.lower()) other_stations=sorted((x for x in stations if x.id not in proposed_ids),key=lambda x:x.display_name.lower())
return render_template("fuel_form.html", vehicles=vehicles, stations=stations, favorite_stations=favorites, top_stations=top_stations, favorite_catalog=favorite_catalog, other_stations=other_stations, now=datetime.now().strftime("%Y-%m-%dT%H:%M"), restricted=bool(allowed), favorite_scope=favorite_scope, company=settings, fuel_cards=FuelCard.query.filter_by(active=True,company_id=settings.id).order_by(FuelCard.name).all()) return render_template("fuel_form.html", vehicles=vehicles, stations=stations, favorite_stations=favorites, top_stations=top_stations, favorite_catalog=favorite_catalog, other_stations=other_stations, now=datetime.now().strftime("%Y-%m-%dT%H:%M"), restricted=access_mode in ("allowed_only", "favorites_allowed_only"), access_mode=access_mode, user_favorite_ids=user_favorite_ids, favorite_scope=favorite_scope, company=settings, fuel_cards=FuelCard.query.filter_by(active=True,company_id=settings.id).order_by(FuelCard.name).all())
@main.route("/api/orlen-price") @main.route("/api/orlen-price")
@login_required @login_required
+1
View File
@@ -56,6 +56,7 @@ class CompanySettings(db.Model):
region = db.Column(db.String(60), default="mazowieckie", nullable=False) region = db.Column(db.String(60), default="mazowieckie", nullable=False)
invoice_split_day = db.Column(db.Integer, default=15, nullable=False) invoice_split_day = db.Column(db.Integer, default=15, nullable=False)
active = db.Column(db.Boolean, default=True, nullable=False) active = db.Column(db.Boolean, default=True, nullable=False)
station_access_mode = db.Column(db.String(40), default="all_prefer_favorites", nullable=False)
users = db.relationship("User", back_populates="company", foreign_keys="User.company_id") users = db.relationship("User", back_populates="company", foreign_keys="User.company_id")
class AppSetting(db.Model): class AppSetting(db.Model):
+1 -1
View File
@@ -59,7 +59,7 @@
</div> </div>
<div class="card mb-4"><div class="card-body d-flex flex-column flex-md-row justify-content-between align-items-md-center gap-3"> <div class="card mb-4"><div class="card-body d-flex flex-column flex-md-row justify-content-between align-items-md-center gap-3">
<div><h2 class="h5 mb-1">Dostęp do stacji</h2>{% if selected_company.allowed_stations %}<p class="text-danger mb-0">Aktywna lista zamknięta: {{ selected_company.allowed_stations|length }} dozwolonych stacji.</p>{% else %}<p class="text-body-secondary mb-0">Wszystkie aktywne stacje są dozwolone.</p>{% endif %}</div> <div><h2 class="h5 mb-1">Dostęp do stacji</h2><p class="text-body-secondary mb-0">{% if selected_company.station_access_mode == 'allowed_only' %}Tylko stacje z listy dozwolonych ({{ selected_company.allowed_stations|length }}).{% elif selected_company.station_access_mode == 'favorites_allowed_only' %}Tylko ulubione i stacje z listy dozwolonych.{% elif selected_company.station_access_mode == 'all' %}Wszystkie stacje, bez preferowania ulubionych.{% else %}Wszystkie stacje są dozwolone, ulubione są preferowane.{% endif %}</p></div>
<a class="btn btn-secondary ajax-nav-link" href="{{ url_for('main.stations', company_id=selected_company.id) }}">Konfiguruj dostęp</a> <a class="btn btn-secondary ajax-nav-link" href="{{ url_for('main.stations', company_id=selected_company.id) }}">Konfiguruj dostęp</a>
</div></div> </div></div>
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -9,7 +9,7 @@
<div class="card mb-3"><div class="card-body"><form method="get" action="{{ url_for('main.stations') }}" class="row g-2 align-items-end ajax-nav-form"><div class="col-md-6"><label class="form-label">Konfigurowana firma</label><select class="form-select" name="company_id" onchange="this.form.requestSubmit()">{% for item in companies %}<option value="{{ item.id }}" {% if item.id == company.id %}selected{% endif %}>{{ item.name }}</option>{% endfor %}</select></div><input type="hidden" name="q" value="{{ q }}"><input type="hidden" name="sort" value="{{ sort }}"><input type="hidden" name="direction" value="{{ direction }}"></form></div></div> <div class="card mb-3"><div class="card-body"><form method="get" action="{{ url_for('main.stations') }}" class="row g-2 align-items-end ajax-nav-form"><div class="col-md-6"><label class="form-label">Konfigurowana firma</label><select class="form-select" name="company_id" onchange="this.form.requestSubmit()">{% for item in companies %}<option value="{{ item.id }}" {% if item.id == company.id %}selected{% endif %}>{{ item.name }}</option>{% endfor %}</select></div><input type="hidden" name="q" value="{{ q }}"><input type="hidden" name="sort" value="{{ sort }}"><input type="hidden" name="direction" value="{{ direction }}"></form></div></div>
{% endif %} {% endif %}
<div class="card mb-4"><div class="card-body"><form method="get" action="{{url_for('main.stations')}}" class="row g-2 ajax-nav-form" id="station-search-form"><input type="hidden" name="company_id" value="{{ company.id }}"><div class="col-lg-6"><input class="form-control" name="q" value="{{q}}" placeholder="Szukaj po nazwie stacji lub firmy" autocomplete="off" data-live-search></div><div class="col-md-3"><select class="form-select" name="sort" data-live-sort><option value="company_name" {% if sort=='company_name' %}selected{% endif %}>Nazwa</option><option value="station_count" {% if sort=='station_count' %}selected{% endif %}>Liczba stacji</option><option value="nip" {% if sort=='nip' %}selected{% endif %}>NIP</option><option value="regon" {% if sort=='regon' %}selected{% endif %}>REGON</option><option value="selected_region" {% if sort=='selected_region' %}selected{% endif %}>Województwo</option></select></div><div class="col-md-2"><select class="form-select" name="direction" data-live-sort><option value="asc" {% if direction=='asc' %}selected{% endif %}>Rosnąco</option><option value="desc" {% if direction=='desc' %}selected{% endif %}>Malejąco</option></select></div><div class="col-md-1"><button class="btn btn-secondary w-100">Szukaj</button></div></form></div></div> <div class="card mb-4"><div class="card-body"><form method="get" action="{{url_for('main.stations')}}" class="row g-2 ajax-nav-form" id="station-search-form"><input type="hidden" name="company_id" value="{{ company.id }}"><div class="col-lg-6"><input class="form-control" name="q" value="{{q}}" placeholder="Szukaj po nazwie stacji lub firmy" autocomplete="off" data-live-search></div><div class="col-md-3"><select class="form-select" name="sort" data-live-sort><option value="company_name" {% if sort=='company_name' %}selected{% endif %}>Nazwa</option><option value="station_count" {% if sort=='station_count' %}selected{% endif %}>Liczba stacji</option><option value="nip" {% if sort=='nip' %}selected{% endif %}>NIP</option><option value="regon" {% if sort=='regon' %}selected{% endif %}>REGON</option><option value="selected_region" {% if sort=='selected_region' %}selected{% endif %}>Województwo</option></select></div><div class="col-md-2"><select class="form-select" name="direction" data-live-sort><option value="asc" {% if direction=='asc' %}selected{% endif %}>Rosnąco</option><option value="desc" {% if direction=='desc' %}selected{% endif %}>Malejąco</option></select></div><div class="col-md-1"><button class="btn btn-secondary w-100">Szukaj</button></div></form></div></div>
<div class="card mb-3"><div class="card-body d-flex flex-column flex-lg-row justify-content-between align-items-lg-center gap-3"><div><h2 class="h5 mb-1">Dostęp do stacji</h2>{% if allowed_ids %}<p class="text-danger mb-0">Aktywna jest lista zamknięta: dozwolonych stacji: {{ allowed_ids|length }}. Niezaznaczone stacje nie pojawią się w formularzu tankowania.</p>{% else %}<p class="text-body-secondary mb-0">Wszystkie aktywne stacje są dozwolone. Ulubione wpływają tylko na kolejność propozycji.</p>{% endif %}</div>{% if allowed_ids %}<form action="/api/companies/{{company.id}}/allowed-stations" data-method="PUT" class="ajax-form"><button class="btn btn-warning">Zezwól na wszystkie stacje</button></form>{% endif %}</div></div> <div class="card mb-3"><div class="card-body"><div class="d-flex flex-column flex-lg-row justify-content-between align-items-lg-start gap-3"><div><h2 class="h5 mb-1">Dostęp do stacji</h2><p class="text-body-secondary mb-0">Wybierz, czy stacje mają być tylko preferowane, czy także ograniczane.</p></div><form action="/api/companies/{{company.id}}/station-access" data-method="PUT" class="ajax-form d-flex flex-column flex-md-row gap-2 align-items-md-end"><div><label class="form-label">Tryb dostępu</label><select class="form-select" name="station_access_mode"><option value="all_prefer_favorites" {% if company.station_access_mode=='all_prefer_favorites' %}selected{% endif %}>Dozwól wszystko, preferuj ulubione</option><option value="all" {% if company.station_access_mode=='all' %}selected{% endif %}>Dozwól wszystkie bez preferencji</option><option value="allowed_only" {% if company.station_access_mode=='allowed_only' %}selected{% endif %}>Dozwól tylko stacje z listy</option><option value="favorites_allowed_only" {% if company.station_access_mode=='favorites_allowed_only' %}selected{% endif %}>Tylko ulubione i dozwolone</option></select></div><button class="btn btn-primary">Zapisz tryb</button></form></div><hr><div class="small text-body-secondary">Domyślnie: wszystkie stacje są dostępne, a ulubione pojawiają się na początku. Lista „dozwolone” jest używana wyłącznie w trybach ograniczających.</div></div></div>
<div class="card"><div class="card-body"><div class="table-responsive"><table class="table table-hover align-middle"><thead><tr><th><a class="link-body-emphasis ajax-nav-link" href="{{url_for('main.stations',company_id=company.id,q=q,sort='company_name',direction='desc' if sort=='company_name' and direction=='asc' else 'asc')}}">Stacja / firma</a></th><th><a class="link-body-emphasis ajax-nav-link" href="{{url_for('main.stations',company_id=company.id,q=q,sort='station_count',direction='asc' if sort=='station_count' and direction=='desc' else 'desc')}}">Punkty</a></th><th>Paliwa</th><th>Ustawienia</th><th>Warunki kart</th></tr></thead><tbody> <div class="card"><div class="card-body"><div class="table-responsive"><table class="table table-hover align-middle"><thead><tr><th><a class="link-body-emphasis ajax-nav-link" href="{{url_for('main.stations',company_id=company.id,q=q,sort='company_name',direction='desc' if sort=='company_name' and direction=='asc' else 'asc')}}">Stacja / firma</a></th><th><a class="link-body-emphasis ajax-nav-link" href="{{url_for('main.stations',company_id=company.id,q=q,sort='station_count',direction='asc' if sort=='station_count' and direction=='desc' else 'desc')}}">Punkty</a></th><th>Paliwa</th><th>Ustawienia</th><th>Warunki kart</th></tr></thead><tbody>
{% for s in station_page.items %}<tr> {% for s in station_page.items %}<tr>
<td>{% if s.station_count > 1 %}<button type="button" class="btn btn-link link-body-emphasis fw-semibold p-0 text-start station-points-open" data-station-id="{{s.id}}" data-station-name="{{s.display_name}}">{{s.display_name}}</button>{% else %}<strong>{{s.display_name}}</strong>{% endif %}{% if s.company_name != s.display_name %}<div class="small text-body-secondary">{{s.company_name}}</div>{% endif %}<div class="small text-body-secondary">NIP: {{s.nip or '—'}} · REGON: {{s.regon or '—'}}</div></td> <td>{% if s.station_count > 1 %}<button type="button" class="btn btn-link link-body-emphasis fw-semibold p-0 text-start station-points-open" data-station-id="{{s.id}}" data-station-name="{{s.display_name}}">{{s.display_name}}</button>{% else %}<strong>{{s.display_name}}</strong>{% endif %}{% if s.company_name != s.display_name %}<div class="small text-body-secondary">{{s.company_name}}</div>{% endif %}<div class="small text-body-secondary">NIP: {{s.nip or '—'}} · REGON: {{s.regon or '—'}}</div></td>