release: v2.8.8 fix economic permissions and order validation
This commit is contained in:
parent
3e8e5ba1cf
commit
87932f8e16
17
MDfile/RELEASE_NOTES_v2.8.8.md
Normal file
17
MDfile/RELEASE_NOTES_v2.8.8.md
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
# BMC Hub v2.8.8
|
||||||
|
|
||||||
|
## Rettelse af adgang til e-conomic-varekataloget
|
||||||
|
|
||||||
|
- Tildeler de nye e-conomic-rettigheder til den eksisterende `Administrators`-gruppe.
|
||||||
|
- Retter fejlen `Missing required permission: economic.catalog.view` for almindelige administratorer.
|
||||||
|
- Andre grupper og brugere får ikke nye rettigheder automatisk.
|
||||||
|
|
||||||
|
## Ordreeksport
|
||||||
|
|
||||||
|
- Stopper eksporten før afsendelse, hvis et varenummer ikke findes i e-conomic.
|
||||||
|
- Viser de manglende varenumre og sender ikke længere prislinjer uden den produktreference, som e-conomic kræver.
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
- Kør migration `244_economic_permissions_backfill.sql` efter migration 242 og 243.
|
||||||
|
- Migrationen er idempotent og ændrer kun gruppens koblinger til eksisterende rettigheder.
|
||||||
@ -44,6 +44,21 @@ def _economic_error_message(status: int, response_text: str) -> str:
|
|||||||
return f"e-conomic afviste ordren ({status})" + (f": {detail}" if detail else "")
|
return f"e-conomic afviste ordren ({status})" + (f": {detail}" if detail else "")
|
||||||
|
|
||||||
|
|
||||||
|
def _require_valid_product_numbers(product_numbers: set[str], valid_product_numbers: set[str]) -> List[str]:
|
||||||
|
"""Reject priced lines before POST when e-conomic cannot resolve their product."""
|
||||||
|
missing = sorted(product_numbers - valid_product_numbers)
|
||||||
|
if missing:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=409,
|
||||||
|
detail=(
|
||||||
|
"Ordren blev ikke sendt. Følgende varenumre findes ikke i e-conomic: "
|
||||||
|
+ ", ".join(missing)
|
||||||
|
+ ". Knyt eller opret varerne under Varer og e-conomic, og prøv igen."
|
||||||
|
),
|
||||||
|
)
|
||||||
|
return missing
|
||||||
|
|
||||||
|
|
||||||
class OrdreEconomicExportService:
|
class OrdreEconomicExportService:
|
||||||
"""e-conomic export service for global ordre page."""
|
"""e-conomic export service for global ordre page."""
|
||||||
|
|
||||||
@ -224,10 +239,7 @@ class OrdreEconomicExportService:
|
|||||||
if product_response.status == 200:
|
if product_response.status == 200:
|
||||||
valid_product_numbers.add(product_number)
|
valid_product_numbers.add(product_number)
|
||||||
elif product_response.status == 404:
|
elif product_response.status == 404:
|
||||||
logger.warning(
|
logger.warning("e-conomic product %s does not exist", product_number)
|
||||||
"e-conomic product %s does not exist; exporting matching lines as free text",
|
|
||||||
product_number,
|
|
||||||
)
|
|
||||||
else:
|
else:
|
||||||
product_text = await product_response.text()
|
product_text = await product_response.text()
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
@ -235,11 +247,7 @@ class OrdreEconomicExportService:
|
|||||||
detail=_economic_error_message(product_response.status, product_text),
|
detail=_economic_error_message(product_response.status, product_text),
|
||||||
)
|
)
|
||||||
|
|
||||||
skipped_product_numbers = sorted(product_numbers - valid_product_numbers)
|
missing_product_numbers = _require_valid_product_numbers(product_numbers, valid_product_numbers)
|
||||||
for line in economic_lines:
|
|
||||||
product_number = str(line.get("product", {}).get("productNumber") or "").strip()
|
|
||||||
if product_number and product_number not in valid_product_numbers:
|
|
||||||
line.pop("product", None)
|
|
||||||
|
|
||||||
customer_layout = economic_customer.get("layout") or {}
|
customer_layout = economic_customer.get("layout") or {}
|
||||||
resolved_layout_number = customer_layout.get("layoutNumber") or self.default_layout
|
resolved_layout_number = customer_layout.get("layoutNumber") or self.default_layout
|
||||||
@ -278,7 +286,8 @@ class OrdreEconomicExportService:
|
|||||||
"read_only": self.read_only,
|
"read_only": self.read_only,
|
||||||
"dry_run": self.dry_run,
|
"dry_run": self.dry_run,
|
||||||
"user_id": user_id,
|
"user_id": user_id,
|
||||||
"skipped_product_numbers": skipped_product_numbers,
|
"skipped_product_numbers": missing_product_numbers,
|
||||||
|
"missing_product_numbers": missing_product_numbers,
|
||||||
"payload": payload,
|
"payload": payload,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@ -314,7 +323,8 @@ class OrdreEconomicExportService:
|
|||||||
"customer_name": customer.get("name"),
|
"customer_name": customer.get("name"),
|
||||||
"selected_line_count": len(selected_lines),
|
"selected_line_count": len(selected_lines),
|
||||||
"user_id": user_id,
|
"user_id": user_id,
|
||||||
"skipped_product_numbers": skipped_product_numbers,
|
"skipped_product_numbers": missing_product_numbers,
|
||||||
|
"missing_product_numbers": missing_product_numbers,
|
||||||
"economic_response": export_result,
|
"economic_response": export_result,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
14
migrations/244_economic_permissions_backfill.sql
Normal file
14
migrations/244_economic_permissions_backfill.sql
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
-- Preserve administrator access after adding the e-conomic catalog permissions.
|
||||||
|
-- Migration 002 grants all permissions known at install time, so permissions
|
||||||
|
-- introduced later must also be assigned to the Administrators group.
|
||||||
|
BEGIN;
|
||||||
|
|
||||||
|
INSERT INTO group_permissions (group_id, permission_id)
|
||||||
|
SELECT g.id, p.id
|
||||||
|
FROM groups g
|
||||||
|
CROSS JOIN permissions p
|
||||||
|
WHERE g.name = 'Administrators'
|
||||||
|
AND p.category = 'economic'
|
||||||
|
ON CONFLICT DO NOTHING;
|
||||||
|
|
||||||
|
COMMIT;
|
||||||
@ -47,3 +47,22 @@ def test_economic_validation_error_is_exposed_without_developer_metadata():
|
|||||||
assert "paymentTerms" in message
|
assert "paymentTerms" in message
|
||||||
assert "Required" in message
|
assert "Required" in message
|
||||||
assert "internal" not in message
|
assert "internal" not in message
|
||||||
|
|
||||||
|
|
||||||
|
def test_order_export_blocks_priced_line_when_product_does_not_exist():
|
||||||
|
from fastapi import HTTPException
|
||||||
|
from app.modules.orders.backend.economic_export import _require_valid_product_numbers
|
||||||
|
|
||||||
|
try:
|
||||||
|
_require_valid_product_numbers({"1000", "UKENDT"}, {"1000"})
|
||||||
|
assert False, "missing product must block the export before POST"
|
||||||
|
except HTTPException as exc:
|
||||||
|
assert exc.status_code == 409
|
||||||
|
assert "UKENDT" in exc.detail
|
||||||
|
assert "Ordren blev ikke sendt" in exc.detail
|
||||||
|
|
||||||
|
|
||||||
|
def test_order_export_accepts_only_verified_products():
|
||||||
|
from app.modules.orders.backend.economic_export import _require_valid_product_numbers
|
||||||
|
|
||||||
|
assert _require_valid_product_numbers({"1000"}, {"1000"}) == []
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user