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:
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user