release: v2.8.5 fix e-conomic order payload
This commit is contained in:
parent
3d4a7c8421
commit
dc304c29fe
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__)
|
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)
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user