This commit is contained in:
Mateusz Gruszczyński
2026-08-02 00:19:34 +02:00
parent 5299a7fc66
commit 84ceb23316
7 changed files with 299 additions and 38 deletions
+25 -16
View File
@@ -15,6 +15,7 @@ from ..extensions import db
from ..models import AppSetting, CompanySettings, FuelCard, FuelCardPolicy, FuelCardStationRule, FuelEntry, FuelStationCompany, FuelStationPoint, OrlenPrice, User, Vehicle 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 ..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 from ..station_catalog import sync_station_catalog
from ..orlen_sync import sync_orlen_prices
FUEL_TYPES = ("PB95", "PB98", "DIESEL", "LPG") FUEL_TYPES = ("PB95", "PB98", "DIESEL", "LPG")
@@ -28,23 +29,31 @@ def orlen_prices():
@api.post('/orlen/sync') @api.post('/orlen/sync')
@role_required('boss','admin') @role_required('boss','admin')
def orlen_sync(): 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)) d = payload()
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] fuels = d.get('fuels', [])
if not fuels:return fail('Wybierz co najmniej jedno paliwo') fuels = [fuels] if isinstance(fuels, str) else fuels
settings=CompanySettings.query.first(); result={}; total=0 fuels = [fuel.upper() for fuel in fuels if fuel.upper() in FUEL_TYPES]
regions = d.get('regions', [])
regions = [regions] if isinstance(regions, str) else regions
regions = [str(region).strip().lower() for region in regions if str(region).strip().lower() in POLISH_REGIONS]
if not fuels:
return fail('Wybierz co najmniej jedno paliwo')
try: try:
for fuel in fuels: year = int(d.get('year', date.today().year))
added=updated=0 settings = CompanySettings.query.first()
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): result, total = sync_orlen_prices(
if fuel=='LPG' and regions and row.get('region') not in regions: continue fuels=fuels,
x=OrlenPrice.query.filter_by(fuel_type=row['fuel_type'],effective_date=row['effective_date'],region=row['region']).first() year=year,
if x: regions=regions,
for k,v in row.items():setattr(x,k,v) default_region=settings.region,
x.fetched_at=datetime.utcnow();updated+=1 full_refresh=boolv(d, 'full_refresh'),
else:db.session.add(OrlenPrice(**row));added+=1 )
result[fuel]={'added':added,'updated':updated};total+=added+updated db.session.commit()
db.session.commit();return response(result,f'Przetworzono {total} rekordów') return response(result, f'Przetworzono {total} rekordów')
except Exception as exc:db.session.rollback();return fail(str(exc),502) except Exception as exc:
db.session.rollback()
return fail(str(exc), 502)
@api.get('/orlen/preview') @api.get('/orlen/preview')
@login_required @login_required
+25 -18
View File
@@ -13,6 +13,7 @@ from .station_catalog import sync_station_catalog
from . import THEMES from . import THEMES
from .vat import entry_vat_rate, freeze_vat_action, global_vat_override, save_global_vat_override from .vat import entry_vat_rate, freeze_vat_action, global_vat_override, save_global_vat_override
from .settlement import calculate as calculate_snapshot_settlement, freeze as freeze_settlement from .settlement import calculate as calculate_snapshot_settlement, freeze as freeze_settlement
from .orlen_sync import sync_orlen_prices
main = Blueprint("main", __name__) main = Blueprint("main", __name__)
FUEL_TYPES = ("PB95", "PB98", "DIESEL", "LPG") FUEL_TYPES = ("PB95", "PB98", "DIESEL", "LPG")
@@ -499,28 +500,34 @@ def orlen_data():
@login_required @login_required
@roles("boss", "admin") @roles("boss", "admin")
def orlen_sync(): def orlen_sync():
payload = request.get_json(silent=True) or request.form data = request.get_json(silent=True) or request.form
fuels = payload.get("fuels", []) fuels = data.get("fuels", [])
if isinstance(fuels, str): fuels = [fuels] if isinstance(fuels, str):
fuels = [f.upper() for f in fuels if f.upper() in FUEL_TYPES] fuels = [fuels]
year = int(payload.get("year", date.today().year)) fuels = [fuel.upper() for fuel in fuels if fuel.upper() in FUEL_TYPES]
if not fuels: return jsonify({"ok": False, "error": "Wybierz co najmniej jedno paliwo."}), 400 if not fuels:
settings = CompanySettings.query.first(); start = date(year,1,1); end = min(date(year,12,31), date.today()) return jsonify({"ok": False, "error": "Wybierz co najmniej jedno paliwo."}), 400
result = {}; total = 0
regions = data.get("regions", [])
if isinstance(regions, str):
regions = [regions]
regions = [str(region).strip().lower() for region in regions if str(region).strip().lower() in POLISH_REGIONS]
full_refresh = data.get("full_refresh") in (True, 1, "1", "true", "on", "yes")
try: try:
for fuel in fuels: settings = CompanySettings.query.first()
imported = 0; updated = 0 result, total = sync_orlen_prices(
for row in fetch_orlen_range(fuel, start, end, "all" if fuel == "LPG" else settings.region): fuels=fuels,
existing = OrlenPrice.query.filter_by(fuel_type=row["fuel_type"], effective_date=row["effective_date"], region=row["region"]).first() year=int(data.get("year", date.today().year)),
if existing: regions=regions,
existing.price_per_liter=row["price_per_liter"]; existing.raw_value=row["raw_value"]; existing.product_name=row["product_name"]; existing.source=row["source"]; existing.fetched_at=datetime.utcnow(); updated += 1 default_region=settings.region,
else: full_refresh=full_refresh,
db.session.add(OrlenPrice(**row)); imported += 1 )
result[fuel] = {"added": imported, "updated": updated}; total += imported + updated
db.session.commit() db.session.commit()
return jsonify({"ok": True, "message": f"Przetworzono {total} rekordów.", "result": result}) return jsonify({"ok": True, "message": f"Przetworzono {total} rekordów.", "result": result})
except Exception as exc: except Exception as exc:
db.session.rollback(); return jsonify({"ok": False, "error": str(exc)}), 502 db.session.rollback()
return jsonify({"ok": False, "error": str(exc)}), 502
+91
View File
@@ -0,0 +1,91 @@
from datetime import date, datetime
from sqlalchemy import func
from .extensions import db
from .models import OrlenPrice
from .services import fetch_orlen_range
def _year_bounds(year, today=None):
today = today or date.today()
start = date(year, 1, 1)
end = min(date(year, 12, 31), today)
if end < start:
raise ValueError("Nie można synchronizować danych dla przyszłego roku")
return start, end
def _latest_date(fuel, start, end, region=""):
query = db.session.query(func.max(OrlenPrice.effective_date)).filter(
OrlenPrice.fuel_type == fuel,
OrlenPrice.effective_date.between(start, end),
)
if fuel == "LPG":
query = query.filter(OrlenPrice.region == region)
else:
query = query.filter(OrlenPrice.region == "")
return query.scalar()
def _sync_start(fuel, start, end, regions, full_refresh):
if full_refresh:
return start
if fuel != "LPG":
return _latest_date(fuel, start, end) or start
latest_dates = [_latest_date("LPG", start, end, region) for region in regions]
return min((value or start) for value in latest_dates)
def sync_orlen_prices(fuels, year, regions, default_region, full_refresh=False, today=None):
start, end = _year_bounds(year, today=today)
target_regions = [str(region).strip().lower() for region in regions if str(region).strip()]
if not target_regions:
target_regions = [str(default_region).strip().lower()]
result = {}
total = 0
for fuel in fuels:
fuel_regions = target_regions if fuel == "LPG" else []
date_from = _sync_start(fuel, start, end, fuel_regions, full_refresh)
added = 0
updated = 0
rows = fetch_orlen_range(
fuel,
date_from,
end,
"all" if fuel == "LPG" else default_region,
)
for row in rows:
if fuel == "LPG" and row.get("region") not in fuel_regions:
continue
existing = OrlenPrice.query.filter_by(
fuel_type=row["fuel_type"],
effective_date=row["effective_date"],
region=row["region"],
).first()
if existing:
for key, value in row.items():
setattr(existing, key, value)
existing.fetched_at = datetime.utcnow()
updated += 1
else:
db.session.add(OrlenPrice(**row))
added += 1
processed = added + updated
result[fuel] = {
"added": added,
"updated": updated,
"from": date_from.isoformat(),
"to": end.isoformat(),
"full_refresh": bool(full_refresh),
}
total += processed
return result, total
+1 -1
View File
@@ -1,2 +1,2 @@
(()=>{const F=FuelTrack;F.bindLpgRegionPicker=()=>{const select=F.qs('#lpg-region-select');if(!select||select.dataset.choicesBound)return;select.dataset.choicesBound='1';const choices=new Choices(select,{removeItemButton:true,searchEnabled:true,shouldSort:false,position:'bottom',placeholder:true,placeholderValue:'Wybierz województwa',noResultsText:'Brak województwa',noChoicesText:'Brak opcji',itemSelectText:'Wybierz'});const modal=F.qs('#lpgRegionsModal');modal?.addEventListener('hidden.bs.modal',()=>{const count=F.qsa('option:checked',select).length;const badge=F.qs('#lpg-region-count');if(badge)badge.textContent=count?`${count} wybrano`:'Domyślne województwo'})};F.bindOrlenSync=()=>{const btn=F.qs('#orlen-sync-btn');if(!btn||btn.dataset.bound)return;btn.dataset.bound='1';btn.addEventListener('click',async()=>{const form=F.qs('#orlen-filter-form'),fuels=F.qsa('[name=fuel]:checked',form).map(x=>x.value),year=F.qs('[name=year]',form).value,status=F.qs('#orlen-sync-status');if(!fuels.length){status.textContent='Wybierz co najmniej jedno paliwo.';return}btn.disabled=true;const old=btn.innerHTML;btn.innerHTML='Pobieranie…';try{const regions=F.qsa('[name=region] option:checked',form).map(x=>x.value);const r=await fetch('/api/orlen/sync',{method:'POST',headers:{'Content-Type':'application/json','X-Requested-With':'XMLHttpRequest'},body:JSON.stringify({fuels,year,regions})}),d=await r.json();if(!r.ok)throw new Error(d.error||'Błąd pobierania');status.textContent=d.message;await F.replaceMain(`/orlen?${new URLSearchParams(new FormData(form))}`,{push:true});F.notify(d.message)}catch(e){status.textContent=`Błąd: ${e.message}`;F.notify(e.message,'danger')}finally{btn.disabled=false;btn.innerHTML=old}})}})(); (()=>{const F=FuelTrack;F.bindLpgRegionPicker=()=>{const select=F.qs('#lpg-region-select');if(!select||select.dataset.choicesBound)return;select.dataset.choicesBound='1';new Choices(select,{removeItemButton:true,searchEnabled:true,shouldSort:false,position:'bottom',placeholder:true,placeholderValue:'Wybierz województwa',noResultsText:'Brak województwa',noChoicesText:'Brak opcji',itemSelectText:'Wybierz'});const modal=F.qs('#lpgRegionsModal');modal?.addEventListener('hidden.bs.modal',()=>{const count=F.qsa('option:checked',select).length;const badge=F.qs('#lpg-region-count');if(badge)badge.textContent=count?`${count} wybrano`:'Domyślne województwo'})};F.bindOrlenSync=()=>{const incrementalBtn=F.qs('#orlen-sync-btn'),fullBtn=F.qs('#orlen-full-sync-btn');if(!incrementalBtn||incrementalBtn.dataset.bound)return;incrementalBtn.dataset.bound='1';if(fullBtn)fullBtn.dataset.bound='1';const sync=async fullRefresh=>{const form=F.qs('#orlen-filter-form'),fuels=F.qsa('[name=fuel]:checked',form).map(x=>x.value),year=F.qs('[name=year]',form).value,status=F.qs('#orlen-sync-status');if(!fuels.length){status.textContent='Wybierz co najmniej jedno paliwo.';return}if(fullRefresh&&!confirm(`Pobrać ponownie wszystkie dane dla ${year} roku? Operacja może potrwać dłużej.`))return;const regions=F.qsa('[name=region] option:checked',form).map(x=>x.value),old=incrementalBtn.innerHTML;incrementalBtn.disabled=true;if(fullBtn)fullBtn.disabled=true;incrementalBtn.innerHTML=fullRefresh?'Pełne pobieranie…':'Pobieranie…';try{const r=await fetch('/api/orlen/sync',{method:'POST',headers:{'Content-Type':'application/json','X-Requested-With':'XMLHttpRequest'},body:JSON.stringify({fuels,year,regions,full_refresh:fullRefresh})}),d=await r.json();if(!r.ok||!d.ok)throw new Error(d.error||d.message||'Błąd pobierania');status.textContent=d.message;await F.replaceMain(`/orlen?${new URLSearchParams(new FormData(form))}`,{push:true});F.notify(d.message)}catch(e){status.textContent=`Błąd: ${e.message}`;F.notify(e.message,'danger')}finally{incrementalBtn.disabled=false;if(fullBtn)fullBtn.disabled=false;incrementalBtn.innerHTML=old}};incrementalBtn.addEventListener('click',()=>sync(false));fullBtn?.addEventListener('click',()=>sync(true))}})();
;(()=>{const F=FuelTrack;F.bindOrlenComparison=()=>{const type=F.qs('#comparison-type'),fuel=F.qs('#compare-fuel');if(type&&!type.dataset.compareBound){type.dataset.compareBound='1';const update=()=>{F.qs('#year-comparison-fields')?.classList.toggle('d-none',type.value!=='years');F.qs('#period-comparison-fields')?.classList.toggle('d-none',type.value!=='periods')};type.addEventListener('change',update);update()}if(fuel&&!fuel.dataset.compareBound){fuel.dataset.compareBound='1';const update=()=>F.qs('#compare-region-wrap')?.classList.toggle('d-none',fuel.value!=='LPG');fuel.addEventListener('change',update);update()}}})(); ;(()=>{const F=FuelTrack;F.bindOrlenComparison=()=>{const type=F.qs('#comparison-type'),fuel=F.qs('#compare-fuel');if(type&&!type.dataset.compareBound){type.dataset.compareBound='1';const update=()=>{F.qs('#year-comparison-fields')?.classList.toggle('d-none',type.value!=='years');F.qs('#period-comparison-fields')?.classList.toggle('d-none',type.value!=='periods')};type.addEventListener('change',update);update()}if(fuel&&!fuel.dataset.compareBound){fuel.dataset.compareBound='1';const update=()=>F.qs('#compare-region-wrap')?.classList.toggle('d-none',fuel.value!=='LPG');fuel.addEventListener('change',update);update()}}})();
+2 -2
View File
@@ -13,7 +13,7 @@
{% else %}<tr><td colspan="8" class="text-center text-body-secondary py-4">Brak tankowań spełniających kryteria.</td></tr>{% endfor %}</tbody></table></div> {% else %}<tr><td colspan="8" class="text-center text-body-secondary py-4">Brak tankowań spełniających kryteria.</td></tr>{% endfor %}</tbody></table></div>
{% if entries_page.pages > 1 %}<nav><ul class="pagination mb-0">{% for p in entries_page.iter_pages() %}{% if p %}<li class="page-item {% if p==entries_page.page %}active{% endif %}"><a class="page-link ajax-nav-link" href="{{url_for('main.admin_fuel_entries',page=p,q=q,month=month,company_id=company_id)}}">{{p}}</a></li>{% else %}<li class="page-item disabled"><span class="page-link"></span></li>{% endif %}{% endfor %}</ul></nav>{% endif %} {% if entries_page.pages > 1 %}<nav><ul class="pagination mb-0">{% for p in entries_page.iter_pages() %}{% if p %}<li class="page-item {% if p==entries_page.page %}active{% endif %}"><a class="page-link ajax-nav-link" href="{{url_for('main.admin_fuel_entries',page=p,q=q,month=month,company_id=company_id)}}">{{p}}</a></li>{% else %}<li class="page-item disabled"><span class="page-link"></span></li>{% endif %}{% endfor %}</ul></nav>{% endif %}
</div></div> </div></div>
<div class="modal fade" id="fuelEntryEditModal" tabindex="-1"><div class="modal-dialog modal-lg modal-dialog-scrollable"><div class="modal-content"><form id="fuel-entry-edit-form" class="ajax-form" data-method="PUT"><div class="modal-header"><h2 class="modal-title fs-5">Edytuj tankowanie</h2><button type="button" class="btn-close" data-bs-dismiss="modal"></button></div><div class="modal-body"><div class="row g-3"> <div class="modal fade" id="fuelEntryEditModal" tabindex="-1"><div class="modal-dialog modal-lg modal-dialog-scrollable"><form id="fuel-entry-edit-form" class="modal-content ajax-form" data-method="PUT"><div class="modal-header"><h2 class="modal-title fs-5">Edytuj tankowanie</h2><button type="button" class="btn-close" data-bs-dismiss="modal"></button></div><div class="modal-body"><div class="row g-3">
<div class="col-md-6"><label class="form-label">Pojazd</label><select class="form-select" name="vehicle_id" required>{% for v in vehicles %}<option value="{{v.id}}">{{v.name}} · {{v.registration}}</option>{% endfor %}</select></div> <div class="col-md-6"><label class="form-label">Pojazd</label><select class="form-select" name="vehicle_id" required>{% for v in vehicles %}<option value="{{v.id}}">{{v.name}} · {{v.registration}}</option>{% endfor %}</select></div>
<div class="col-md-6"><label class="form-label">Data i godzina</label><input class="form-control" type="datetime-local" name="fueled_at" required></div> <div class="col-md-6"><label class="form-label">Data i godzina</label><input class="form-control" type="datetime-local" name="fueled_at" required></div>
<div class="col-md-4"><label class="form-label">Paliwo</label><select class="form-select" name="fuel_type">{% for f in ['PB95','PB98','DIESEL','LPG'] %}<option>{{f}}</option>{% endfor %}</select></div> <div class="col-md-4"><label class="form-label">Paliwo</label><select class="form-select" name="fuel_type">{% for f in ['PB95','PB98','DIESEL','LPG'] %}<option>{{f}}</option>{% endfor %}</select></div>
@@ -36,5 +36,5 @@
<div class="col-md-3"><label class="form-label">Dopłata netto / l</label><input class="form-control" type="number" step="0.0001" name="snapshot_surcharge_per_liter" required></div> <div class="col-md-3"><label class="form-label">Dopłata netto / l</label><input class="form-control" type="number" step="0.0001" name="snapshot_surcharge_per_liter" required></div>
<div class="col-md-6"><div class="form-check mt-4"><input class="form-check-input" type="checkbox" name="snapshot_uses_last_price" id="edit-snapshot-last-price"><label class="form-check-label" for="edit-snapshot-last-price">Użyj zapisanej ceny hurtowej jako Last Price</label></div></div> <div class="col-md-6"><div class="form-check mt-4"><input class="form-check-input" type="checkbox" name="snapshot_uses_last_price" id="edit-snapshot-last-price"><label class="form-check-label" for="edit-snapshot-last-price">Użyj zapisanej ceny hurtowej jako Last Price</label></div></div>
<div class="col-md-6"><label class="form-label">Źródło / opis reguły</label><input class="form-control" name="snapshot_rule_source" maxlength="160"></div> <div class="col-md-6"><label class="form-label">Źródło / opis reguły</label><input class="form-control" name="snapshot_rule_source" maxlength="160"></div>
</div></div><div class="modal-footer"><button type="button" class="btn btn-danger me-auto" id="fuel-entry-delete-btn">Usuń tankowanie</button><button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Anuluj</button><button class="btn btn-primary">Zapisz zmiany</button></div></form></div></div></div> </div></div><div class="modal-footer"><button type="button" class="btn btn-danger me-auto" id="fuel-entry-delete-btn">Usuń tankowanie</button><button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Anuluj</button><button class="btn btn-primary">Zapisz zmiany</button></div></form></div></div>
{% endblock %} {% endblock %}
+12 -1
View File
@@ -20,7 +20,18 @@
<div class="col-md-3"><div class="form-check mb-2"><input class="form-check-input" type="checkbox" name="gross" value="1" id="gross-price" {% if include_vat %}checked{% endif %}><label class="form-check-label" for="gross-price">Dolicz VAT</label></div></div> <div class="col-md-3"><div class="form-check mb-2"><input class="form-check-input" type="checkbox" name="gross" value="1" id="gross-price" {% if include_vat %}checked{% endif %}><label class="form-check-label" for="gross-price">Dolicz VAT</label></div></div>
<div class="col-md-3"><button class="btn btn-secondary w-100">Pokaż dane</button></div> <div class="col-md-3"><button class="btn btn-secondary w-100">Pokaż dane</button></div>
</form> </form>
{% if current_user.role in ['boss','admin'] %}<div class="d-flex align-items-center gap-3 mt-3"><button type="button" id="orlen-sync-btn" class="btn btn-primary">Pobierz z API</button><span id="orlen-sync-status" class="small text-body-secondary" aria-live="polite"></span></div>{% endif %} {% if current_user.role in ['boss','admin'] %}
<div class="d-flex flex-wrap align-items-center gap-3 mt-3">
<div class="btn-group">
<button type="button" id="orlen-sync-btn" class="btn btn-primary">Pobierz nowe dane</button>
<button type="button" class="btn btn-primary dropdown-toggle dropdown-toggle-split" data-bs-toggle="dropdown" aria-expanded="false"><span class="visually-hidden">Opcje pobierania</span></button>
<ul class="dropdown-menu">
<li><button type="button" id="orlen-full-sync-btn" class="dropdown-item">Pobierz cały wybrany rok ponownie</button></li>
</ul>
</div>
<span id="orlen-sync-status" class="small text-body-secondary" aria-live="polite"></span>
</div>
{% endif %}
</div></div> </div></div>
<div class="modal fade lpg-region-modal" id="lpgRegionsModal" tabindex="-1" aria-hidden="true"><div class="modal-dialog modal-lg modal-dialog-centered"><div class="modal-content"><div class="modal-header"><h2 class="modal-title fs-5">Województwa LPG</h2><button type="button" class="btn-close" data-bs-dismiss="modal"></button></div><div class="modal-body"><label class="form-label" for="lpg-region-select">Wybierz jedno lub wiele województw</label><select class="form-select" name="region" id="lpg-region-select" form="orlen-filter-form" multiple>{% for r in regions %}<option value="{{r}}" {% if r in selected_regions %}selected{% endif %}>{{r}}</option>{% endfor %}</select></div><div class="modal-footer"><button type="button" class="btn btn-primary" data-bs-dismiss="modal">Gotowe</button></div></div></div></div> <div class="modal fade lpg-region-modal" id="lpgRegionsModal" tabindex="-1" aria-hidden="true"><div class="modal-dialog modal-lg modal-dialog-centered"><div class="modal-content"><div class="modal-header"><h2 class="modal-title fs-5">Województwa LPG</h2><button type="button" class="btn-close" data-bs-dismiss="modal"></button></div><div class="modal-body"><label class="form-label" for="lpg-region-select">Wybierz jedno lub wiele województw</label><select class="form-select" name="region" id="lpg-region-select" form="orlen-filter-form" multiple>{% for r in regions %}<option value="{{r}}" {% if r in selected_regions %}selected{% endif %}>{{r}}</option>{% endfor %}</select></div><div class="modal-footer"><button type="button" class="btn btn-primary" data-bs-dismiss="modal">Gotowe</button></div></div></div></div>
+143
View File
@@ -362,3 +362,146 @@ def test_last_price_zero_and_full_vat_deduction():
from app.domain.costs import calculate_net_price_costs from app.domain.costs import calculate_net_price_costs
assert round(float(calculate_net_price_costs(10, 23, 0)["effective_cost"]), 2) == 12.30 assert round(float(calculate_net_price_costs(10, 23, 0)["effective_cost"]), 2) == 12.30
assert round(float(calculate_net_price_costs(10, 23, 100)["effective_cost"]), 2) == 10.00 assert round(float(calculate_net_price_costs(10, 23, 100)["effective_cost"]), 2) == 10.00
def test_fuel_entry_edit_modal_uses_scrollable_bootstrap_structure(tmp_path):
app = make_app(tmp_path, "fuel-entry-modal.db")
client = app.test_client()
client.post('/login', data={'email': 'admin@example.com', 'password': 'admin123!'})
html = client.get('/admin/fuel-entries').get_data(as_text=True)
assert (
'modal-dialog modal-lg modal-dialog-scrollable">'
'<form id="fuel-entry-edit-form" class="modal-content ajax-form"'
in html
)
assert '<div class="modal-content"><form id="fuel-entry-edit-form"' not in html
def test_orlen_incremental_sync_starts_at_latest_saved_record(tmp_path, monkeypatch):
from datetime import date
from decimal import Decimal
from app.extensions import db
from app.models import OrlenPrice
from app.orlen_sync import sync_orlen_prices
import app.orlen_sync as orlen_sync_module
app = make_app(tmp_path, "orlen-incremental.db")
calls = []
def fake_fetch(fuel, date_from, date_to, region):
calls.append((fuel, date_from, date_to, region))
return []
monkeypatch.setattr(orlen_sync_module, 'fetch_orlen_range', fake_fetch)
with app.app_context():
db.session.add_all([
OrlenPrice(fuel_type='PB95', effective_date=date(2026, 1, 2), price_per_liter=Decimal('4.1000'), raw_value=Decimal('4100'), region='', product_name='PB95', source='test'),
OrlenPrice(fuel_type='PB95', effective_date=date(2026, 7, 15), price_per_liter=Decimal('4.5000'), raw_value=Decimal('4500'), region='', product_name='PB95', source='test'),
])
db.session.commit()
result, total = sync_orlen_prices(
fuels=['PB95'],
year=2026,
regions=[],
default_region='mazowieckie',
full_refresh=False,
today=date(2026, 8, 2),
)
assert calls == [('PB95', date(2026, 7, 15), date(2026, 8, 2), 'mazowieckie')]
assert result['PB95']['from'] == '2026-07-15'
assert result['PB95']['full_refresh'] is False
assert total == 0
def test_orlen_full_refresh_starts_at_beginning_of_selected_year(tmp_path, monkeypatch):
from datetime import date
from decimal import Decimal
from app.extensions import db
from app.models import OrlenPrice
from app.orlen_sync import sync_orlen_prices
import app.orlen_sync as orlen_sync_module
app = make_app(tmp_path, "orlen-full-refresh.db")
calls = []
def fake_fetch(fuel, date_from, date_to, region):
calls.append((fuel, date_from, date_to, region))
return []
monkeypatch.setattr(orlen_sync_module, 'fetch_orlen_range', fake_fetch)
with app.app_context():
db.session.add(OrlenPrice(fuel_type='DIESEL', effective_date=date(2026, 7, 20), price_per_liter=Decimal('4.9000'), raw_value=Decimal('4900'), region='', product_name='DIESEL', source='test'))
db.session.commit()
result, _ = sync_orlen_prices(
fuels=['DIESEL'],
year=2026,
regions=[],
default_region='mazowieckie',
full_refresh=True,
today=date(2026, 8, 2),
)
assert calls == [('DIESEL', date(2026, 1, 1), date(2026, 8, 2), 'mazowieckie')]
assert result['DIESEL']['from'] == '2026-01-01'
assert result['DIESEL']['full_refresh'] is True
def test_orlen_lpg_incremental_sync_uses_oldest_latest_region_date(tmp_path, monkeypatch):
from datetime import date
from decimal import Decimal
from app.extensions import db
from app.models import OrlenPrice
from app.orlen_sync import sync_orlen_prices
import app.orlen_sync as orlen_sync_module
app = make_app(tmp_path, "orlen-lpg-incremental.db")
calls = []
def fake_fetch(fuel, date_from, date_to, region):
calls.append((fuel, date_from, date_to, region))
return [
{'fuel_type': 'LPG', 'effective_date': date(2026, 8, 1), 'price_per_liter': Decimal('2.1000'), 'raw_value': Decimal('2.1000'), 'region': 'mazowieckie', 'product_name': 'LPG', 'source': 'test'},
{'fuel_type': 'LPG', 'effective_date': date(2026, 8, 1), 'price_per_liter': Decimal('2.2000'), 'raw_value': Decimal('2.2000'), 'region': 'pomorskie', 'product_name': 'LPG', 'source': 'test'},
]
monkeypatch.setattr(orlen_sync_module, 'fetch_orlen_range', fake_fetch)
with app.app_context():
db.session.add_all([
OrlenPrice(fuel_type='LPG', effective_date=date(2026, 7, 20), price_per_liter=Decimal('2.0000'), raw_value=Decimal('2.0000'), region='mazowieckie', product_name='LPG', source='test'),
OrlenPrice(fuel_type='LPG', effective_date=date(2026, 7, 25), price_per_liter=Decimal('2.0000'), raw_value=Decimal('2.0000'), region='śląskie', product_name='LPG', source='test'),
])
db.session.commit()
result, total = sync_orlen_prices(
fuels=['LPG'],
year=2026,
regions=['mazowieckie', 'śląskie'],
default_region='mazowieckie',
full_refresh=False,
today=date(2026, 8, 2),
)
db.session.commit()
assert OrlenPrice.query.filter_by(fuel_type='LPG', region='pomorskie').count() == 0
assert calls == [('LPG', date(2026, 7, 20), date(2026, 8, 2), 'all')]
assert result['LPG']['from'] == '2026-07-20'
assert total == 1
def test_orlen_page_has_incremental_and_full_refresh_controls(tmp_path):
app = make_app(tmp_path, "orlen-sync-ui.db")
client = app.test_client()
client.post('/login', data={'email': 'admin@example.com', 'password': 'admin123!'})
html = client.get('/orlen?mode=history').get_data(as_text=True)
assert 'id="orlen-sync-btn"' in html
assert 'Pobierz nowe dane' in html
assert 'id="orlen-full-sync-btn"' in html
assert 'Pobierz cały wybrany rok ponownie' in html