| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112 |
- """Pydantic schemas for the supplier master list (#2988)."""
- from datetime import datetime
- from pydantic import BaseModel, Field, field_validator
- # The CSV `suppliers` column joins names with "; " (#2988). A name carrying
- # the separator would split into unknown names on import and silently drop the
- # assignments, so it is refused at the edge rather than escaped — the column
- # stays readable and hand-editable.
- SUPPLIER_NAME_SEPARATOR = ";"
- def validate_supplier_name(value: str) -> str:
- trimmed = value.strip()
- if not trimmed:
- raise ValueError("name must not be empty")
- if SUPPLIER_NAME_SEPARATOR in trimmed:
- raise ValueError("name must not contain ';' — it separates suppliers in the CSV export")
- return trimmed
- class SupplierBase(BaseModel):
- name: str = Field(..., min_length=1, max_length=200)
- website: str | None = Field(default=None, max_length=500)
- # The business's own customer number AT this supplier.
- customer_number: str | None = Field(default=None, max_length=100)
- note: str | None = Field(default=None, max_length=500)
- class SupplierCreate(SupplierBase):
- # Only on the write schemas: SupplierResponse inherits SupplierBase, and a
- # row written before the rule existed must still be readable. mode="before"
- # so the name is trimmed before max_length sees it — a padded name at the
- # cap is not too long once stripped. Non-strings pass through to the type
- # check.
- @field_validator("name", mode="before")
- @classmethod
- def _normalize_name(cls, value: object) -> object:
- return validate_supplier_name(value) if isinstance(value, str) else value
- class SupplierUpdate(BaseModel):
- # Optional so a PATCH can leave the name alone. An explicit null is a 422
- # here instead of a NOT NULL violation surfacing as a 500.
- name: str | None = Field(default=None, min_length=1, max_length=200)
- website: str | None = Field(default=None, max_length=500)
- customer_number: str | None = Field(default=None, max_length=100)
- note: str | None = Field(default=None, max_length=500)
- @field_validator("name", mode="before")
- @classmethod
- def _normalize_name(cls, value: object) -> object:
- if value is None:
- raise ValueError("name must not be null")
- return validate_supplier_name(value) if isinstance(value, str) else value
- class SupplierResponse(SupplierBase):
- id: int
- # Number of spools referencing this supplier — shown in the settings list
- # and the reason a delete is refused (409) instead of orphaning links.
- spool_count: int = 0
- created_at: datetime
- updated_at: datetime
- class Config:
- from_attributes = True
- class SpoolSupplierLinkInput(BaseModel):
- """One spool-to-supplier assignment as written by the spool dialog."""
- supplier_id: int = Field(..., gt=0)
- # The supplier's own article number for the product — NOT the internal
- # material number (#2870).
- supplier_article_number: str | None = Field(default=None, max_length=100)
- # Quoted price per kg at this supplier, for comparing sources. Never a
- # cost basis — ``spool.cost_per_kg`` stays authoritative and is not
- # written from assignments.
- quoted_price_per_kg: float | None = Field(default=None, ge=0)
- # Marks where this concrete spool was actually bought; the other
- # assignments are alternative sources.
- is_purchase_source: bool = False
- class SpoolSupplierResponse(BaseModel):
- id: int
- supplier_id: int
- supplier_name: str
- supplier_article_number: str | None = None
- quoted_price_per_kg: float | None = None
- is_purchase_source: bool = False
- class Config:
- from_attributes = True
- class SupplierStats(BaseModel):
- """Per-supplier inventory aggregate (#2988), purchase-source spools only.
- ``spool_count`` and ``remaining_g`` cover active spools bought at this
- supplier; ``consumed_g`` and ``cost`` sum the recorded usage history of
- every purchase-source spool, archived included.
- """
- supplier_id: int
- supplier_name: str
- spool_count: int
- remaining_g: float
- consumed_g: float
- cost: float
|