abbe7a18fc
- P0: hooks.py 3-tuple fix, trigger_dispatcher Contract, contacts/plugin unregister_actions_by_owner - P0: 5 test files — check_permission mocks removed, hardcoded DB credential → env var - P1: attachment_service DmsFile via Contract helper, restore_registry/history_hooks dedup - P1: mail/plugin restore unregister, mcp_client datetime.now(UTC), saved_views/filters patterns - P1: ProtectedRoute fail-closed, 13 test assertion fixes (bcrypt, DB-URLs, SECRET_KEYs) - P2: deprecated notifications → post_system_message (3 files), forgejo Base, report_generator lazy import - P2: webhooks permissions, deps.py/roles.py plugin perms removed, import_export default - P2: address/tags/entity_links patterns removed, worker.py Contract-Umgehungen fixed - P2: 28 frontend TODOs (hardcoded constants, deprecated notification API) - P3: dead code, duplicates, deprecated imports, private attr, __import__ inline - P3: 8 frontend TODOs (LucideIcons, inline styles, XSS, i18n) - ruff: 838 → 0 (612 auto-fix + 246 manual + 27 F821 regression fix) - F821: 30 → 0 (AutomationDefinition, DmsFile, user_id, Path, Any, String) - Contract-Umgehungen: 2 neue gefunden (worker.py:169, worker.py:280) und gefixt
312 lines
13 KiB
Python
312 lines
13 KiB
Python
"""Unified contact schemas — create, update, read, list, contactpersons."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from decimal import Decimal
|
|
|
|
from pydantic import BaseModel, Field
|
|
|
|
# ── 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)$")
|
|
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)
|
|
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
|
|
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)$")
|
|
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)
|
|
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)
|
|
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
|
|
status: str = "lead"
|
|
name: str | None = None
|
|
firstname: str | None = None
|
|
surname: str | None = None
|
|
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
|
|
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
|