bmc_hub/app/modules/orders/backend/router.py

281 lines
9.6 KiB
Python
Raw Permalink Normal View History

import logging
import json
from datetime import datetime
from typing import Any, Dict, List, Optional
from fastapi import APIRouter, HTTPException, Query, Request
from pydantic import BaseModel, Field
from app.modules.orders.backend.economic_export import ordre_economic_export_service
from app.modules.orders.backend.service import aggregate_order_lines
logger = logging.getLogger(__name__)
router = APIRouter()
class OrdreLineInput(BaseModel):
line_key: str
source_type: str
source_id: int
description: str
quantity: float = Field(gt=0)
unit_price: float = Field(ge=0)
discount_percentage: float = Field(default=0, ge=0, le=100)
unit: Optional[str] = None
product_id: Optional[int] = None
selected: bool = True
class OrdreExportRequest(BaseModel):
customer_id: int
lines: List[OrdreLineInput]
notes: Optional[str] = None
layout_number: Optional[int] = None
draft_id: Optional[int] = None
class OrdreDraftUpsertRequest(BaseModel):
title: str = Field(min_length=1, max_length=120)
customer_id: Optional[int] = None
lines: List[Dict[str, Any]] = Field(default_factory=list)
notes: Optional[str] = None
layout_number: Optional[int] = None
def _safe_json_field(value: Any) -> Any:
if value is None:
return None
if isinstance(value, (dict, list)):
return value
if isinstance(value, str):
try:
return json.loads(value)
except json.JSONDecodeError:
return value
return value
def _get_user_id_from_request(http_request: Request) -> Optional[int]:
state_user_id = getattr(http_request.state, "user_id", None)
if state_user_id is None:
return None
try:
return int(state_user_id)
except (TypeError, ValueError):
return None
@router.get("/ordre/aggregate")
async def get_ordre_aggregate(
customer_id: Optional[int] = Query(None),
sag_id: Optional[int] = Query(None),
q: Optional[str] = Query(None),
):
"""Aggregate global ordre lines from subscriptions, hardware and sales."""
try:
return aggregate_order_lines(customer_id=customer_id, sag_id=sag_id, q=q)
except Exception as e:
logger.error("❌ Error aggregating ordre lines: %s", e, exc_info=True)
raise HTTPException(status_code=500, detail="Failed to aggregate ordre lines")
@router.get("/ordre/config")
async def get_ordre_config():
"""Return ordre module safety config for frontend banner."""
return {
"economic_read_only": ordre_economic_export_service.read_only,
"economic_dry_run": ordre_economic_export_service.dry_run,
"default_layout": ordre_economic_export_service.default_layout,
"default_product": ordre_economic_export_service.default_product,
}
@router.post("/ordre/export")
async def export_ordre(request: OrdreExportRequest, http_request: Request):
"""Export selected ordre lines to e-conomic draft order."""
try:
user_id = _get_user_id_from_request(http_request)
line_payload = [line.model_dump() for line in request.lines]
export_result = await ordre_economic_export_service.export_order(
customer_id=request.customer_id,
lines=line_payload,
notes=request.notes,
layout_number=request.layout_number,
user_id=user_id,
)
exported_line_keys = [line.get("line_key") for line in line_payload if line.get("line_key")]
export_result["exported_line_keys"] = exported_line_keys
if request.draft_id:
from app.core.database import execute_query_single, execute_query
existing = execute_query_single("SELECT export_status_json FROM ordre_drafts WHERE id = %s", (request.draft_id,))
existing_status = _safe_json_field((existing or {}).get("export_status_json")) or {}
if not isinstance(existing_status, dict):
existing_status = {}
line_status = "dry-run" if export_result.get("dry_run") else "exported"
for line_key in exported_line_keys:
existing_status[line_key] = {
"status": line_status,
"timestamp": datetime.utcnow().isoformat(),
}
execute_query(
"""
UPDATE ordre_drafts
SET export_status_json = %s::jsonb,
last_exported_at = CURRENT_TIMESTAMP,
updated_at = CURRENT_TIMESTAMP
WHERE id = %s
""",
(json.dumps(existing_status, ensure_ascii=False), request.draft_id),
)
return export_result
except HTTPException:
raise
except Exception as e:
logger.error("❌ Error exporting ordre to e-conomic: %s", e, exc_info=True)
raise HTTPException(status_code=500, detail="Failed to export ordre")
@router.get("/ordre/drafts")
async def list_ordre_drafts(
http_request: Request,
limit: int = Query(25, ge=1, le=100)
):
"""List all ordre drafts (no user filtering)."""
try:
query = """
SELECT id, title, customer_id, notes, layout_number, created_by_user_id,
created_at, updated_at, last_exported_at
FROM ordre_drafts
ORDER BY updated_at DESC, id DESC
LIMIT %s
"""
params = (limit,)
from app.core.database import execute_query
return execute_query(query, params) or []
except Exception as e:
logger.error("❌ Error listing ordre drafts: %s", e, exc_info=True)
raise HTTPException(status_code=500, detail="Failed to list ordre drafts")
@router.get("/ordre/drafts/{draft_id}")
async def get_ordre_draft(draft_id: int, http_request: Request):
"""Get single ordre draft with lines payload (no user filtering)."""
try:
query = "SELECT * FROM ordre_drafts WHERE id = %s LIMIT 1"
params = (draft_id,)
from app.core.database import execute_query_single
draft = execute_query_single(query, params)
if not draft:
raise HTTPException(status_code=404, detail="Draft not found")
draft["lines_json"] = _safe_json_field(draft.get("lines_json")) or []
draft["export_status_json"] = _safe_json_field(draft.get("export_status_json")) or {}
return draft
except HTTPException:
raise
except Exception as e:
logger.error("❌ Error fetching ordre draft: %s", e, exc_info=True)
raise HTTPException(status_code=500, detail="Failed to fetch ordre draft")
@router.post("/ordre/drafts")
async def create_ordre_draft(request: OrdreDraftUpsertRequest, http_request: Request):
"""Create a new ordre draft."""
try:
user_id = _get_user_id_from_request(http_request)
from app.core.database import execute_query
query = """
INSERT INTO ordre_drafts (
title,
customer_id,
lines_json,
notes,
layout_number,
created_by_user_id,
export_status_json,
updated_at
) VALUES (%s, %s, %s::jsonb, %s, %s, %s, %s::jsonb, CURRENT_TIMESTAMP)
RETURNING *
"""
params = (
request.title,
request.customer_id,
json.dumps(request.lines, ensure_ascii=False),
request.notes,
request.layout_number,
user_id,
json.dumps({}, ensure_ascii=False),
)
result = execute_query(query, params)
return result[0]
except Exception as e:
logger.error("❌ Error creating ordre draft: %s", e, exc_info=True)
raise HTTPException(status_code=500, detail="Failed to create ordre draft")
@router.patch("/ordre/drafts/{draft_id}")
async def update_ordre_draft(draft_id: int, request: OrdreDraftUpsertRequest, http_request: Request):
"""Update existing ordre draft."""
try:
from app.core.database import execute_query
query = """
UPDATE ordre_drafts
SET title = %s,
customer_id = %s,
lines_json = %s::jsonb,
notes = %s,
layout_number = %s,
updated_at = CURRENT_TIMESTAMP
WHERE id = %s
RETURNING *
"""
params = (
request.title,
request.customer_id,
json.dumps(request.lines, ensure_ascii=False),
request.notes,
request.layout_number,
draft_id,
)
result = execute_query(query, params)
if not result:
raise HTTPException(status_code=404, detail="Draft not found")
return result[0]
except HTTPException:
raise
except Exception as e:
logger.error("❌ Error updating ordre draft: %s", e, exc_info=True)
raise HTTPException(status_code=500, detail="Failed to update ordre draft")
@router.delete("/ordre/drafts/{draft_id}")
async def delete_ordre_draft(draft_id: int, http_request: Request):
"""Delete ordre draft."""
try:
from app.core.database import execute_query
query = "DELETE FROM ordre_drafts WHERE id = %s RETURNING id"
params = (draft_id,)
result = execute_query(query, params)
if not result:
raise HTTPException(status_code=404, detail="Draft not found")
return {"status": "deleted", "id": draft_id}
except HTTPException:
raise
except Exception as e:
logger.error("❌ Error deleting ordre draft: %s", e, exc_info=True)
raise HTTPException(status_code=500, detail="Failed to delete ordre draft")