fix(backend): arrival-confirmation 500, receipt state, RBAC, Casdoor user sync
- pm.py/inventory.py: remove db.begin() on already-begun sessions — the scope dependencies read from the session first, so opening a new transaction raised InvalidRequestError -> 500 on every receipt/transaction (JWT path); get_db already commits/rolls back atomically - pm.py: receipt response returns the real derived state (was hardcoded 'arrived'); dev-user creation is race-free (insert on conflict) - expand_service: aggregate 1:N receipts per line item (received_qty sum + count) so the ledger can show partial-receipt totals - users.py: role changes are admin-only (RBAC); new POST /users/sync mirrors Casdoor org users into the app users table (identity only, roles untouched) - config/compose: CASDOOR_ADMIN_USER/PASSWORD for the sync endpoint - seed_casdoor: redirect URI updated for the Traefik edge
This commit is contained in:
@@ -25,11 +25,13 @@ inventory router.
|
||||
"""
|
||||
import base64
|
||||
import uuid
|
||||
from contextlib import nullcontext
|
||||
from dataclasses import dataclass
|
||||
from decimal import Decimal
|
||||
|
||||
from fastapi import APIRouter, Depends, Request
|
||||
from sqlalchemy import func, select
|
||||
from sqlalchemy.dialects.postgresql import insert as pg_insert
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.auth import UserContext, get_optional_user
|
||||
@@ -113,9 +115,14 @@ async def arrivals_scope(request: Request, db: AsyncSession = Depends(get_db)) -
|
||||
from app.models.procurement import User
|
||||
dev = await db.scalar(select(User).where(User.casdoor_id == "dev"))
|
||||
if dev is None:
|
||||
dev = User(casdoor_id="dev", first_name="کاربر", last_name="آزمایشی", role="pm")
|
||||
db.add(dev)
|
||||
await db.flush()
|
||||
# Insert-or-ignore: concurrent dev-mode requests must not race on the
|
||||
# unique casdoor_id ("dev") and 500 with a unique violation.
|
||||
await db.execute(
|
||||
pg_insert(User)
|
||||
.values(casdoor_id="dev", first_name="کاربر", last_name="آزمایشی", role="pm")
|
||||
.on_conflict_do_nothing(index_elements=["casdoor_id"])
|
||||
)
|
||||
dev = await db.scalar(select(User).where(User.casdoor_id == "dev"))
|
||||
return ArrivalScope(role=role, user_id=dev.id, project_id=project_id, display_name=actor or "کاربر آزمایشی")
|
||||
|
||||
|
||||
@@ -222,7 +229,11 @@ async def pm_confirm_receipt(
|
||||
ctx: ArrivalScope = Depends(arrivals_scope),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
async with db.begin():
|
||||
# No explicit db.begin(): the session is already inside an implicit
|
||||
# transaction (arrivals_scope read from it above). get_db commits on
|
||||
# success and rolls back on failure, so atomicity is preserved — opening
|
||||
# another transaction here would raise "A transaction is already begun".
|
||||
with nullcontext():
|
||||
# --- decision must be chosen ---
|
||||
decision = await db.get(ItemDecision, payload.item_decision_id)
|
||||
if decision is None or decision.status != "chosen":
|
||||
@@ -400,9 +411,10 @@ async def pm_confirm_receipt(
|
||||
quote.status = "arrived"
|
||||
quote.arrived_date = payload.received_at
|
||||
|
||||
item_state = arrival_state(li.quantity, already_received + payload.received_qty)
|
||||
return {"data": {
|
||||
"receipt_id": str(receipt.id),
|
||||
"state": "arrived",
|
||||
"state": item_state,
|
||||
"posted_transactions": [str(tx.id) for tx in posted],
|
||||
}}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user