444 lines
12 KiB
Python
444 lines
12 KiB
Python
from enum import Enum
|
|
from typing import Any, Dict, 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
|
|
name_en: Optional[str] = None
|
|
name_gr: Optional[str] = None
|
|
description_en: Optional[str] = None
|
|
description_gr: 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
|
|
name_en: Optional[str] = None
|
|
name_gr: Optional[str] = None
|
|
description_en: Optional[str] = None
|
|
description_gr: 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):
|
|
address: Optional[str] = None
|
|
city: Optional[str] = None
|
|
postal_code: Optional[str] = None
|
|
region: Optional[str] = None
|
|
country: Optional[str] = None
|
|
|
|
|
|
# ── New customer status models ────────────────────────────────────────────────
|
|
|
|
class TechnicalIssue(BaseModel):
|
|
active: bool = True
|
|
opened_date: str # ISO string
|
|
resolved_date: Optional[str] = None
|
|
note: str
|
|
opened_by: str
|
|
resolved_by: Optional[str] = None
|
|
|
|
|
|
class InstallSupportEntry(BaseModel):
|
|
active: bool = True
|
|
opened_date: str # ISO string
|
|
resolved_date: Optional[str] = None
|
|
note: str
|
|
opened_by: str
|
|
resolved_by: Optional[str] = None
|
|
|
|
|
|
class TransactionEntry(BaseModel):
|
|
date: str # ISO string
|
|
flow: str # "invoice" | "payment" | "refund" | "credit"
|
|
payment_type: Optional[str] = None # "cash" | "bank_transfer" | "card" | "paypal" — null for invoices
|
|
category: str # "full_payment" | "advance" | "installment"
|
|
amount: float
|
|
currency: str = "EUR"
|
|
invoice_ref: Optional[str] = None
|
|
order_ref: Optional[str] = None
|
|
recorded_by: str
|
|
note: str = ""
|
|
|
|
|
|
# Lightweight summary stored on customer doc for fast CustomerList expanded view
|
|
class CrmSummary(BaseModel):
|
|
active_order_status: Optional[str] = None
|
|
active_order_status_date: Optional[str] = None
|
|
active_order_title: Optional[str] = None
|
|
active_issues_count: int = 0
|
|
latest_issue_date: Optional[str] = None
|
|
active_support_count: int = 0
|
|
latest_support_date: Optional[str] = None
|
|
|
|
|
|
class CustomerCreate(BaseModel):
|
|
title: Optional[str] = None
|
|
name: str
|
|
surname: Optional[str] = None
|
|
organization: Optional[str] = None
|
|
religion: 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
|
|
relationship_status: str = "lead"
|
|
technical_issues: List[Dict[str, Any]] = []
|
|
install_support: List[Dict[str, Any]] = []
|
|
transaction_history: List[Dict[str, Any]] = []
|
|
crm_summary: Optional[Dict[str, Any]] = None
|
|
|
|
|
|
class CustomerUpdate(BaseModel):
|
|
title: Optional[str] = None
|
|
name: Optional[str] = None
|
|
surname: Optional[str] = None
|
|
organization: Optional[str] = None
|
|
religion: 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
|
|
relationship_status: 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):
|
|
negotiating = "negotiating"
|
|
awaiting_quotation = "awaiting_quotation"
|
|
awaiting_customer_confirmation = "awaiting_customer_confirmation"
|
|
awaiting_fulfilment = "awaiting_fulfilment"
|
|
awaiting_payment = "awaiting_payment"
|
|
manufacturing = "manufacturing"
|
|
shipped = "shipped"
|
|
installed = "installed"
|
|
declined = "declined"
|
|
complete = "complete"
|
|
|
|
|
|
class OrderPaymentStatus(BaseModel):
|
|
required_amount: float = 0
|
|
received_amount: float = 0
|
|
balance_due: float = 0
|
|
advance_required: bool = False
|
|
advance_amount: Optional[float] = None
|
|
payment_complete: bool = False
|
|
|
|
|
|
class OrderTimelineEvent(BaseModel):
|
|
date: str # ISO string
|
|
type: str # "quote_request" | "quote_sent" | "quote_accepted" | "quote_declined"
|
|
# | "mfg_started" | "mfg_complete" | "order_shipped" | "installed"
|
|
# | "payment_received" | "invoice_sent" | "note"
|
|
note: str = ""
|
|
updated_by: str
|
|
|
|
|
|
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
|
|
title: Optional[str] = None
|
|
created_by: Optional[str] = None
|
|
status: OrderStatus = OrderStatus.negotiating
|
|
status_updated_date: Optional[str] = None
|
|
status_updated_by: Optional[str] = None
|
|
items: List[OrderItem] = []
|
|
subtotal: float = 0
|
|
discount: Optional[OrderDiscount] = None
|
|
total_price: float = 0
|
|
currency: str = "EUR"
|
|
shipping: Optional[OrderShipping] = None
|
|
payment_status: Optional[Dict[str, Any]] = None
|
|
invoice_path: Optional[str] = None
|
|
notes: Optional[str] = None
|
|
timeline: List[Dict[str, Any]] = []
|
|
|
|
|
|
class OrderUpdate(BaseModel):
|
|
order_number: Optional[str] = None
|
|
title: Optional[str] = None
|
|
status: Optional[OrderStatus] = None
|
|
status_updated_date: Optional[str] = None
|
|
status_updated_by: Optional[str] = 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[Dict[str, Any]] = 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):
|
|
type: Optional[CommType] = None
|
|
direction: Optional[CommDirection] = None
|
|
subject: Optional[str] = None
|
|
body: Optional[str] = None
|
|
logged_by: 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
|
|
thumbnail_path: 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
|
|
thumbnail_path: Optional[str] = None
|
|
|
|
|
|
class MediaListResponse(BaseModel):
|
|
items: List[MediaInDB]
|
|
total: int
|