354 lines
9.3 KiB
Python
354 lines
9.3 KiB
Python
from enum import Enum
|
|
from typing import List, Optional
|
|
from pydantic import BaseModel
|
|
|
|
|
|
class ProductCategory(str, Enum):
|
|
controller = "controller"
|
|
striker = "striker"
|
|
clock = "clock"
|
|
part = "part"
|
|
repair_service = "repair_service"
|
|
|
|
|
|
class CostLineItem(BaseModel):
|
|
name: str
|
|
quantity: float = 1
|
|
price: float = 0.0
|
|
|
|
|
|
class ProductCosts(BaseModel):
|
|
labor_hours: Optional[float] = None
|
|
labor_rate: Optional[float] = None
|
|
items: List[CostLineItem] = []
|
|
total: Optional[float] = None
|
|
|
|
|
|
class ProductStock(BaseModel):
|
|
on_hand: int = 0
|
|
reserved: int = 0
|
|
available: int = 0
|
|
|
|
|
|
class ProductCreate(BaseModel):
|
|
name: str
|
|
sku: Optional[str] = None
|
|
category: ProductCategory
|
|
description: Optional[str] = None
|
|
price: float
|
|
currency: str = "EUR"
|
|
costs: Optional[ProductCosts] = None
|
|
stock: Optional[ProductStock] = None
|
|
active: bool = True
|
|
status: str = "active" # active | discontinued | planned
|
|
photo_url: Optional[str] = None
|
|
|
|
|
|
class ProductUpdate(BaseModel):
|
|
name: Optional[str] = None
|
|
sku: Optional[str] = None
|
|
category: Optional[ProductCategory] = None
|
|
description: Optional[str] = None
|
|
price: Optional[float] = None
|
|
currency: Optional[str] = None
|
|
costs: Optional[ProductCosts] = None
|
|
stock: Optional[ProductStock] = None
|
|
active: Optional[bool] = None
|
|
status: Optional[str] = None
|
|
photo_url: Optional[str] = None
|
|
|
|
|
|
class ProductInDB(ProductCreate):
|
|
id: str
|
|
created_at: str
|
|
updated_at: str
|
|
|
|
|
|
class ProductListResponse(BaseModel):
|
|
products: List[ProductInDB]
|
|
total: int
|
|
|
|
|
|
# ── Customers ────────────────────────────────────────────────────────────────
|
|
|
|
class ContactType(str, Enum):
|
|
email = "email"
|
|
phone = "phone"
|
|
whatsapp = "whatsapp"
|
|
other = "other"
|
|
|
|
|
|
class CustomerContact(BaseModel):
|
|
type: ContactType
|
|
label: str
|
|
value: str
|
|
primary: bool = False
|
|
|
|
|
|
class CustomerNote(BaseModel):
|
|
text: str
|
|
by: str
|
|
at: str
|
|
|
|
|
|
class OwnedItemType(str, Enum):
|
|
console_device = "console_device"
|
|
product = "product"
|
|
freetext = "freetext"
|
|
|
|
|
|
class OwnedItem(BaseModel):
|
|
type: OwnedItemType
|
|
# console_device fields
|
|
device_id: Optional[str] = None
|
|
label: Optional[str] = None
|
|
# product fields
|
|
product_id: Optional[str] = None
|
|
product_name: Optional[str] = None
|
|
quantity: Optional[int] = None
|
|
serial_numbers: Optional[List[str]] = None
|
|
# freetext fields
|
|
description: Optional[str] = None
|
|
serial_number: Optional[str] = None
|
|
notes: Optional[str] = None
|
|
|
|
|
|
class CustomerLocation(BaseModel):
|
|
city: Optional[str] = None
|
|
country: Optional[str] = None
|
|
region: Optional[str] = None
|
|
|
|
|
|
class CustomerCreate(BaseModel):
|
|
title: Optional[str] = None
|
|
name: str
|
|
surname: Optional[str] = None
|
|
organization: Optional[str] = None
|
|
contacts: List[CustomerContact] = []
|
|
notes: List[CustomerNote] = []
|
|
location: Optional[CustomerLocation] = None
|
|
language: str = "el"
|
|
tags: List[str] = []
|
|
owned_items: List[OwnedItem] = []
|
|
linked_user_ids: List[str] = []
|
|
nextcloud_folder: Optional[str] = None
|
|
folder_id: Optional[str] = None # Human-readable Nextcloud folder name, e.g. "saint-john-corfu"
|
|
|
|
|
|
class CustomerUpdate(BaseModel):
|
|
title: Optional[str] = None
|
|
name: Optional[str] = None
|
|
surname: Optional[str] = None
|
|
organization: Optional[str] = None
|
|
contacts: Optional[List[CustomerContact]] = None
|
|
notes: Optional[List[CustomerNote]] = None
|
|
location: Optional[CustomerLocation] = None
|
|
language: Optional[str] = None
|
|
tags: Optional[List[str]] = None
|
|
owned_items: Optional[List[OwnedItem]] = None
|
|
linked_user_ids: Optional[List[str]] = None
|
|
nextcloud_folder: Optional[str] = None
|
|
# folder_id intentionally excluded from update — set once at creation
|
|
|
|
|
|
class CustomerInDB(CustomerCreate):
|
|
id: str
|
|
created_at: str
|
|
updated_at: str
|
|
|
|
|
|
class CustomerListResponse(BaseModel):
|
|
customers: List[CustomerInDB]
|
|
total: int
|
|
|
|
|
|
# ── Orders ───────────────────────────────────────────────────────────────────
|
|
|
|
class OrderStatus(str, Enum):
|
|
draft = "draft"
|
|
confirmed = "confirmed"
|
|
in_production = "in_production"
|
|
shipped = "shipped"
|
|
delivered = "delivered"
|
|
cancelled = "cancelled"
|
|
|
|
|
|
class PaymentStatus(str, Enum):
|
|
pending = "pending"
|
|
partial = "partial"
|
|
paid = "paid"
|
|
|
|
|
|
class OrderDiscount(BaseModel):
|
|
type: str # "percentage" | "fixed"
|
|
value: float = 0
|
|
reason: Optional[str] = None
|
|
|
|
|
|
class OrderShipping(BaseModel):
|
|
method: Optional[str] = None
|
|
tracking_number: Optional[str] = None
|
|
carrier: Optional[str] = None
|
|
shipped_at: Optional[str] = None
|
|
delivered_at: Optional[str] = None
|
|
destination: Optional[str] = None
|
|
|
|
|
|
class OrderItem(BaseModel):
|
|
type: str # console_device | product | freetext
|
|
product_id: Optional[str] = None
|
|
product_name: Optional[str] = None
|
|
description: Optional[str] = None
|
|
quantity: int = 1
|
|
unit_price: float = 0.0
|
|
serial_numbers: List[str] = []
|
|
|
|
|
|
class OrderCreate(BaseModel):
|
|
customer_id: str
|
|
order_number: Optional[str] = None
|
|
status: OrderStatus = OrderStatus.draft
|
|
items: List[OrderItem] = []
|
|
subtotal: float = 0
|
|
discount: Optional[OrderDiscount] = None
|
|
total_price: float = 0
|
|
currency: str = "EUR"
|
|
shipping: Optional[OrderShipping] = None
|
|
payment_status: PaymentStatus = PaymentStatus.pending
|
|
invoice_path: Optional[str] = None
|
|
notes: Optional[str] = None
|
|
|
|
|
|
class OrderUpdate(BaseModel):
|
|
customer_id: Optional[str] = None
|
|
order_number: Optional[str] = None
|
|
status: Optional[OrderStatus] = None
|
|
items: Optional[List[OrderItem]] = None
|
|
subtotal: Optional[float] = None
|
|
discount: Optional[OrderDiscount] = None
|
|
total_price: Optional[float] = None
|
|
currency: Optional[str] = None
|
|
shipping: Optional[OrderShipping] = None
|
|
payment_status: Optional[PaymentStatus] = None
|
|
invoice_path: Optional[str] = None
|
|
notes: Optional[str] = None
|
|
|
|
|
|
class OrderInDB(OrderCreate):
|
|
id: str
|
|
created_at: str
|
|
updated_at: str
|
|
|
|
|
|
class OrderListResponse(BaseModel):
|
|
orders: List[OrderInDB]
|
|
total: int
|
|
|
|
|
|
# ── Comms Log ─────────────────────────────────────────────────────────────────
|
|
|
|
class CommType(str, Enum):
|
|
email = "email"
|
|
whatsapp = "whatsapp"
|
|
call = "call"
|
|
sms = "sms"
|
|
note = "note"
|
|
in_person = "in_person"
|
|
|
|
|
|
class CommDirection(str, Enum):
|
|
inbound = "inbound"
|
|
outbound = "outbound"
|
|
internal = "internal"
|
|
|
|
|
|
class CommAttachment(BaseModel):
|
|
filename: str
|
|
nextcloud_path: Optional[str] = None
|
|
content_type: Optional[str] = None
|
|
size: Optional[int] = None
|
|
|
|
|
|
class CommCreate(BaseModel):
|
|
customer_id: Optional[str] = None
|
|
type: CommType
|
|
mail_account: Optional[str] = None
|
|
direction: CommDirection
|
|
subject: Optional[str] = None
|
|
body: Optional[str] = None
|
|
body_html: Optional[str] = None
|
|
attachments: List[CommAttachment] = []
|
|
ext_message_id: Optional[str] = None
|
|
from_addr: Optional[str] = None
|
|
to_addrs: Optional[List[str]] = None
|
|
logged_by: Optional[str] = None
|
|
occurred_at: Optional[str] = None # defaults to now if not provided
|
|
|
|
|
|
class CommUpdate(BaseModel):
|
|
subject: Optional[str] = None
|
|
body: Optional[str] = None
|
|
occurred_at: Optional[str] = None
|
|
|
|
|
|
class CommInDB(BaseModel):
|
|
id: str
|
|
customer_id: Optional[str] = None
|
|
type: CommType
|
|
mail_account: Optional[str] = None
|
|
direction: CommDirection
|
|
subject: Optional[str] = None
|
|
body: Optional[str] = None
|
|
body_html: Optional[str] = None
|
|
attachments: List[CommAttachment] = []
|
|
ext_message_id: Optional[str] = None
|
|
from_addr: Optional[str] = None
|
|
to_addrs: Optional[List[str]] = None
|
|
logged_by: Optional[str] = None
|
|
occurred_at: str
|
|
created_at: str
|
|
is_important: bool = False
|
|
is_read: bool = False
|
|
|
|
|
|
class CommListResponse(BaseModel):
|
|
entries: List[CommInDB]
|
|
total: int
|
|
|
|
|
|
# ── Media ─────────────────────────────────────────────────────────────────────
|
|
|
|
class MediaDirection(str, Enum):
|
|
received = "received"
|
|
sent = "sent"
|
|
internal = "internal"
|
|
|
|
|
|
class MediaCreate(BaseModel):
|
|
customer_id: Optional[str] = None
|
|
order_id: Optional[str] = None
|
|
filename: str
|
|
nextcloud_path: str
|
|
mime_type: Optional[str] = None
|
|
direction: Optional[MediaDirection] = None
|
|
tags: List[str] = []
|
|
uploaded_by: Optional[str] = None
|
|
|
|
|
|
class MediaInDB(BaseModel):
|
|
id: str
|
|
customer_id: Optional[str] = None
|
|
order_id: Optional[str] = None
|
|
filename: str
|
|
nextcloud_path: str
|
|
mime_type: Optional[str] = None
|
|
direction: Optional[MediaDirection] = None
|
|
tags: List[str] = []
|
|
uploaded_by: Optional[str] = None
|
|
created_at: str
|
|
|
|
|
|
class MediaListResponse(BaseModel):
|
|
items: List[MediaInDB]
|
|
total: int
|