release: v2.8.5 fix e-conomic order payload

This commit is contained in:
Christian 2026-09-11 16:46:51 +02:00
parent 3d4a7c8421
commit dc304c29fe
4 changed files with 123 additions and 34 deletions

View File

@ -0,0 +1,13 @@
# BMC Hub v2.8.5
## e-conomic ordreeksport
- Henter kundens standardopsætning fra e-conomic før oprettelse af en ordrekladde.
- Medsender obligatoriske betalingsbetingelser, modtageroplysninger og momszone.
- Bruger kundens valuta og adresseoplysninger fra e-conomic.
- Viser e-conomics konkrete valideringsfejl i stedet for kun HTTP-status 400.
## Verifikation
- 9 relevante regressionstests består.
- Python-syntaks og diff-kontrol består.

View File

@ -1 +1 @@
2.8.4 2.8.5

View File

@ -12,6 +12,36 @@ from app.core.database import execute_query, execute_query_single
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
def _economic_error_message(status: int, response_text: str) -> str:
"""Return a useful, bounded e-conomic validation error without headers or credentials."""
raw = str(response_text or "").strip()
messages: List[str] = []
try:
payload = json.loads(raw)
except (TypeError, ValueError):
payload = None
def collect(value: Any, prefix: str = "") -> None:
if isinstance(value, dict):
for key, child in value.items():
if str(key).lower() in {"developerhint", "logid", "httpstatuscode"}:
continue
collect(child, f"{prefix}.{key}".strip("."))
elif isinstance(value, list):
for child in value:
collect(child, prefix)
elif value not in (None, "") and str(value) not in messages:
label = f"{prefix}: " if prefix else ""
messages.append(f"{label}{value}")
if payload is not None:
collect(payload)
elif raw:
messages.append(raw)
detail = " · ".join(messages)[:1200]
return f"e-conomic afviste ordren ({status})" + (f": {detail}" if detail else "")
class OrdreEconomicExportService: class OrdreEconomicExportService:
"""e-conomic export service for global ordre page.""" """e-conomic export service for global ordre page."""
@ -138,11 +168,45 @@ class OrdreEconomicExportService:
economic_lines.append(line_payload) economic_lines.append(line_payload)
operation = f"Export ordre for customer {customer_id} to e-conomic"
write_allowed = self._check_write_permission(operation)
async with aiohttp.ClientSession() as session:
customer_number = int(customer["economic_customer_number"])
async with session.get(
f"{self.api_url}/customers/{customer_number}",
headers=self._headers(),
timeout=aiohttp.ClientTimeout(total=30),
) as customer_response:
customer_text = await customer_response.text()
if customer_response.status != 200:
logger.error("❌ e-conomic customer lookup failed (%s): %s", customer_response.status, customer_text)
raise HTTPException(
status_code=502,
detail=_economic_error_message(customer_response.status, customer_text),
)
economic_customer = await customer_response.json(content_type=None)
payment_terms = economic_customer.get("paymentTerms")
vat_zone = economic_customer.get("vatZone")
if not payment_terms or not vat_zone:
raise HTTPException(
status_code=502,
detail="e-conomic-kunden mangler betalingsbetingelser eller momszone",
)
payload: Dict[str, Any] = { payload: Dict[str, Any] = {
"date": date.today().isoformat(), "date": date.today().isoformat(),
"currency": "DKK", "currency": str(economic_customer.get("currency") or "DKK"),
"customer": { "customer": {"customerNumber": customer_number},
"customerNumber": int(customer["economic_customer_number"]), "paymentTerms": payment_terms,
"recipient": {
"name": str(economic_customer.get("name") or customer.get("name") or "Kunde"),
"address": str(economic_customer.get("address") or ""),
"zip": str(economic_customer.get("zip") or ""),
"city": str(economic_customer.get("city") or ""),
"country": str(economic_customer.get("country") or ""),
"vatZone": vat_zone,
}, },
"layout": { "layout": {
"layoutNumber": int(layout_number or self.default_layout), "layoutNumber": int(layout_number or self.default_layout),
@ -151,10 +215,9 @@ class OrdreEconomicExportService:
} }
if notes: if notes:
payload["notes"] = {"textLine1": str(notes)[:250]} payload["notes"] = {"textLine1": str(notes)[:1000]}
operation = f"Export ordre for customer {customer_id} to e-conomic" if not write_allowed:
if not self._check_write_permission(operation):
return { return {
"success": True, "success": True,
"dry_run": True, "dry_run": True,
@ -172,7 +235,6 @@ class OrdreEconomicExportService:
logger.info("📤 Sending ordre payload to e-conomic: %s", json.dumps(payload, default=str)) logger.info("📤 Sending ordre payload to e-conomic: %s", json.dumps(payload, default=str))
async with aiohttp.ClientSession() as session:
async with session.post( async with session.post(
f"{self.api_url}/orders/drafts", f"{self.api_url}/orders/drafts",
headers=self._headers(), headers=self._headers(),
@ -184,7 +246,7 @@ class OrdreEconomicExportService:
logger.error("❌ e-conomic export failed (%s): %s", response.status, response_text) logger.error("❌ e-conomic export failed (%s): %s", response.status, response_text)
raise HTTPException( raise HTTPException(
status_code=502, status_code=502,
detail=f"e-conomic export fejlede ({response.status})", detail=_economic_error_message(response.status, response_text),
) )
export_result = await response.json(content_type=None) export_result = await response.json(content_type=None)

View File

@ -33,3 +33,17 @@ def test_order_export_respects_explicit_order_safety_flags(monkeypatch):
assert service.read_only is True assert service.read_only is True
assert service.dry_run is True assert service.dry_run is True
def test_economic_validation_error_is_exposed_without_developer_metadata():
from app.modules.orders.backend.economic_export import _economic_error_message
message = _economic_error_message(
400,
'{"message":"Validation failed","errors":{"paymentTerms":"Required"},"developerHint":"internal"}',
)
assert "Validation failed" in message
assert "paymentTerms" in message
assert "Required" in message
assert "internal" not in message