870 lines
43 KiB
Python
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)
|