diff --git a/api/app/config.py b/api/app/config.py index 1822843..dcea628 100644 --- a/api/app/config.py +++ b/api/app/config.py @@ -38,7 +38,6 @@ class Settings(BaseSettings): # ROA Import Settings ID_POL: int = 0 - ID_GESTIUNE: int = 0 ID_SECTIE: int = 0 # GoMag API diff --git a/api/app/routers/sync.py b/api/app/routers/sync.py index 81b2693..5863fa9 100644 --- a/api/app/routers/sync.py +++ b/api/app/routers/sync.py @@ -1,7 +1,10 @@ import asyncio import json +import logging from datetime import datetime +logger = logging.getLogger(__name__) + from fastapi import APIRouter, Request, BackgroundTasks from fastapi.templating import Jinja2Templates from fastapi.responses import HTMLResponse @@ -10,6 +13,7 @@ from pathlib import Path from typing import Optional from ..services import sync_service, scheduler_service, sqlite_service, invoice_service +from .. import database router = APIRouter(tags=["sync"]) templates = Jinja2Templates(directory=str(Path(__file__).parent.parent / "templates")) @@ -29,6 +33,10 @@ class AppSettingsUpdate(BaseModel): discount_id_pol: str = "" id_pol: str = "" id_sectie: str = "" + gomag_api_key: str = "" + gomag_api_shop: str = "" + gomag_order_days_back: str = "7" + gomag_limit: str = "100" # API endpoints @@ -457,16 +465,21 @@ async def get_schedule(): @router.get("/api/settings") async def get_app_settings(): """Get application settings.""" - settings = await sqlite_service.get_app_settings() + from ..config import settings as config_settings + s = await sqlite_service.get_app_settings() return { - "transport_codmat": settings.get("transport_codmat", ""), - "transport_vat": settings.get("transport_vat", "21"), - "discount_codmat": settings.get("discount_codmat", ""), - "transport_id_pol": settings.get("transport_id_pol", ""), - "discount_vat": settings.get("discount_vat", "19"), - "discount_id_pol": settings.get("discount_id_pol", ""), - "id_pol": settings.get("id_pol", ""), - "id_sectie": settings.get("id_sectie", ""), + "transport_codmat": s.get("transport_codmat", ""), + "transport_vat": s.get("transport_vat", "21"), + "discount_codmat": s.get("discount_codmat", ""), + "transport_id_pol": s.get("transport_id_pol", ""), + "discount_vat": s.get("discount_vat", "19"), + "discount_id_pol": s.get("discount_id_pol", ""), + "id_pol": s.get("id_pol", ""), + "id_sectie": s.get("id_sectie", ""), + "gomag_api_key": s.get("gomag_api_key", "") or config_settings.GOMAG_API_KEY, + "gomag_api_shop": s.get("gomag_api_shop", "") or config_settings.GOMAG_API_SHOP, + "gomag_order_days_back": s.get("gomag_order_days_back", "") or str(config_settings.GOMAG_ORDER_DAYS_BACK), + "gomag_limit": s.get("gomag_limit", "") or str(config_settings.GOMAG_LIMIT), } @@ -481,4 +494,48 @@ async def update_app_settings(config: AppSettingsUpdate): await sqlite_service.set_app_setting("discount_id_pol", config.discount_id_pol) await sqlite_service.set_app_setting("id_pol", config.id_pol) await sqlite_service.set_app_setting("id_sectie", config.id_sectie) + await sqlite_service.set_app_setting("gomag_api_key", config.gomag_api_key) + await sqlite_service.set_app_setting("gomag_api_shop", config.gomag_api_shop) + await sqlite_service.set_app_setting("gomag_order_days_back", config.gomag_order_days_back) + await sqlite_service.set_app_setting("gomag_limit", config.gomag_limit) return {"success": True} + + +@router.get("/api/settings/sectii") +async def get_sectii(): + """Get list of sections from Oracle for dropdown.""" + def _query(): + conn = database.get_oracle_connection() + try: + with conn.cursor() as cur: + cur.execute( + "SELECT id_sectie, sectie FROM nom_sectii WHERE sters=0 AND inactiv=0 ORDER BY id_sectie" + ) + return [{"id": str(row[0]), "label": f"{row[0]} - {row[1]}"} for row in cur] + finally: + database.pool.release(conn) + try: + return await asyncio.to_thread(_query) + except Exception as e: + logger.error(f"get_sectii error: {e}") + return [] + + +@router.get("/api/settings/politici") +async def get_politici(): + """Get list of price policies from Oracle for dropdown.""" + def _query(): + conn = database.get_oracle_connection() + try: + with conn.cursor() as cur: + cur.execute( + "SELECT id_pol, nume_lista_preturi FROM crm_politici_preturi WHERE sters=0 ORDER BY id_pol" + ) + return [{"id": str(row[0]), "label": f"{row[0]} - {row[1]}"} for row in cur] + finally: + database.pool.release(conn) + try: + return await asyncio.to_thread(_query) + except Exception as e: + logger.error(f"get_politici error: {e}") + return [] diff --git a/api/app/services/gomag_client.py b/api/app/services/gomag_client.py index 038435e..9cd29bf 100644 --- a/api/app/services/gomag_client.py +++ b/api/app/services/gomag_client.py @@ -16,19 +16,27 @@ logger = logging.getLogger(__name__) async def download_orders( json_dir: str, days_back: int = None, + api_key: str = None, + api_shop: str = None, + limit: int = None, log_fn: Callable[[str], None] = None, ) -> dict: """Download orders from GoMag API and save as JSON files. Returns dict with keys: pages, total, files (list of saved file paths). If API keys are not configured, returns immediately with empty result. + Optional api_key, api_shop, limit override config.settings values. """ def _log(msg: str): logger.info(msg) if log_fn: log_fn(msg) - if not settings.GOMAG_API_KEY or not settings.GOMAG_API_SHOP: + effective_key = api_key or settings.GOMAG_API_KEY + effective_shop = api_shop or settings.GOMAG_API_SHOP + effective_limit = limit or settings.GOMAG_LIMIT + + if not effective_key or not effective_shop: _log("GoMag API keys neconfigurați, skip download") return {"pages": 0, "total": 0, "files": []} @@ -40,8 +48,8 @@ async def download_orders( out_dir.mkdir(parents=True, exist_ok=True) headers = { - "Apikey": settings.GOMAG_API_KEY, - "ApiShop": settings.GOMAG_API_SHOP, + "Apikey": effective_key, + "ApiShop": effective_shop, "User-Agent": "Mozilla/5.0", "Content-Type": "application/json", } @@ -57,7 +65,7 @@ async def download_orders( params = { "startDate": start_date, "page": page, - "limit": settings.GOMAG_LIMIT, + "limit": effective_limit, } try: response = await client.get(settings.GOMAG_API_URL, headers=headers, params=params) diff --git a/api/app/services/import_service.py b/api/app/services/import_service.py index 4ec99e3..0aa58e1 100644 --- a/api/app/services/import_service.py +++ b/api/app/services/import_service.py @@ -92,7 +92,8 @@ def build_articles_json(items, order=None, settings=None) -> str: articles.append(article_dict) # Discount total with quantity -1 (positive price) if order.discount_total > 0 and discount_codmat: - discount_vat = settings.get("discount_vat", "19") + # Use GoMag JSON discount VAT if available, fallback to settings + discount_vat = getattr(order, 'discount_vat', None) or settings.get("discount_vat", "19") article_dict = { "sku": discount_codmat, "quantity": "-1", diff --git a/api/app/services/order_reader.py b/api/app/services/order_reader.py index 8c12787..4826c43 100644 --- a/api/app/services/order_reader.py +++ b/api/app/services/order_reader.py @@ -57,6 +57,7 @@ class OrderData: total: float = 0.0 delivery_cost: float = 0.0 discount_total: float = 0.0 + discount_vat: Optional[str] = None payment_name: str = "" delivery_name: str = "" source_file: str = "" @@ -160,11 +161,14 @@ def _parse_order(order_id: str, data: dict, source_file: str) -> OrderData: # Parse delivery cost delivery_cost = float(delivery.get("total", 0) or 0) if isinstance(delivery, dict) else 0.0 - # Parse discount total (sum of all discount values) + # Parse discount total (sum of all discount values) and VAT from first discount item discount_total = 0.0 + discount_vat = None for d in data.get("discounts", []): if isinstance(d, dict): discount_total += float(d.get("value", 0) or 0) + if discount_vat is None and d.get("vat") is not None: + discount_vat = str(d["vat"]) return OrderData( id=str(data.get("id", order_id)), @@ -178,6 +182,7 @@ def _parse_order(order_id: str, data: dict, source_file: str) -> OrderData: total=float(data.get("total", 0) or 0), delivery_cost=delivery_cost, discount_total=discount_total, + discount_vat=discount_vat, payment_name=str(payment.get("name", "")) if isinstance(payment, dict) else "", delivery_name=str(delivery.get("name", "")) if isinstance(delivery, dict) else "", source_file=source_file diff --git a/api/app/services/sync_service.py b/api/app/services/sync_service.py index d20e5e8..21a236d 100644 --- a/api/app/services/sync_service.py +++ b/api/app/services/sync_service.py @@ -129,8 +129,18 @@ async def run_sync(id_pol: int = None, id_sectie: int = None, run_id: str = None # Phase 0: Download orders from GoMag API _update_progress("downloading", "Descărcare comenzi din GoMag API...") _log_line(run_id, "Descărcare comenzi din GoMag API...") + # Read GoMag settings from SQLite (override config defaults) + dl_settings = await sqlite_service.get_app_settings() + gomag_key = dl_settings.get("gomag_api_key") or None + gomag_shop = dl_settings.get("gomag_api_shop") or None + gomag_days_str = dl_settings.get("gomag_order_days_back") + gomag_days = int(gomag_days_str) if gomag_days_str else None + gomag_limit_str = dl_settings.get("gomag_limit") + gomag_limit = int(gomag_limit_str) if gomag_limit_str else None dl_result = await gomag_client.download_orders( - json_dir, log_fn=lambda msg: _log_line(run_id, msg) + json_dir, log_fn=lambda msg: _log_line(run_id, msg), + api_key=gomag_key, api_shop=gomag_shop, + days_back=gomag_days, limit=gomag_limit, ) if dl_result["files"]: _log_line(run_id, f"GoMag: {dl_result['total']} comenzi în {dl_result['pages']} pagini → {len(dl_result['files'])} fișiere") diff --git a/api/app/static/js/settings.js b/api/app/static/js/settings.js index 9219d39..611c250 100644 --- a/api/app/static/js/settings.js +++ b/api/app/static/js/settings.js @@ -1,11 +1,57 @@ let settAcTimeout = null; -document.addEventListener('DOMContentLoaded', () => { - loadSettings(); +document.addEventListener('DOMContentLoaded', async () => { + await loadDropdowns(); + await loadSettings(); wireAutocomplete('settTransportCodmat', 'settTransportAc'); wireAutocomplete('settDiscountCodmat', 'settDiscountAc'); }); +async function loadDropdowns() { + try { + const [sectiiRes, politiciRes] = await Promise.all([ + fetch('/api/settings/sectii'), + fetch('/api/settings/politici') + ]); + const sectii = await sectiiRes.json(); + const politici = await politiciRes.json(); + + const sectieEl = document.getElementById('settIdSectie'); + if (sectieEl) { + sectieEl.innerHTML = ''; + sectii.forEach(s => { + sectieEl.innerHTML += ``; + }); + } + + const polEl = document.getElementById('settIdPol'); + if (polEl) { + polEl.innerHTML = ''; + politici.forEach(p => { + polEl.innerHTML += ``; + }); + } + + const tPolEl = document.getElementById('settTransportIdPol'); + if (tPolEl) { + tPolEl.innerHTML = ''; + politici.forEach(p => { + tPolEl.innerHTML += ``; + }); + } + + const dPolEl = document.getElementById('settDiscountIdPol'); + if (dPolEl) { + dPolEl.innerHTML = ''; + politici.forEach(p => { + dPolEl.innerHTML += ``; + }); + } + } catch (err) { + console.error('loadDropdowns error:', err); + } +} + async function loadSettings() { try { const res = await fetch('/api/settings'); @@ -19,6 +65,10 @@ async function loadSettings() { if (el('settDiscountIdPol')) el('settDiscountIdPol').value = data.discount_id_pol || ''; if (el('settIdPol')) el('settIdPol').value = data.id_pol || ''; if (el('settIdSectie')) el('settIdSectie').value = data.id_sectie || ''; + if (el('settGomagApiKey')) el('settGomagApiKey').value = data.gomag_api_key || ''; + if (el('settGomagApiShop')) el('settGomagApiShop').value = data.gomag_api_shop || ''; + if (el('settGomagDaysBack')) el('settGomagDaysBack').value = data.gomag_order_days_back || '7'; + if (el('settGomagLimit')) el('settGomagLimit').value = data.gomag_limit || '100'; } catch (err) { console.error('loadSettings error:', err); } @@ -35,6 +85,10 @@ async function saveSettings() { discount_id_pol: el('settDiscountIdPol')?.value?.trim() || '', id_pol: el('settIdPol')?.value?.trim() || '', id_sectie: el('settIdSectie')?.value?.trim() || '', + gomag_api_key: el('settGomagApiKey')?.value?.trim() || '', + gomag_api_shop: el('settGomagApiShop')?.value?.trim() || '', + gomag_order_days_back: el('settGomagDaysBack')?.value?.trim() || '7', + gomag_limit: el('settGomagLimit')?.value?.trim() || '100', }; try { const res = await fetch('/api/settings', { diff --git a/api/app/templates/settings.html b/api/app/templates/settings.html index adca72a..61f1a65 100644 --- a/api/app/templates/settings.html +++ b/api/app/templates/settings.html @@ -3,96 +3,133 @@ {% block nav_settings %}active{% endblock %} {% block content %} -

Setari

+

Setari

- -
-
Import ROA
-
-
-
- - - Lasa gol pentru valoarea implicita din config -
-
- - - Lasa gol pentru valoarea implicita din config -
-
-
-
- - -
-
Transport
-
-
-
- -
- -
+
+ +
+
+
GoMag API
+
+
+ +
- Lasa gol pentru a nu adauga transport la import -
-
- - -
-
- - - Politica de pret specifica pentru transport -
-
-
-
- - -
-
Discount
-
-
-
- -
- -
+
+ + +
+
+
+ + +
+
+ + +
- Lasa gol pentru a nu adauga discount la import
-
- - -
-
- - - Politica de pret specifica pentru discount +
+
+ + +
+
+
Import ROA
+
+
+ + +
+
+ + +
-
- - +
+ +
+
+
Transport
+
+
+ +
+ +
+
+
+
+
+ + +
+
+ + +
+
+
+
+
+ + +
+
+
Discount
+
+
+ +
+ +
+
+
+
+
+ + +
+
+ + +
+
+
+
+
+
+ +
+ +
{% endblock %} {% block scripts %} - + {% endblock %}