314 lines
9.7 KiB
Python
314 lines
9.7 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 (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
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 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 UserRoleUpdate(APIModel):
|
|
role: Literal["pm", "procurement_manager", "admin"]
|
|
|
|
|
|
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
|