feat(sync): add delivery cost, discount tracking and import settings

Parse delivery.total and discounts[] from GoMag JSON into new
delivery_cost/discount_total fields. Add app_settings table for
configuring transport/discount CODMAT codes. When configured,
transport and discount are appended as extra articles in the
Oracle import JSON. Reorder Total column in dashboard/logs tables
and show transport/discount breakdown in order detail modals.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Claude Agent
2026-03-16 10:15:17 +00:00
parent 137c4a8b0b
commit 25aa9e544c
10 changed files with 302 additions and 22 deletions

View File

@@ -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)

View File

@@ -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

View File

@@ -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()

View File

@@ -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)