release: v2.8.5 fix e-conomic order payload
This commit is contained in:
parent
3d4a7c8421
commit
7c1a748012
13
MDfile/RELEASE_NOTES_v2.8.5.md
Normal file
13
MDfile/RELEASE_NOTES_v2.8.5.md
Normal 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.
|
||||
@ -12,6 +12,36 @@ from app.core.database import execute_query, execute_query_single
|
||||
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:
|
||||
"""e-conomic export service for global ordre page."""
|
||||
|
||||
@ -138,41 +168,73 @@ class OrdreEconomicExportService:
|
||||
|
||||
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))
|
||||
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] = {
|
||||
"date": date.today().isoformat(),
|
||||
"currency": str(economic_customer.get("currency") or "DKK"),
|
||||
"customer": {"customerNumber": 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": {
|
||||
"layoutNumber": int(layout_number or self.default_layout),
|
||||
},
|
||||
"lines": economic_lines,
|
||||
}
|
||||
|
||||
if notes:
|
||||
payload["notes"] = {"textLine1": str(notes)[:1000]}
|
||||
|
||||
if not write_allowed:
|
||||
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 session.post(
|
||||
f"{self.api_url}/orders/drafts",
|
||||
headers=self._headers(),
|
||||
@ -184,7 +246,7 @@ class OrdreEconomicExportService:
|
||||
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})",
|
||||
detail=_economic_error_message(response.status, response_text),
|
||||
)
|
||||
|
||||
export_result = await response.json(content_type=None)
|
||||
|
||||
@ -33,3 +33,17 @@ def test_order_export_respects_explicit_order_safety_flags(monkeypatch):
|
||||
|
||||
assert service.read_only 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
|
||||
|
||||
Loading…
Reference in New Issue
Block a user