Files
ERP/backend/app/schemas/procurement.py
T
MMD 0ea0322967 Revert 253c922: restore the PM/RBAC work to the integration
Reverting the production revert so the pm branch can merge cleanly on
top of the restored RBAC base. Conflicts against newer integration work
resolved by combining:
- App.tsx: v7's bot-draft wizard bypass + login returnUrl kept alongside
  the supervisor gating (no-project block screen, panel scoping)
- LoginPage: returnUrl navigation kept, supervisor routing added
- migration.sql: bot_drafts table kept, unified-projects ALTERs restored
2026-10-01 15:24:56 +03:30

355 lines
11 KiB
Python

"""Pydantic v2 request schemas — quotation & procurement domain."""
from datetime import date, datetime
from decimal import Decimal
from typing import Literal
from uuid import UUID
from pydantic import Field
from app.schemas.common import APIModel
QuoteStatus = Literal["offered", "accepted", "arrived", "rejected", "expired"]
TermsheetStatus = Literal["offered", "accepted", "rejected"]
DiscountType = Literal["none", "percent", "fixed"]
DecisionStatus = Literal["pending", "chosen", "rejected"]
# ---------------------------------------------------------------------------
# Suppliers / projects
# ---------------------------------------------------------------------------
class SupplierCreate(APIModel):
name: str = Field(min_length=1)
category_id: UUID # v2.6: 1 supplier → exactly 1 category (required)
location: str | None = None
phone: str | None = None
contact_person: str | None = None
tax_id: str | None = None
class SupplierUpdate(APIModel):
name: str | None = Field(default=None, min_length=1)
category_id: UUID | None = None
location: str | None = None
phone: str | None = None
contact_person: str | None = None
tax_id: str | None = None
class SupplierMaterialCreate(APIModel):
"""Link a global material to a supplier."""
material_id: UUID
class ProjectCreate(APIModel):
name: str = Field(min_length=1)
location: str | None = None
project_manager: str | None = None
description: str | None = None
start_date: date | None = None
class ProjectUpdate(APIModel):
name: str | None = Field(default=None, min_length=1)
location: str | None = None
project_manager: str | None = None
description: str | None = None
start_date: date | None = None
status: str | None = None
# PM assignment (1:N — one PM per project, a PM may own many projects).
# Admin / procurement manager only; None unassigns this project's PM.
pm_user_id: UUID | None = None
# ---------------------------------------------------------------------------
# Quotes
# ---------------------------------------------------------------------------
class QuoteLineItemIn(APIModel):
temp_id: str = Field(min_length=1)
material_id: UUID
brand_material_id: UUID | None = None
unit_id: int
quantity: Decimal = Field(gt=0)
notes: str | None = None
project_ids: list[UUID] | None = None # پروژه‌های تخصیص‌یافته به این قلم
class ScheduleStepIn(APIModel):
step_no: int | None = Field(default=None, ge=1)
due_date: date
amount: Decimal
note: str | None = None
class TermsheetIn(APIModel):
temp_id: str = Field(min_length=1)
label: str = Field(min_length=1)
discount_type: DiscountType = "none"
discount_value: Decimal = Decimal("0")
schedule_steps: list[ScheduleStepIn] = Field(default_factory=list)
class ItemTermsheetLinkIn(APIModel):
line_item_temp_id: str = Field(min_length=1)
termsheet_temp_id: str = Field(min_length=1)
unit_price: Decimal = Field(gt=0)
min_order_qty: Decimal | None = Field(default=None, gt=0)
class QuoteCreate(APIModel):
supplier_id: UUID
project_id: UUID | None = None
project_ids: list[UUID] = Field(default_factory=list) # چند پروژه برای یک پیشنهاد
quote_date: datetime | None = None
expiry_date: date | None = None
is_official: bool = True
notes: str | None = None
# اقلام/شرایط پرداخت/قیمت‌ها می‌توانند خالی باشند — کاربر ممکن است بخواهد
# پیشنهادی ناقص را ذخیره کند (task: عدم اجبار تکمیل فیلدها)
line_items: list[QuoteLineItemIn] = Field(default_factory=list)
termsheets: list[TermsheetIn] = Field(default_factory=list)
item_termsheets: list[ItemTermsheetLinkIn] = Field(default_factory=list)
class QuoteChildrenReplaceIn(APIModel):
"""Full replacement of a quote's line items + termsheets (edit modal)."""
line_items: list[QuoteLineItemIn] = Field(default_factory=list)
termsheets: list[TermsheetIn] = Field(default_factory=list)
item_termsheets: list[ItemTermsheetLinkIn] = Field(default_factory=list)
class QuoteUpdate(APIModel):
supplier_id: UUID | None = None
project_id: UUID | None = None
project_ids: list[UUID] | None = None
quote_date: datetime | None = None
expiry_date: date | None = None
is_official: bool | None = None
notes: str | None = None
status: QuoteStatus | None = None
class AcceptTermsheetIn(APIModel):
id: UUID | None = None
label: str | None = Field(default=None, min_length=1)
discount_type: DiscountType = "none"
discount_value: Decimal = Decimal("0")
schedule_steps: list[ScheduleStepIn] = Field(min_length=1)
class ProjectAllocationIn(APIModel):
project_id: UUID
quantity: Decimal = Field(gt=0, description="Allocated quantity for this project")
class ItemDecisionIn(APIModel):
line_item_id: UUID
status: Literal["chosen", "rejected"]
accepted_termsheet_index: int | None = Field(default=None, ge=0)
adjusted_unit_price: Decimal | None = Field(default=None, gt=0)
adjusted_notes: str | None = None
project_id: UUID | None = Field(default=None, description="The ONE purchased project (v2.6 — required when the offer spans several projects)")
allocations: list[ProjectAllocationIn] = Field(default_factory=list, description="Multi-project split allocations for this line item")
class ItemProjectsIn(APIModel):
line_item_id: UUID
project_ids: list[UUID] = Field(default_factory=list)
class AcceptQuoteIn(APIModel):
accepted_termsheets: list[AcceptTermsheetIn] = Field(min_length=1)
item_decisions: list[ItemDecisionIn] = Field(min_length=1)
item_projects: list[ItemProjectsIn] = Field(default_factory=list) # تخصیص پروژه به هر قلم
class ArriveItemIn(APIModel):
line_item_id: UUID
received_qty: Decimal = Field(gt=0)
received_at: datetime | None = None
on_time: bool | None = None
quality_note: str | None = None
class ArriveItemsIn(APIModel):
items: list[ArriveItemIn] = Field(default_factory=list)
# ---------------------------------------------------------------------------
# v2.6 — PM arrival confirmation (item_decisions.project_id + allocations)
# ---------------------------------------------------------------------------
class ReceiptAllocationIn(APIModel):
warehouse_id: UUID
quantity: Decimal = Field(gt=0)
class PMReceiptCreate(APIModel):
"""PM confirms a delivery: receipt + warehouse split (free count).
Allocations must sum exactly to received_qty and every warehouse must
belong to item_decisions.project_id.
"""
item_decision_id: UUID
received_qty: Decimal = Field(gt=0)
allocations: list[ReceiptAllocationIn] = Field(min_length=1)
received_at: datetime | None = None
on_time: bool | None = None
quality_note: str | None = None
class UserCreate(APIModel):
"""Define a new user from the Organization panel (admin / procurement manager).
casdoor_id is optional: real Casdoor users auto-provision on their first
login; a manual entry gets a synthetic casdoor_id so the row can exist
before the person ever signs in.
"""
first_name: str = Field(min_length=1)
last_name: str = ""
phone: str | None = None
casdoor_id: str | None = None
role: Literal["pm", "procurement_manager", "admin", "supervisor"] | None = None
class UserRoleUpdate(APIModel):
"""role=None revokes the role (and any project assignment)."""
role: Literal["pm", "procurement_manager", "admin", "supervisor"] | None = None
class UserProjectUpdate(APIModel):
"""Assign the user to one project (multi-project aware).
project_id given → that project gets this PM (previous PM replaced,
the user's other projects kept); None → clears every assignment.
"""
project_id: UUID | None = None
class RejectQuoteIn(APIModel):
reason: str | None = None
class NoteEventIn(APIModel):
event_type: Literal["note"] = "note"
payload: dict = Field(default_factory=dict)
actor: str | None = None
# ---------------------------------------------------------------------------
# Line items / specs
# ---------------------------------------------------------------------------
class LineItemCreate(APIModel):
material_id: UUID
brand_material_id: UUID | None = None
unit_id: int
quantity: Decimal = Field(gt=0)
notes: str | None = None
class LineItemUpdate(APIModel):
material_id: UUID | None = None
brand_material_id: UUID | None = None
unit_id: int | None = None
quantity: Decimal | None = Field(default=None, gt=0)
notes: str | None = None
class SpecIn(APIModel):
attribute_definition_id: UUID
value: int | float | str | bool | None = None
unit_id_at_time: int | None = None
class SpecsPutIn(APIModel):
specs: list[SpecIn] = Field(default_factory=list)
# ---------------------------------------------------------------------------
# Termsheets / schedule steps
# ---------------------------------------------------------------------------
class TermsheetCreate(APIModel):
label: str = Field(min_length=1)
discount_type: DiscountType = "none"
discount_value: Decimal = Decimal("0")
schedule_steps: list[ScheduleStepIn] = Field(min_length=1)
class TermsheetUpdate(APIModel):
label: str | None = Field(default=None, min_length=1)
discount_type: DiscountType | None = None
discount_value: Decimal | None = None
class TermsheetStatusUpdate(APIModel):
status: Literal["accepted", "rejected"]
class ScheduleStepCreate(APIModel):
step_no: int | None = Field(default=None, ge=1)
due_date: date
amount: Decimal
note: str | None = None
class ScheduleStepUpdate(APIModel):
step_no: int | None = Field(default=None, ge=1)
due_date: date | None = None
amount: Decimal | None = None
note: str | None = None
# ---------------------------------------------------------------------------
# Item termsheets / decisions / receipts
# ---------------------------------------------------------------------------
class ItemTermsheetCreate(APIModel):
termsheet_id: UUID
unit_price: Decimal = Field(gt=0)
min_order_qty: Decimal | None = Field(default=None, gt=0)
class ItemTermsheetUpdate(APIModel):
unit_price: Decimal | None = Field(default=None, gt=0)
min_order_qty: Decimal | None = Field(default=None, gt=0)
class DecisionPut(APIModel):
chosen_item_termsheet_id: UUID | None = None
status: Literal["chosen", "rejected"]
adjusted_unit_price: Decimal | None = Field(default=None, gt=0)
adjusted_notes: str | None = None
class DecisionStatusUpdate(APIModel):
status: Literal["chosen", "rejected"]
class DecisionPatch(APIModel):
chosen_item_termsheet_id: UUID | None = None
adjusted_unit_price: Decimal | None = Field(default=None, gt=0)
adjusted_notes: str | None = None
class ReceiptCreate(APIModel):
received_at: datetime | None = None
received_qty: Decimal = Field(gt=0)
on_time: bool | None = None
quality_note: str | None = None
class ReceiptUpdate(APIModel):
received_at: datetime | None = None
received_qty: Decimal | None = Field(default=None, gt=0)
on_time: bool | None = None
quality_note: str | None = None