"""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 (derives materials.category_id).""" material_id: UUID class ProjectCreate(APIModel): name: str = Field(min_length=1) description: str | None = None start_date: date | None = None class ProjectUpdate(APIModel): name: str | None = Field(default=None, min_length=1) description: str | None = None start_date: date | 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 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)") 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"] | None = None class UserRoleUpdate(APIModel): """role=None revokes the role (and any project assignment).""" role: Literal["pm", "procurement_manager", "admin"] | 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