bmc_hub/app/modules/orders/backend/economic_export.py
Christian 3cddb71cec feat: Add Technician Dashboard V1, V2, and V3 with enhanced UI and functionality
- Introduced Technician Dashboard V1 (tech_v1_overview.html) with KPI cards and new cases overview.
- Implemented Technician Dashboard V2 (tech_v2_workboard.html) featuring a workboard layout for daily tasks and opportunities.
- Developed Technician Dashboard V3 (tech_v3_table_focus.html) with a power table for detailed case management.
- Created a dashboard selector page (technician_dashboard_selector.html) for easy navigation between dashboard versions.
- Added user dashboard preferences migration (130_user_dashboard_preferences.sql) to store default dashboard paths.
- Enhanced sag_sager table with assigned group ID (131_sag_assignment_group.sql) for better case management.
- Updated sag_subscriptions table to include cancellation rules and billing dates (132_subscription_cancellation.sql, 134_subscription_billing_dates.sql).
- Implemented subscription staging for CRM integration (136_simply_subscription_staging.sql).
- Added a script to move time tracking section in detail view (move_time_section.py).
- Created a test script for subscription processing (test_subscription_processing.py).
2026-02-17 08:29:05 +01:00

198 lines
7.5 KiB
Python

import json
import logging
from datetime import date
from typing import Any, Dict, List, Optional
import aiohttp
from fastapi import HTTPException
from app.core.config import settings
from app.core.database import execute_query, execute_query_single
logger = logging.getLogger(__name__)
class OrdreEconomicExportService:
"""e-conomic export service for global ordre page."""
def __init__(self):
self.api_url = settings.ECONOMIC_API_URL
self.app_secret_token = settings.ECONOMIC_APP_SECRET_TOKEN
self.agreement_grant_token = settings.ECONOMIC_AGREEMENT_GRANT_TOKEN
self.read_only = settings.ORDRE_ECONOMIC_READ_ONLY
self.dry_run = settings.ORDRE_ECONOMIC_DRY_RUN
self.default_layout = settings.ORDRE_ECONOMIC_LAYOUT
self.default_product = settings.ORDRE_ECONOMIC_PRODUCT
if self.read_only:
logger.warning("🔒 ORDRE e-conomic READ-ONLY mode: Enabled")
if self.dry_run:
logger.warning("🏃 ORDRE e-conomic DRY-RUN mode: Enabled")
if not self.read_only:
logger.error("⚠️ WARNING: ORDRE e-conomic READ-ONLY disabled!")
def _headers(self) -> Dict[str, str]:
return {
"X-AppSecretToken": self.app_secret_token,
"X-AgreementGrantToken": self.agreement_grant_token,
"Content-Type": "application/json",
}
def _check_write_permission(self, operation: str) -> bool:
if self.read_only:
logger.error("🚫 BLOCKED: %s - READ_ONLY mode enabled", operation)
return False
if self.dry_run:
logger.warning("🏃 DRY-RUN: %s - Would execute but not sending", operation)
return False
logger.warning("⚠️ EXECUTING WRITE: %s", operation)
return True
async def export_order(
self,
customer_id: int,
lines: List[Dict[str, Any]],
notes: Optional[str] = None,
layout_number: Optional[int] = None,
user_id: Optional[int] = None,
) -> Dict[str, Any]:
customer = execute_query_single(
"SELECT id, name, economic_customer_number FROM customers WHERE id = %s",
(customer_id,),
)
if not customer:
raise HTTPException(status_code=404, detail="Customer not found")
if not customer.get("economic_customer_number"):
raise HTTPException(
status_code=400,
detail="Kunden mangler e-conomic kundenummer i Customers modulet",
)
selected_lines = [line for line in lines if bool(line.get("selected", True))]
if not selected_lines:
raise HTTPException(status_code=400, detail="Ingen linjer valgt til eksport")
product_ids = [int(line["product_id"]) for line in selected_lines if line.get("product_id")]
product_map: Dict[int, str] = {}
if product_ids:
product_rows = execute_query(
"SELECT id, sku_internal FROM products WHERE id = ANY(%s)",
(product_ids,),
) or []
product_map = {
int(row["id"]): str(row["sku_internal"])
for row in product_rows
if row.get("sku_internal")
}
economic_lines: List[Dict[str, Any]] = []
for line in selected_lines:
try:
quantity = float(line.get("quantity") or 0)
unit_price = float(line.get("unit_price") or 0)
discount = float(line.get("discount_percentage") or 0)
except (TypeError, ValueError):
raise HTTPException(status_code=400, detail="Ugyldige tal i linjer")
if quantity <= 0:
raise HTTPException(status_code=400, detail="Linje quantity skal være > 0")
if unit_price < 0:
raise HTTPException(status_code=400, detail="Linje unit_price skal være >= 0")
line_payload: Dict[str, Any] = {
"description": line.get("description") or "Ordrelinje",
"quantity": quantity,
"unitNetPrice": unit_price,
}
product_id = line.get("product_id")
product_number = None
if product_id is not None:
try:
product_number = product_map.get(int(product_id))
except (TypeError, ValueError):
product_number = None
if not product_number:
product_number = self.default_product
if product_number:
line_payload["product"] = {"productNumber": str(product_number)}
if discount > 0:
line_payload["discountPercentage"] = discount
economic_lines.append(line_payload)
payload: Dict[str, Any] = {
"date": date.today().isoformat(),
"currency": "DKK",
"customer": {
"customerNumber": int(customer["economic_customer_number"]),
},
"layout": {
"layoutNumber": int(layout_number or self.default_layout),
},
"lines": economic_lines,
}
if notes:
payload["notes"] = {"textLine1": str(notes)[:250]}
operation = f"Export ordre for customer {customer_id} to e-conomic"
if not self._check_write_permission(operation):
return {
"success": True,
"dry_run": True,
"message": "DRY-RUN: Export blocked by safety flags",
"details": {
"customer_id": customer_id,
"customer_name": customer.get("name"),
"selected_line_count": len(selected_lines),
"read_only": self.read_only,
"dry_run": self.dry_run,
"user_id": user_id,
"payload": payload,
},
}
logger.info("📤 Sending ordre payload to e-conomic: %s", json.dumps(payload, default=str))
async with aiohttp.ClientSession() as session:
async with session.post(
f"{self.api_url}/orders/drafts",
headers=self._headers(),
json=payload,
timeout=aiohttp.ClientTimeout(total=30),
) as response:
response_text = await response.text()
if response.status not in [200, 201]:
logger.error("❌ e-conomic export failed (%s): %s", response.status, response_text)
raise HTTPException(
status_code=502,
detail=f"e-conomic export fejlede ({response.status})",
)
export_result = await response.json(content_type=None)
draft_number = export_result.get("draftOrderNumber") or export_result.get("orderNumber")
logger.info("✅ Ordre exported to e-conomic draft %s", draft_number)
return {
"success": True,
"dry_run": False,
"message": f"Ordre eksporteret til e-conomic draft {draft_number}",
"economic_draft_id": draft_number,
"details": {
"customer_id": customer_id,
"customer_name": customer.get("name"),
"selected_line_count": len(selected_lines),
"user_id": user_id,
"economic_response": export_result,
},
}
ordre_economic_export_service = OrdreEconomicExportService()