split api pythons
This commit is contained in:
+4
-4
@@ -53,11 +53,11 @@ def create_app(test_config=None):
|
|||||||
if request.endpoint == "static":
|
if request.endpoint == "static":
|
||||||
filename = request.view_args.get("filename", "") if request.view_args else ""
|
filename = request.view_args.get("filename", "") if request.view_args else ""
|
||||||
if filename.endswith(".css"):
|
if filename.endswith(".css"):
|
||||||
response.headers["Cache-Control"] = "public, max-age=1800, immutable"
|
response.headers["Cache-Control"] = "public, max-age=3600, immutable"
|
||||||
else:
|
else:
|
||||||
response.headers["Cache-Control"] = "public, max-age=1800, immutable"
|
response.headers["Cache-Control"] = "public, max-age=3600, immutable"
|
||||||
elif response.mimetype == "text/html":
|
elif response.mimetype == "text/html":
|
||||||
response.headers["Cache-Control"] = "private, no-store"
|
response.headers["Cache-Control"] = "private, no-store,"
|
||||||
return response
|
return response
|
||||||
|
|
||||||
@login_manager.user_loader
|
@login_manager.user_loader
|
||||||
@@ -77,7 +77,7 @@ def create_app(test_config=None):
|
|||||||
if request.path.startswith("/api/"):
|
if request.path.startswith("/api/"):
|
||||||
return {"ok": False, "message": "Wymagane uwierzytelnienie Bearer lub aktywna sesja"}, 401
|
return {"ok": False, "message": "Wymagane uwierzytelnienie Bearer lub aktywna sesja"}, 401
|
||||||
from flask import redirect
|
from flask import redirect
|
||||||
return redirect(url_for("auth.login", next=request.url))
|
return redirect(url_for("auth.login", next=request.full_path.rstrip("?")))
|
||||||
|
|
||||||
@app.context_processor
|
@app.context_processor
|
||||||
def inject_theme():
|
def inject_theme():
|
||||||
|
|||||||
-518
@@ -1,518 +0,0 @@
|
|||||||
from collections import defaultdict
|
|
||||||
from datetime import datetime, date
|
|
||||||
from decimal import Decimal
|
|
||||||
from functools import wraps
|
|
||||||
from flask import Blueprint, jsonify, request, url_for, current_app
|
|
||||||
from flask_login import current_user, login_required, login_user, logout_user
|
|
||||||
from sqlalchemy import extract
|
|
||||||
from sqlalchemy.exc import IntegrityError
|
|
||||||
from .extensions import db
|
|
||||||
from .models import User, Vehicle, FuelEntry, CompanySettings, AppSetting, OrlenPrice, FuelCardPolicy, FuelStationCompany, FuelStationPoint, FuelCard, FuelCardStationRule
|
|
||||||
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 . import THEMES
|
|
||||||
from .api_tokens import create_access_token
|
|
||||||
|
|
||||||
api = Blueprint('api', __name__, url_prefix='/api')
|
|
||||||
FUEL_TYPES = ('PB95','PB98','DIESEL','LPG')
|
|
||||||
|
|
||||||
def response(data=None, message=None, status=200):
|
|
||||||
payload={'ok': status < 400}
|
|
||||||
if message: payload['message']=message
|
|
||||||
if data is not None: payload['data']=data
|
|
||||||
return jsonify(payload), status
|
|
||||||
|
|
||||||
def fail(message, status=400): return response(message=message,status=status)
|
|
||||||
|
|
||||||
def role_required(*allowed):
|
|
||||||
def deco(fn):
|
|
||||||
@wraps(fn)
|
|
||||||
@login_required
|
|
||||||
def wrapped(*a,**kw):
|
|
||||||
if current_user.role not in allowed: return fail('Brak uprawnień',403)
|
|
||||||
return fn(*a,**kw)
|
|
||||||
return wrapped
|
|
||||||
return deco
|
|
||||||
|
|
||||||
def payload():
|
|
||||||
return request.get_json(silent=True) or request.form
|
|
||||||
|
|
||||||
def boolv(data,key):
|
|
||||||
v=data.get(key)
|
|
||||||
return v in (True,1,'1','true','on','yes')
|
|
||||||
|
|
||||||
def accessible_vehicles():
|
|
||||||
if current_user.role=='admin': return Vehicle.query.order_by(Vehicle.name).all()
|
|
||||||
ids={v.id for v in current_user.owned_vehicles+current_user.shared_vehicles}
|
|
||||||
return Vehicle.query.filter(Vehicle.id.in_(ids)).order_by(Vehicle.name).all() if ids else []
|
|
||||||
|
|
||||||
def user_dict(u): return {'id':u.id,'name':u.name,'email':u.email,'role':u.role,'active':u.active,'fuel_card_id':u.fuel_card_id,'fuel_card':u.fuel_card.name if u.fuel_card else None,'company_id':u.company_id,'company':u.company.name if u.company else None}
|
|
||||||
def vehicle_dict(v): return {'id':v.id,'name':v.name,'make':v.make,'model':v.model,'registration':v.registration,'fuel_type':v.fuel_type,'owner':user_dict(v.owner),'drivers':[user_dict(x) for x in v.drivers],'has_fuel_card':bool(v.fuel_card_id),'fuel_card_id':v.fuel_card_id,'fuel_card':v.fuel_card.name if v.fuel_card else None,'current_odometer':v.current_odometer}
|
|
||||||
def station_dict(s):
|
|
||||||
settings=CompanySettings.query.first()
|
|
||||||
return {'id':s.id,'brand_name':s.display_name,'company_name':s.company_name,'nip':s.nip,'regon':s.regon,'brands':s.station_name_list,'regions':s.region_list,'selected_region':s.selected_region,'station_count':s.station_count,'has_petrol':s.has_petrol,'has_diesel':s.has_diesel,'has_lpg':s.has_lpg,'use_orlen_last_price':s.use_orlen_last_price,'active':s.active,'favorite':s in current_user.favorite_stations if current_user.is_authenticated else False,'allowed':s in settings.allowed_stations if settings else True}
|
|
||||||
|
|
||||||
@api.post('/auth/login')
|
|
||||||
def login():
|
|
||||||
d=payload(); user=User.query.filter_by(email=(d.get('email') or '').strip().lower()).first()
|
|
||||||
if not user or not user.check_password(d.get('password') or '') or not user.active: return fail('Nieprawidłowy e-mail lub hasło',401)
|
|
||||||
login_user(user); return response({'user':user_dict(user),'redirect':url_for('main.dashboard')},'Zalogowano')
|
|
||||||
|
|
||||||
@api.post('/auth/token')
|
|
||||||
def token_login():
|
|
||||||
d = payload()
|
|
||||||
email = (d.get('email') or d.get('username') or '').strip().lower()
|
|
||||||
user = User.query.filter_by(email=email).first()
|
|
||||||
if not user or not user.check_password(d.get('password') or '') or not user.active:
|
|
||||||
return jsonify({'error':'invalid_grant','error_description':'Nieprawidłowy e-mail lub hasło'}), 401
|
|
||||||
expires_in = int(current_app.config['API_TOKEN_MAX_AGE_SECONDS'])
|
|
||||||
return jsonify({
|
|
||||||
'access_token': create_access_token(user),
|
|
||||||
'token_type': 'Bearer',
|
|
||||||
'expires_in': expires_in,
|
|
||||||
'user': user_dict(user),
|
|
||||||
})
|
|
||||||
|
|
||||||
@api.post('/auth/logout')
|
|
||||||
@login_required
|
|
||||||
def logout(): logout_user(); return response({'redirect':url_for('auth.login')},'Wylogowano')
|
|
||||||
|
|
||||||
@api.get('/me')
|
|
||||||
@login_required
|
|
||||||
def me(): return response(user_dict(current_user))
|
|
||||||
|
|
||||||
@api.get('/vehicles')
|
|
||||||
@login_required
|
|
||||||
def get_vehicles(): return response([vehicle_dict(v) for v in accessible_vehicles()])
|
|
||||||
|
|
||||||
@api.post('/vehicles')
|
|
||||||
@login_required
|
|
||||||
def create_vehicle():
|
|
||||||
d=payload()
|
|
||||||
try:
|
|
||||||
card_id=int(d['fuel_card_id']) if d.get('fuel_card_id') else None
|
|
||||||
owner_id=int(d.get('owner_id') or current_user.id);owner=db.session.get(User,owner_id)
|
|
||||||
if not owner: return fail('Nie znaleziono właściciela',404)
|
|
||||||
if current_user.role not in ('boss','admin') and owner.id!=current_user.id:return fail('Brak uprawnień',403)
|
|
||||||
if current_user.role=='boss' and owner.company_id!=current_user.company_id:return fail('Użytkownik jest w innej firmie',403)
|
|
||||||
v=Vehicle(name=d['name'].strip(),make=d['make'].strip(),model=d['model'].strip(),registration=d['registration'].strip().upper(),fuel_type=d['fuel_type'],owner_id=owner.id,company_id=owner.company_id,has_fuel_card=bool(card_id),fuel_card_id=card_id,current_odometer=int(d.get('current_odometer') or 0))
|
|
||||||
db.session.add(v); db.session.commit(); return response(vehicle_dict(v),'Dodano pojazd',201)
|
|
||||||
except (KeyError,ValueError,IntegrityError) as exc: db.session.rollback(); return fail(str(getattr(exc,'orig',exc)),400)
|
|
||||||
|
|
||||||
@api.put('/vehicles/<int:vehicle_id>/fuel-card')
|
|
||||||
@login_required
|
|
||||||
def assign_vehicle_fuel_card(vehicle_id):
|
|
||||||
v=Vehicle.query.get_or_404(vehicle_id)
|
|
||||||
if current_user.role not in ('boss','admin') and v.owner_id!=current_user.id:return fail('Brak uprawnień',403)
|
|
||||||
d=payload();card_id=int(d['fuel_card_id']) if d.get('fuel_card_id') else None
|
|
||||||
card=db.session.get(FuelCard,card_id) if card_id else None
|
|
||||||
if card_id and not card:return fail('Nie znaleziono karty paliwowej',404)
|
|
||||||
if card and v.company_id and card.company_id!=v.company_id:return fail('Karta należy do innej firmy',400)
|
|
||||||
v.fuel_card_id=card_id;v.has_fuel_card=bool(card_id);db.session.commit();return response(vehicle_dict(v),'Zapisano kartę pojazdu')
|
|
||||||
|
|
||||||
@api.post('/vehicles/<int:vehicle_id>/shares')
|
|
||||||
@login_required
|
|
||||||
def share_vehicle(vehicle_id):
|
|
||||||
v=Vehicle.query.get_or_404(vehicle_id)
|
|
||||||
if current_user.role!='admin' and v.owner_id!=current_user.id: return fail('Brak uprawnień',403)
|
|
||||||
d=payload(); u=User.query.filter_by(email=(d.get('email') or '').strip().lower()).first()
|
|
||||||
if not u:return fail('Nie znaleziono użytkownika',404)
|
|
||||||
if u not in v.drivers:v.drivers.append(u);db.session.commit();return response(vehicle_dict(v),'Udostępniono pojazd')
|
|
||||||
|
|
||||||
@api.delete('/vehicles/<int:vehicle_id>/shares/<int:user_id>')
|
|
||||||
@login_required
|
|
||||||
def unshare_vehicle(vehicle_id,user_id):
|
|
||||||
v=Vehicle.query.get_or_404(vehicle_id)
|
|
||||||
if current_user.role!='admin' and v.owner_id!=current_user.id:return fail('Brak uprawnień',403)
|
|
||||||
u=User.query.get_or_404(user_id)
|
|
||||||
if u in v.drivers:v.drivers.remove(u);db.session.commit();return response(vehicle_dict(v),'Odebrano dostęp')
|
|
||||||
|
|
||||||
@api.put('/vehicles/<int:vehicle_id>/card-policy')
|
|
||||||
@login_required
|
|
||||||
def card_policy(vehicle_id):
|
|
||||||
v=Vehicle.query.get_or_404(vehicle_id)
|
|
||||||
if current_user.role!='admin' and v.owner_id!=current_user.id:return fail('Brak uprawnień',403)
|
|
||||||
d=payload();p=v.fuel_card_policy or FuelCardPolicy(vehicle=v)
|
|
||||||
p.use_orlen_last_price=False;p.discount_percent=Decimal(d.get('discount_percent') or 0);p.surcharge_per_liter=Decimal(d.get('surcharge_per_liter') or 0);p.description=(d.get('description') or '').strip() or None
|
|
||||||
db.session.add(p);db.session.commit();return response(message='Zapisano zasady rozliczeń')
|
|
||||||
|
|
||||||
@api.post('/fuel-entries')
|
|
||||||
@login_required
|
|
||||||
def create_fuel_entry():
|
|
||||||
d=payload(); vehicles=accessible_vehicles()
|
|
||||||
try: vehicle=next((v for v in vehicles if v.id==int(d['vehicle_id'])),None)
|
|
||||||
except Exception: vehicle=None
|
|
||||||
if not vehicle:return fail('Brak dostępu do pojazdu',403)
|
|
||||||
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
|
|
||||||
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:
|
|
||||||
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
|
|
||||||
try:
|
|
||||||
p=fetch_orlen_price(d['fuel_type'],fueled_at.date(),region);wholesale=p['price_per_liter'];source=p['source']
|
|
||||||
except Exception: pass
|
|
||||||
card_id=int(d['fuel_card_id']) if d.get('fuel_card_id') else (vehicle.fuel_card_id or current_user.fuel_card_id)
|
|
||||||
card=db.session.get(FuelCard,card_id) if card_id else None
|
|
||||||
used_card=boolv(d,'used_fuel_card') and bool(card)
|
|
||||||
if used_card and station_company:
|
|
||||||
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)
|
|
||||||
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)
|
|
||||||
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)
|
|
||||||
|
|
||||||
@api.get('/orlen/prices')
|
|
||||||
@login_required
|
|
||||||
def orlen_prices():
|
|
||||||
year=request.args.get('year',type=int) or date.today().year; fuels=[x.upper() for x in request.args.getlist('fuel') if x.upper() in FUEL_TYPES] or ['PB95']; regions=[x.lower() for x in request.args.getlist('region') if x.lower() in POLISH_REGIONS]
|
|
||||||
rows=OrlenPrice.query.filter(OrlenPrice.fuel_type.in_(fuels),extract('year',OrlenPrice.effective_date)==year).order_by(OrlenPrice.effective_date).all()
|
|
||||||
return response([{'fuel_type':r.fuel_type,'date':r.effective_date.isoformat(),'price_per_liter':float(r.price_per_liter),'region':r.region,'source':r.source} for r in rows if r.fuel_type!='LPG' or not regions or r.region in regions])
|
|
||||||
|
|
||||||
@api.post('/orlen/sync')
|
|
||||||
@role_required('boss','admin')
|
|
||||||
def orlen_sync():
|
|
||||||
d=payload(); fuels=d.get('fuels',[]); fuels=[fuels] if isinstance(fuels,str) else fuels; fuels=[f.upper() for f in fuels if f.upper() in FUEL_TYPES]; year=int(d.get('year',date.today().year))
|
|
||||||
regions=d.get('regions',[]); regions=[regions] if isinstance(regions,str) else regions; regions=[str(r).strip().lower() for r in regions if str(r).strip().lower() in POLISH_REGIONS]
|
|
||||||
if not fuels:return fail('Wybierz co najmniej jedno paliwo')
|
|
||||||
settings=CompanySettings.query.first(); result={}; total=0
|
|
||||||
try:
|
|
||||||
for fuel in fuels:
|
|
||||||
added=updated=0
|
|
||||||
for row in fetch_orlen_range(fuel,date(year,1,1),min(date(year,12,31),date.today()),'all' if fuel=='LPG' else settings.region):
|
|
||||||
if fuel=='LPG' and regions and row.get('region') not in regions: continue
|
|
||||||
x=OrlenPrice.query.filter_by(fuel_type=row['fuel_type'],effective_date=row['effective_date'],region=row['region']).first()
|
|
||||||
if x:
|
|
||||||
for k,v in row.items():setattr(x,k,v)
|
|
||||||
x.fetched_at=datetime.utcnow();updated+=1
|
|
||||||
else:db.session.add(OrlenPrice(**row));added+=1
|
|
||||||
result[fuel]={'added':added,'updated':updated};total+=added+updated
|
|
||||||
db.session.commit();return response(result,f'Przetworzono {total} rekordów')
|
|
||||||
except Exception as exc:db.session.rollback();return fail(str(exc),502)
|
|
||||||
|
|
||||||
@api.get('/orlen/preview')
|
|
||||||
@login_required
|
|
||||||
def orlen_preview():
|
|
||||||
try:return response(fetch_orlen_price(request.args['fuel_type'],datetime.fromisoformat(request.args['date']).date(),request.args.get('region') or CompanySettings.query.first().region))
|
|
||||||
except Exception as exc:return fail(str(exc),502)
|
|
||||||
|
|
||||||
@api.get('/users')
|
|
||||||
@role_required('admin')
|
|
||||||
def users():
|
|
||||||
page=request.args.get('page',1,type=int);per=min(request.args.get('per_page',25,type=int),100);q=request.args.get('q','').strip();query=User.query
|
|
||||||
if q: query=query.filter(db.or_(User.name.ilike(f'%{q}%'),User.email.ilike(f'%{q}%')))
|
|
||||||
p=query.order_by(User.name).paginate(page=page,per_page=per,error_out=False);return response({'items':[user_dict(x) for x in p.items],'page':p.page,'pages':p.pages,'total':p.total})
|
|
||||||
|
|
||||||
@api.post('/users')
|
|
||||||
@role_required('boss','admin')
|
|
||||||
def create_user():
|
|
||||||
d=payload()
|
|
||||||
try:
|
|
||||||
if len(d.get('password',''))<8:raise ValueError('Hasło musi mieć co najmniej 8 znaków')
|
|
||||||
company_id=int(d.get('company_id') or current_user.company_id or CompanySettings.query.first().id)
|
|
||||||
if current_user.role=='boss' and company_id!=current_user.company_id:return fail('Brak uprawnień',403)
|
|
||||||
u=User(name=d['user_name'].strip(),email=d['email'].strip().lower(),role=d.get('role','user'),company_id=company_id);u.set_password(d['password']);db.session.add(u);db.session.commit();return response(user_dict(u),'Dodano użytkownika',201)
|
|
||||||
except Exception as exc:db.session.rollback();return fail(str(getattr(exc,'orig',exc)))
|
|
||||||
|
|
||||||
@api.put('/users/<int:user_id>')
|
|
||||||
@role_required('admin')
|
|
||||||
def update_user(user_id):
|
|
||||||
u=User.query.get_or_404(user_id);d=payload()
|
|
||||||
try:
|
|
||||||
u.name=d['name'].strip();u.email=d['email'].strip().lower();u.role=d['role'];u.active=boolv(d,'active');u.company_id=int(d.get('company_id') or u.company_id or CompanySettings.query.first().id);u.fuel_card_id=int(d['fuel_card_id']) if d.get('fuel_card_id') else None;pw=d.get('password','')
|
|
||||||
if pw:
|
|
||||||
if len(pw)<8:raise ValueError('Nowe hasło musi mieć co najmniej 8 znaków')
|
|
||||||
u.set_password(pw)
|
|
||||||
db.session.commit();return response(user_dict(u),'Zaktualizowano użytkownika')
|
|
||||||
except Exception as exc:db.session.rollback();return fail(str(getattr(exc,'orig',exc)))
|
|
||||||
|
|
||||||
@api.delete('/users/<int:user_id>')
|
|
||||||
@role_required('admin')
|
|
||||||
def delete_user(user_id):
|
|
||||||
user = User.query.get_or_404(user_id)
|
|
||||||
if user.id == current_user.id:
|
|
||||||
return fail('Nie możesz usunąć własnego konta administratora', 400)
|
|
||||||
if user.role == 'admin':
|
|
||||||
remaining_active_admins = User.query.filter(
|
|
||||||
User.role == 'admin', User.active.is_(True), User.id != user.id
|
|
||||||
).count()
|
|
||||||
if remaining_active_admins < 1:
|
|
||||||
return fail('Nie można usunąć ostatniego aktywnego administratora', 409)
|
|
||||||
if user.owned_vehicles:
|
|
||||||
return fail('Najpierw przypisz pojazdy tego użytkownika innemu właścicielowi', 409)
|
|
||||||
if FuelEntry.query.filter_by(user_id=user.id).first():
|
|
||||||
return fail('Użytkownik ma historię tankowań i nie może zostać usunięty. Możesz go dezaktywować.', 409)
|
|
||||||
user.shared_vehicles.clear()
|
|
||||||
user.favorite_stations.clear()
|
|
||||||
db.session.delete(user)
|
|
||||||
db.session.commit()
|
|
||||||
return response(message='Usunięto użytkownika')
|
|
||||||
|
|
||||||
@api.get('/settings')
|
|
||||||
@role_required('boss','admin')
|
|
||||||
def get_settings():
|
|
||||||
s=CompanySettings.query.first();return response({'name':s.name,'entity_type':s.entity_type,'vat_rate':float(s.vat_rate),'vat_deduction_percent':float(s.vat_deduction_percent),'region':s.region,'invoice_split_day':s.invoice_split_day,'invoice_split_enabled':(db.session.get(AppSetting,'invoice_split_enabled').value=='1')})
|
|
||||||
|
|
||||||
@api.put('/settings')
|
|
||||||
@role_required('boss','admin')
|
|
||||||
def update_settings():
|
|
||||||
d=payload();s=CompanySettings.query.first()
|
|
||||||
try:
|
|
||||||
s.name=d['name'];s.entity_type=d['entity_type'];s.vat_rate=Decimal(d['vat_rate']);s.vat_deduction_percent=Decimal(d['vat_deduction_percent']);s.region=d['region'].strip().lower();s.invoice_split_day=int(d.get('invoice_split_day') or 15)
|
|
||||||
db.session.merge(AppSetting(key='invoice_split_enabled',value='1' if boolv(d,'invoice_split_enabled') else '0'));db.session.commit();return response(message='Zapisano ustawienia')
|
|
||||||
except Exception as exc:db.session.rollback();return fail(str(exc))
|
|
||||||
|
|
||||||
|
|
||||||
@api.get('/app-settings')
|
|
||||||
@role_required('boss','admin')
|
|
||||||
def get_app_settings():
|
|
||||||
return response({'theme':(db.session.get(AppSetting,'theme') or AppSetting(value='bootstrap')).value})
|
|
||||||
|
|
||||||
@api.put('/app-settings')
|
|
||||||
@role_required('boss','admin')
|
|
||||||
def update_app_settings():
|
|
||||||
d=payload();theme=d.get('theme','bootstrap')
|
|
||||||
if theme not in THEMES:return fail('Nieprawidłowy motyw')
|
|
||||||
db.session.merge(AppSetting(key='theme',value=theme));db.session.commit();return response(message='Zapisano ustawienia aplikacji')
|
|
||||||
|
|
||||||
@api.get('/fuel-cards')
|
|
||||||
@login_required
|
|
||||||
def get_fuel_cards():
|
|
||||||
rows=FuelCard.query.order_by(FuelCard.name).all()
|
|
||||||
return response([{'id':x.id,'name':x.name,'provider':x.provider,'active':x.active,'description':x.description} for x in rows])
|
|
||||||
|
|
||||||
@api.post('/fuel-cards')
|
|
||||||
@role_required('boss','admin')
|
|
||||||
def create_fuel_card():
|
|
||||||
d=payload()
|
|
||||||
try:
|
|
||||||
company_id=int(d.get('company_id') or current_user.company_id or CompanySettings.query.first().id)
|
|
||||||
if current_user.role=='boss' and company_id!=current_user.company_id:return fail('Brak uprawnień',403)
|
|
||||||
card=FuelCard(name=d['name'].strip(),provider=d['provider'].strip(),description=(d.get('description') or '').strip() or None,active=True,company_id=company_id)
|
|
||||||
db.session.add(card);db.session.commit();return response({'id':card.id},'Dodano kartę paliwową',201)
|
|
||||||
except Exception as exc:db.session.rollback();return fail(str(getattr(exc,'orig',exc)))
|
|
||||||
|
|
||||||
@api.put('/fuel-cards/<int:card_id>')
|
|
||||||
@role_required('boss','admin')
|
|
||||||
def update_fuel_card(card_id):
|
|
||||||
card=FuelCard.query.get_or_404(card_id);d=payload();card.name=d.get('name',card.name).strip();card.provider=d.get('provider',card.provider).strip();card.description=(d.get('description') or '').strip() or None;card.active=boolv(d,'active');db.session.commit();return response(message='Zapisano kartę')
|
|
||||||
|
|
||||||
@api.delete('/fuel-cards/<int:card_id>')
|
|
||||||
@role_required('boss','admin')
|
|
||||||
def delete_fuel_card(card_id):
|
|
||||||
card = FuelCard.query.get_or_404(card_id)
|
|
||||||
if current_user.role == 'boss' and card.company_id != current_user.company_id:
|
|
||||||
return fail('Brak uprawnień', 403)
|
|
||||||
|
|
||||||
users_count = User.query.filter_by(fuel_card_id=card.id).count()
|
|
||||||
vehicles_count = Vehicle.query.filter_by(fuel_card_id=card.id).count()
|
|
||||||
entries_count = FuelEntry.query.filter_by(fuel_card_id=card.id).count()
|
|
||||||
blockers = []
|
|
||||||
if users_count:
|
|
||||||
blockers.append(f'{users_count} użytkowników')
|
|
||||||
if vehicles_count:
|
|
||||||
blockers.append(f'{vehicles_count} pojazdów')
|
|
||||||
if entries_count:
|
|
||||||
blockers.append(f'{entries_count} tankowań')
|
|
||||||
if blockers:
|
|
||||||
return fail(
|
|
||||||
'Nie można usunąć karty, ponieważ jest przypisana do: ' + ', '.join(blockers) +
|
|
||||||
'. Najpierw odłącz kartę od użytkowników i pojazdów. Karty użytej w tankowaniach nie można usunąć; możesz ją wyłączyć.',
|
|
||||||
409,
|
|
||||||
)
|
|
||||||
|
|
||||||
company_id = card.company_id
|
|
||||||
db.session.delete(card)
|
|
||||||
db.session.commit()
|
|
||||||
return response(
|
|
||||||
{'redirect':url_for('main.admin_companies', company_id=company_id)},
|
|
||||||
'Usunięto kartę flotową',
|
|
||||||
)
|
|
||||||
|
|
||||||
@api.put('/fuel-cards/<int:card_id>/stations/<int:station_id>')
|
|
||||||
@role_required('boss','admin')
|
|
||||||
def update_card_station_rule(card_id,station_id):
|
|
||||||
FuelCard.query.get_or_404(card_id);FuelStationCompany.query.get_or_404(station_id);d=payload()
|
|
||||||
rule=FuelCardStationRule.query.filter_by(fuel_card_id=card_id,station_company_id=station_id).first() or FuelCardStationRule(fuel_card_id=card_id,station_company_id=station_id)
|
|
||||||
rule.allowed=boolv(d,'allowed');rule.use_orlen_last_price=boolv(d,'use_orlen_last_price');rule.discount_net_percent=Decimal(d.get('discount_net_percent') or 0);rule.surcharge_net_per_liter=Decimal(d.get('surcharge_net_per_liter') or 0)
|
|
||||||
db.session.add(rule);db.session.commit();return response(message='Zapisano warunki karty dla stacji')
|
|
||||||
|
|
||||||
@api.get('/stations')
|
|
||||||
@login_required
|
|
||||||
def stations():
|
|
||||||
q=request.args.get('q','').strip();page=request.args.get('page',1,type=int);per=min(request.args.get('per_page',30,type=int),100);query=FuelStationCompany.query.filter_by(active=True)
|
|
||||||
if q:
|
|
||||||
for term in [x for x in q.split() if x]:
|
|
||||||
needle=f'%{term}%'
|
|
||||||
query=query.filter(db.or_(FuelStationCompany.brand_name.ilike(needle),FuelStationCompany.company_name.ilike(needle),FuelStationCompany.nip.ilike(needle),FuelStationCompany.regon.ilike(needle),FuelStationCompany.station_names.ilike(needle)))
|
|
||||||
sort=request.args.get('sort','company_name');direction=request.args.get('direction','asc')
|
|
||||||
allowed={'company_name':FuelStationCompany.brand_name,'station_count':FuelStationCompany.station_count,'nip':FuelStationCompany.nip,'regon':FuelStationCompany.regon,'selected_region':FuelStationCompany.selected_region}
|
|
||||||
col=allowed.get(sort,FuelStationCompany.company_name);query=query.order_by(col.desc() if direction=='desc' else col.asc(),FuelStationCompany.brand_name.asc())
|
|
||||||
p=query.paginate(page=page,per_page=per,error_out=False);return response({'items':[station_dict(x) for x in p.items],'page':p.page,'pages':p.pages,'total':p.total,'sort':sort,'direction':direction})
|
|
||||||
|
|
||||||
@api.get('/stations/<int:station_id>/points')
|
|
||||||
@login_required
|
|
||||||
def station_points(station_id):
|
|
||||||
station=FuelStationCompany.query.get_or_404(station_id)
|
|
||||||
page=max(request.args.get('page',1,type=int),1)
|
|
||||||
per_page=min(max(request.args.get('per_page',50,type=int),1),100)
|
|
||||||
q=(request.args.get('q') or '').strip()
|
|
||||||
query=FuelStationPoint.query.filter_by(station_company_id=station.id)
|
|
||||||
if q:
|
|
||||||
needle=f'%{q}%'
|
|
||||||
query=query.filter(db.or_(FuelStationPoint.station_name.ilike(needle),FuelStationPoint.street.ilike(needle),FuelStationPoint.city.ilike(needle),FuelStationPoint.postal_code.ilike(needle)))
|
|
||||||
query=query.order_by(FuelStationPoint.city.asc(),FuelStationPoint.street.asc(),FuelStationPoint.street_number.asc())
|
|
||||||
result=query.paginate(page=page,per_page=per_page,error_out=False)
|
|
||||||
items=[{'id':x.id,'dkn':x.ure_dkn.split(':',1)[0] if x.ure_dkn else '','name':x.station_name,'address':x.address,'city':x.city,'region':x.region,'coordinates':x.coordinates,'has_petrol':x.has_petrol,'has_diesel':x.has_diesel,'has_lpg':x.has_lpg} for x in result.items]
|
|
||||||
return response({'station':station_dict(station),'items':items,'page':result.page,'pages':result.pages,'total':result.total})
|
|
||||||
|
|
||||||
@api.put('/me/favorite-stations')
|
|
||||||
@login_required
|
|
||||||
def favorite_stations():
|
|
||||||
d=payload();ids=request.form.getlist('station_ids') if request.form else d.get('station_ids',[]);ids=[ids] if isinstance(ids,(str,int)) else ids
|
|
||||||
try: ids=[int(x) for x in ids]
|
|
||||||
except Exception: return fail('Nieprawidłowa lista stacji')
|
|
||||||
if len(ids)>10:return fail('Użytkownik może mieć maksymalnie 10 ulubionych stacji')
|
|
||||||
current_user.favorite_stations=FuelStationCompany.query.filter(FuelStationCompany.id.in_(ids),FuelStationCompany.active.is_(True)).all() if ids else []
|
|
||||||
db.session.commit();return response([station_dict(x) for x in current_user.favorite_stations],'Zapisano ulubione stacje')
|
|
||||||
|
|
||||||
@api.put('/settings/allowed-stations')
|
|
||||||
@role_required('boss','admin')
|
|
||||||
def allowed_stations():
|
|
||||||
d=payload();ids=request.form.getlist('station_ids') if request.form else d.get('station_ids',[]);ids=[ids] if isinstance(ids,(str,int)) else ids
|
|
||||||
try: ids=[int(x) for x in ids]
|
|
||||||
except Exception: return fail('Nieprawidłowa lista stacji')
|
|
||||||
settings=CompanySettings.query.first();settings.allowed_stations=FuelStationCompany.query.filter(FuelStationCompany.id.in_(ids),FuelStationCompany.active.is_(True)).all() if ids else []
|
|
||||||
db.session.commit();return response([station_dict(x) for x in settings.allowed_stations],'Zapisano dozwolone stacje')
|
|
||||||
|
|
||||||
@api.post('/stations/sync')
|
|
||||||
@role_required('boss','admin')
|
|
||||||
def stations_sync():
|
|
||||||
try:
|
|
||||||
companies=aggregate_ure_companies(fetch_ure_stations())
|
|
||||||
added,updated=sync_station_catalog(companies)
|
|
||||||
db.session.commit();return response({'added':added,'updated':updated},f'Katalog URE: dodano {added}, zaktualizowano {updated}')
|
|
||||||
except Exception as exc:db.session.rollback();return fail(f'Nie udało się pobrać danych URE: {exc}',502)
|
|
||||||
|
|
||||||
@api.put('/stations/<int:station_id>')
|
|
||||||
@role_required('boss','admin')
|
|
||||||
def update_station(station_id):
|
|
||||||
s=FuelStationCompany.query.get_or_404(station_id);d=payload()
|
|
||||||
region=None
|
|
||||||
if 'selected_region' in d:
|
|
||||||
region=(d.get('selected_region') or '').strip().lower()
|
|
||||||
if region and region not in s.region_list:return fail('Wybrane województwo nie występuje w danych tej firmy')
|
|
||||||
try:
|
|
||||||
company_id=int(d.get('company_id') or current_user.company_id or 0)
|
|
||||||
except (TypeError,ValueError):
|
|
||||||
return fail('Nieprawidłowa firma')
|
|
||||||
settings=CompanySettings.query.get_or_404(company_id)
|
|
||||||
if current_user.role=='boss' and current_user.company_id!=settings.id:return fail('Brak uprawnień',403)
|
|
||||||
if region is not None: s.selected_region=region
|
|
||||||
s.active=boolv(d,'active');allowed=boolv(d,'allowed')
|
|
||||||
if allowed and s not in settings.allowed_stations: settings.allowed_stations.append(s)
|
|
||||||
if not allowed and s in settings.allowed_stations: settings.allowed_stations.remove(s)
|
|
||||||
db.session.commit();return response(station_dict(s),f'Zapisano ustawienia stacji dla firmy {settings.name}')
|
|
||||||
|
|
||||||
@api.get('/companies')
|
|
||||||
@role_required('boss','admin')
|
|
||||||
def companies_list():
|
|
||||||
return response([{'id':c.id,'name':c.name,'entity_type':c.entity_type,'region':c.region,'active':c.active,'users_count':len(c.users),'cards_count':FuelCard.query.filter_by(company_id=c.id).count()} for c in CompanySettings.query.order_by(CompanySettings.name).all()])
|
|
||||||
|
|
||||||
@api.post('/companies')
|
|
||||||
@role_required('admin')
|
|
||||||
def companies_create():
|
|
||||||
d=payload()
|
|
||||||
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,station_access_mode='all_prefer_favorites')
|
|
||||||
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)
|
|
||||||
except Exception as exc:db.session.rollback();return fail(str(exc))
|
|
||||||
|
|
||||||
@api.delete('/companies/<int:company_id>')
|
|
||||||
@role_required('admin')
|
|
||||||
def companies_delete(company_id):
|
|
||||||
company = CompanySettings.query.get_or_404(company_id)
|
|
||||||
blockers = []
|
|
||||||
users_count = User.query.filter_by(company_id=company.id).count()
|
|
||||||
vehicles_count = Vehicle.query.filter_by(company_id=company.id).count()
|
|
||||||
cards_count = FuelCard.query.filter_by(company_id=company.id).count()
|
|
||||||
if users_count:
|
|
||||||
blockers.append(f'{users_count} użytkowników')
|
|
||||||
if vehicles_count:
|
|
||||||
blockers.append(f'{vehicles_count} pojazdów')
|
|
||||||
if cards_count:
|
|
||||||
blockers.append(f'{cards_count} kart paliwowych')
|
|
||||||
if blockers:
|
|
||||||
return fail(
|
|
||||||
'Nie można usunąć firmy, ponieważ ma przypisane: ' + ', '.join(blockers) +
|
|
||||||
'. Najpierw przenieś lub usuń te dane albo dezaktywuj firmę.',
|
|
||||||
409,
|
|
||||||
)
|
|
||||||
company.favorite_stations.clear()
|
|
||||||
company.allowed_stations.clear()
|
|
||||||
db.session.delete(company)
|
|
||||||
db.session.commit()
|
|
||||||
return response({'redirect':url_for('main.admin_companies')}, 'Usunięto firmę')
|
|
||||||
|
|
||||||
@api.put('/companies/<int:company_id>')
|
|
||||||
@role_required('boss','admin')
|
|
||||||
def companies_update(company_id):
|
|
||||||
c=CompanySettings.query.get_or_404(company_id);d=payload()
|
|
||||||
if current_user.role=='boss' and current_user.company_id!=c.id:return fail('Brak uprawnień',403)
|
|
||||||
try:
|
|
||||||
c.name=(d.get('name') or c.name).strip();c.entity_type=d.get('entity_type',c.entity_type);c.vat_rate=Decimal(d.get('vat_rate') or c.vat_rate);c.vat_deduction_percent=Decimal(d.get('vat_deduction_percent') or c.vat_deduction_percent);c.region=(d.get('region') or c.region).lower();c.invoice_split_day=int(d.get('invoice_split_day') or c.invoice_split_day);c.active=boolv(d,'active')
|
|
||||||
db.session.commit();return response(message='Zapisano firmę')
|
|
||||||
except Exception as exc:db.session.rollback();return fail(str(exc))
|
|
||||||
|
|
||||||
@api.put('/companies/<int:company_id>/favorite-stations')
|
|
||||||
@role_required('boss','admin')
|
|
||||||
def company_favorites(company_id):
|
|
||||||
c=CompanySettings.query.get_or_404(company_id)
|
|
||||||
if current_user.role=='boss' and current_user.company_id!=c.id:return fail('Brak uprawnień',403)
|
|
||||||
d=payload();ids=request.form.getlist('station_ids') if request.form else d.get('station_ids',[]);ids=[ids] if isinstance(ids,(str,int)) else ids
|
|
||||||
try: ids=[int(x) for x in ids]
|
|
||||||
except Exception:return fail('Nieprawidłowa lista stacji')
|
|
||||||
if len(ids)>25:return fail('Firma może mieć maksymalnie 25 ulubionych stacji')
|
|
||||||
c.favorite_stations=FuelStationCompany.query.filter(FuelStationCompany.id.in_(ids),FuelStationCompany.active.is_(True)).all() if ids else []
|
|
||||||
db.session.commit();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()
|
|
||||||
return response({'station_access_mode':mode},'Zapisano politykę dostępu do stacji')
|
|
||||||
|
|
||||||
@api.put('/companies/<int:company_id>/allowed-stations')
|
|
||||||
@role_required('boss','admin')
|
|
||||||
def company_allowed_stations(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();ids=request.form.getlist('station_ids') if request.form else d.get('station_ids',[]);ids=[ids] if isinstance(ids,(str,int)) else ids
|
|
||||||
try: ids=[int(x) for x in ids]
|
|
||||||
except Exception:return fail('Nieprawidłowa lista stacji')
|
|
||||||
company.allowed_stations=FuelStationCompany.query.filter(FuelStationCompany.id.in_(ids),FuelStationCompany.active.is_(True)).all() if ids else []
|
|
||||||
db.session.commit()
|
|
||||||
message='Wszystkie aktywne stacje są dozwolone' if not ids else 'Zapisano zamkniętą listę dozwolonych stacji'
|
|
||||||
return response([station_dict(x) for x in company.allowed_stations],message)
|
|
||||||
|
|
||||||
@api.post('/companies/<int:company_id>/favorite-stations/<int:station_id>/toggle')
|
|
||||||
@role_required('boss','admin')
|
|
||||||
def company_favorite_toggle(company_id,station_id):
|
|
||||||
c=CompanySettings.query.get_or_404(company_id);s=FuelStationCompany.query.get_or_404(station_id)
|
|
||||||
if current_user.role=='boss' and current_user.company_id!=c.id:return fail('Brak uprawnień',403)
|
|
||||||
if s in c.favorite_stations:c.favorite_stations.remove(s);state=False
|
|
||||||
else:
|
|
||||||
if len(c.favorite_stations)>=25:return fail('Firma może mieć maksymalnie 25 ulubionych stacji')
|
|
||||||
c.favorite_stations.append(s);state=True
|
|
||||||
db.session.commit();return response({'favorite':state},'Zmieniono ulubione stacje')
|
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
from flask import Blueprint
|
||||||
|
|
||||||
|
api = Blueprint("api", __name__, url_prefix="/api")
|
||||||
|
|
||||||
|
from . import auth, companies, fuel_cards, fuel_entries, orlen, settings, stations, users, vehicles # noqa: E402,F401
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
from collections import defaultdict
|
||||||
|
from datetime import date, datetime
|
||||||
|
from decimal import Decimal
|
||||||
|
|
||||||
|
from flask import current_app, jsonify, request, url_for
|
||||||
|
from flask_login import current_user, login_required, login_user
|
||||||
|
from sqlalchemy import extract
|
||||||
|
from sqlalchemy.exc import IntegrityError
|
||||||
|
|
||||||
|
from . import api
|
||||||
|
from .common import accessible_vehicles, boolv, fail, payload, response, role_required, station_dict, user_dict, vehicle_dict
|
||||||
|
from .. import THEMES
|
||||||
|
from ..api_tokens import create_access_token
|
||||||
|
from ..extensions import db
|
||||||
|
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 ..station_catalog import sync_station_catalog
|
||||||
|
|
||||||
|
FUEL_TYPES = ("PB95", "PB98", "DIESEL", "LPG")
|
||||||
|
|
||||||
|
@api.post('/auth/login')
|
||||||
|
def login():
|
||||||
|
d=payload(); user=User.query.filter_by(email=(d.get('email') or '').strip().lower()).first()
|
||||||
|
if not user or not user.check_password(d.get('password') or '') or not user.active: return fail('Nieprawidłowy e-mail lub hasło',401)
|
||||||
|
login_user(user); return response({'user':user_dict(user),'redirect':url_for('main.dashboard')},'Zalogowano')
|
||||||
|
|
||||||
|
@api.post('/auth/token')
|
||||||
|
def token_login():
|
||||||
|
d = payload()
|
||||||
|
email = (d.get('email') or d.get('username') or '').strip().lower()
|
||||||
|
user = User.query.filter_by(email=email).first()
|
||||||
|
if not user or not user.check_password(d.get('password') or '') or not user.active:
|
||||||
|
return jsonify({'error':'invalid_grant','error_description':'Nieprawidłowy e-mail lub hasło'}), 401
|
||||||
|
expires_in = int(current_app.config['API_TOKEN_MAX_AGE_SECONDS'])
|
||||||
|
return jsonify({
|
||||||
|
'access_token': create_access_token(user),
|
||||||
|
'token_type': 'Bearer',
|
||||||
|
'expires_in': expires_in,
|
||||||
|
'user': user_dict(user),
|
||||||
|
})
|
||||||
|
|
||||||
|
@api.get('/me')
|
||||||
|
@login_required
|
||||||
|
def me(): return response(user_dict(current_user))
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
from functools import wraps
|
||||||
|
|
||||||
|
from flask import jsonify, request
|
||||||
|
from flask_login import current_user, login_required
|
||||||
|
|
||||||
|
from ..extensions import db
|
||||||
|
from ..models import CompanySettings, FuelStationCompany, User, Vehicle
|
||||||
|
|
||||||
|
|
||||||
|
def response(data=None, message=None, status=200):
|
||||||
|
payload = {"ok": status < 400}
|
||||||
|
if message:
|
||||||
|
payload["message"] = message
|
||||||
|
if data is not None:
|
||||||
|
payload["data"] = data
|
||||||
|
return jsonify(payload), status
|
||||||
|
|
||||||
|
|
||||||
|
def fail(message, status=400):
|
||||||
|
return response(message=message, status=status)
|
||||||
|
|
||||||
|
|
||||||
|
def role_required(*allowed):
|
||||||
|
def decorator(function):
|
||||||
|
@wraps(function)
|
||||||
|
@login_required
|
||||||
|
def wrapped(*args, **kwargs):
|
||||||
|
if current_user.role not in allowed:
|
||||||
|
return fail("Brak uprawnień", 403)
|
||||||
|
return function(*args, **kwargs)
|
||||||
|
return wrapped
|
||||||
|
return decorator
|
||||||
|
|
||||||
|
|
||||||
|
def payload():
|
||||||
|
return request.get_json(silent=True) or request.form
|
||||||
|
|
||||||
|
|
||||||
|
def boolv(data, key):
|
||||||
|
value = data.get(key)
|
||||||
|
return value in (True, 1, "1", "true", "on", "yes")
|
||||||
|
|
||||||
|
|
||||||
|
def accessible_vehicles():
|
||||||
|
if current_user.role == "admin":
|
||||||
|
return Vehicle.query.order_by(Vehicle.name).all()
|
||||||
|
ids = {vehicle.id for vehicle in current_user.owned_vehicles + current_user.shared_vehicles}
|
||||||
|
return Vehicle.query.filter(Vehicle.id.in_(ids)).order_by(Vehicle.name).all() if ids else []
|
||||||
|
|
||||||
|
|
||||||
|
def user_dict(user):
|
||||||
|
return {
|
||||||
|
"id": user.id, "name": user.name, "email": user.email, "role": user.role,
|
||||||
|
"active": user.active, "fuel_card_id": user.fuel_card_id,
|
||||||
|
"fuel_card": user.fuel_card.name if user.fuel_card else None,
|
||||||
|
"company_id": user.company_id, "company": user.company.name if user.company else None,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def vehicle_dict(vehicle):
|
||||||
|
return {
|
||||||
|
"id": vehicle.id, "name": vehicle.name, "make": vehicle.make, "model": vehicle.model,
|
||||||
|
"registration": vehicle.registration, "fuel_type": vehicle.fuel_type,
|
||||||
|
"owner": user_dict(vehicle.owner), "drivers": [user_dict(x) for x in vehicle.drivers],
|
||||||
|
"has_fuel_card": bool(vehicle.fuel_card_id), "fuel_card_id": vehicle.fuel_card_id,
|
||||||
|
"fuel_card": vehicle.fuel_card.name if vehicle.fuel_card else None,
|
||||||
|
"current_odometer": vehicle.current_odometer,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def station_dict(station):
|
||||||
|
settings = CompanySettings.query.first()
|
||||||
|
return {
|
||||||
|
"id": station.id, "brand_name": station.display_name, "company_name": station.company_name,
|
||||||
|
"nip": station.nip, "regon": station.regon, "brands": station.station_name_list,
|
||||||
|
"regions": station.region_list, "selected_region": station.selected_region,
|
||||||
|
"station_count": station.station_count, "has_petrol": station.has_petrol,
|
||||||
|
"has_diesel": station.has_diesel, "has_lpg": station.has_lpg,
|
||||||
|
"use_orlen_last_price": station.use_orlen_last_price, "active": station.active,
|
||||||
|
"favorite": station in current_user.favorite_stations if current_user.is_authenticated else False,
|
||||||
|
"allowed": station in settings.allowed_stations if settings else True,
|
||||||
|
}
|
||||||
@@ -0,0 +1,119 @@
|
|||||||
|
from collections import defaultdict
|
||||||
|
from datetime import date, datetime
|
||||||
|
from decimal import Decimal
|
||||||
|
|
||||||
|
from flask import current_app, jsonify, request, url_for
|
||||||
|
from flask_login import current_user, login_required, login_user
|
||||||
|
from sqlalchemy import extract
|
||||||
|
from sqlalchemy.exc import IntegrityError
|
||||||
|
|
||||||
|
from . import api
|
||||||
|
from .common import accessible_vehicles, boolv, fail, payload, response, role_required, station_dict, user_dict, vehicle_dict
|
||||||
|
from .. import THEMES
|
||||||
|
from ..api_tokens import create_access_token
|
||||||
|
from ..extensions import db
|
||||||
|
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 ..station_catalog import sync_station_catalog
|
||||||
|
|
||||||
|
FUEL_TYPES = ("PB95", "PB98", "DIESEL", "LPG")
|
||||||
|
|
||||||
|
@api.get('/companies')
|
||||||
|
@role_required('boss','admin')
|
||||||
|
def companies_list():
|
||||||
|
return response([{'id':c.id,'name':c.name,'entity_type':c.entity_type,'region':c.region,'active':c.active,'users_count':len(c.users),'cards_count':FuelCard.query.filter_by(company_id=c.id).count()} for c in CompanySettings.query.order_by(CompanySettings.name).all()])
|
||||||
|
|
||||||
|
@api.post('/companies')
|
||||||
|
@role_required('admin')
|
||||||
|
def companies_create():
|
||||||
|
d=payload()
|
||||||
|
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,station_access_mode='all_prefer_favorites')
|
||||||
|
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)
|
||||||
|
except Exception as exc:db.session.rollback();return fail(str(exc))
|
||||||
|
|
||||||
|
@api.delete('/companies/<int:company_id>')
|
||||||
|
@role_required('admin')
|
||||||
|
def companies_delete(company_id):
|
||||||
|
company = CompanySettings.query.get_or_404(company_id)
|
||||||
|
blockers = []
|
||||||
|
users_count = User.query.filter_by(company_id=company.id).count()
|
||||||
|
vehicles_count = Vehicle.query.filter_by(company_id=company.id).count()
|
||||||
|
cards_count = FuelCard.query.filter_by(company_id=company.id).count()
|
||||||
|
if users_count:
|
||||||
|
blockers.append(f'{users_count} użytkowników')
|
||||||
|
if vehicles_count:
|
||||||
|
blockers.append(f'{vehicles_count} pojazdów')
|
||||||
|
if cards_count:
|
||||||
|
blockers.append(f'{cards_count} kart paliwowych')
|
||||||
|
if blockers:
|
||||||
|
return fail(
|
||||||
|
'Nie można usunąć firmy, ponieważ ma przypisane: ' + ', '.join(blockers) +
|
||||||
|
'. Najpierw przenieś lub usuń te dane albo dezaktywuj firmę.',
|
||||||
|
409,
|
||||||
|
)
|
||||||
|
company.favorite_stations.clear()
|
||||||
|
company.allowed_stations.clear()
|
||||||
|
db.session.delete(company)
|
||||||
|
db.session.commit()
|
||||||
|
return response({'redirect':url_for('main.admin_companies')}, 'Usunięto firmę')
|
||||||
|
|
||||||
|
@api.put('/companies/<int:company_id>')
|
||||||
|
@role_required('boss','admin')
|
||||||
|
def companies_update(company_id):
|
||||||
|
c=CompanySettings.query.get_or_404(company_id);d=payload()
|
||||||
|
if current_user.role=='boss' and current_user.company_id!=c.id:return fail('Brak uprawnień',403)
|
||||||
|
try:
|
||||||
|
c.name=(d.get('name') or c.name).strip();c.entity_type=d.get('entity_type',c.entity_type);c.vat_rate=Decimal(d.get('vat_rate') or c.vat_rate);c.vat_deduction_percent=Decimal(d.get('vat_deduction_percent') or c.vat_deduction_percent);c.region=(d.get('region') or c.region).lower();c.invoice_split_day=int(d.get('invoice_split_day') or c.invoice_split_day);c.active=boolv(d,'active')
|
||||||
|
db.session.commit();return response(message='Zapisano firmę')
|
||||||
|
except Exception as exc:db.session.rollback();return fail(str(exc))
|
||||||
|
|
||||||
|
@api.put('/companies/<int:company_id>/favorite-stations')
|
||||||
|
@role_required('boss','admin')
|
||||||
|
def company_favorites(company_id):
|
||||||
|
c=CompanySettings.query.get_or_404(company_id)
|
||||||
|
if current_user.role=='boss' and current_user.company_id!=c.id:return fail('Brak uprawnień',403)
|
||||||
|
d=payload();ids=request.form.getlist('station_ids') if request.form else d.get('station_ids',[]);ids=[ids] if isinstance(ids,(str,int)) else ids
|
||||||
|
try: ids=[int(x) for x in ids]
|
||||||
|
except Exception:return fail('Nieprawidłowa lista stacji')
|
||||||
|
if len(ids)>25:return fail('Firma może mieć maksymalnie 25 ulubionych stacji')
|
||||||
|
c.favorite_stations=FuelStationCompany.query.filter(FuelStationCompany.id.in_(ids),FuelStationCompany.active.is_(True)).all() if ids else []
|
||||||
|
db.session.commit();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()
|
||||||
|
return response({'station_access_mode':mode},'Zapisano politykę dostępu do stacji')
|
||||||
|
|
||||||
|
@api.put('/companies/<int:company_id>/allowed-stations')
|
||||||
|
@role_required('boss','admin')
|
||||||
|
def company_allowed_stations(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();ids=request.form.getlist('station_ids') if request.form else d.get('station_ids',[]);ids=[ids] if isinstance(ids,(str,int)) else ids
|
||||||
|
try: ids=[int(x) for x in ids]
|
||||||
|
except Exception:return fail('Nieprawidłowa lista stacji')
|
||||||
|
company.allowed_stations=FuelStationCompany.query.filter(FuelStationCompany.id.in_(ids),FuelStationCompany.active.is_(True)).all() if ids else []
|
||||||
|
db.session.commit()
|
||||||
|
message='Wszystkie aktywne stacje są dozwolone' if not ids else 'Zapisano zamkniętą listę dozwolonych stacji'
|
||||||
|
return response([station_dict(x) for x in company.allowed_stations],message)
|
||||||
|
|
||||||
|
@api.post('/companies/<int:company_id>/favorite-stations/<int:station_id>/toggle')
|
||||||
|
@role_required('boss','admin')
|
||||||
|
def company_favorite_toggle(company_id,station_id):
|
||||||
|
c=CompanySettings.query.get_or_404(company_id);s=FuelStationCompany.query.get_or_404(station_id)
|
||||||
|
if current_user.role=='boss' and current_user.company_id!=c.id:return fail('Brak uprawnień',403)
|
||||||
|
if s in c.favorite_stations:c.favorite_stations.remove(s);state=False
|
||||||
|
else:
|
||||||
|
if len(c.favorite_stations)>=25:return fail('Firma może mieć maksymalnie 25 ulubionych stacji')
|
||||||
|
c.favorite_stations.append(s);state=True
|
||||||
|
db.session.commit();return response({'favorite':state},'Zmieniono ulubione stacje')
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
from collections import defaultdict
|
||||||
|
from datetime import date, datetime
|
||||||
|
from decimal import Decimal
|
||||||
|
|
||||||
|
from flask import current_app, jsonify, request, url_for
|
||||||
|
from flask_login import current_user, login_required, login_user
|
||||||
|
from sqlalchemy import extract
|
||||||
|
from sqlalchemy.exc import IntegrityError
|
||||||
|
|
||||||
|
from . import api
|
||||||
|
from .common import accessible_vehicles, boolv, fail, payload, response, role_required, station_dict, user_dict, vehicle_dict
|
||||||
|
from .. import THEMES
|
||||||
|
from ..api_tokens import create_access_token
|
||||||
|
from ..extensions import db
|
||||||
|
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 ..station_catalog import sync_station_catalog
|
||||||
|
|
||||||
|
FUEL_TYPES = ("PB95", "PB98", "DIESEL", "LPG")
|
||||||
|
|
||||||
|
@api.get('/fuel-cards')
|
||||||
|
@login_required
|
||||||
|
def get_fuel_cards():
|
||||||
|
rows=FuelCard.query.order_by(FuelCard.name).all()
|
||||||
|
return response([{'id':x.id,'name':x.name,'provider':x.provider,'active':x.active,'description':x.description} for x in rows])
|
||||||
|
|
||||||
|
@api.post('/fuel-cards')
|
||||||
|
@role_required('boss','admin')
|
||||||
|
def create_fuel_card():
|
||||||
|
d=payload()
|
||||||
|
try:
|
||||||
|
company_id=int(d.get('company_id') or current_user.company_id or CompanySettings.query.first().id)
|
||||||
|
if current_user.role=='boss' and company_id!=current_user.company_id:return fail('Brak uprawnień',403)
|
||||||
|
card=FuelCard(name=d['name'].strip(),provider=d['provider'].strip(),description=(d.get('description') or '').strip() or None,active=True,company_id=company_id)
|
||||||
|
db.session.add(card);db.session.commit();return response({'id':card.id},'Dodano kartę paliwową',201)
|
||||||
|
except Exception as exc:db.session.rollback();return fail(str(getattr(exc,'orig',exc)))
|
||||||
|
|
||||||
|
@api.put('/fuel-cards/<int:card_id>')
|
||||||
|
@role_required('boss','admin')
|
||||||
|
def update_fuel_card(card_id):
|
||||||
|
card=FuelCard.query.get_or_404(card_id);d=payload();card.name=d.get('name',card.name).strip();card.provider=d.get('provider',card.provider).strip();card.description=(d.get('description') or '').strip() or None;card.active=boolv(d,'active');db.session.commit();return response(message='Zapisano kartę')
|
||||||
|
|
||||||
|
@api.delete('/fuel-cards/<int:card_id>')
|
||||||
|
@role_required('boss','admin')
|
||||||
|
def delete_fuel_card(card_id):
|
||||||
|
card = FuelCard.query.get_or_404(card_id)
|
||||||
|
if current_user.role == 'boss' and card.company_id != current_user.company_id:
|
||||||
|
return fail('Brak uprawnień', 403)
|
||||||
|
|
||||||
|
users_count = User.query.filter_by(fuel_card_id=card.id).count()
|
||||||
|
vehicles_count = Vehicle.query.filter_by(fuel_card_id=card.id).count()
|
||||||
|
entries_count = FuelEntry.query.filter_by(fuel_card_id=card.id).count()
|
||||||
|
blockers = []
|
||||||
|
if users_count:
|
||||||
|
blockers.append(f'{users_count} użytkowników')
|
||||||
|
if vehicles_count:
|
||||||
|
blockers.append(f'{vehicles_count} pojazdów')
|
||||||
|
if entries_count:
|
||||||
|
blockers.append(f'{entries_count} tankowań')
|
||||||
|
if blockers:
|
||||||
|
return fail(
|
||||||
|
'Nie można usunąć karty, ponieważ jest przypisana do: ' + ', '.join(blockers) +
|
||||||
|
'. Najpierw odłącz kartę od użytkowników i pojazdów. Karty użytej w tankowaniach nie można usunąć; możesz ją wyłączyć.',
|
||||||
|
409,
|
||||||
|
)
|
||||||
|
|
||||||
|
company_id = card.company_id
|
||||||
|
db.session.delete(card)
|
||||||
|
db.session.commit()
|
||||||
|
return response(
|
||||||
|
{'redirect':url_for('main.admin_companies', company_id=company_id)},
|
||||||
|
'Usunięto kartę flotową',
|
||||||
|
)
|
||||||
|
|
||||||
|
@api.put('/fuel-cards/<int:card_id>/stations/<int:station_id>')
|
||||||
|
@role_required('boss','admin')
|
||||||
|
def update_card_station_rule(card_id,station_id):
|
||||||
|
FuelCard.query.get_or_404(card_id);FuelStationCompany.query.get_or_404(station_id);d=payload()
|
||||||
|
rule=FuelCardStationRule.query.filter_by(fuel_card_id=card_id,station_company_id=station_id).first() or FuelCardStationRule(fuel_card_id=card_id,station_company_id=station_id)
|
||||||
|
rule.allowed=boolv(d,'allowed');rule.use_orlen_last_price=boolv(d,'use_orlen_last_price');rule.discount_net_percent=Decimal(d.get('discount_net_percent') or 0);rule.surcharge_net_per_liter=Decimal(d.get('surcharge_net_per_liter') or 0)
|
||||||
|
db.session.add(rule);db.session.commit();return response(message='Zapisano warunki karty dla stacji')
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
from collections import defaultdict
|
||||||
|
from datetime import date, datetime
|
||||||
|
from decimal import Decimal
|
||||||
|
|
||||||
|
from flask import current_app, jsonify, request, url_for
|
||||||
|
from flask_login import current_user, login_required, login_user
|
||||||
|
from sqlalchemy import extract
|
||||||
|
from sqlalchemy.exc import IntegrityError
|
||||||
|
|
||||||
|
from . import api
|
||||||
|
from .common import accessible_vehicles, boolv, fail, payload, response, role_required, station_dict, user_dict, vehicle_dict
|
||||||
|
from .. import THEMES
|
||||||
|
from ..api_tokens import create_access_token
|
||||||
|
from ..extensions import db
|
||||||
|
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 ..station_catalog import sync_station_catalog
|
||||||
|
|
||||||
|
FUEL_TYPES = ("PB95", "PB98", "DIESEL", "LPG")
|
||||||
|
|
||||||
|
@api.post('/fuel-entries')
|
||||||
|
@login_required
|
||||||
|
def create_fuel_entry():
|
||||||
|
d=payload(); vehicles=accessible_vehicles()
|
||||||
|
try: vehicle=next((v for v in vehicles if v.id==int(d['vehicle_id'])),None)
|
||||||
|
except Exception: vehicle=None
|
||||||
|
if not vehicle:return fail('Brak dostępu do pojazdu',403)
|
||||||
|
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
|
||||||
|
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:
|
||||||
|
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
|
||||||
|
try:
|
||||||
|
p=fetch_orlen_price(d['fuel_type'],fueled_at.date(),region);wholesale=p['price_per_liter'];source=p['source']
|
||||||
|
except Exception: pass
|
||||||
|
card_id=int(d['fuel_card_id']) if d.get('fuel_card_id') else (vehicle.fuel_card_id or current_user.fuel_card_id)
|
||||||
|
card=db.session.get(FuelCard,card_id) if card_id else None
|
||||||
|
used_card=boolv(d,'used_fuel_card') and bool(card)
|
||||||
|
if used_card and station_company:
|
||||||
|
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)
|
||||||
|
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)
|
||||||
|
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)
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
from collections import defaultdict
|
||||||
|
from datetime import date, datetime
|
||||||
|
from decimal import Decimal
|
||||||
|
|
||||||
|
from flask import current_app, jsonify, request, url_for
|
||||||
|
from flask_login import current_user, login_required, login_user
|
||||||
|
from sqlalchemy import extract
|
||||||
|
from sqlalchemy.exc import IntegrityError
|
||||||
|
|
||||||
|
from . import api
|
||||||
|
from .common import accessible_vehicles, boolv, fail, payload, response, role_required, station_dict, user_dict, vehicle_dict
|
||||||
|
from .. import THEMES
|
||||||
|
from ..api_tokens import create_access_token
|
||||||
|
from ..extensions import db
|
||||||
|
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 ..station_catalog import sync_station_catalog
|
||||||
|
|
||||||
|
FUEL_TYPES = ("PB95", "PB98", "DIESEL", "LPG")
|
||||||
|
|
||||||
|
@api.get('/orlen/prices')
|
||||||
|
@login_required
|
||||||
|
def orlen_prices():
|
||||||
|
year=request.args.get('year',type=int) or date.today().year; fuels=[x.upper() for x in request.args.getlist('fuel') if x.upper() in FUEL_TYPES] or ['PB95']; regions=[x.lower() for x in request.args.getlist('region') if x.lower() in POLISH_REGIONS]
|
||||||
|
rows=OrlenPrice.query.filter(OrlenPrice.fuel_type.in_(fuels),extract('year',OrlenPrice.effective_date)==year).order_by(OrlenPrice.effective_date).all()
|
||||||
|
return response([{'fuel_type':r.fuel_type,'date':r.effective_date.isoformat(),'price_per_liter':float(r.price_per_liter),'region':r.region,'source':r.source} for r in rows if r.fuel_type!='LPG' or not regions or r.region in regions])
|
||||||
|
|
||||||
|
@api.post('/orlen/sync')
|
||||||
|
@role_required('boss','admin')
|
||||||
|
def orlen_sync():
|
||||||
|
d=payload(); fuels=d.get('fuels',[]); fuels=[fuels] if isinstance(fuels,str) else fuels; fuels=[f.upper() for f in fuels if f.upper() in FUEL_TYPES]; year=int(d.get('year',date.today().year))
|
||||||
|
regions=d.get('regions',[]); regions=[regions] if isinstance(regions,str) else regions; regions=[str(r).strip().lower() for r in regions if str(r).strip().lower() in POLISH_REGIONS]
|
||||||
|
if not fuels:return fail('Wybierz co najmniej jedno paliwo')
|
||||||
|
settings=CompanySettings.query.first(); result={}; total=0
|
||||||
|
try:
|
||||||
|
for fuel in fuels:
|
||||||
|
added=updated=0
|
||||||
|
for row in fetch_orlen_range(fuel,date(year,1,1),min(date(year,12,31),date.today()),'all' if fuel=='LPG' else settings.region):
|
||||||
|
if fuel=='LPG' and regions and row.get('region') not in regions: continue
|
||||||
|
x=OrlenPrice.query.filter_by(fuel_type=row['fuel_type'],effective_date=row['effective_date'],region=row['region']).first()
|
||||||
|
if x:
|
||||||
|
for k,v in row.items():setattr(x,k,v)
|
||||||
|
x.fetched_at=datetime.utcnow();updated+=1
|
||||||
|
else:db.session.add(OrlenPrice(**row));added+=1
|
||||||
|
result[fuel]={'added':added,'updated':updated};total+=added+updated
|
||||||
|
db.session.commit();return response(result,f'Przetworzono {total} rekordów')
|
||||||
|
except Exception as exc:db.session.rollback();return fail(str(exc),502)
|
||||||
|
|
||||||
|
@api.get('/orlen/preview')
|
||||||
|
@login_required
|
||||||
|
def orlen_preview():
|
||||||
|
try:return response(fetch_orlen_price(request.args['fuel_type'],datetime.fromisoformat(request.args['date']).date(),request.args.get('region') or CompanySettings.query.first().region))
|
||||||
|
except Exception as exc:return fail(str(exc),502)
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
from collections import defaultdict
|
||||||
|
from datetime import date, datetime
|
||||||
|
from decimal import Decimal
|
||||||
|
|
||||||
|
from flask import current_app, jsonify, request, url_for
|
||||||
|
from flask_login import current_user, login_required, login_user
|
||||||
|
from sqlalchemy import extract
|
||||||
|
from sqlalchemy.exc import IntegrityError
|
||||||
|
|
||||||
|
from . import api
|
||||||
|
from .common import accessible_vehicles, boolv, fail, payload, response, role_required, station_dict, user_dict, vehicle_dict
|
||||||
|
from .. import THEMES
|
||||||
|
from ..api_tokens import create_access_token
|
||||||
|
from ..extensions import db
|
||||||
|
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 ..station_catalog import sync_station_catalog
|
||||||
|
|
||||||
|
FUEL_TYPES = ("PB95", "PB98", "DIESEL", "LPG")
|
||||||
|
|
||||||
|
@api.get('/settings')
|
||||||
|
@role_required('boss','admin')
|
||||||
|
def get_settings():
|
||||||
|
s=CompanySettings.query.first();return response({'name':s.name,'entity_type':s.entity_type,'vat_rate':float(s.vat_rate),'vat_deduction_percent':float(s.vat_deduction_percent),'region':s.region,'invoice_split_day':s.invoice_split_day,'invoice_split_enabled':(db.session.get(AppSetting,'invoice_split_enabled').value=='1')})
|
||||||
|
|
||||||
|
@api.put('/settings')
|
||||||
|
@role_required('boss','admin')
|
||||||
|
def update_settings():
|
||||||
|
d=payload();s=CompanySettings.query.first()
|
||||||
|
try:
|
||||||
|
s.name=d['name'];s.entity_type=d['entity_type'];s.vat_rate=Decimal(d['vat_rate']);s.vat_deduction_percent=Decimal(d['vat_deduction_percent']);s.region=d['region'].strip().lower();s.invoice_split_day=int(d.get('invoice_split_day') or 15)
|
||||||
|
db.session.merge(AppSetting(key='invoice_split_enabled',value='1' if boolv(d,'invoice_split_enabled') else '0'));db.session.commit();return response(message='Zapisano ustawienia')
|
||||||
|
except Exception as exc:db.session.rollback();return fail(str(exc))
|
||||||
|
|
||||||
|
|
||||||
|
@api.get('/app-settings')
|
||||||
|
@role_required('boss','admin')
|
||||||
|
def get_app_settings():
|
||||||
|
return response({'theme':(db.session.get(AppSetting,'theme') or AppSetting(value='bootstrap')).value})
|
||||||
|
|
||||||
|
@api.put('/app-settings')
|
||||||
|
@role_required('boss','admin')
|
||||||
|
def update_app_settings():
|
||||||
|
d=payload();theme=d.get('theme','bootstrap')
|
||||||
|
if theme not in THEMES:return fail('Nieprawidłowy motyw')
|
||||||
|
db.session.merge(AppSetting(key='theme',value=theme));db.session.commit();return response(message='Zapisano ustawienia aplikacji')
|
||||||
@@ -0,0 +1,96 @@
|
|||||||
|
from collections import defaultdict
|
||||||
|
from datetime import date, datetime
|
||||||
|
from decimal import Decimal
|
||||||
|
|
||||||
|
from flask import current_app, jsonify, request, url_for
|
||||||
|
from flask_login import current_user, login_required, login_user
|
||||||
|
from sqlalchemy import extract
|
||||||
|
from sqlalchemy.exc import IntegrityError
|
||||||
|
|
||||||
|
from . import api
|
||||||
|
from .common import accessible_vehicles, boolv, fail, payload, response, role_required, station_dict, user_dict, vehicle_dict
|
||||||
|
from .. import THEMES
|
||||||
|
from ..api_tokens import create_access_token
|
||||||
|
from ..extensions import db
|
||||||
|
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 ..station_catalog import sync_station_catalog
|
||||||
|
|
||||||
|
FUEL_TYPES = ("PB95", "PB98", "DIESEL", "LPG")
|
||||||
|
|
||||||
|
@api.get('/stations')
|
||||||
|
@login_required
|
||||||
|
def stations():
|
||||||
|
q=request.args.get('q','').strip();page=request.args.get('page',1,type=int);per=min(request.args.get('per_page',30,type=int),100);query=FuelStationCompany.query.filter_by(active=True)
|
||||||
|
if q:
|
||||||
|
for term in [x for x in q.split() if x]:
|
||||||
|
needle=f'%{term}%'
|
||||||
|
query=query.filter(db.or_(FuelStationCompany.brand_name.ilike(needle),FuelStationCompany.company_name.ilike(needle),FuelStationCompany.nip.ilike(needle),FuelStationCompany.regon.ilike(needle),FuelStationCompany.station_names.ilike(needle)))
|
||||||
|
sort=request.args.get('sort','company_name');direction=request.args.get('direction','asc')
|
||||||
|
allowed={'company_name':FuelStationCompany.brand_name,'station_count':FuelStationCompany.station_count,'nip':FuelStationCompany.nip,'regon':FuelStationCompany.regon,'selected_region':FuelStationCompany.selected_region}
|
||||||
|
col=allowed.get(sort,FuelStationCompany.company_name);query=query.order_by(col.desc() if direction=='desc' else col.asc(),FuelStationCompany.brand_name.asc())
|
||||||
|
p=query.paginate(page=page,per_page=per,error_out=False);return response({'items':[station_dict(x) for x in p.items],'page':p.page,'pages':p.pages,'total':p.total,'sort':sort,'direction':direction})
|
||||||
|
|
||||||
|
@api.get('/stations/<int:station_id>/points')
|
||||||
|
@login_required
|
||||||
|
def station_points(station_id):
|
||||||
|
station=FuelStationCompany.query.get_or_404(station_id)
|
||||||
|
page=max(request.args.get('page',1,type=int),1)
|
||||||
|
per_page=min(max(request.args.get('per_page',50,type=int),1),100)
|
||||||
|
q=(request.args.get('q') or '').strip()
|
||||||
|
query=FuelStationPoint.query.filter_by(station_company_id=station.id)
|
||||||
|
if q:
|
||||||
|
needle=f'%{q}%'
|
||||||
|
query=query.filter(db.or_(FuelStationPoint.station_name.ilike(needle),FuelStationPoint.street.ilike(needle),FuelStationPoint.city.ilike(needle),FuelStationPoint.postal_code.ilike(needle)))
|
||||||
|
query=query.order_by(FuelStationPoint.city.asc(),FuelStationPoint.street.asc(),FuelStationPoint.street_number.asc())
|
||||||
|
result=query.paginate(page=page,per_page=per_page,error_out=False)
|
||||||
|
items=[{'id':x.id,'dkn':x.ure_dkn.split(':',1)[0] if x.ure_dkn else '','name':x.station_name,'address':x.address,'city':x.city,'region':x.region,'coordinates':x.coordinates,'has_petrol':x.has_petrol,'has_diesel':x.has_diesel,'has_lpg':x.has_lpg} for x in result.items]
|
||||||
|
return response({'station':station_dict(station),'items':items,'page':result.page,'pages':result.pages,'total':result.total})
|
||||||
|
|
||||||
|
@api.put('/me/favorite-stations')
|
||||||
|
@login_required
|
||||||
|
def favorite_stations():
|
||||||
|
d=payload();ids=request.form.getlist('station_ids') if request.form else d.get('station_ids',[]);ids=[ids] if isinstance(ids,(str,int)) else ids
|
||||||
|
try: ids=[int(x) for x in ids]
|
||||||
|
except Exception: return fail('Nieprawidłowa lista stacji')
|
||||||
|
if len(ids)>10:return fail('Użytkownik może mieć maksymalnie 10 ulubionych stacji')
|
||||||
|
current_user.favorite_stations=FuelStationCompany.query.filter(FuelStationCompany.id.in_(ids),FuelStationCompany.active.is_(True)).all() if ids else []
|
||||||
|
db.session.commit();return response([station_dict(x) for x in current_user.favorite_stations],'Zapisano ulubione stacje')
|
||||||
|
|
||||||
|
@api.put('/settings/allowed-stations')
|
||||||
|
@role_required('boss','admin')
|
||||||
|
def allowed_stations():
|
||||||
|
d=payload();ids=request.form.getlist('station_ids') if request.form else d.get('station_ids',[]);ids=[ids] if isinstance(ids,(str,int)) else ids
|
||||||
|
try: ids=[int(x) for x in ids]
|
||||||
|
except Exception: return fail('Nieprawidłowa lista stacji')
|
||||||
|
settings=CompanySettings.query.first();settings.allowed_stations=FuelStationCompany.query.filter(FuelStationCompany.id.in_(ids),FuelStationCompany.active.is_(True)).all() if ids else []
|
||||||
|
db.session.commit();return response([station_dict(x) for x in settings.allowed_stations],'Zapisano dozwolone stacje')
|
||||||
|
|
||||||
|
@api.post('/stations/sync')
|
||||||
|
@role_required('boss','admin')
|
||||||
|
def stations_sync():
|
||||||
|
try:
|
||||||
|
companies=aggregate_ure_companies(fetch_ure_stations())
|
||||||
|
added,updated=sync_station_catalog(companies)
|
||||||
|
db.session.commit();return response({'added':added,'updated':updated},f'Katalog URE: dodano {added}, zaktualizowano {updated}')
|
||||||
|
except Exception as exc:db.session.rollback();return fail(f'Nie udało się pobrać danych URE: {exc}',502)
|
||||||
|
|
||||||
|
@api.put('/stations/<int:station_id>')
|
||||||
|
@role_required('boss','admin')
|
||||||
|
def update_station(station_id):
|
||||||
|
s=FuelStationCompany.query.get_or_404(station_id);d=payload()
|
||||||
|
region=None
|
||||||
|
if 'selected_region' in d:
|
||||||
|
region=(d.get('selected_region') or '').strip().lower()
|
||||||
|
if region and region not in s.region_list:return fail('Wybrane województwo nie występuje w danych tej firmy')
|
||||||
|
try:
|
||||||
|
company_id=int(d.get('company_id') or current_user.company_id or 0)
|
||||||
|
except (TypeError,ValueError):
|
||||||
|
return fail('Nieprawidłowa firma')
|
||||||
|
settings=CompanySettings.query.get_or_404(company_id)
|
||||||
|
if current_user.role=='boss' and current_user.company_id!=settings.id:return fail('Brak uprawnień',403)
|
||||||
|
if region is not None: s.selected_region=region
|
||||||
|
s.active=boolv(d,'active');allowed=boolv(d,'allowed')
|
||||||
|
if allowed and s not in settings.allowed_stations: settings.allowed_stations.append(s)
|
||||||
|
if not allowed and s in settings.allowed_stations: settings.allowed_stations.remove(s)
|
||||||
|
db.session.commit();return response(station_dict(s),f'Zapisano ustawienia stacji dla firmy {settings.name}')
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
from collections import defaultdict
|
||||||
|
from datetime import date, datetime
|
||||||
|
from decimal import Decimal
|
||||||
|
|
||||||
|
from flask import current_app, jsonify, request, url_for
|
||||||
|
from flask_login import current_user, login_required, login_user
|
||||||
|
from sqlalchemy import extract
|
||||||
|
from sqlalchemy.exc import IntegrityError
|
||||||
|
|
||||||
|
from . import api
|
||||||
|
from .common import accessible_vehicles, boolv, fail, payload, response, role_required, station_dict, user_dict, vehicle_dict
|
||||||
|
from .. import THEMES
|
||||||
|
from ..api_tokens import create_access_token
|
||||||
|
from ..extensions import db
|
||||||
|
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 ..station_catalog import sync_station_catalog
|
||||||
|
|
||||||
|
FUEL_TYPES = ("PB95", "PB98", "DIESEL", "LPG")
|
||||||
|
|
||||||
|
@api.get('/users')
|
||||||
|
@role_required('admin')
|
||||||
|
def users():
|
||||||
|
page=request.args.get('page',1,type=int);per=min(request.args.get('per_page',25,type=int),100);q=request.args.get('q','').strip();query=User.query
|
||||||
|
if q: query=query.filter(db.or_(User.name.ilike(f'%{q}%'),User.email.ilike(f'%{q}%')))
|
||||||
|
p=query.order_by(User.name).paginate(page=page,per_page=per,error_out=False);return response({'items':[user_dict(x) for x in p.items],'page':p.page,'pages':p.pages,'total':p.total})
|
||||||
|
|
||||||
|
@api.post('/users')
|
||||||
|
@role_required('boss','admin')
|
||||||
|
def create_user():
|
||||||
|
d=payload()
|
||||||
|
try:
|
||||||
|
if len(d.get('password',''))<8:raise ValueError('Hasło musi mieć co najmniej 8 znaków')
|
||||||
|
company_id=int(d.get('company_id') or current_user.company_id or CompanySettings.query.first().id)
|
||||||
|
if current_user.role=='boss' and company_id!=current_user.company_id:return fail('Brak uprawnień',403)
|
||||||
|
u=User(name=d['user_name'].strip(),email=d['email'].strip().lower(),role=d.get('role','user'),company_id=company_id);u.set_password(d['password']);db.session.add(u);db.session.commit();return response(user_dict(u),'Dodano użytkownika',201)
|
||||||
|
except Exception as exc:db.session.rollback();return fail(str(getattr(exc,'orig',exc)))
|
||||||
|
|
||||||
|
@api.put('/users/<int:user_id>')
|
||||||
|
@role_required('admin')
|
||||||
|
def update_user(user_id):
|
||||||
|
u=User.query.get_or_404(user_id);d=payload()
|
||||||
|
try:
|
||||||
|
u.name=d['name'].strip();u.email=d['email'].strip().lower();u.role=d['role'];u.active=boolv(d,'active');u.company_id=int(d.get('company_id') or u.company_id or CompanySettings.query.first().id);u.fuel_card_id=int(d['fuel_card_id']) if d.get('fuel_card_id') else None;pw=d.get('password','')
|
||||||
|
if pw:
|
||||||
|
if len(pw)<8:raise ValueError('Nowe hasło musi mieć co najmniej 8 znaków')
|
||||||
|
u.set_password(pw)
|
||||||
|
db.session.commit();return response(user_dict(u),'Zaktualizowano użytkownika')
|
||||||
|
except Exception as exc:db.session.rollback();return fail(str(getattr(exc,'orig',exc)))
|
||||||
|
|
||||||
|
@api.delete('/users/<int:user_id>')
|
||||||
|
@role_required('admin')
|
||||||
|
def delete_user(user_id):
|
||||||
|
user = User.query.get_or_404(user_id)
|
||||||
|
if user.id == current_user.id:
|
||||||
|
return fail('Nie możesz usunąć własnego konta administratora', 400)
|
||||||
|
if user.role == 'admin':
|
||||||
|
remaining_active_admins = User.query.filter(
|
||||||
|
User.role == 'admin', User.active.is_(True), User.id != user.id
|
||||||
|
).count()
|
||||||
|
if remaining_active_admins < 1:
|
||||||
|
return fail('Nie można usunąć ostatniego aktywnego administratora', 409)
|
||||||
|
if user.owned_vehicles:
|
||||||
|
return fail('Najpierw przypisz pojazdy tego użytkownika innemu właścicielowi', 409)
|
||||||
|
if FuelEntry.query.filter_by(user_id=user.id).first():
|
||||||
|
return fail('Użytkownik ma historię tankowań i nie może zostać usunięty. Możesz go dezaktywować.', 409)
|
||||||
|
user.shared_vehicles.clear()
|
||||||
|
user.favorite_stations.clear()
|
||||||
|
db.session.delete(user)
|
||||||
|
db.session.commit()
|
||||||
|
return response(message='Usunięto użytkownika')
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
from collections import defaultdict
|
||||||
|
from datetime import date, datetime
|
||||||
|
from decimal import Decimal
|
||||||
|
|
||||||
|
from flask import current_app, jsonify, request, url_for
|
||||||
|
from flask_login import current_user, login_required, login_user
|
||||||
|
from sqlalchemy import extract
|
||||||
|
from sqlalchemy.exc import IntegrityError
|
||||||
|
|
||||||
|
from . import api
|
||||||
|
from .common import accessible_vehicles, boolv, fail, payload, response, role_required, station_dict, user_dict, vehicle_dict
|
||||||
|
from .. import THEMES
|
||||||
|
from ..api_tokens import create_access_token
|
||||||
|
from ..extensions import db
|
||||||
|
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 ..station_catalog import sync_station_catalog
|
||||||
|
|
||||||
|
FUEL_TYPES = ("PB95", "PB98", "DIESEL", "LPG")
|
||||||
|
|
||||||
|
@api.get('/vehicles')
|
||||||
|
@login_required
|
||||||
|
def get_vehicles(): return response([vehicle_dict(v) for v in accessible_vehicles()])
|
||||||
|
|
||||||
|
@api.post('/vehicles')
|
||||||
|
@login_required
|
||||||
|
def create_vehicle():
|
||||||
|
d=payload()
|
||||||
|
try:
|
||||||
|
card_id=int(d['fuel_card_id']) if d.get('fuel_card_id') else None
|
||||||
|
owner_id=int(d.get('owner_id') or current_user.id);owner=db.session.get(User,owner_id)
|
||||||
|
if not owner: return fail('Nie znaleziono właściciela',404)
|
||||||
|
if current_user.role not in ('boss','admin') and owner.id!=current_user.id:return fail('Brak uprawnień',403)
|
||||||
|
if current_user.role=='boss' and owner.company_id!=current_user.company_id:return fail('Użytkownik jest w innej firmie',403)
|
||||||
|
v=Vehicle(name=d['name'].strip(),make=d['make'].strip(),model=d['model'].strip(),registration=d['registration'].strip().upper(),fuel_type=d['fuel_type'],owner_id=owner.id,company_id=owner.company_id,has_fuel_card=bool(card_id),fuel_card_id=card_id,current_odometer=int(d.get('current_odometer') or 0))
|
||||||
|
db.session.add(v); db.session.commit(); return response(vehicle_dict(v),'Dodano pojazd',201)
|
||||||
|
except (KeyError,ValueError,IntegrityError) as exc: db.session.rollback(); return fail(str(getattr(exc,'orig',exc)),400)
|
||||||
|
|
||||||
|
@api.put('/vehicles/<int:vehicle_id>/fuel-card')
|
||||||
|
@login_required
|
||||||
|
def assign_vehicle_fuel_card(vehicle_id):
|
||||||
|
v=Vehicle.query.get_or_404(vehicle_id)
|
||||||
|
if current_user.role not in ('boss','admin') and v.owner_id!=current_user.id:return fail('Brak uprawnień',403)
|
||||||
|
d=payload();card_id=int(d['fuel_card_id']) if d.get('fuel_card_id') else None
|
||||||
|
card=db.session.get(FuelCard,card_id) if card_id else None
|
||||||
|
if card_id and not card:return fail('Nie znaleziono karty paliwowej',404)
|
||||||
|
if card and v.company_id and card.company_id!=v.company_id:return fail('Karta należy do innej firmy',400)
|
||||||
|
v.fuel_card_id=card_id;v.has_fuel_card=bool(card_id);db.session.commit();return response(vehicle_dict(v),'Zapisano kartę pojazdu')
|
||||||
|
|
||||||
|
@api.post('/vehicles/<int:vehicle_id>/shares')
|
||||||
|
@login_required
|
||||||
|
def share_vehicle(vehicle_id):
|
||||||
|
v=Vehicle.query.get_or_404(vehicle_id)
|
||||||
|
if current_user.role!='admin' and v.owner_id!=current_user.id: return fail('Brak uprawnień',403)
|
||||||
|
d=payload(); u=User.query.filter_by(email=(d.get('email') or '').strip().lower()).first()
|
||||||
|
if not u:return fail('Nie znaleziono użytkownika',404)
|
||||||
|
if u not in v.drivers:v.drivers.append(u);db.session.commit();return response(vehicle_dict(v),'Udostępniono pojazd')
|
||||||
|
|
||||||
|
@api.delete('/vehicles/<int:vehicle_id>/shares/<int:user_id>')
|
||||||
|
@login_required
|
||||||
|
def unshare_vehicle(vehicle_id,user_id):
|
||||||
|
v=Vehicle.query.get_or_404(vehicle_id)
|
||||||
|
if current_user.role!='admin' and v.owner_id!=current_user.id:return fail('Brak uprawnień',403)
|
||||||
|
u=User.query.get_or_404(user_id)
|
||||||
|
if u in v.drivers:v.drivers.remove(u);db.session.commit();return response(vehicle_dict(v),'Odebrano dostęp')
|
||||||
|
|
||||||
|
@api.put('/vehicles/<int:vehicle_id>/card-policy')
|
||||||
|
@login_required
|
||||||
|
def card_policy(vehicle_id):
|
||||||
|
v=Vehicle.query.get_or_404(vehicle_id)
|
||||||
|
if current_user.role!='admin' and v.owner_id!=current_user.id:return fail('Brak uprawnień',403)
|
||||||
|
d=payload();p=v.fuel_card_policy or FuelCardPolicy(vehicle=v)
|
||||||
|
p.use_orlen_last_price=False;p.discount_percent=Decimal(d.get('discount_percent') or 0);p.surcharge_per_liter=Decimal(d.get('surcharge_per_liter') or 0);p.description=(d.get('description') or '').strip() or None
|
||||||
|
db.session.add(p);db.session.commit();return response(message='Zapisano zasady rozliczeń')
|
||||||
+48
-9
@@ -1,19 +1,58 @@
|
|||||||
from flask import Blueprint, render_template, request, redirect, url_for, flash
|
from urllib.parse import urljoin, urlparse
|
||||||
from flask_login import login_user, logout_user, login_required
|
from flask import Blueprint, flash, redirect, render_template, request, url_for
|
||||||
|
from flask_login import current_user, login_required, login_user, logout_user
|
||||||
from .models import User
|
from .models import User
|
||||||
|
|
||||||
auth = Blueprint("auth", __name__)
|
auth = Blueprint("auth", __name__)
|
||||||
|
|
||||||
|
def is_safe_redirect_url(target: str | None) -> bool:
|
||||||
|
if not target:
|
||||||
|
return False
|
||||||
|
|
||||||
|
host_url = urlparse(request.host_url)
|
||||||
|
redirect_url = urlparse(urljoin(request.host_url, target))
|
||||||
|
|
||||||
|
return (
|
||||||
|
redirect_url.scheme in {"http", "https"}
|
||||||
|
and redirect_url.netloc == host_url.netloc
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@auth.route("/login", methods=["GET", "POST"])
|
@auth.route("/login", methods=["GET", "POST"])
|
||||||
def login():
|
def login():
|
||||||
if request.method == "POST":
|
if current_user.is_authenticated:
|
||||||
user = User.query.filter_by(email=request.form["email"].strip().lower()).first()
|
return redirect(url_for("main.dashboard"))
|
||||||
if user and user.check_password(request.form["password"]):
|
|
||||||
login_user(user); return redirect(url_for("main.dashboard"))
|
if request.method == "GET":
|
||||||
flash("Nieprawidłowy e-mail lub hasło", "danger")
|
|
||||||
return render_template("login.html")
|
return render_template("login.html")
|
||||||
|
|
||||||
@auth.route("/logout")
|
email = request.form.get("email", "").strip().lower()
|
||||||
|
password = request.form.get("password", "")
|
||||||
|
remember = request.form.get("remember") == "on"
|
||||||
|
|
||||||
|
if not email or not password:
|
||||||
|
flash("Podaj adres e-mail i hasło.", "warning")
|
||||||
|
return render_template("login.html"), 400
|
||||||
|
|
||||||
|
user = User.query.filter_by(email=email).first()
|
||||||
|
|
||||||
|
if user is None or not user.check_password(password):
|
||||||
|
flash("Nieprawidłowy e-mail lub hasło.", "danger")
|
||||||
|
return render_template("login.html"), 401
|
||||||
|
|
||||||
|
login_user(user, remember=remember)
|
||||||
|
|
||||||
|
next_url = request.args.get("next")
|
||||||
|
|
||||||
|
if is_safe_redirect_url(next_url):
|
||||||
|
return redirect(next_url)
|
||||||
|
|
||||||
|
return redirect(url_for("main.dashboard"))
|
||||||
|
|
||||||
|
|
||||||
|
@auth.route("/logout", methods=["POST"])
|
||||||
@login_required
|
@login_required
|
||||||
def logout():
|
def logout():
|
||||||
logout_user(); return redirect(url_for("auth.login"))
|
logout_user()
|
||||||
|
flash("Zostałeś wylogowany.", "success")
|
||||||
|
return redirect(url_for("auth.login"))
|
||||||
@@ -19,7 +19,6 @@ def spec():
|
|||||||
'paths':{
|
'paths':{
|
||||||
'/api/auth/login':{'post':{'tags':['Auth'],'security':[],'summary':'Logowanie','requestBody':{'required':True,'content':{'application/json':{'schema':{'type':'object','required':['email','password'],'properties':{'email':{'type':'string'},'password':{'type':'string'}}}}}},'responses':{'200':{'description':'Zalogowano'},'401':{'description':'Błędne dane'}}}},
|
'/api/auth/login':{'post':{'tags':['Auth'],'security':[],'summary':'Logowanie','requestBody':{'required':True,'content':{'application/json':{'schema':{'type':'object','required':['email','password'],'properties':{'email':{'type':'string'},'password':{'type':'string'}}}}}},'responses':{'200':{'description':'Zalogowano'},'401':{'description':'Błędne dane'}}}},
|
||||||
'/api/auth/token':{'post':{'tags':['Auth'],'security':[],'summary':'Pobranie tokenu Bearer','description':'Endpoint zgodny z OAuth2 Password używany przez przycisk Authorize w Swagger UI. W polu username wpisz adres e-mail.','requestBody':{'required':True,'content':{'application/x-www-form-urlencoded':{'schema':{'type':'object','required':['username','password'],'properties':{'username':{'type':'string','format':'email','description':'Adres e-mail'},'password':{'type':'string','format':'password'}}}},'application/json':{'schema':{'type':'object','required':['email','password'],'properties':{'email':{'type':'string','format':'email'},'password':{'type':'string','format':'password'}}}}}},'responses':{'200':{'description':'Token wydany','content':{'application/json':{'schema':{'type':'object','properties':{'access_token':{'type':'string'},'token_type':{'type':'string','example':'Bearer'},'expires_in':{'type':'integer'}}}}}},'401':{'description':'Błędne dane logowania'}}}},
|
'/api/auth/token':{'post':{'tags':['Auth'],'security':[],'summary':'Pobranie tokenu Bearer','description':'Endpoint zgodny z OAuth2 Password używany przez przycisk Authorize w Swagger UI. W polu username wpisz adres e-mail.','requestBody':{'required':True,'content':{'application/x-www-form-urlencoded':{'schema':{'type':'object','required':['username','password'],'properties':{'username':{'type':'string','format':'email','description':'Adres e-mail'},'password':{'type':'string','format':'password'}}}},'application/json':{'schema':{'type':'object','required':['email','password'],'properties':{'email':{'type':'string','format':'email'},'password':{'type':'string','format':'password'}}}}}},'responses':{'200':{'description':'Token wydany','content':{'application/json':{'schema':{'type':'object','properties':{'access_token':{'type':'string'},'token_type':{'type':'string','example':'Bearer'},'expires_in':{'type':'integer'}}}}}},'401':{'description':'Błędne dane logowania'}}}},
|
||||||
'/api/auth/logout':{'post':{'tags':['Auth'],'summary':'Wylogowanie','responses':{'200':{'description':'Wylogowano'}}}},
|
|
||||||
'/api/me':{'get':{'tags':['Auth'],'summary':'Bieżący użytkownik','responses':{'200':{'description':'Użytkownik'}}}},
|
'/api/me':{'get':{'tags':['Auth'],'summary':'Bieżący użytkownik','responses':{'200':{'description':'Użytkownik'}}}},
|
||||||
'/api/vehicles':{'get':{'tags':['Vehicles'],'summary':'Lista dostępnych pojazdów','responses':{'200':{'description':'Lista'}}},'post':{'tags':['Vehicles'],'summary':'Dodanie pojazdu','responses':{'201':{'description':'Utworzono'}}}},
|
'/api/vehicles':{'get':{'tags':['Vehicles'],'summary':'Lista dostępnych pojazdów','responses':{'200':{'description':'Lista'}}},'post':{'tags':['Vehicles'],'summary':'Dodanie pojazdu','responses':{'201':{'description':'Utworzono'}}}},
|
||||||
'/api/vehicles/{vehicle_id}/shares':{'post':{'tags':['Vehicles'],'summary':'Udostępnienie pojazdu','parameters':[{'in':'path','name':'vehicle_id','required':True,'schema':{'type':'integer'}}],'responses':{'200':{'description':'Udostępniono'}}}},
|
'/api/vehicles/{vehicle_id}/shares':{'post':{'tags':['Vehicles'],'summary':'Udostępnienie pojazdu','parameters':[{'in':'path','name':'vehicle_id','required':True,'schema':{'type':'integer'}}],'responses':{'200':{'description':'Udostępniono'}}}},
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
(()=>{const F=FuelTrack;F.bindAuth=()=>{const login=F.qs('form.api-login-form');if(login&&!login.dataset.bound){login.dataset.bound='1';login.addEventListener('submit',async e=>{e.preventDefault();try{const r=await fetch('/api/auth/login',{method:'POST',body:new FormData(login),headers:{Accept:'application/json'}}),d=await r.json();if(!r.ok||!d.ok)throw new Error(d.message||'Błąd logowania');location.href=(d.data||{}).redirect||'/'}catch(x){F.notify(x.message,'danger')}})}F.qsa('[data-api-logout]').forEach(a=>{if(a.dataset.bound)return;a.dataset.bound='1';a.addEventListener('click',async e=>{e.preventDefault();const d=await (await fetch('/api/auth/logout',{method:'POST'})).json();location.href=(d.data||{}).redirect||'/login'})})}})();
|
(()=>{const F=FuelTrack;F.bindAuth=()=>{const login=F.qs('form.api-login-form');if(login&&!login.dataset.bound){login.dataset.bound='1';login.addEventListener('submit',async e=>{e.preventDefault();try{const r=await fetch('/api/auth/login',{method:'POST',body:new FormData(login),headers:{Accept:'application/json'}}),d=await r.json();if(!r.ok||!d.ok)throw new Error(d.message||'Błąd logowania');location.href=(d.data||{}).redirect||'/'}catch(x){F.notify(x.message,'danger')}})}}})();
|
||||||
|
|||||||
@@ -31,7 +31,9 @@
|
|||||||
<a class="nav-link {% if request.endpoint and request.endpoint.startswith('main.admin') %}active{% endif %}" href="{{ url_for('main.admin_companies') }}">Administracja</a>
|
<a class="nav-link {% if request.endpoint and request.endpoint.startswith('main.admin') %}active{% endif %}" href="{{ url_for('main.admin_companies') }}">Administracja</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<span class="navbar-divider d-none d-lg-block"></span>
|
<span class="navbar-divider d-none d-lg-block"></span>
|
||||||
<a class="btn btn-outline-danger btn-sm ms-lg-2" href="/api/auth/logout" data-api-logout>Wyloguj</a>
|
<form method="post" action="{{ url_for('auth.logout') }}" class="d-inline ms-lg-2">
|
||||||
|
<button class="btn btn-outline-danger btn-sm" type="submit">Wyloguj</button>
|
||||||
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user