Files

756 lines
33 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
ORM-модели для service portal (SQLAlchemy 2.0).
Основные таблицы:
- users: сотрудники (owner/engineer/technician)
- customers: клиенты
- objects: объекты обслуживания
- tasks / incidents: задачи и инциденты
- leads: лиды (обращения из бота, звонков, сайта, email)
- call_records: звонки (CDR из Yeastar S20)
- client_notifications: уведомления
- pbx_settings: настройки АТС
- blog/cases/ideas: контент
"""
from datetime import datetime
from sqlalchemy import (
Column, Integer, String, Boolean, Text, DateTime, Numeric, Date, BigInteger, SmallInteger, JSON, ForeignKey, CheckConstraint, func
)
from sqlalchemy.orm import relationship, declarative_base
Base = declarative_base()
class User(Base):
__tablename__ = "users"
id = Column(Integer, primary_key=True, autoincrement=True)
login = Column(String(50), unique=True, nullable=False)
password_hash = Column(String(255), nullable=False)
role = Column(String(20), nullable=False, default="technician")
full_name = Column(String(255), nullable=False)
phone = Column(String(50), default="")
email = Column(String(255), default="")
max_user_id = Column(BigInteger, nullable=True)
extension = Column(String(10), default="") # Внутренний номер АТС (1001, 1002...)
is_active = Column(Boolean, nullable=False, default=True)
created_at = Column(DateTime, nullable=False, server_default="NOW()")
updated_at = Column(DateTime, nullable=False, server_default="NOW()", onupdate=func.now())
__table_args__ = (
CheckConstraint("role IN ('owner','engineer','technician')", name="ck_users_role"),
)
objects_created = relationship("Object", foreign_keys="Object.created_by", back_populates="creator")
tasks_created = relationship("Task", foreign_keys="Task.created_by", back_populates="creator")
tasks_assigned = relationship("Task", foreign_keys="Task.assigned_to", back_populates="assignee")
class LoginAttempt(Base):
__tablename__ = "login_attempts"
id = Column(Integer, primary_key=True, autoincrement=True)
ip_address = Column(String(45), nullable=False)
login = Column(String(50), nullable=False, default="")
attempt_time = Column(DateTime, nullable=False, server_default="NOW()")
class AuditLog(Base):
"""Лог действий пользователей (аудит).
Записывает все изменения в системе: кто, что, когда изменил.
Используется для отслеживания дедупликации и ручных правок.
"""
__tablename__ = "audit_log"
id = Column(BigInteger, primary_key=True, autoincrement=True)
user_id = Column(Integer, ForeignKey("users.id", ondelete="SET NULL"))
action = Column(String(255), nullable=False)
entity_type = Column(String(50), default="") # customer, lead, call_record, task
entity_id = Column(Integer, default=0)
field = Column(String(100), default="") # какое поле изменено
old_value = Column(Text, default="") # старое значение
new_value = Column(Text, default="") # новое значение
details = Column(Text)
ip_address = Column(String(45), default="")
created_at = Column(DateTime, nullable=False, server_default="NOW()")
class Customer(Base):
"""Клиент (контрагент).
Хранит контактные данные, ИНН/КПП, статус.
Статус 'draft' — автосоздан системой из лида/звонка, ожидает заполнения.
Статус 'merged' — объединён с другим клиентом.
"""
__tablename__ = "customers"
id = Column(Integer, primary_key=True, autoincrement=True)
name = Column(String(255), nullable=False)
inn = Column(String(12), default="")
kpp = Column(String(9), default="")
legal_address = Column(String(500), default="")
contact_person = Column(String(255), default="")
contact_phone = Column(String(50), default="")
contact_email = Column(String(255), default="")
status = Column(String(20), nullable=False, default="active")
notes = Column(Text)
lead_source = Column(String(20), default="") # Откуда пришёл клиент
last_call_at = Column(DateTime) # Последний звонок
total_calls = Column(Integer, default=0) # Количество звонков
total_leads = Column(Integer, default=0) # Количество лидов
merged_into = Column(Integer, nullable=True) # ID клиента, с которым слили
created_by = Column(Integer, ForeignKey("users.id", ondelete="SET NULL"))
created_at = Column(DateTime, nullable=False, server_default="NOW()")
updated_at = Column(DateTime, nullable=False, server_default="NOW()", onupdate=func.now())
__table_args__ = (
CheckConstraint(
"status IN ('active','inactive','prospect','draft','merged')",
name="ck_customers_status"
),
)
objects = relationship("Object", back_populates="customer")
sla_contracts = relationship("SLAContract", back_populates="customer")
class Object(Base):
__tablename__ = "objects"
id = Column(Integer, primary_key=True, autoincrement=True)
customer_id = Column(Integer, ForeignKey("customers.id", ondelete="SET NULL"))
name = Column(String(255), nullable=False)
address = Column(String(500), default="")
object_type = Column(String(100), default="")
area_sqm = Column(Numeric(10, 2), default=0)
employees_count = Column(Integer, default=0)
contact_person = Column(String(255), default="")
contact_phone = Column(String(50), default="")
status = Column(String(20), nullable=False, default="active")
notes = Column(Text)
risk_score = Column(Numeric(5, 2), default=0)
complexity_index = Column(Numeric(5, 2), default=0)
infrastructure_load = Column(Numeric(5, 2), default=0)
service_history = Column(Numeric(5, 2), default=0)
object_index = Column(Numeric(5, 2), default=0)
sla_price_monthly = Column(Numeric(12, 2), default=0)
region_factor = Column(Numeric(3, 2), default=1.00)
created_by = Column(Integer, ForeignKey("users.id", ondelete="SET NULL"))
created_at = Column(DateTime, nullable=False, server_default="NOW()")
updated_at = Column(DateTime, nullable=False, server_default="NOW()", onupdate=func.now())
__table_args__ = (
CheckConstraint("status IN ('active','inactive','audit','prospective')", name="ck_objects_status"),
)
customer = relationship("Customer", back_populates="objects")
creator = relationship("User", foreign_keys=[created_by], back_populates="objects_created")
assignments = relationship("ObjectAssignment", back_populates="object")
tasks = relationship("Task", back_populates="object")
reports = relationship("Report", back_populates="object")
incidents = relationship("Incident", back_populates="object")
sla_contracts = relationship("SLAContract", back_populates="object")
passports = relationship("ObjectPassport", back_populates="object")
class ObjectAssignment(Base):
__tablename__ = "object_assignments"
id = Column(Integer, primary_key=True, autoincrement=True)
object_id = Column(Integer, ForeignKey("objects.id", ondelete="CASCADE"), nullable=False)
user_id = Column(Integer, ForeignKey("users.id", ondelete="CASCADE"), nullable=False)
role = Column(String(20), nullable=False)
assigned_at = Column(DateTime, nullable=False, server_default="NOW()")
unassigned_at = Column(DateTime)
__table_args__ = (
CheckConstraint("role IN ('engineer','technician')", name="ck_assignments_role"),
)
object = relationship("Object", back_populates="assignments")
user = relationship("User")
class Task(Base):
"""Задача (привязана к объекту).
Owner/engineer создают и назначают на любого.
Technician может создать только себе напоминание.
lead_id — связь задачи с лидом (если задача создана из лида).
"""
__tablename__ = "tasks"
id = Column(Integer, primary_key=True, autoincrement=True)
object_id = Column(Integer, ForeignKey("objects.id", ondelete="CASCADE"), nullable=False)
assigned_to = Column(Integer, ForeignKey("users.id", ondelete="SET NULL"))
created_by = Column(Integer, ForeignKey("users.id", ondelete="SET NULL"))
lead_id = Column(Integer, ForeignKey("leads.id", ondelete="SET NULL")) # Связь с лидом
title = Column(String(500), nullable=False)
description = Column(Text)
priority = Column(String(4), nullable=False, default="P3")
status = Column(String(20), nullable=False, default="open")
deadline = Column(DateTime)
sla_remaining_hours = Column(Numeric(6, 1), default=0)
response_time_minutes = Column(Integer, default=0)
resolution_time_minutes = Column(Integer, default=0)
sla_compliant = Column(Boolean)
checklist_data = Column(JSON)
result_notes = Column(Text)
closed_at = Column(DateTime)
created_at = Column(DateTime, nullable=False, server_default="NOW()")
updated_at = Column(DateTime, nullable=False, server_default="NOW()", onupdate=func.now())
__table_args__ = (
CheckConstraint("priority IN ('P1','P2','P3','P4')", name="ck_tasks_priority"),
CheckConstraint("status IN ('open','in_progress','completed','cancelled')", name="ck_tasks_status"),
)
object = relationship("Object", back_populates="tasks")
creator = relationship("User", foreign_keys=[created_by], back_populates="tasks_created")
assignee = relationship("User", foreign_keys=[assigned_to], back_populates="tasks_assigned")
comments = relationship("TaskComment", back_populates="task")
photos = relationship("TaskPhoto", back_populates="task")
class TaskComment(Base):
__tablename__ = "task_comments"
id = Column(Integer, primary_key=True, autoincrement=True)
task_id = Column(Integer, ForeignKey("tasks.id", ondelete="CASCADE"), nullable=False)
user_id = Column(Integer, ForeignKey("users.id", ondelete="SET NULL"))
comment = Column(Text, nullable=False)
created_at = Column(DateTime, nullable=False, server_default="NOW()")
task = relationship("Task", back_populates="comments")
user = relationship("User")
class TaskPhoto(Base):
__tablename__ = "task_photos"
id = Column(Integer, primary_key=True, autoincrement=True)
task_id = Column(Integer, ForeignKey("tasks.id", ondelete="SET NULL"))
report_id = Column(Integer, ForeignKey("reports.id", ondelete="SET NULL"))
file_path = Column(String(500), nullable=False)
description = Column(String(500), default="")
type = Column(String(20), default="other")
uploaded_by = Column(Integer, ForeignKey("users.id", ondelete="SET NULL"))
created_at = Column(DateTime, nullable=False, server_default="NOW()")
__table_args__ = (
CheckConstraint("type IN ('before','after','evidence','other')", name="ck_photos_type"),
)
task = relationship("Task", back_populates="photos")
uploader = relationship("User")
class Report(Base):
__tablename__ = "reports"
id = Column(Integer, primary_key=True, autoincrement=True)
object_id = Column(Integer, ForeignKey("objects.id", ondelete="CASCADE"), nullable=False)
created_by = Column(Integer, ForeignKey("users.id", ondelete="SET NULL"))
report_type = Column(String(20), nullable=False, default="service")
title = Column(String(500), default="")
description = Column(Text)
findings = Column(Text)
recommendations = Column(Text)
work_done = Column(Text)
cause_classification = Column(String(100))
result_status = Column(String(20), default="ok")
status = Column(String(20), nullable=False, default="draft")
client_confirmed = Column(Boolean, default=False)
created_at = Column(DateTime, nullable=False, server_default="NOW()")
updated_at = Column(DateTime, nullable=False, server_default="NOW()", onupdate=func.now())
__table_args__ = (
CheckConstraint("report_type IN ('inspection','service','emergency','audit','monthly')", name="ck_reports_type"),
CheckConstraint("result_status IN ('fixed','partial','revisit','ok')", name="ck_reports_result"),
CheckConstraint("status IN ('draft','final','cancelled')", name="ck_reports_status"),
)
object = relationship("Object", back_populates="reports")
creator = relationship("User")
class QuestionnaireSession(Base):
__tablename__ = "questionnaire_sessions"
id = Column(Integer, primary_key=True, autoincrement=True)
object_id = Column(Integer, ForeignKey("objects.id", ondelete="SET NULL"))
client_name = Column(String(255), default="")
created_by = Column(Integer, ForeignKey("users.id", ondelete="SET NULL"))
status = Column(String(20), nullable=False, default="draft")
current_step = Column(SmallInteger, nullable=False, default=1)
risk_score = Column(Numeric(5, 2), default=0)
complexity_index = Column(Numeric(5, 2), default=0)
infrastructure_load = Column(Numeric(5, 2), default=0)
service_history = Column(Numeric(5, 2), default=0)
object_index = Column(Numeric(5, 2), default=0)
sla_price_monthly = Column(Numeric(12, 2), default=0)
created_at = Column(DateTime, nullable=False, server_default="NOW()")
updated_at = Column(DateTime, nullable=False, server_default="NOW()", onupdate=func.now())
__table_args__ = (
CheckConstraint("status IN ('draft','completed','cancelled')", name="ck_qsessions_status"),
)
answers = relationship("QuestionnaireAnswer", back_populates="session")
passport = relationship("ObjectPassport", back_populates="session")
class QuestionnaireAnswer(Base):
__tablename__ = "questionnaire_answers"
id = Column(Integer, primary_key=True, autoincrement=True)
session_id = Column(Integer, ForeignKey("questionnaire_sessions.id", ondelete="CASCADE"), nullable=False)
step = Column(SmallInteger, nullable=False)
question_key = Column(String(100), nullable=False)
answer_value = Column(Text)
created_at = Column(DateTime, nullable=False, server_default="NOW()")
session = relationship("QuestionnaireSession", back_populates="answers")
class QuestionnaireItem(Base):
__tablename__ = "questionnaire_items"
id = Column(Integer, primary_key=True, autoincrement=True)
step = Column(Integer, nullable=False, default=1)
section = Column(String(100), nullable=False, default="")
question_key = Column(String(100), unique=True, nullable=False)
label = Column(String(255), nullable=False)
type = Column(String(20), nullable=False, default="text")
options = Column(JSON)
required = Column(Boolean, nullable=False, default=False)
sort_order = Column(Integer, nullable=False, default=0)
is_active = Column(Boolean, nullable=False, default=True)
help_text = Column(String(500), default="")
created_at = Column(DateTime, nullable=False, server_default="NOW()")
updated_at = Column(DateTime, nullable=False, server_default="NOW()", onupdate=func.now())
__table_args__ = (
CheckConstraint("type IN ('text','number','select','radio','checkbox','textarea')", name="ck_qitems_type"),
)
class ObjectPassport(Base):
__tablename__ = "object_passports"
id = Column(Integer, primary_key=True, autoincrement=True)
session_id = Column(Integer, ForeignKey("questionnaire_sessions.id", ondelete="SET NULL"))
object_id = Column(Integer, ForeignKey("objects.id", ondelete="SET NULL"))
object_index = Column(Numeric(5, 2), default=0)
risk_score = Column(Numeric(5, 2), default=0)
complexity_index = Column(Numeric(5, 2), default=0)
sla_price_monthly = Column(Numeric(12, 2), default=0)
passport_data = Column(JSON)
created_by = Column(Integer, ForeignKey("users.id", ondelete="SET NULL"))
created_at = Column(DateTime, nullable=False, server_default="NOW()")
session = relationship("QuestionnaireSession", back_populates="passport")
object = relationship("Object", back_populates="passports")
creator = relationship("User")
class SLAContract(Base):
__tablename__ = "sla_contracts"
id = Column(Integer, primary_key=True, autoincrement=True)
customer_id = Column(Integer, ForeignKey("customers.id", ondelete="SET NULL"))
object_id = Column(Integer, ForeignKey("objects.id", ondelete="CASCADE"), nullable=False)
contract_number = Column(String(50), default="")
client_name = Column(String(255), default="")
description = Column(Text)
start_date = Column(Date, nullable=False)
end_date = Column(Date)
sla_price_monthly = Column(Numeric(12, 2), nullable=False, default=0)
penalty_rate = Column(Numeric(5, 2), default=0)
response_time_p1 = Column(Integer, default=4)
response_time_p2 = Column(Integer, default=8)
response_time_p3 = Column(Integer, default=24)
response_time_hours = Column(Numeric(5, 1))
service_level = Column(String(20), nullable=False, default="business")
status = Column(String(20), nullable=False, default="negotiation")
created_by = Column(Integer, ForeignKey("users.id", ondelete="SET NULL"))
created_at = Column(DateTime, nullable=False, server_default="NOW()")
updated_at = Column(DateTime, nullable=False, server_default="NOW()", onupdate=func.now())
__table_args__ = (
CheckConstraint("service_level IN ('start','business','enterprise')", name="ck_sla_level"),
CheckConstraint("status IN ('active','expired','cancelled','negotiation')", name="ck_sla_status"),
)
customer = relationship("Customer", back_populates="sla_contracts")
object = relationship("Object", back_populates="sla_contracts")
creator = relationship("User")
class Incident(Base):
__tablename__ = "incidents"
id = Column(Integer, primary_key=True, autoincrement=True)
object_id = Column(Integer, ForeignKey("objects.id", ondelete="CASCADE"), nullable=False)
reported_by = Column(Integer, ForeignKey("users.id", ondelete="SET NULL"))
assigned_to = Column(Integer, ForeignKey("users.id", ondelete="SET NULL"))
title = Column(String(500), nullable=False)
description = Column(Text)
severity = Column(String(4), nullable=False, default="P3")
status = Column(String(20), nullable=False, default="open")
resolution = Column(Text)
cause = Column(String(500), default="")
resolution_time_minutes = Column(Integer, default=0)
sla_breached = Column(Boolean, default=False)
resolved_at = Column(DateTime)
created_at = Column(DateTime, nullable=False, server_default="NOW()")
updated_at = Column(DateTime, nullable=False, server_default="NOW()", onupdate=func.now())
__table_args__ = (
CheckConstraint("severity IN ('P1','P2','P3')", name="ck_incidents_severity"),
CheckConstraint("status IN ('open','in_progress','resolved','closed')", name="ck_incidents_status"),
)
object = relationship("Object", back_populates="incidents")
reporter = relationship("User", foreign_keys=[reported_by])
assignee = relationship("User", foreign_keys=[assigned_to])
class EngineerKPI(Base):
__tablename__ = "engineer_kpi"
id = Column(Integer, primary_key=True, autoincrement=True)
engineer_id = Column(Integer, ForeignKey("users.id", ondelete="CASCADE"), nullable=False)
period_start = Column(Date, nullable=False)
period_end = Column(Date, nullable=False)
sla_compliance = Column(Numeric(5, 2), default=0)
response_time_score = Column(Numeric(5, 2), default=0)
resolution_time_score = Column(Numeric(5, 2), default=0)
diagnosis_accuracy = Column(Numeric(5, 2), default=0)
reopen_rate = Column(Numeric(5, 2), default=0)
risk_coverage_score = Column(Numeric(5, 2), default=0)
documentation_quality = Column(Numeric(5, 2), default=0)
engineer_score = Column(Numeric(5, 2), default=0)
grade = Column(String(20), default="")
components_json = Column(JSON)
created_at = Column(DateTime, nullable=False, server_default="NOW()")
engineer = relationship("User")
class SHSRecord(Base):
__tablename__ = "shs_records"
id = Column(Integer, primary_key=True, autoincrement=True)
recorded_by = Column(Integer, ForeignKey("users.id", ondelete="SET NULL"))
period_start = Column(Date, nullable=False)
period_end = Column(Date, nullable=False)
sla_stability = Column(Numeric(5, 2), default=0)
revenue_stability = Column(Numeric(5, 2), default=0)
retention = Column(Numeric(5, 2), default=0)
engineer_performance = Column(Numeric(5, 2), default=0)
incident_stability = Column(Numeric(5, 2), default=0)
sales_flow = Column(Numeric(5, 2), default=0)
operational_efficiency = Column(Numeric(5, 2), default=0)
shs_score = Column(Numeric(5, 2), default=0)
shs_status = Column(String(20), default="")
components_json = Column(JSON)
created_at = Column(DateTime, nullable=False, server_default="NOW()")
recorder = relationship("User")
class BlogPost(Base):
__tablename__ = "blog_posts"
id = Column(Integer, primary_key=True, autoincrement=True)
title = Column(String(500), nullable=False)
slug = Column(String(500), unique=True, nullable=False)
content = Column(Text, nullable=False)
excerpt = Column(String(1000), default="")
category = Column(String(50), nullable=False, default="audit")
author_id = Column(Integer, ForeignKey("users.id", ondelete="SET NULL"))
status = Column(String(20), nullable=False, default="draft")
published_at = Column(DateTime)
created_at = Column(DateTime, nullable=False, server_default="NOW()")
updated_at = Column(DateTime, nullable=False, server_default="NOW()", onupdate=func.now())
__table_args__ = (
CheckConstraint("category IN ('audit','sla','incident','supervision','documentation','risk','cases')", name="ck_blog_category"),
CheckConstraint("status IN ('draft','published','archived')", name="ck_blog_status"),
)
author = relationship("User")
class FormulaCoefficient(Base):
__tablename__ = "formula_coefficients"
id = Column(Integer, primary_key=True, autoincrement=True)
key = Column(String(100), unique=True, nullable=False)
value = Column(Numeric(10, 4), nullable=False, default=0)
description = Column(String(500), default="")
formula_ref = Column(String(255), default="")
is_active = Column(Boolean, nullable=False, default=True)
updated_by = Column(Integer, ForeignKey("users.id", ondelete="SET NULL"))
updated_at = Column(DateTime, nullable=False, server_default="NOW()", onupdate=func.now())
updater = relationship("User")
class Case(Base):
__tablename__ = "cases"
id = Column(Integer, primary_key=True, autoincrement=True)
title = Column(String(500), nullable=False)
text = Column(Text, nullable=False)
effect = Column(Text, nullable=False)
sort_order = Column(Integer, nullable=False, default=0)
is_active = Column(Boolean, nullable=False, default=True)
created_at = Column(DateTime, nullable=False, server_default="NOW()")
updated_at = Column(DateTime, nullable=False, server_default="NOW()", onupdate=func.now())
class Idea(Base):
__tablename__ = "ideas"
id = Column(Integer, primary_key=True, autoincrement=True)
title = Column(String(500), nullable=False)
description = Column(Text, default="")
status = Column(String(20), nullable=False, default="normal")
created_by = Column(Integer, ForeignKey("users.id", ondelete="SET NULL"))
created_at = Column(DateTime, nullable=False, server_default="NOW()")
updated_at = Column(DateTime, nullable=False, server_default="NOW()", onupdate=func.now())
__table_args__ = (
CheckConstraint("status IN ('normal','in_progress','completed')", name="ck_ideas_status"),
)
creator = relationship("User")
class QuickMenuItem(Base):
__tablename__ = "quick_menu_items"
id = Column(Integer, primary_key=True, autoincrement=True)
role = Column(String(20), nullable=False)
label = Column(String(100), nullable=False)
url = Column(String(500), nullable=False)
sort_order = Column(Integer, nullable=False, default=0)
created_by = Column(Integer, ForeignKey("users.id", ondelete="SET NULL"))
updated_at = Column(DateTime, nullable=False, server_default="NOW()", onupdate=func.now())
creator = relationship("User")
class RoleMenuPermission(Base):
__tablename__ = "role_menu_permissions"
id = Column(Integer, primary_key=True, autoincrement=True)
role = Column(String(20), nullable=False)
menu_key = Column(String(50), nullable=False)
visible = Column(Boolean, nullable=False, default=True)
__table_args__ = (
CheckConstraint("role IN ('engineer','technician')", name="ck_role_menu_role"),
)
# ─── Модели для лидов, звонков, уведомлений, настроек АТС ──────────────────────
class Lead(Base):
"""Лид — обращение из бота, звонка, сайта или email.
Источники: bot, phone_in, phone_out, site, email, manual.
Автопривязка к клиенту: ИНН → телефон → email → создание draft.
"""
__tablename__ = "leads"
id = Column(Integer, primary_key=True, autoincrement=True)
customer_id = Column(Integer, ForeignKey("customers.id", ondelete="SET NULL"))
# Источник обращения
source = Column(String(20), nullable=False, default="manual")
# bot | phone_in | phone_out | site | email | manual
# Данные клиента
company_name = Column(String(255), default="")
contact_name = Column(String(255), default="")
contact_phone = Column(String(50), default="")
contact_email = Column(String(255), default="")
inn = Column(String(12), default="")
# Содержание обращения
title = Column(String(500), default="")
service_type = Column(String(255), default="")
description = Column(Text, default="")
urgency = Column(String(10), default="medium")
# critical | high | medium | low
# Статус
status = Column(String(30), default="new")
# new | contact | qualification | proposal | negotiation | approved | won | lost | merged
# Привязка
assigned_to = Column(Integer, ForeignKey("users.id", ondelete="SET NULL"))
bot_ticket_id = Column(BigInteger) # ID заявки из бота
# Воронка
deal_amount = Column(Numeric(12, 2), default=0)
expected_close_date = Column(Date)
# Мета
created_by = Column(Integer, ForeignKey("users.id", ondelete="SET NULL"))
created_at = Column(DateTime, nullable=False, server_default="NOW()")
updated_at = Column(DateTime, nullable=False, server_default="NOW()", onupdate=func.now())
__table_args__ = (
CheckConstraint(
"status IN ('new','contact','qualification','proposal','negotiation','approved','won','lost','merged')",
name="ck_leads_status"
),
CheckConstraint(
"urgency IN ('critical','high','medium','low')",
name="ck_leads_urgency"
),
CheckConstraint(
"source IN ('bot','phone_in','phone_out','site','email','manual')",
name="ck_leads_source"
),
)
customer = relationship("Customer", foreign_keys=[customer_id])
assignee = relationship("User", foreign_keys=[assigned_to])
creator = relationship("User", foreign_keys=[created_by])
class CallRecord(Base):
"""Звонок (CDR из Yeastar S20 через MySQL).
Синхронизируется каждые 5 минут из таблицы cdr.
Автопривязка к клиенту по номеру телефона.
Автопривязка к сотруднику по внутреннему номеру (extension).
"""
__tablename__ = "call_records"
id = Column(Integer, primary_key=True, autoincrement=True)
# Данные из CDR
cdr_uniqueid = Column(String(32), unique=True) # Уникальный ID из CDR
call_datetime = Column(DateTime, nullable=False)
caller_number = Column(String(80), default="") # Номер звонящего (src)
callee_number = Column(String(80), default="") # Номер получателя (dst)
trunk = Column(String(80), default="") # Транк (Mango, Tele2)
duration = Column(Integer, default=0) # Общая длительность (сек)
billable_seconds = Column(Integer, default=0) # Биллируемая (сек)
disposition = Column(String(30), default="") # ANSWERED, NO ANSWER, BUSY, VOICEMAIL
call_type = Column(String(20), default="") # Inbound, Outbound, Internal
queue_name = Column(String(30), default="") # Очередь (6700, 6701, 6702)
# Привязка к клиенту
customer_id = Column(Integer, ForeignKey("customers.id", ondelete="SET NULL"))
lead_id = Column(Integer, ForeignKey("leads.id", ondelete="SET NULL"))
# Инженер/техник кто принял звонок
answered_by = Column(Integer, ForeignKey("users.id", ondelete="SET NULL"))
answered_by_name = Column(String(100), default="")
# Запись
has_recording = Column(Boolean, default=False)
recording_path = Column(String(500), default="") # Путь на PBX
recording_filename = Column(String(200), default="") # Имя WAV-файла
# Уведомления
notification_sent = Column(Boolean, default=False)
created_at = Column(DateTime, nullable=False, server_default="NOW()")
customer = relationship("Customer", foreign_keys=[customer_id])
lead = relationship("Lead", foreign_keys=[lead_id])
employee = relationship("User", foreign_keys=[answered_by])
class ClientNotification(Base):
"""Уведомление для сотрудника.
Типы: пропущенный звонок, новый лид, изменение статуса лида,
нарушение SLA, просроченная задача.
"""
__tablename__ = "client_notifications"
id = Column(Integer, primary_key=True, autoincrement=True)
user_id = Column(Integer, ForeignKey("users.id", ondelete="SET NULL"))
customer_id = Column(Integer, ForeignKey("customers.id", ondelete="SET NULL"))
lead_id = Column(Integer, ForeignKey("leads.id", ondelete="SET NULL"))
call_record_id = Column(Integer, ForeignKey("call_records.id", ondelete="SET NULL"))
type = Column(String(30), nullable=False)
# missed_call | new_lead | lead_status_change | sla_breach | task_overdue
title = Column(String(500), nullable=False)
message = Column(Text, default="")
is_read = Column(Boolean, default=False)
created_at = Column(DateTime, nullable=False, server_default="NOW()")
user = relationship("User", foreign_keys=[user_id])
customer = relationship("Customer", foreign_keys=[customer_id])
lead = relationship("Lead", foreign_keys=[lead_id])
call_record = relationship("CallRecord", foreign_keys=[call_record_id])
class PbxSettings(Base):
"""Настройки подключения к АТС (Yeastar S20).
Всегда одна запись (id=1). Пароли шифруются через cryptography.fernet.
"""
__tablename__ = "pbx_settings"
id = Column(Integer, primary_key=True, default=1)
# Подключение MySQL (CDR)
mysql_host = Column(String(255), default="192.168.1.150")
mysql_port = Column(Integer, default=3306)
mysql_user = Column(String(100), default="")
mysql_password = Column(String(255), default="") # зашифровано
mysql_database = Column(String(100), default="cdr")
# AMI
ami_host = Column(String(255), default="192.168.1.150")
ami_port = Column(Integer, default=5038)
ami_user = Column(String(100), default="")
ami_password = Column(String(255), default="") # зашифровано
# FTP (записи WAV)
ftp_host = Column(String(255), default="192.168.1.150")
ftp_port = Column(Integer, default=21)
ftp_user = Column(String(100), default="")
ftp_password = Column(String(255), default="") # зашифровано
# Веб-интерфейс
web_host = Column(String(255), default="192.168.1.150")
web_port = Column(Integer, default=8088)
web_user = Column(String(100), default="")
web_password = Column(String(255), default="") # зашифровано
# Информация
serial_number = Column(String(50), default="") # Ручной ввод
actual_serial = Column(String(50), default="") # Из FTP
# Статус подключения
mysql_status = Column(String(20), default="unknown") # ok | error | unknown
ami_status = Column(String(20), default="unknown")
ftp_status = Column(String(20), default="unknown")
serial_match = Column(Boolean, default=False)
# Синхронизация
last_sync_at = Column(DateTime)
sync_interval = Column(Integer, default=5) # минут
updated_at = Column(DateTime, nullable=False, server_default="NOW()", onupdate=func.now())