From 84ceb23316895cbeae18f035159963ff08054cbc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Gruszczy=C5=84ski?= Date: Sun, 2 Aug 2026 00:19:34 +0200 Subject: [PATCH] fix --- app/api/orlen.py | 41 +++++--- app/main.py | 43 ++++---- app/orlen_sync.py | 91 ++++++++++++++++ app/static/js/orlen.js | 2 +- app/templates/admin_fuel_entries.html | 4 +- app/templates/orlen.html | 13 ++- tests/test_app.py | 143 ++++++++++++++++++++++++++ 7 files changed, 299 insertions(+), 38 deletions(-) create mode 100644 app/orlen_sync.py diff --git a/app/api/orlen.py b/app/api/orlen.py index 5e03b3c..2b52780 100644 --- a/app/api/orlen.py +++ b/app/api/orlen.py @@ -15,6 +15,7 @@ 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 +from ..orlen_sync import sync_orlen_prices FUEL_TYPES = ("PB95", "PB98", "DIESEL", "LPG") @@ -28,23 +29,31 @@ def orlen_prices(): @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 + d = payload() + fuels = d.get('fuels', []) + fuels = [fuels] if isinstance(fuels, str) else fuels + 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: - 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) + year = int(d.get('year', date.today().year)) + settings = CompanySettings.query.first() + result, total = sync_orlen_prices( + fuels=fuels, + year=year, + regions=regions, + default_region=settings.region, + full_refresh=boolv(d, 'full_refresh'), + ) + 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 diff --git a/app/main.py b/app/main.py index bb1f5e3..1609f1c 100644 --- a/app/main.py +++ b/app/main.py @@ -13,6 +13,7 @@ from .station_catalog import sync_station_catalog from . import THEMES 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 .orlen_sync import sync_orlen_prices main = Blueprint("main", __name__) FUEL_TYPES = ("PB95", "PB98", "DIESEL", "LPG") @@ -499,28 +500,34 @@ def orlen_data(): @login_required @roles("boss", "admin") def orlen_sync(): - payload = request.get_json(silent=True) or request.form - fuels = payload.get("fuels", []) - if isinstance(fuels, str): fuels = [fuels] - fuels = [f.upper() for f in fuels if f.upper() in FUEL_TYPES] - year = int(payload.get("year", date.today().year)) - if not fuels: return jsonify({"ok": False, "error": "Wybierz co najmniej jedno paliwo."}), 400 - settings = CompanySettings.query.first(); start = date(year,1,1); end = min(date(year,12,31), date.today()) - result = {}; total = 0 + data = request.get_json(silent=True) or request.form + fuels = data.get("fuels", []) + if isinstance(fuels, str): + fuels = [fuels] + 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 + + 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: - for fuel in fuels: - imported = 0; updated = 0 - for row in fetch_orlen_range(fuel, start, end, "all" if fuel == "LPG" else settings.region): - existing = OrlenPrice.query.filter_by(fuel_type=row["fuel_type"], effective_date=row["effective_date"], region=row["region"]).first() - if existing: - 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 - else: - db.session.add(OrlenPrice(**row)); imported += 1 - result[fuel] = {"added": imported, "updated": updated}; total += imported + updated + settings = CompanySettings.query.first() + result, total = sync_orlen_prices( + fuels=fuels, + year=int(data.get("year", date.today().year)), + regions=regions, + default_region=settings.region, + full_refresh=full_refresh, + ) db.session.commit() return jsonify({"ok": True, "message": f"Przetworzono {total} rekordów.", "result": result}) 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 diff --git a/app/orlen_sync.py b/app/orlen_sync.py new file mode 100644 index 0000000..6f04ada --- /dev/null +++ b/app/orlen_sync.py @@ -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 diff --git a/app/static/js/orlen.js b/app/static/js/orlen.js index 62dec5e..3a05b4a 100644 --- a/app/static/js/orlen.js +++ b/app/static/js/orlen.js @@ -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()}}})(); diff --git a/app/templates/admin_fuel_entries.html b/app/templates/admin_fuel_entries.html index d6f667a..dca32f4 100644 --- a/app/templates/admin_fuel_entries.html +++ b/app/templates/admin_fuel_entries.html @@ -13,7 +13,7 @@ {% else %}Brak tankowań spełniających kryteria.{% endfor %} {% if entries_page.pages > 1 %}{% endif %} - {% endblock %} diff --git a/app/templates/orlen.html b/app/templates/orlen.html index 80fb6de..4c352fa 100644 --- a/app/templates/orlen.html +++ b/app/templates/orlen.html @@ -20,7 +20,18 @@
-{% if current_user.role in ['boss','admin'] %}
{% endif %} +{% if current_user.role in ['boss','admin'] %} +
+
+ + + +
+ +
+{% endif %} diff --git a/tests/test_app.py b/tests/test_app.py index 71aef01..0cd4d2a 100644 --- a/tests/test_app.py +++ b/tests/test_app.py @@ -362,3 +362,146 @@ def test_last_price_zero_and_full_vat_deduction(): 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, 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">' + '