diff --git a/api/app/database.py b/api/app/database.py index 93048d3..8f520ae 100644 --- a/api/app/database.py +++ b/api/app/database.py @@ -104,7 +104,9 @@ CREATE TABLE IF NOT EXISTS orders ( factura_total_tva REAL, factura_total_cu_tva REAL, invoice_checked_at TEXT, - order_total REAL + order_total REAL, + delivery_cost REAL, + discount_total REAL ); CREATE INDEX IF NOT EXISTS idx_orders_status ON orders(status); CREATE INDEX IF NOT EXISTS idx_orders_date ON orders(order_date); @@ -140,6 +142,11 @@ CREATE TABLE IF NOT EXISTS web_products ( order_count INTEGER DEFAULT 0 ); +CREATE TABLE IF NOT EXISTS app_settings ( + key TEXT PRIMARY KEY, + value TEXT +); + CREATE TABLE IF NOT EXISTS order_items ( order_number TEXT, sku TEXT, @@ -303,6 +310,8 @@ def init_sqlite(): ("factura_total_cu_tva", "REAL"), ("invoice_checked_at", "TEXT"), ("order_total", "REAL"), + ("delivery_cost", "REAL"), + ("discount_total", "REAL"), ]: if col not in order_cols: conn.execute(f"ALTER TABLE orders ADD COLUMN {col} {typedef}") diff --git a/api/app/routers/sync.py b/api/app/routers/sync.py index 9b1d316..df6e09d 100644 --- a/api/app/routers/sync.py +++ b/api/app/routers/sync.py @@ -20,6 +20,12 @@ class ScheduleConfig(BaseModel): interval_minutes: int = 5 +class AppSettingsUpdate(BaseModel): + transport_codmat: str = "" + transport_vat: str = "21" + discount_codmat: str = "" + + # API endpoints @router.post("/api/sync/start") async def start_sync(background_tasks: BackgroundTasks): @@ -429,3 +435,23 @@ async def update_schedule(config: ScheduleConfig): async def get_schedule(): """Get current scheduler status.""" return scheduler_service.get_scheduler_status() + + +@router.get("/api/settings") +async def get_app_settings(): + """Get application settings.""" + settings = 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", ""), + } + + +@router.put("/api/settings") +async def update_app_settings(config: AppSettingsUpdate): + """Update application settings.""" + await sqlite_service.set_app_setting("transport_codmat", config.transport_codmat) + await sqlite_service.set_app_setting("transport_vat", config.transport_vat) + await sqlite_service.set_app_setting("discount_codmat", config.discount_codmat) + return {"success": True} diff --git a/api/app/services/import_service.py b/api/app/services/import_service.py index f99c64d..2fa83bf 100644 --- a/api/app/services/import_service.py +++ b/api/app/services/import_service.py @@ -60,8 +60,9 @@ def format_address_for_oracle(address: str, city: str, region: str) -> str: return f"JUD:{region_clean};{city_clean};{address_clean}" -def build_articles_json(items) -> str: - """Build JSON string for Oracle PACK_IMPORT_COMENZI.importa_comanda.""" +def build_articles_json(items, order=None, settings=None) -> str: + """Build JSON string for Oracle PACK_IMPORT_COMENZI.importa_comanda. + Includes transport and discount as extra articles if configured.""" articles = [] for item in items: articles.append({ @@ -71,10 +72,35 @@ def build_articles_json(items) -> str: "vat": str(item.vat), "name": clean_web_text(item.name) }) + + if order and settings: + transport_codmat = settings.get("transport_codmat", "") + transport_vat = settings.get("transport_vat", "21") + discount_codmat = settings.get("discount_codmat", "") + + # Transport as article with quantity +1 + if order.delivery_cost > 0 and transport_codmat: + articles.append({ + "sku": transport_codmat, + "quantity": "1", + "price": str(order.delivery_cost), + "vat": transport_vat, + "name": "Transport" + }) + # Discount total with quantity -1 (positive price) + if order.discount_total > 0 and discount_codmat: + articles.append({ + "sku": discount_codmat, + "quantity": "-1", + "price": str(order.discount_total), + "vat": "21", + "name": "Discount" + }) + return json.dumps(articles) -def import_single_order(order, id_pol: int = None, id_sectie: int = None) -> dict: +def import_single_order(order, id_pol: int = None, id_sectie: int = None, app_settings: dict = None) -> dict: """Import a single order into Oracle ROA. Returns dict with: @@ -203,7 +229,7 @@ def import_single_order(order, id_pol: int = None, id_sectie: int = None) -> dic result["id_adresa_livrare"] = int(addr_livr_id) # Step 4: Build articles JSON and import order - articles_json = build_articles_json(order.items) + articles_json = build_articles_json(order.items, order, app_settings) # Use CLOB for the JSON clob_var = cur.var(oracledb.DB_TYPE_CLOB) diff --git a/api/app/services/order_reader.py b/api/app/services/order_reader.py index ce85b9d..8c12787 100644 --- a/api/app/services/order_reader.py +++ b/api/app/services/order_reader.py @@ -55,6 +55,8 @@ class OrderData: billing: OrderBilling = field(default_factory=OrderBilling) shipping: Optional[OrderShipping] = None total: float = 0.0 + delivery_cost: float = 0.0 + discount_total: float = 0.0 payment_name: str = "" delivery_name: str = "" source_file: str = "" @@ -155,6 +157,15 @@ def _parse_order(order_id: str, data: dict, source_file: str) -> OrderData: payment = data.get("payment", {}) or {} delivery = data.get("delivery", {}) or {} + # 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) + discount_total = 0.0 + for d in data.get("discounts", []): + if isinstance(d, dict): + discount_total += float(d.get("value", 0) or 0) + return OrderData( id=str(data.get("id", order_id)), number=str(data.get("number", "")), @@ -165,6 +176,8 @@ def _parse_order(order_id: str, data: dict, source_file: str) -> OrderData: billing=billing, shipping=shipping, total=float(data.get("total", 0) or 0), + delivery_cost=delivery_cost, + discount_total=discount_total, 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/sqlite_service.py b/api/app/services/sqlite_service.py index 7d4b573..6093f70 100644 --- a/api/app/services/sqlite_service.py +++ b/api/app/services/sqlite_service.py @@ -51,7 +51,8 @@ async def upsert_order(sync_run_id: str, order_number: str, order_date: str, missing_skus: list = None, items_count: int = 0, shipping_name: str = None, billing_name: str = None, payment_method: str = None, delivery_method: str = None, - order_total: float = None): + order_total: float = None, + delivery_cost: float = None, discount_total: float = None): """Upsert a single order — one row per order_number, status updated in place.""" db = await get_sqlite() try: @@ -60,8 +61,9 @@ async def upsert_order(sync_run_id: str, order_number: str, order_date: str, (order_number, order_date, customer_name, status, id_comanda, id_partener, error_message, missing_skus, items_count, last_sync_run_id, shipping_name, billing_name, - payment_method, delivery_method, order_total) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + payment_method, delivery_method, order_total, + delivery_cost, discount_total) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ON CONFLICT(order_number) DO UPDATE SET status = CASE WHEN orders.status = 'IMPORTED' AND excluded.status = 'ALREADY_IMPORTED' @@ -82,12 +84,15 @@ async def upsert_order(sync_run_id: str, order_number: str, order_date: str, payment_method = COALESCE(excluded.payment_method, orders.payment_method), delivery_method = COALESCE(excluded.delivery_method, orders.delivery_method), order_total = COALESCE(excluded.order_total, orders.order_total), + delivery_cost = COALESCE(excluded.delivery_cost, orders.delivery_cost), + discount_total = COALESCE(excluded.discount_total, orders.discount_total), updated_at = datetime('now') """, (order_number, order_date, customer_name, status, id_comanda, id_partener, error_message, json.dumps(missing_skus) if missing_skus else None, items_count, sync_run_id, shipping_name, billing_name, - payment_method, delivery_method, order_total)) + payment_method, delivery_method, order_total, + delivery_cost, discount_total)) await db.commit() finally: await db.close() @@ -112,7 +117,7 @@ async def save_orders_batch(orders_data: list[dict]): Each dict must have: sync_run_id, order_number, order_date, customer_name, status, id_comanda, id_partener, error_message, missing_skus (list|None), items_count, shipping_name, billing_name, payment_method, delivery_method, status_at_run, - items (list of item dicts). + items (list of item dicts), delivery_cost (optional), discount_total (optional). """ if not orders_data: return @@ -124,8 +129,9 @@ async def save_orders_batch(orders_data: list[dict]): (order_number, order_date, customer_name, status, id_comanda, id_partener, error_message, missing_skus, items_count, last_sync_run_id, shipping_name, billing_name, - payment_method, delivery_method, order_total) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + payment_method, delivery_method, order_total, + delivery_cost, discount_total) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ON CONFLICT(order_number) DO UPDATE SET status = CASE WHEN orders.status = 'IMPORTED' AND excluded.status = 'ALREADY_IMPORTED' @@ -146,6 +152,8 @@ async def save_orders_batch(orders_data: list[dict]): payment_method = COALESCE(excluded.payment_method, orders.payment_method), delivery_method = COALESCE(excluded.delivery_method, orders.delivery_method), order_total = COALESCE(excluded.order_total, orders.order_total), + delivery_cost = COALESCE(excluded.delivery_cost, orders.delivery_cost), + discount_total = COALESCE(excluded.discount_total, orders.discount_total), updated_at = datetime('now') """, [ (d["order_number"], d["order_date"], d["customer_name"], d["status"], @@ -154,7 +162,8 @@ async def save_orders_batch(orders_data: list[dict]): d.get("items_count", 0), d["sync_run_id"], d.get("shipping_name"), d.get("billing_name"), d.get("payment_method"), d.get("delivery_method"), - d.get("order_total")) + d.get("order_total"), + d.get("delivery_cost"), d.get("discount_total")) for d in orders_data ]) @@ -768,3 +777,29 @@ async def update_order_invoice(order_number: str, serie: str = None, await db.commit() finally: await db.close() + + +# ── App Settings ───────────────────────────────── + +async def get_app_settings() -> dict: + """Get all app settings as a dict.""" + db = await get_sqlite() + try: + cursor = await db.execute("SELECT key, value FROM app_settings") + rows = await cursor.fetchall() + return {row["key"]: row["value"] for row in rows} + finally: + await db.close() + + +async def set_app_setting(key: str, value: str): + """Set a single app setting value.""" + db = await get_sqlite() + try: + await db.execute(""" + INSERT OR REPLACE INTO app_settings (key, value) + VALUES (?, ?) + """, (key, value)) + await db.commit() + finally: + await db.close() diff --git a/api/app/services/sync_service.py b/api/app/services/sync_service.py index dc96e6d..05908a4 100644 --- a/api/app/services/sync_service.py +++ b/api/app/services/sync_service.py @@ -287,6 +287,8 @@ async def run_sync(id_pol: int = None, id_sectie: int = None, run_id: str = None "shipping_name": shipping_name, "billing_name": billing_name, "payment_method": payment_method, "delivery_method": delivery_method, "order_total": order.total or None, + "delivery_cost": order.delivery_cost or None, + "discount_total": order.discount_total or None, "items": order_items_data, }) _log_line(run_id, f"#{order.number} [{order.date or '?'}] {customer} → DEJA IMPORTAT (ID: {id_comanda_roa})") @@ -315,6 +317,8 @@ async def run_sync(id_pol: int = None, id_sectie: int = None, run_id: str = None "shipping_name": shipping_name, "billing_name": billing_name, "payment_method": payment_method, "delivery_method": delivery_method, "order_total": order.total or None, + "delivery_cost": order.delivery_cost or None, + "discount_total": order.discount_total or None, "items": order_items_data, }) _log_line(run_id, f"#{order.number} [{order.date or '?'}] {customer} → OMIS (lipsa: {', '.join(missing_skus)})") @@ -327,6 +331,9 @@ async def run_sync(id_pol: int = None, id_sectie: int = None, run_id: str = None imported_count = 0 error_count = 0 + # Load app settings for transport/discount CODMAT config + app_settings = await sqlite_service.get_app_settings() + for i, order in enumerate(truly_importable): shipping_name, billing_name, customer, payment_method, delivery_method = _derive_customer_info(order) @@ -338,7 +345,8 @@ async def run_sync(id_pol: int = None, id_sectie: int = None, run_id: str = None result = await asyncio.to_thread( import_service.import_single_order, - order, id_pol=id_pol, id_sectie=id_sectie + order, id_pol=id_pol, id_sectie=id_sectie, + app_settings=app_settings ) # Build order items data for storage (R9) @@ -368,6 +376,8 @@ async def run_sync(id_pol: int = None, id_sectie: int = None, run_id: str = None payment_method=payment_method, delivery_method=delivery_method, order_total=order.total or None, + delivery_cost=order.delivery_cost or None, + discount_total=order.discount_total or None, ) await sqlite_service.add_sync_run_order(run_id, order.number, "IMPORTED") # Store ROA address IDs (R9) @@ -394,6 +404,8 @@ async def run_sync(id_pol: int = None, id_sectie: int = None, run_id: str = None payment_method=payment_method, delivery_method=delivery_method, order_total=order.total or None, + delivery_cost=order.delivery_cost or None, + discount_total=order.discount_total or None, ) await sqlite_service.add_sync_run_order(run_id, order.number, "ERROR") await sqlite_service.add_order_items(order.number, order_items_data) diff --git a/api/app/static/js/dashboard.js b/api/app/static/js/dashboard.js index 380deb2..7cde8a6 100644 --- a/api/app/static/js/dashboard.js +++ b/api/app/static/js/dashboard.js @@ -313,10 +313,10 @@ async function loadDashOrders() {
${esc(o.order_number)}