split api pythons
This commit is contained in:
@@ -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ń')
|
||||
Reference in New Issue
Block a user