Files
leocrm/app/schemas/contact.py
T

328 lines
14 KiB
Python
Raw Normal View History

"""Unified contact schemas — create, update, read, list, contactpersons."""
from __future__ import annotations
2026-07-25 21:03:46 +02:00
from decimal import Decimal
from pydantic import BaseModel, Field, model_validator
# ── ContactPerson ──
class ContactPersonCreate(BaseModel):
firstname: str | None = Field(None, max_length=100)
middle_name: str | None = Field(None, max_length=100)
lastname: str | None = Field(None, max_length=100)
function: str | None = Field(None, max_length=255)
phone: str | None = Field(None, max_length=50)
mobilephone: str | None = Field(None, max_length=50)
email: str | None = Field(None, max_length=255)
street: str | None = Field(None, max_length=255)
number: str | None = Field(None, max_length=20)
postalcode: str | None = Field(None, max_length=20)
city: str | None = Field(None, max_length=100)
state: str | None = Field(None, max_length=100)
country: str | None = Field(None, max_length=2)
tags: str | None = Field(None, max_length=500)
custom: dict | None = None
class ContactPersonUpdate(BaseModel):
firstname: str | None = Field(None, max_length=100)
middle_name: str | None = Field(None, max_length=100)
lastname: str | None = Field(None, max_length=100)
function: str | None = Field(None, max_length=255)
phone: str | None = Field(None, max_length=50)
mobilephone: str | None = Field(None, max_length=50)
email: str | None = Field(None, max_length=255)
street: str | None = Field(None, max_length=255)
number: str | None = Field(None, max_length=20)
postalcode: str | None = Field(None, max_length=20)
city: str | None = Field(None, max_length=100)
state: str | None = Field(None, max_length=100)
country: str | None = Field(None, max_length=2)
tags: str | None = Field(None, max_length=500)
custom: dict | None = None
class ContactPersonResponse(BaseModel):
id: str
contact_id: str
displayname: str
firstname: str | None = None
middle_name: str | None = None
lastname: str | None = None
function: str | None = None
phone: str | None = None
mobilephone: str | None = None
email: str | None = None
street: str | None = None
number: str | None = None
postalcode: str | None = None
city: str | None = None
state: str | None = None
country: str | None = None
tags: str | None = None
custom: dict | None = None
created_at: str | None = None
updated_at: str | None = None
# ── Contact ──
class ContactCreate(BaseModel):
type: str = Field("company", pattern="^(company|person)$")
@model_validator(mode="after")
def _require_name_or_firstname(self):
"""Ensure company has a name or person has a firstname."""
# Infer type from payload when the caller did not send it explicitly:
# person-like fields (firstname/surname) -> person, otherwise company.
if "type" not in self.model_fields_set:
if self.firstname or self.surname:
self.type = "person"
elif self.name:
self.type = "company"
if self.type == "company" and not self.name:
raise ValueError("Company contacts require a 'name' field")
if self.type == "person" and not self.firstname:
raise ValueError("Person contacts require a 'firstname' field")
return self
2026-07-25 21:03:46 +02:00
status: str | None = Field(None, pattern="^(lead|qualified|customer|inactive)$")
name: str | None = Field(None, max_length=255)
firstname: str | None = Field(None, max_length=100)
surname: str | None = Field(None, max_length=100)
2026-07-25 21:03:46 +02:00
suffix: str | None = Field(None, max_length=50)
ext_name_line: str | None = Field(None, max_length=255)
gender: str | None = Field(None, max_length=20)
code: str | None = Field(None, max_length=100)
accounting_code: str | None = Field(None, max_length=100)
vendor_accounting_code: str | None = Field(None, max_length=100)
# Mailing
mailing_street: str | None = Field(None, max_length=255)
mailing_number: str | None = Field(None, max_length=20)
mailing_unit_number: str | None = Field(None, max_length=50)
mailing_district: str | None = Field(None, max_length=100)
mailing_extra_address_line: str | None = Field(None, max_length=255)
mailing_postalcode: str | None = Field(None, max_length=20)
mailing_city: str | None = Field(None, max_length=100)
mailing_state: str | None = Field(None, max_length=100)
mailing_country: str | None = Field(None, max_length=2)
# Visit
visit_street: str | None = Field(None, max_length=255)
visit_number: str | None = Field(None, max_length=20)
visit_unit_number: str | None = Field(None, max_length=50)
visit_district: str | None = Field(None, max_length=100)
visit_extra_address_line: str | None = Field(None, max_length=255)
visit_postalcode: str | None = Field(None, max_length=20)
visit_city: str | None = Field(None, max_length=100)
visit_state: str | None = Field(None, max_length=100)
# Invoice
invoice_street: str | None = Field(None, max_length=255)
invoice_number: str | None = Field(None, max_length=20)
invoice_unit_number: str | None = Field(None, max_length=50)
invoice_district: str | None = Field(None, max_length=100)
invoice_extra_address_line: str | None = Field(None, max_length=255)
invoice_postalcode: str | None = Field(None, max_length=20)
invoice_city: str | None = Field(None, max_length=100)
invoice_state: str | None = Field(None, max_length=100)
invoice_country: str | None = Field(None, max_length=2)
# General
country: str | None = Field(None, max_length=2)
# Communication
phone_1: str | None = Field(None, max_length=50)
phone_2: str | None = Field(None, max_length=50)
email_1: str | None = Field(None, max_length=255)
email_2: str | None = Field(None, max_length=255)
website: str | None = Field(None, max_length=500)
# Financial
vat_code: str | None = Field(None, max_length=50)
fiscal_code: str | None = Field(None, max_length=50)
commerce_code: str | None = Field(None, max_length=100)
purchase_number: str | None = Field(None, max_length=100)
bic: str | None = Field(None, max_length=50)
bank_account: str | None = Field(None, max_length=50)
# Discounts
2026-07-25 21:03:46 +02:00
discount_crew: Decimal = Decimal("0")
discount_transport: Decimal = Decimal("0")
discount_rental: Decimal = Decimal("0")
discount_sale: Decimal = Decimal("0")
discount_subrent: Decimal = Decimal("0")
discount_total: Decimal = Decimal("0")
# Geo
latitude: float | None = None
longitude: float | None = None
# Notes
projectnote: str | None = None
projectnote_title: str | None = Field(None, max_length=255)
contact_warning: str | None = None
tags: str | None = Field(None, max_length=500)
image: str | None = None
# Custom
custom: dict | None = None
# Folder assignment
folder_id: str | None = None
# Contact persons (optional inline create)
contact_persons: list[ContactPersonCreate] | None = None
# Company IDs for N:M linking (person contacts to companies)
company_ids: list[str] | None = None
class ContactUpdate(BaseModel):
type: str | None = Field(None, pattern="^(company|person)$")
2026-07-25 21:03:46 +02:00
status: str | None = Field(None, pattern="^(lead|qualified|customer|inactive)$")
name: str | None = Field(None, max_length=255)
firstname: str | None = Field(None, max_length=100)
surname: str | None = Field(None, max_length=100)
2026-07-25 21:03:46 +02:00
suffix: str | None = Field(None, max_length=50)
ext_name_line: str | None = Field(None, max_length=255)
gender: str | None = Field(None, max_length=20)
code: str | None = Field(None, max_length=100)
accounting_code: str | None = Field(None, max_length=100)
vendor_accounting_code: str | None = Field(None, max_length=100)
mailing_street: str | None = Field(None, max_length=255)
mailing_number: str | None = Field(None, max_length=20)
mailing_unit_number: str | None = Field(None, max_length=50)
mailing_district: str | None = Field(None, max_length=100)
mailing_extra_address_line: str | None = Field(None, max_length=255)
mailing_postalcode: str | None = Field(None, max_length=20)
mailing_city: str | None = Field(None, max_length=100)
mailing_state: str | None = Field(None, max_length=100)
mailing_country: str | None = Field(None, max_length=2)
visit_street: str | None = Field(None, max_length=255)
visit_number: str | None = Field(None, max_length=20)
visit_unit_number: str | None = Field(None, max_length=50)
visit_district: str | None = Field(None, max_length=100)
visit_extra_address_line: str | None = Field(None, max_length=255)
visit_postalcode: str | None = Field(None, max_length=20)
visit_city: str | None = Field(None, max_length=100)
visit_state: str | None = Field(None, max_length=100)
invoice_street: str | None = Field(None, max_length=255)
invoice_number: str | None = Field(None, max_length=20)
invoice_unit_number: str | None = Field(None, max_length=50)
invoice_district: str | None = Field(None, max_length=100)
invoice_extra_address_line: str | None = Field(None, max_length=255)
invoice_postalcode: str | None = Field(None, max_length=20)
invoice_city: str | None = Field(None, max_length=100)
invoice_state: str | None = Field(None, max_length=100)
invoice_country: str | None = Field(None, max_length=2)
country: str | None = Field(None, max_length=2)
phone_1: str | None = Field(None, max_length=50)
phone_2: str | None = Field(None, max_length=50)
email_1: str | None = Field(None, max_length=255)
email_2: str | None = Field(None, max_length=255)
website: str | None = Field(None, max_length=500)
vat_code: str | None = Field(None, max_length=50)
fiscal_code: str | None = Field(None, max_length=50)
commerce_code: str | None = Field(None, max_length=100)
purchase_number: str | None = Field(None, max_length=100)
bic: str | None = Field(None, max_length=50)
bank_account: str | None = Field(None, max_length=50)
2026-07-25 21:03:46 +02:00
discount_crew: Decimal | None = None
discount_transport: Decimal | None = None
discount_rental: Decimal | None = None
discount_sale: Decimal | None = None
discount_subrent: Decimal | None = None
discount_total: Decimal | None = None
latitude: float | None = None
longitude: float | None = None
projectnote: str | None = None
projectnote_title: str | None = Field(None, max_length=255)
contact_warning: str | None = None
tags: str | None = Field(None, max_length=500)
image: str | None = None
custom: dict | None = None
folder_id: str | None = None
default_person_id: str | None = None
admin_contactperson_id: str | None = None
class ContactResponse(BaseModel):
id: str
type: str
displayname: str
2026-07-25 21:03:46 +02:00
status: str = "lead"
name: str | None = None
firstname: str | None = None
surname: str | None = None
2026-07-25 21:03:46 +02:00
suffix: str | None = None
ext_name_line: str | None = None
gender: str | None = None
code: str | None = None
accounting_code: str | None = None
vendor_accounting_code: str | None = None
# Addresses (flat)
mailing_street: str | None = None
mailing_number: str | None = None
mailing_unit_number: str | None = None
mailing_district: str | None = None
mailing_extra_address_line: str | None = None
mailing_postalcode: str | None = None
mailing_city: str | None = None
mailing_state: str | None = None
mailing_country: str | None = None
visit_street: str | None = None
visit_number: str | None = None
visit_unit_number: str | None = None
visit_district: str | None = None
visit_extra_address_line: str | None = None
visit_postalcode: str | None = None
visit_city: str | None = None
visit_state: str | None = None
invoice_street: str | None = None
invoice_number: str | None = None
invoice_unit_number: str | None = None
invoice_district: str | None = None
invoice_extra_address_line: str | None = None
invoice_postalcode: str | None = None
invoice_city: str | None = None
invoice_state: str | None = None
invoice_country: str | None = None
country: str | None = None
phone_1: str | None = None
phone_2: str | None = None
email_1: str | None = None
email_2: str | None = None
website: str | None = None
vat_code: str | None = None
fiscal_code: str | None = None
commerce_code: str | None = None
purchase_number: str | None = None
bic: str | None = None
bank_account: str | None = None
2026-07-25 21:03:46 +02:00
discount_crew: Decimal = Decimal("0")
discount_transport: Decimal = Decimal("0")
discount_rental: Decimal = Decimal("0")
discount_sale: Decimal = Decimal("0")
discount_subrent: Decimal = Decimal("0")
discount_total: Decimal = Decimal("0")
latitude: float | None = None
longitude: float | None = None
projectnote: str | None = None
projectnote_title: str | None = None
contact_warning: str | None = None
tags: str | None = None
image: str | None = None
custom: dict | None = None
folder_id: str | None = None
default_person_id: str | None = None
admin_contactperson_id: str | None = None
created_at: str | None = None
updated_at: str | None = None
class ContactDetailResponse(ContactResponse):
contact_persons: list[ContactPersonResponse] = Field(default_factory=list)
class ContactListResponse(BaseModel):
items: list[ContactResponse]
total: int
page: int
page_size: int
# Keep old names for compat
class CompanyLinkRequest(BaseModel):
role_at_company: str | None = Field(None, max_length=100)
is_primary: bool = False