mi-proyecto/app/models.py

870 lines
43 KiB
Python

from datetime import datetime, date, timedelta
import json
from flask_login import UserMixin
from werkzeug.security import generate_password_hash, check_password_hash
from . import db
service_professionals = db.Table(
'service_professionals',
db.Column('service_id', db.Integer, db.ForeignKey('service.id'), primary_key=True),
db.Column('professional_id', db.Integer, db.ForeignKey('professional_profile.id'), primary_key=True)
)
class TimestampMixin:
created_at = db.Column(db.DateTime, default=datetime.utcnow)
updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
class Institution(TimestampMixin, db.Model):
__tablename__ = 'institution'
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(180), nullable=False, index=True)
cuit = db.Column(db.String(30), unique=True, index=True)
domicilio = db.Column(db.String(220))
ciudad = db.Column(db.String(120))
provincia = db.Column(db.String(120))
telefono = db.Column(db.String(80))
responsable = db.Column(db.String(160))
situacion_juridica = db.Column(db.String(120))
nro_habilitacion = db.Column(db.String(120))
expediente_autorizacion = db.Column(db.String(160))
email = db.Column(db.String(160))
telefono_responsable = db.Column(db.String(80))
logo_path = db.Column(db.String(255))
active = db.Column(db.Boolean, default=True, index=True)
users = db.relationship('User', back_populates='institution')
professionals = db.relationship('ProfessionalProfile', back_populates='institution')
patients = db.relationship('Patient', back_populates='institution')
branches = db.relationship('InstitutionBranch', back_populates='institution', cascade='all, delete-orphan')
@property
def display_name(self):
return self.name or f'Institución #{self.id}'
class InstitutionBranch(TimestampMixin, db.Model):
__tablename__ = 'institution_branch'
id = db.Column(db.Integer, primary_key=True)
institution_id = db.Column(db.Integer, db.ForeignKey('institution.id'), nullable=False, index=True)
name = db.Column(db.String(160), nullable=False)
address = db.Column(db.String(220))
province = db.Column(db.String(120), index=True)
city = db.Column(db.String(120), index=True)
phone = db.Column(db.String(80))
email = db.Column(db.String(160))
notes = db.Column(db.Text)
active = db.Column(db.Boolean, default=True, index=True)
institution = db.relationship('Institution', back_populates='branches')
appointments = db.relationship('Appointment', back_populates='branch')
@property
def display_name(self):
return self.name or f'Sede #{self.id}'
@property
def full_address(self):
bits = [self.address, self.city, self.province]
return ', '.join([b for b in bits if b])
class User(UserMixin, TimestampMixin, db.Model):
id = db.Column(db.Integer, primary_key=True)
full_name = db.Column(db.String(150), nullable=False)
email = db.Column(db.String(150), unique=True, nullable=False)
password_hash = db.Column(db.String(255), nullable=False)
role = db.Column(db.String(30), nullable=False, default='professional')
is_active_user = db.Column(db.Boolean, default=True)
institution_id = db.Column(db.Integer, db.ForeignKey('institution.id'), index=True)
institution = db.relationship('Institution', back_populates='users')
professional_profile = db.relationship('ProfessionalProfile', back_populates='user', uselist=False)
issued_prescriptions = db.relationship('Prescription', back_populates='issued_by_user', foreign_keys='Prescription.issued_by_user_id')
def set_password(self, password):
self.password_hash = generate_password_hash(password)
def check_password(self, password):
return check_password_hash(self.password_hash, password)
@property
def is_active(self):
return self.is_active_user
class Specialty(TimestampMixin, db.Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(150), unique=True, nullable=False)
active = db.Column(db.Boolean, default=True)
class AppSetting(TimestampMixin, db.Model):
id = db.Column(db.Integer, primary_key=True)
key = db.Column(db.String(100), unique=True, nullable=False)
value = db.Column(db.Text)
class ProfessionalProfile(TimestampMixin, db.Model):
id = db.Column(db.Integer, primary_key=True)
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), unique=True)
display_name = db.Column(db.String(150), nullable=False)
specialty = db.Column(db.String(150), nullable=False)
bio = db.Column(db.Text)
location = db.Column(db.String(150), default='Consultorio Central')
phone = db.Column(db.String(50))
contact_email = db.Column(db.String(150))
is_bookable = db.Column(db.Boolean, default=True)
color = db.Column(db.String(20), default='#0d6efd')
institution_id = db.Column(db.Integer, db.ForeignKey('institution.id'), index=True)
source_mode = db.Column(db.String(20), default='manual')
matricula = db.Column(db.String(100))
profession_name = db.Column(db.String(150))
jurisdiction_name = db.Column(db.String(150))
state_name = db.Column(db.String(120))
province = db.Column(db.String(120))
municipality = db.Column(db.String(120))
city = db.Column(db.String(120))
address = db.Column(db.String(200))
address_number = db.Column(db.String(30))
specialty_id = db.Column(db.Integer, db.ForeignKey('specialty.id'))
institution = db.relationship('Institution', back_populates='professionals')
user = db.relationship('User', back_populates='professional_profile')
specialty_manual = db.relationship('Specialty')
working_hours = db.relationship('WorkingHour', back_populates='professional', cascade='all, delete-orphan')
leaves = db.relationship('Leave', back_populates='professional', cascade='all, delete-orphan')
appointments = db.relationship('Appointment', back_populates='professional')
services = db.relationship('Service', secondary=service_professionals, back_populates='professionals')
prescriptions = db.relationship('Prescription', back_populates='professional')
@property
def full_address(self):
bits = [self.address, self.address_number, self.city, self.municipality, self.province]
return ', '.join([b for b in bits if b])
class Service(TimestampMixin, db.Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(150), nullable=False, unique=True)
description = db.Column(db.Text)
duration_minutes = db.Column(db.Integer, nullable=False, default=30)
buffer_before_minutes = db.Column(db.Integer, nullable=False, default=0)
buffer_after_minutes = db.Column(db.Integer, nullable=False, default=0)
price = db.Column(db.Float, default=0)
mode = db.Column(db.String(30), default='Presencial')
active = db.Column(db.Boolean, default=True)
institution_id = db.Column(db.Integer, db.ForeignKey('institution.id'), index=True)
min_notice_hours = db.Column(db.Integer, default=4)
cancel_notice_hours = db.Column(db.Integer, default=12)
allow_online_cancel = db.Column(db.Boolean, default=True)
institution = db.relationship('Institution')
appointments = db.relationship('Appointment', back_populates='service')
professionals = db.relationship('ProfessionalProfile', secondary=service_professionals, back_populates='services')
class WorkingHour(TimestampMixin, db.Model):
id = db.Column(db.Integer, primary_key=True)
professional_id = db.Column(db.Integer, db.ForeignKey('professional_profile.id'), nullable=False)
weekday = db.Column(db.Integer, nullable=False)
start_time = db.Column(db.Time, nullable=False)
end_time = db.Column(db.Time, nullable=False)
is_active = db.Column(db.Boolean, default=True)
professional = db.relationship('ProfessionalProfile', back_populates='working_hours')
class Leave(TimestampMixin, db.Model):
id = db.Column(db.Integer, primary_key=True)
professional_id = db.Column(db.Integer, db.ForeignKey('professional_profile.id'), nullable=False)
start_date = db.Column(db.Date, nullable=False)
end_date = db.Column(db.Date, nullable=False)
reason = db.Column(db.String(200), nullable=False)
professional = db.relationship('ProfessionalProfile', back_populates='leaves')
class ObraSocialPageSnapshot(TimestampMixin, db.Model):
id = db.Column(db.Integer, primary_key=True)
tipo = db.Column(db.Integer, unique=True, nullable=False)
categoria_oficial = db.Column(db.String(255))
origen_datos = db.Column(db.String(255))
status = db.Column(db.String(50), default='never_synced')
row_count = db.Column(db.Integer, default=0)
last_hash = db.Column(db.String(128))
last_error = db.Column(db.Text)
last_synced_at = db.Column(db.DateTime)
class ObraSocialCatalog(TimestampMixin, db.Model):
__tablename__ = 'obras_sociales_catalogo'
id = db.Column(db.Integer, primary_key=True)
tipo = db.Column(db.Integer, index=True)
categoria_oficial = db.Column(db.String(255), index=True)
rnas = db.Column(db.String(50), index=True)
denominacion = db.Column(db.String(255), nullable=False, index=True)
domicilio = db.Column(db.String(255))
localidad = db.Column(db.String(255), index=True)
telefono = db.Column(db.String(120))
linea_gratuita = db.Column(db.String(120))
habilitada_opciones = db.Column(db.String(50), index=True)
vigente = db.Column(db.Boolean, default=True, index=True)
row_hash = db.Column(db.String(128))
last_seen_at = db.Column(db.DateTime)
page_snapshot_id = db.Column(db.Integer, db.ForeignKey('obra_social_page_snapshot.id'))
class Patient(TimestampMixin, db.Model):
id = db.Column(db.Integer, primary_key=True)
nombre = db.Column(db.String(120), nullable=False)
apellido = db.Column(db.String(120), nullable=False)
documento = db.Column(db.String(40), unique=True, nullable=False, index=True)
tipo_documento = db.Column(db.String(30), default='DNI')
fecha_nacimiento = db.Column(db.String(20))
genero = db.Column(db.String(30))
telefono = db.Column(db.String(60))
email = db.Column(db.String(150))
calle = db.Column(db.String(150))
numero = db.Column(db.String(30))
piso = db.Column(db.String(30))
provincia = db.Column(db.String(120))
municipio = db.Column(db.String(120))
localidad = db.Column(db.String(120))
cp = db.Column(db.String(20))
obra_social_id = db.Column(db.Integer, db.ForeignKey('obras_sociales_catalogo.id'))
afiliado_nro = db.Column(db.String(80))
observaciones = db.Column(db.Text)
nombre_contacto = db.Column(db.String(150))
telefono_contacto = db.Column(db.String(60))
estado = db.Column(db.String(30), default='Activo', index=True)
fecha_creacion = db.Column(db.DateTime, default=datetime.utcnow)
institution_id = db.Column(db.Integer, db.ForeignKey('institution.id'), index=True)
institution = db.relationship('Institution', back_populates='patients')
obra_social = db.relationship('ObraSocialCatalog')
appointments = db.relationship('Appointment', back_populates='patient')
prescriptions = db.relationship('Prescription', back_populates='patient')
@property
def nombre_completo(self):
return f"{self.apellido}, {self.nombre}"
@property
def domicilio_completo(self):
bits = [self.calle, self.numero, self.piso, self.localidad, self.municipio, self.provincia]
return ', '.join([b for b in bits if b])
class Appointment(TimestampMixin, db.Model):
id = db.Column(db.Integer, primary_key=True)
service_id = db.Column(db.Integer, db.ForeignKey('service.id'), nullable=False)
professional_id = db.Column(db.Integer, db.ForeignKey('professional_profile.id'), nullable=False)
patient_id = db.Column(db.Integer, db.ForeignKey('patient.id'))
client_name = db.Column(db.String(150), nullable=False)
client_email = db.Column(db.String(150), nullable=False)
client_phone = db.Column(db.String(50))
notes = db.Column(db.Text)
appointment_date = db.Column(db.Date, nullable=False)
start_time = db.Column(db.Time, nullable=False)
end_time = db.Column(db.Time, nullable=False)
status = db.Column(db.String(30), default='confirmed')
booking_source = db.Column(db.String(50), default='website')
public_token = db.Column(db.String(120), unique=True, nullable=False)
internal_notes = db.Column(db.Text)
reminder_sent = db.Column(db.Boolean, default=False)
institution_id = db.Column(db.Integer, db.ForeignKey('institution.id'), index=True)
branch_id = db.Column(db.Integer, db.ForeignKey('institution_branch.id'), index=True)
institution = db.relationship('Institution')
branch = db.relationship('InstitutionBranch', back_populates='appointments')
service = db.relationship('Service', back_populates='appointments')
professional = db.relationship('ProfessionalProfile', back_populates='appointments')
patient = db.relationship('Patient', back_populates='appointments')
@property
def starts_at(self):
return datetime.combine(self.appointment_date, self.start_time)
@property
def ends_at(self):
return datetime.combine(self.appointment_date, self.end_time)
@property
def is_future(self):
return self.starts_at >= datetime.now()
class Prescription(TimestampMixin, db.Model):
id = db.Column(db.Integer, primary_key=True)
patient_id = db.Column(db.Integer, db.ForeignKey('patient.id'), nullable=False, index=True)
professional_id = db.Column(db.Integer, db.ForeignKey('professional_profile.id'), nullable=False, index=True)
issued_by_user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
issued_at = db.Column(db.DateTime, default=datetime.utcnow, nullable=False, index=True)
prescription_date = db.Column(db.Date, nullable=False, default=date.today)
expires_at = db.Column(db.Date, nullable=False)
status = db.Column(db.String(30), default='Activa', nullable=False, index=True)
institution_id = db.Column(db.Integer, db.ForeignKey('institution.id'), index=True)
legal_number = db.Column(db.String(40), unique=True, nullable=False, index=True)
cuir = db.Column(db.String(50), unique=True, nullable=False, index=True)
platform_number = db.Column(db.String(4))
repository_number = db.Column(db.String(4))
jurisdiction_code = db.Column(db.String(2))
prescription_type = db.Column(db.String(2), default='01')
prescription_subtype = db.Column(db.String(2), default='02')
prescription_group = db.Column(db.String(25))
item_number = db.Column(db.String(2), default='01')
document_kind = db.Column(db.String(30), default='recipe', nullable=False, index=True)
document_title = db.Column(db.String(255))
document_body = db.Column(db.Text)
portal_visible = db.Column(db.Boolean, default=True, nullable=False)
patient_full_name = db.Column(db.String(150), nullable=False)
patient_document = db.Column(db.String(40), nullable=False)
patient_birth_date = db.Column(db.String(20))
patient_gender = db.Column(db.String(30))
patient_obra_social = db.Column(db.String(255))
patient_plan = db.Column(db.String(120))
professional_display_name = db.Column(db.String(150), nullable=False)
professional_profession_name = db.Column(db.String(150))
professional_specialty = db.Column(db.String(150))
professional_matricula = db.Column(db.String(120))
professional_jurisdiction_name = db.Column(db.String(150))
professional_address = db.Column(db.String(255))
medication_generic_name = db.Column(db.String(255), nullable=False)
medication_presentation = db.Column(db.String(255))
pharmaceutical_form = db.Column(db.String(255))
quantity_units = db.Column(db.String(120))
diagnosis = db.Column(db.String(255))
dosage_instructions = db.Column(db.Text)
barcode_value = db.Column(db.String(64))
platform_registry_legend = db.Column(db.String(255))
internal_notes = db.Column(db.Text)
suspended_at = db.Column(db.DateTime)
suspended_reason = db.Column(db.String(255))
institution = db.relationship('Institution')
patient = db.relationship('Patient', back_populates='prescriptions')
professional = db.relationship('ProfessionalProfile', back_populates='prescriptions')
issued_by_user = db.relationship('User', back_populates='issued_prescriptions', foreign_keys=[issued_by_user_id])
@property
def computed_status(self):
if (self.status or '').lower() in {'suspendida', 'anulada'}:
return self.status
if self.expires_at and date.today() > self.expires_at:
return 'Vencida'
return self.status or 'Activa'
@property
def patient_document_last4(self):
digits = ''.join(ch for ch in (self.patient_document or '') if ch.isdigit())
return digits[-4:] if len(digits) >= 4 else digits
class BackupRecord(TimestampMixin, db.Model):
id = db.Column(db.Integer, primary_key=True)
scope = db.Column(db.String(30), nullable=False, default='full', index=True)
provider = db.Column(db.String(30), nullable=False, default='local')
filename = db.Column(db.String(255), nullable=False)
relative_path = db.Column(db.String(255))
target_path = db.Column(db.String(255))
status = db.Column(db.String(30), nullable=False, default='created', index=True)
size_bytes = db.Column(db.Integer, default=0)
sha256 = db.Column(db.String(128))
notes = db.Column(db.Text)
class FrontendBlock(TimestampMixin, db.Model):
id = db.Column(db.Integer, primary_key=True)
section = db.Column(db.String(80), nullable=False, index=True)
key = db.Column(db.String(80), default='default', index=True)
title = db.Column(db.String(255))
subtitle = db.Column(db.String(255))
body = db.Column(db.Text)
icon = db.Column(db.String(80))
image_path = db.Column(db.String(255))
link_url = db.Column(db.String(255))
link_text = db.Column(db.String(120))
sort_order = db.Column(db.Integer, default=0, index=True)
is_active = db.Column(db.Boolean, default=True, index=True)
extra_json = db.Column(db.Text)
class ContactInquiry(TimestampMixin, db.Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(150), nullable=False)
email = db.Column(db.String(150), nullable=False, index=True)
phone = db.Column(db.String(60))
detail = db.Column(db.Text, nullable=False)
status = db.Column(db.String(30), default='No Leido', index=True)
source = db.Column(db.String(50), default='frontend')
read_at = db.Column(db.DateTime)
class ClinicalRecord(TimestampMixin, db.Model):
id = db.Column(db.Integer, primary_key=True)
patient_id = db.Column(db.Integer, db.ForeignKey('patient.id'), nullable=False, unique=True, index=True)
legajo_number = db.Column(db.String(80), nullable=False, unique=True, index=True)
opened_at = db.Column(db.DateTime, default=datetime.utcnow, nullable=False)
status = db.Column(db.String(30), default='Activo', index=True)
confidentiality_level = db.Column(db.String(30), default='Restringido')
retention_until = db.Column(db.Date)
notes = db.Column(db.Text)
summary_payload = db.Column(db.Text)
institution_id = db.Column(db.Integer, db.ForeignKey('institution.id'), index=True)
patient = db.relationship('Patient', backref=db.backref('clinical_record', uselist=False, cascade='all, delete-orphan'))
episodes = db.relationship('ClinicalEpisode', back_populates='record', cascade='all, delete-orphan', order_by='ClinicalEpisode.started_at.desc()')
entries = db.relationship('ClinicalEntry', back_populates='record', cascade='all, delete-orphan', order_by='ClinicalEntry.entry_datetime.desc()')
class ClinicalEpisode(TimestampMixin, db.Model):
id = db.Column(db.Integer, primary_key=True)
record_id = db.Column(db.Integer, db.ForeignKey('clinical_record.id'), nullable=False, index=True)
patient_id = db.Column(db.Integer, db.ForeignKey('patient.id'), nullable=False, index=True)
created_by_user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
title = db.Column(db.String(180), nullable=False)
specialty_name = db.Column(db.String(150))
reason = db.Column(db.String(255))
diagnosis_summary = db.Column(db.String(255))
care_level = db.Column(db.String(80), default='Ambulatorio')
status = db.Column(db.String(30), default='Abierto', index=True)
visibility_scope = db.Column(db.String(30), default='Institucional')
started_at = db.Column(db.DateTime, default=datetime.utcnow, nullable=False, index=True)
closed_at = db.Column(db.DateTime)
notes = db.Column(db.Text)
institution_id = db.Column(db.Integer, db.ForeignKey('institution.id'), index=True)
record = db.relationship('ClinicalRecord', back_populates='episodes')
patient = db.relationship('Patient', backref=db.backref('clinical_episodes', lazy='dynamic'))
created_by_user = db.relationship('User', backref=db.backref('clinical_episodes_created', lazy='dynamic'))
members = db.relationship('ClinicalEpisodeMember', back_populates='episode', cascade='all, delete-orphan', order_by='ClinicalEpisodeMember.created_at.asc()')
entries = db.relationship('ClinicalEntry', back_populates='episode', order_by='ClinicalEntry.entry_datetime.desc()')
class ClinicalEpisodeTemplate(TimestampMixin, db.Model):
"""Template reutilizable para abrir episodios clínicos frecuentes.
No representa una nueva Historia Clínica: solo guarda la estructura de apertura
de un episodio para que el profesional pueda reutilizarla en otros pacientes.
"""
id = db.Column(db.Integer, primary_key=True)
professional_id = db.Column(db.Integer, db.ForeignKey('professional_profile.id'), nullable=False, index=True)
created_by_user_id = db.Column(db.Integer, db.ForeignKey('user.id'), index=True)
title = db.Column(db.String(180), nullable=False, index=True)
category = db.Column(db.String(120), default='General', index=True)
specialty_name = db.Column(db.String(150), index=True)
reason = db.Column(db.String(255))
diagnosis_summary = db.Column(db.String(255))
care_level = db.Column(db.String(80), default='Ambulatorio')
visibility_scope = db.Column(db.String(30), default='Institucional')
notes = db.Column(db.Text)
source_episode_id = db.Column(db.Integer, db.ForeignKey('clinical_episode.id'), index=True)
usage_count = db.Column(db.Integer, default=0)
is_active = db.Column(db.Boolean, default=True, index=True)
institution_id = db.Column(db.Integer, db.ForeignKey('institution.id'), index=True)
professional = db.relationship('ProfessionalProfile', backref=db.backref('clinical_episode_templates', lazy='dynamic', cascade='all, delete-orphan'))
created_by_user = db.relationship('User', backref=db.backref('clinical_episode_templates_created', lazy='dynamic'))
source_episode = db.relationship('ClinicalEpisode', backref=db.backref('saved_as_episode_templates', lazy='dynamic'))
def to_payload(self):
return {
'id': self.id,
'title': self.title or '',
'category': self.category or 'General',
'specialty_name': self.specialty_name or '',
'reason': self.reason or '',
'diagnosis_summary': self.diagnosis_summary or '',
'care_level': self.care_level or 'Ambulatorio',
'visibility_scope': self.visibility_scope or 'Institucional',
'notes': self.notes or '',
'usage_count': self.usage_count or 0,
}
class ClinicalEpisodeMember(TimestampMixin, db.Model):
id = db.Column(db.Integer, primary_key=True)
episode_id = db.Column(db.Integer, db.ForeignKey('clinical_episode.id'), nullable=False, index=True)
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False, index=True)
added_by_user_id = db.Column(db.Integer, db.ForeignKey('user.id'))
role_label = db.Column(db.String(120), default='Integrante')
can_view = db.Column(db.Boolean, default=True)
can_write = db.Column(db.Boolean, default=False)
can_sign = db.Column(db.Boolean, default=False)
can_export = db.Column(db.Boolean, default=False)
is_active = db.Column(db.Boolean, default=True, index=True)
joined_at = db.Column(db.DateTime, default=datetime.utcnow)
left_at = db.Column(db.DateTime)
episode = db.relationship('ClinicalEpisode', back_populates='members')
user = db.relationship('User', foreign_keys=[user_id], backref=db.backref('clinical_episode_memberships', lazy='dynamic'))
added_by_user = db.relationship('User', foreign_keys=[added_by_user_id], backref=db.backref('clinical_episode_members_added', lazy='dynamic'))
class ClinicalEntry(TimestampMixin, db.Model):
id = db.Column(db.Integer, primary_key=True)
record_id = db.Column(db.Integer, db.ForeignKey('clinical_record.id'), nullable=False, index=True)
episode_id = db.Column(db.Integer, db.ForeignKey('clinical_episode.id'), index=True)
patient_id = db.Column(db.Integer, db.ForeignKey('patient.id'), nullable=False, index=True)
professional_id = db.Column(db.Integer, db.ForeignKey('professional_profile.id'), nullable=False, index=True)
created_by_user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
last_edited_by_user_id = db.Column(db.Integer, db.ForeignKey('user.id'))
entry_datetime = db.Column(db.DateTime, default=datetime.utcnow, nullable=False, index=True)
folio_number = db.Column(db.Integer, nullable=False, index=True)
specialty_name = db.Column(db.String(150))
encounter_type = db.Column(db.String(80), default='Evolución médica')
diagnosis_text = db.Column(db.String(255))
chief_complaint = db.Column(db.String(255))
provisional_diagnosis = db.Column(db.String(255))
specialty_template = db.Column(db.String(80), default='general_medicine', index=True)
cie10_code = db.Column(db.String(20), index=True)
snomed_term = db.Column(db.String(255))
snomed_code = db.Column(db.String(50), index=True)
subjective = db.Column(db.Text)
objective = db.Column(db.Text)
assessment = db.Column(db.Text)
plan = db.Column(db.Text)
treatment = db.Column(db.Text)
study_results = db.Column(db.Text)
vitals_payload = db.Column(db.Text)
structured_payload = db.Column(db.Text)
consent_reference = db.Column(db.String(120))
entry_status = db.Column(db.String(30), default='Firmado', index=True)
locked_at = db.Column(db.DateTime)
signed_name = db.Column(db.String(150))
signed_hash = db.Column(db.String(128))
signature_payload = db.Column(db.Text)
visibility_scope = db.Column(db.String(30), default='Institucional')
edit_revision = db.Column(db.Integer, default=1)
last_edited_at = db.Column(db.DateTime)
institution_id = db.Column(db.Integer, db.ForeignKey('institution.id'), index=True)
record = db.relationship('ClinicalRecord', back_populates='entries')
episode = db.relationship('ClinicalEpisode', back_populates='entries')
patient = db.relationship('Patient', backref=db.backref('clinical_entries', lazy='dynamic'))
professional = db.relationship('ProfessionalProfile', backref=db.backref('clinical_entries', lazy='dynamic'))
created_by_user = db.relationship('User', foreign_keys=[created_by_user_id], backref=db.backref('clinical_entries_created', lazy='dynamic'))
last_edited_by_user = db.relationship('User', foreign_keys=[last_edited_by_user_id], backref=db.backref('clinical_entries_edited', lazy='dynamic'))
attachments = db.relationship('ClinicalAttachment', back_populates='entry', cascade='all, delete-orphan')
class ClinicalEntryTemplate(TimestampMixin, db.Model):
id = db.Column(db.Integer, primary_key=True)
professional_id = db.Column(db.Integer, db.ForeignKey('professional_profile.id'), nullable=False, index=True)
created_by_user_id = db.Column(db.Integer, db.ForeignKey('user.id'), index=True)
title = db.Column(db.String(180), nullable=False, index=True)
category = db.Column(db.String(120), default='General', index=True)
specialty_name = db.Column(db.String(150), index=True)
specialty_template = db.Column(db.String(80), default='general_medicine', index=True)
encounter_type = db.Column(db.String(80), default='Evolución médica')
chief_complaint = db.Column(db.String(255))
provisional_diagnosis = db.Column(db.String(255))
diagnosis_text = db.Column(db.String(255))
cie10_code = db.Column(db.String(20))
snomed_term = db.Column(db.String(255))
snomed_code = db.Column(db.String(50))
subjective = db.Column(db.Text)
objective = db.Column(db.Text)
assessment = db.Column(db.Text)
plan = db.Column(db.Text)
treatment = db.Column(db.Text)
study_results = db.Column(db.Text)
vitals_payload = db.Column(db.Text)
structured_payload = db.Column(db.Text)
consent_reference = db.Column(db.String(120))
visibility_scope = db.Column(db.String(30), default='Institucional')
source_entry_id = db.Column(db.Integer, db.ForeignKey('clinical_entry.id'), index=True)
usage_count = db.Column(db.Integer, default=0)
is_active = db.Column(db.Boolean, default=True, index=True)
institution_id = db.Column(db.Integer, db.ForeignKey('institution.id'), index=True)
professional = db.relationship('ProfessionalProfile', backref=db.backref('clinical_entry_templates', lazy='dynamic', cascade='all, delete-orphan'))
created_by_user = db.relationship('User', backref=db.backref('clinical_templates_created', lazy='dynamic'))
source_entry = db.relationship('ClinicalEntry', backref=db.backref('saved_as_templates', lazy='dynamic'))
def _safe_json(self, raw):
try:
payload = json.loads(raw or '{}')
return payload if isinstance(payload, dict) else {}
except Exception:
return {}
def to_payload(self):
return {
'id': self.id,
'title': self.title,
'category': self.category or 'General',
'specialty_name': self.specialty_name or '',
'specialty_template': self.specialty_template or 'general_medicine',
'encounter_type': self.encounter_type or 'Evolución médica',
'chief_complaint': self.chief_complaint or '',
'provisional_diagnosis': self.provisional_diagnosis or '',
'diagnosis_text': self.diagnosis_text or '',
'cie10_code': self.cie10_code or '',
'snomed_term': self.snomed_term or '',
'snomed_code': self.snomed_code or '',
'subjective': self.subjective or '',
'objective': self.objective or '',
'assessment': self.assessment or '',
'plan': self.plan or '',
'treatment': self.treatment or '',
'study_results': self.study_results or '',
'vitals': self._safe_json(self.vitals_payload),
'structured': self._safe_json(self.structured_payload),
'consent_reference': self.consent_reference or '',
'visibility_scope': self.visibility_scope or 'Institucional',
'usage_count': self.usage_count or 0,
}
class ClinicalAttachment(TimestampMixin, db.Model):
id = db.Column(db.Integer, primary_key=True)
entry_id = db.Column(db.Integer, db.ForeignKey('clinical_entry.id'), nullable=False, index=True)
filename = db.Column(db.String(255), nullable=False)
file_path = db.Column(db.String(255), nullable=False)
mime_type = db.Column(db.String(120))
is_patient_visible = db.Column(db.Boolean, default=False)
entry = db.relationship('ClinicalEntry', back_populates='attachments')
class AccountingEntry(TimestampMixin, db.Model):
id = db.Column(db.Integer, primary_key=True)
entry_date = db.Column(db.Date, nullable=False, default=date.today, index=True)
entry_type = db.Column(db.String(20), nullable=False, index=True) # ingreso / egreso
category = db.Column(db.String(120), nullable=False, index=True)
description = db.Column(db.String(255), nullable=False)
amount = db.Column(db.Float, nullable=False, default=0)
tax_profile = db.Column(db.String(80))
receipt_type = db.Column(db.String(40))
receipt_number = db.Column(db.String(80), index=True)
counterparty_name = db.Column(db.String(150))
counterparty_cuit = db.Column(db.String(20))
payment_method = db.Column(db.String(60))
status = db.Column(db.String(30), default='Registrado', index=True)
notes = db.Column(db.Text)
created_by_user_id = db.Column(db.Integer, db.ForeignKey('user.id'))
institution_id = db.Column(db.Integer, db.ForeignKey('institution.id'), index=True)
created_by_user = db.relationship('User', backref=db.backref('accounting_entries_created', lazy='dynamic'))
class SaasPlan(TimestampMixin, db.Model):
__tablename__ = 'saas_plan'
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(140), nullable=False, unique=True)
description = db.Column(db.Text)
monthly_price = db.Column(db.Float, nullable=False, default=0)
price_per_appointment = db.Column(db.Float, nullable=False, default=0)
included_professionals = db.Column(db.Integer, default=1)
extra_professional_price = db.Column(db.Float, default=0)
included_appointments = db.Column(db.Integer, default=0)
active = db.Column(db.Boolean, default=True, index=True)
class InstitutionSubscription(TimestampMixin, db.Model):
__tablename__ = 'institution_subscription'
id = db.Column(db.Integer, primary_key=True)
institution_id = db.Column(db.Integer, db.ForeignKey('institution.id'), nullable=False, index=True)
plan_id = db.Column(db.Integer, db.ForeignKey('saas_plan.id'), nullable=False, index=True)
start_date = db.Column(db.Date, default=date.today, nullable=False)
billing_day = db.Column(db.Integer, default=1)
status = db.Column(db.String(30), default='activa', index=True)
notes = db.Column(db.Text)
institution = db.relationship('Institution', backref=db.backref('subscriptions', lazy='dynamic'))
plan = db.relationship('SaasPlan', backref=db.backref('subscriptions', lazy='dynamic'))
class SaasInvoice(TimestampMixin, db.Model):
__tablename__ = 'saas_invoice'
id = db.Column(db.Integer, primary_key=True)
institution_id = db.Column(db.Integer, db.ForeignKey('institution.id'), nullable=False, index=True)
subscription_id = db.Column(db.Integer, db.ForeignKey('institution_subscription.id'), index=True)
period = db.Column(db.String(7), nullable=False, index=True) # YYYY-MM
issue_date = db.Column(db.Date, default=date.today, nullable=False)
due_date = db.Column(db.Date, nullable=False)
monthly_amount = db.Column(db.Float, default=0)
appointment_count = db.Column(db.Integer, default=0)
appointment_amount = db.Column(db.Float, default=0)
professional_count = db.Column(db.Integer, default=0)
extra_professional_amount = db.Column(db.Float, default=0)
total_amount = db.Column(db.Float, default=0)
paid_amount = db.Column(db.Float, default=0)
status = db.Column(db.String(30), default='pendiente', index=True)
payment_method = db.Column(db.String(40))
mercadopago_preference_id = db.Column(db.String(160), index=True)
mercadopago_payment_id = db.Column(db.String(160), index=True)
mercadopago_status = db.Column(db.String(80), index=True)
mercadopago_status_detail = db.Column(db.String(160))
mercadopago_init_point = db.Column(db.String(600))
mercadopago_sandbox_init_point = db.Column(db.String(600))
mercadopago_payload = db.Column(db.Text)
notes = db.Column(db.Text)
institution = db.relationship('Institution', backref=db.backref('saas_invoices', lazy='dynamic'))
subscription = db.relationship('InstitutionSubscription', backref=db.backref('invoices', lazy='dynamic'))
@property
def debt_amount(self):
return max((self.total_amount or 0) - (self.paid_amount or 0), 0)
class SaasPayment(TimestampMixin, db.Model):
__tablename__ = 'saas_payment'
id = db.Column(db.Integer, primary_key=True)
invoice_id = db.Column(db.Integer, db.ForeignKey('saas_invoice.id'), nullable=False, index=True)
payment_date = db.Column(db.Date, default=date.today, nullable=False)
amount = db.Column(db.Float, nullable=False, default=0)
method = db.Column(db.String(40), default='efectivo')
reference = db.Column(db.String(160))
status = db.Column(db.String(40), default='registrado', index=True)
mercadopago_payment_id = db.Column(db.String(160), index=True)
mercadopago_status = db.Column(db.String(80))
mercadopago_status_detail = db.Column(db.String(160))
mercadopago_payload = db.Column(db.Text)
notes = db.Column(db.Text)
created_by_user_id = db.Column(db.Integer, db.ForeignKey('user.id'))
invoice = db.relationship('SaasInvoice', backref=db.backref('payments', lazy='dynamic'))
created_by_user = db.relationship('User')
class AuditLog(TimestampMixin, db.Model):
id = db.Column(db.Integer, primary_key=True)
actor_email = db.Column(db.String(150))
action = db.Column(db.String(120), nullable=False)
entity_type = db.Column(db.String(80), nullable=False)
entity_id = db.Column(db.String(50))
details = db.Column(db.Text)
class ErrorLog(TimestampMixin, db.Model):
id = db.Column(db.Integer, primary_key=True)
actor_email = db.Column(db.String(150))
route = db.Column(db.String(255))
function_name = db.Column(db.String(120))
error_type = db.Column(db.String(120))
message = db.Column(db.Text)
stacktrace = db.Column(db.Text)
extra = db.Column(db.Text)
class ChatConversation(TimestampMixin, db.Model):
id = db.Column(db.Integer, primary_key=True)
title = db.Column(db.String(180))
is_group = db.Column(db.Boolean, default=False, index=True)
created_by_user_id = db.Column(db.Integer, db.ForeignKey('user.id'))
is_active = db.Column(db.Boolean, default=True, index=True)
institution_id = db.Column(db.Integer, db.ForeignKey('institution.id'), index=True)
institution = db.relationship('Institution')
created_by_user = db.relationship('User', backref=db.backref('chat_conversations_created', lazy='dynamic'))
members = db.relationship('ChatConversationMember', back_populates='conversation', cascade='all, delete-orphan')
messages = db.relationship('ChatMessage', back_populates='conversation', cascade='all, delete-orphan', order_by='ChatMessage.created_at.asc()')
@property
def display_title(self):
return self.title or ('Grupo' if self.is_group else 'Chat privado')
class ChatConversationMember(TimestampMixin, db.Model):
id = db.Column(db.Integer, primary_key=True)
conversation_id = db.Column(db.Integer, db.ForeignKey('chat_conversation.id'), nullable=False, index=True)
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False, index=True)
is_admin = db.Column(db.Boolean, default=False)
last_read_at = db.Column(db.DateTime)
conversation = db.relationship('ChatConversation', back_populates='members')
user = db.relationship('User', backref=db.backref('chat_memberships', lazy='dynamic'))
class ChatMessage(TimestampMixin, db.Model):
id = db.Column(db.Integer, primary_key=True)
conversation_id = db.Column(db.Integer, db.ForeignKey('chat_conversation.id'), nullable=False, index=True)
sender_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False, index=True)
body = db.Column(db.Text)
system_flag = db.Column(db.Boolean, default=False)
conversation = db.relationship('ChatConversation', back_populates='messages')
sender = db.relationship('User', backref=db.backref('chat_messages_sent', lazy='dynamic'))
attachments = db.relationship('ChatAttachment', back_populates='message', cascade='all, delete-orphan')
class ChatAttachment(TimestampMixin, db.Model):
id = db.Column(db.Integer, primary_key=True)
message_id = db.Column(db.Integer, db.ForeignKey('chat_message.id'), nullable=False, index=True)
filename = db.Column(db.String(255), nullable=False)
file_path = db.Column(db.String(255), nullable=False)
mime_type = db.Column(db.String(120))
file_size = db.Column(db.Integer)
message = db.relationship('ChatMessage', back_populates='attachments')
class WhatsappBotRule(TimestampMixin, db.Model):
id = db.Column(db.Integer, primary_key=True)
trigger = db.Column(db.String(120), nullable=False, index=True)
title = db.Column(db.String(180))
response_html = db.Column(db.Text, nullable=False)
match_mode = db.Column(db.String(30), default='contains')
sort_order = db.Column(db.Integer, default=0, index=True)
is_active = db.Column(db.Boolean, default=True, index=True)
institution_id = db.Column(db.Integer, db.ForeignKey('institution.id'), index=True)
class WhatsappBotKnowledge(TimestampMixin, db.Model):
id = db.Column(db.Integer, primary_key=True)
source_url = db.Column(db.String(500), index=True)
title = db.Column(db.String(255))
content = db.Column(db.Text, nullable=False)
keywords = db.Column(db.String(500))
is_active = db.Column(db.Boolean, default=True, index=True)
institution_id = db.Column(db.Integer, db.ForeignKey('institution.id'), index=True)
class WhatsappMessageLog(TimestampMixin, db.Model):
id = db.Column(db.Integer, primary_key=True)
wa_id = db.Column(db.String(80), index=True)
customer_name = db.Column(db.String(180))
incoming_text = db.Column(db.Text)
outgoing_text = db.Column(db.Text)
provider_message_id = db.Column(db.String(120))
status = db.Column(db.String(40), default='received', index=True)
raw_payload = db.Column(db.Text)
error = db.Column(db.Text)
institution_id = db.Column(db.Integer, db.ForeignKey('institution.id'), index=True)
class AiKnowledgeItem(TimestampMixin, db.Model):
__tablename__ = 'ai_knowledge_item'
id = db.Column(db.Integer, primary_key=True)
source_type = db.Column(db.String(60), default='manual', index=True)
source_ref = db.Column(db.String(500), index=True)
title = db.Column(db.String(255), nullable=False)
content = db.Column(db.Text, nullable=False)
keywords = db.Column(db.String(500))
is_active = db.Column(db.Boolean, default=True, index=True)
institution_id = db.Column(db.Integer, db.ForeignKey('institution.id'), index=True)
last_synced_at = db.Column(db.DateTime)
class AiChatLog(TimestampMixin, db.Model):
__tablename__ = 'ai_chat_log'
id = db.Column(db.Integer, primary_key=True)
session_id = db.Column(db.String(120), index=True)
customer_name = db.Column(db.String(180))
customer_email = db.Column(db.String(180))
incoming_text = db.Column(db.Text)
outgoing_text = db.Column(db.Text)
intent = db.Column(db.String(80), index=True)
handoff_status = db.Column(db.String(40), default='auto', index=True)
context_snapshot = db.Column(db.Text)
ip_address = db.Column(db.String(80))
institution_id = db.Column(db.Integer, db.ForeignKey('institution.id'), index=True)