Initial Switch to V2. Completely Overhauled Backend, Frontend and General Structure.
This commit is contained in:
239
backend/crm/orm.py
Normal file
239
backend/crm/orm.py
Normal file
@@ -0,0 +1,239 @@
|
||||
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")
|
||||
Reference in New Issue
Block a user