240 lines
12 KiB
Python
240 lines
12 KiB
Python
from datetime import datetime, timezone
|
|
from sqlalchemy import (
|
|
BigInteger, Boolean, Column, DateTime, ForeignKey, Index, Integer,
|
|
Numeric, String, Text, UniqueConstraint,
|
|
)
|
|
from sqlalchemy.dialects.postgresql import ARRAY, JSONB
|
|
from sqlalchemy.orm import relationship
|
|
from database.postgres import Base
|
|
|
|
|
|
def _now():
|
|
return datetime.now(timezone.utc)
|
|
|
|
|
|
class CrmProduct(Base):
|
|
__tablename__ = "crm_products"
|
|
|
|
id = Column(String(128), primary_key=True) # Firestore doc ID
|
|
firestore_id = Column(String(128), unique=True) # same as id during transition
|
|
name = Column(String(500), nullable=False)
|
|
sku = Column(String(128))
|
|
category = Column(String(128))
|
|
description = Column(Text)
|
|
unit_cost = Column(Numeric(12, 2), nullable=False, default=0)
|
|
currency = Column(String(10), nullable=False, default="EUR")
|
|
unit_type = Column(String(32), nullable=False, default="pcs")
|
|
is_active = Column(Boolean, nullable=False, default=True)
|
|
created_at = Column(DateTime(timezone=True), nullable=False, default=_now)
|
|
updated_at = Column(DateTime(timezone=True), nullable=False, default=_now, onupdate=_now)
|
|
|
|
|
|
class CrmCustomer(Base):
|
|
__tablename__ = "crm_customers"
|
|
__table_args__ = (
|
|
Index("idx_crm_customers_rel_status", "relationship_status"),
|
|
Index("idx_crm_customers_name", "name", "surname"),
|
|
Index("idx_crm_customers_tags", "tags", postgresql_using="gin"),
|
|
)
|
|
|
|
id = Column(String(128), primary_key=True) # Firestore doc ID
|
|
firestore_id = Column(String(128), unique=True)
|
|
title = Column(String(32))
|
|
name = Column(String(255), nullable=False)
|
|
surname = Column(String(255))
|
|
organization = Column(String(500))
|
|
religion = Column(String(64))
|
|
language = Column(String(10), nullable=False, default="el")
|
|
folder_id = Column(String(128), unique=True, nullable=False)
|
|
relationship_status = Column(String(64), nullable=False, default="lead")
|
|
nextcloud_folder = Column(String(500))
|
|
contacts = Column(JSONB, nullable=False, default=list)
|
|
notes = Column(JSONB, nullable=False, default=list)
|
|
location = Column(JSONB)
|
|
tags = Column(ARRAY(String), nullable=False, default=list)
|
|
owned_items = Column(JSONB, nullable=False, default=list)
|
|
linked_user_ids = Column(ARRAY(String), nullable=False, default=list)
|
|
technical_issues = Column(JSONB, nullable=False, default=list)
|
|
install_support = Column(JSONB, nullable=False, default=list)
|
|
transaction_history = Column(JSONB, nullable=False, default=list)
|
|
crm_summary = Column(JSONB)
|
|
created_at = Column(DateTime(timezone=True), nullable=False)
|
|
updated_at = Column(DateTime(timezone=True), nullable=False)
|
|
|
|
orders = relationship("CrmOrder", back_populates="customer",
|
|
cascade="all, delete-orphan", lazy="noload")
|
|
quotations = relationship("CrmQuotation", back_populates="customer",
|
|
cascade="all, delete-orphan", lazy="noload")
|
|
comms = relationship("CrmCommsLog", back_populates="customer",
|
|
cascade="all, delete-orphan", lazy="noload")
|
|
media = relationship("CrmMedia", back_populates="customer", lazy="noload")
|
|
|
|
|
|
class CrmOrder(Base):
|
|
__tablename__ = "crm_orders"
|
|
__table_args__ = (
|
|
Index("idx_crm_orders_customer", "customer_id"),
|
|
Index("idx_crm_orders_status", "status"),
|
|
)
|
|
|
|
id = Column(String(128), primary_key=True) # Firestore doc ID
|
|
customer_id = Column(String(128), ForeignKey("crm_customers.id", ondelete="CASCADE"),
|
|
nullable=False)
|
|
order_number = Column(String(64), unique=True, nullable=False)
|
|
title = Column(String(500))
|
|
created_by = Column(String(128))
|
|
status = Column(String(64), nullable=False, default="negotiating")
|
|
status_updated_date = Column(DateTime(timezone=True))
|
|
status_updated_by = Column(String(128))
|
|
items = Column(JSONB, nullable=False, default=list)
|
|
subtotal = Column(Numeric(12, 2), nullable=False, default=0)
|
|
discount = Column(JSONB)
|
|
total_price = Column(Numeric(12, 2), nullable=False, default=0)
|
|
currency = Column(String(10), nullable=False, default="EUR")
|
|
shipping = Column(JSONB)
|
|
payment_status = Column(JSONB, nullable=False, default=dict)
|
|
invoice_path = Column(String(500))
|
|
notes = Column(Text)
|
|
timeline = Column(JSONB, nullable=False, default=list)
|
|
created_at = Column(DateTime(timezone=True), nullable=False)
|
|
updated_at = Column(DateTime(timezone=True), nullable=False)
|
|
|
|
customer = relationship("CrmCustomer", back_populates="orders")
|
|
|
|
|
|
class CrmCommsLog(Base):
|
|
__tablename__ = "crm_comms_log"
|
|
__table_args__ = (
|
|
Index("idx_crm_comms_customer", "customer_id", "occurred_at"),
|
|
)
|
|
|
|
id = Column(String(128), primary_key=True)
|
|
customer_id = Column(String(128), ForeignKey("crm_customers.id", ondelete="SET NULL"),
|
|
nullable=True)
|
|
type = Column(String(32), nullable=False) # email | sms | call | note | ...
|
|
mail_account = Column(String(256))
|
|
direction = Column(String(16), nullable=False) # inbound | outbound
|
|
subject = Column(String(500))
|
|
body = Column(Text)
|
|
body_html = Column(Text)
|
|
attachments = Column(JSONB, nullable=False, default=list)
|
|
ext_message_id = Column(String(500))
|
|
from_addr = Column(String(500))
|
|
to_addrs = Column(Text) # JSON array as text or comma-sep
|
|
logged_by = Column(String(128))
|
|
is_important = Column(Boolean, nullable=False, default=False)
|
|
is_read = Column(Boolean, nullable=False, default=True)
|
|
occurred_at = Column(DateTime(timezone=True), nullable=False)
|
|
created_at = Column(DateTime(timezone=True), nullable=False, default=_now)
|
|
|
|
customer = relationship("CrmCustomer", back_populates="comms")
|
|
|
|
|
|
class CrmMedia(Base):
|
|
__tablename__ = "crm_media"
|
|
__table_args__ = (
|
|
Index("idx_crm_media_customer", "customer_id"),
|
|
Index("idx_crm_media_order", "order_id"),
|
|
)
|
|
|
|
id = Column(String(128), primary_key=True)
|
|
customer_id = Column(String(128), ForeignKey("crm_customers.id", ondelete="SET NULL"),
|
|
nullable=True)
|
|
order_id = Column(String(128))
|
|
filename = Column(String(500), nullable=False)
|
|
nextcloud_path = Column(String(1000), nullable=False)
|
|
thumbnail_path = Column(String(1000))
|
|
mime_type = Column(String(128))
|
|
direction = Column(String(16))
|
|
tags = Column(JSONB, nullable=False, default=list)
|
|
uploaded_by = Column(String(128))
|
|
created_at = Column(DateTime(timezone=True), nullable=False, default=_now)
|
|
|
|
customer = relationship("CrmCustomer", back_populates="media")
|
|
|
|
|
|
class CrmSyncState(Base):
|
|
__tablename__ = "crm_sync_state"
|
|
|
|
key = Column(String(128), primary_key=True)
|
|
value = Column(Text)
|
|
|
|
|
|
class CrmQuotation(Base):
|
|
__tablename__ = "crm_quotations"
|
|
__table_args__ = (
|
|
Index("idx_crm_quotations_customer", "customer_id"),
|
|
)
|
|
|
|
id = Column(String(128), primary_key=True)
|
|
quotation_number = Column(String(64), unique=True, nullable=False)
|
|
title = Column(String(500))
|
|
subtitle = Column(String(500))
|
|
customer_id = Column(String(128), ForeignKey("crm_customers.id", ondelete="CASCADE"),
|
|
nullable=False)
|
|
language = Column(String(10), nullable=False, default="en")
|
|
status = Column(String(32), nullable=False, default="draft")
|
|
order_type = Column(String(64))
|
|
shipping_method = Column(String(64))
|
|
estimated_shipping_date = Column(String(32)) # stored as DATE string
|
|
global_discount_label = Column(String(128))
|
|
global_discount_percent = Column(Numeric(8, 4), nullable=False, default=0)
|
|
vat_percent = Column(Numeric(8, 4), nullable=False, default=24)
|
|
global_vat_percent = Column(Numeric(8, 4), nullable=False, default=24)
|
|
shipping_cost = Column(Numeric(12, 2), nullable=False, default=0)
|
|
shipping_cost_discount = Column(Numeric(12, 2), nullable=False, default=0)
|
|
install_cost = Column(Numeric(12, 2), nullable=False, default=0)
|
|
install_cost_discount = Column(Numeric(12, 2), nullable=False, default=0)
|
|
extras_label = Column(String(256))
|
|
extras_cost = Column(Numeric(12, 2), nullable=False, default=0)
|
|
comments = Column(JSONB, nullable=False, default=list)
|
|
quick_notes = Column(JSONB, nullable=False, default=dict)
|
|
subtotal_before_discount = Column(Numeric(12, 2), nullable=False, default=0)
|
|
global_discount_amount = Column(Numeric(12, 2), nullable=False, default=0)
|
|
new_subtotal = Column(Numeric(12, 2), nullable=False, default=0)
|
|
vat_amount = Column(Numeric(12, 2), nullable=False, default=0)
|
|
final_total = Column(Numeric(12, 2), nullable=False, default=0)
|
|
nextcloud_pdf_path = Column(String(1000))
|
|
nextcloud_pdf_url = Column(String(1000))
|
|
# Client snapshot fields (denormalised for PDF generation)
|
|
client_org = Column(String(500))
|
|
client_name = Column(String(500))
|
|
client_location = Column(String(500))
|
|
client_phone = Column(String(64))
|
|
client_email = Column(String(256))
|
|
# Legacy quotation fields
|
|
is_legacy = Column(Boolean, nullable=False, default=False)
|
|
legacy_date = Column(String(32))
|
|
legacy_pdf_path = Column(String(1000))
|
|
created_at = Column(DateTime(timezone=True), nullable=False)
|
|
updated_at = Column(DateTime(timezone=True), nullable=False)
|
|
|
|
customer = relationship("CrmCustomer", back_populates="quotations")
|
|
items = relationship("CrmQuotationItem", back_populates="quotation",
|
|
cascade="all, delete-orphan",
|
|
order_by="CrmQuotationItem.sort_order", lazy="noload")
|
|
|
|
|
|
class CrmQuotationItem(Base):
|
|
__tablename__ = "crm_quotation_items"
|
|
__table_args__ = (
|
|
Index("idx_crm_quotation_items_quotation", "quotation_id", "sort_order"),
|
|
)
|
|
|
|
id = Column(String(128), primary_key=True)
|
|
quotation_id = Column(String(128), ForeignKey("crm_quotations.id", ondelete="CASCADE"),
|
|
nullable=False)
|
|
product_id = Column(String(128))
|
|
description = Column(Text)
|
|
description_en = Column(Text)
|
|
description_gr = Column(Text)
|
|
unit_type = Column(String(32), nullable=False, default="pcs")
|
|
unit_cost = Column(Numeric(12, 4), nullable=False, default=0)
|
|
discount_percent = Column(Numeric(8, 4), nullable=False, default=0)
|
|
vat_percent = Column(Numeric(8, 4), nullable=False, default=24)
|
|
quantity = Column(Numeric(12, 4), nullable=False, default=1)
|
|
line_total = Column(Numeric(12, 2), nullable=False, default=0)
|
|
sort_order = Column(Integer, nullable=False, default=0)
|
|
|
|
quotation = relationship("CrmQuotation", back_populates="items")
|