Coverage for apps/appointments/models.py: 65%
212 statements
« prev ^ index » next coverage.py v6.4.4, created at 2024-05-23 11:03 -0600
« prev ^ index » next coverage.py v6.4.4, created at 2024-05-23 11:03 -0600
1from datetime import date, datetime, time, timedelta
2from functools import cached_property
4import pytz
5from django.contrib.postgres.fields import ArrayField
6from django.db import models
7from django.db.models import F, Sum
8from django.template.loader import render_to_string
9from django_fsm import RETURN_VALUE, FSMField, transition
10from djmoney.models.fields import MoneyField
11from djmoney.money import Money
12from simple_history.models import HistoricalRecords
14from apps.accounting.models import PositiveMovement
15from apps.payments.models import Payment, PaymentAllocation
16from base.models import RandomSlugModel, TimeStampedModel
19class AppointmentType(RandomSlugModel):
20 """
21 Model to represent appointment type
22 """
24 practitioner = models.ForeignKey(
25 "practitioners.Practitioner", on_delete=models.CASCADE, related_name="appointment_types"
26 )
27 products = models.ManyToManyField("products.Product", through="appointments.AppointmentTypeProduct")
28 duration = models.DurationField(default=timedelta(hours=1), null=True)
29 is_active = models.BooleanField(default=True)
31 name = models.CharField(max_length=512)
33 class Meta:
34 ordering = ["name"]
36 def __str__(self):
37 return self.name
40class AppointmentTypeProduct(models.Model):
41 appointment_type = models.ForeignKey("appointments.AppointmentType", on_delete=models.CASCADE)
42 product = models.ForeignKey("products.Product", on_delete=models.CASCADE)
43 is_binding = models.BooleanField(default=True)
46class Appointment(RandomSlugModel, TimeStampedModel):
47 """
48 Model to represent the appointment of patients
49 """
51 organization = models.ForeignKey(
52 "organizations.Organization", on_delete=models.PROTECT, related_name="appointments"
53 )
54 patient = models.ForeignKey("patients.Patient", on_delete=models.PROTECT, related_name="appointments")
55 type_of = models.ForeignKey(
56 "appointments.AppointmentType", on_delete=models.PROTECT, related_name="appointments", null=True, blank=True
57 )
58 comments = models.TextField(blank=True)
60 date = models.DateField(default=date.today)
61 start_time = models.TimeField(null=True, blank=True)
62 end_time = models.TimeField(null=True, blank=True)
63 arrival_timestamp = models.DateTimeField(null=True, blank=True)
64 start_timestamp = models.DateTimeField(null=True, blank=True)
65 end_timestamp = models.DateTimeField(null=True, blank=True)
66 departure_timestamp = models.DateTimeField(null=True, blank=True)
67 is_cancellable = models.BooleanField(default=True)
68 is_confirmed = models.BooleanField(default=False)
69 is_past = models.BooleanField(default=False)
71 class StatusChoices(models.TextChoices):
72 SCHEDULED = "SCHEDULED", "Agendada"
73 CONFIRMED = "CONFIRMED", "Confirmada"
74 WAITING = "WAITING", "Esperando"
75 ONGOING = "ONGOING", "En Curso"
76 FINISHED = "FINISHED", "Finalizada" # The doctor ended the appointment
77 OVERDUE = "OVERDUE", "Con Adeudo"
78 CLOSED = "CLOSED", "Concluida"
79 CANCELED = "CANCELED", "Cancelada"
81 status = FSMField(
82 max_length=16,
83 choices=StatusChoices.choices,
84 default=StatusChoices.SCHEDULED,
85 )
87 class Meta:
88 verbose_name = "Cita"
89 verbose_name_plural = "Cita"
90 ordering = ["date", "start_time"]
92 def __str__(self, *args, **kwargs):
93 type_of = getattr(getattr(self, "type_of", None), "name", None)
94 return f"{type_of or 'Cita'} de {self.patient.full_name} del {self.date}"
96 def owner_practitioners(self):
97 from apps.practitioners.models import Practitioner
99 return Practitioner.objects.filter(appointments__appointment=self)
101 @transition(field=status, source=StatusChoices.SCHEDULED, target=StatusChoices.CONFIRMED)
102 def patient_confirmed(self):
103 pass
105 @transition(field=status, source=[StatusChoices.SCHEDULED, StatusChoices.CONFIRMED], target=StatusChoices.CANCELED)
106 def patient_canceled(self):
107 pass
109 @transition(field=status, source=[StatusChoices.SCHEDULED, StatusChoices.CONFIRMED], target=StatusChoices.WAITING)
110 def patient_arrived(self):
111 from .services import PractitionerCommunication
113 for practitioner in self.owner_practitioners():
114 pc = PractitionerCommunication(practitioner=practitioner).send_patient_arrived(self)
116 @transition(field=status, source=StatusChoices.WAITING, target=StatusChoices.ONGOING)
117 def patient_entered(self):
118 pass
120 @transition(field=status, source=StatusChoices.ONGOING, target=StatusChoices.FINISHED)
121 def patient_exited(self):
122 pass
124 @transition(
125 field=status,
126 source=[StatusChoices.ONGOING, StatusChoices.FINISHED],
127 target=RETURN_VALUE(StatusChoices.OVERDUE, StatusChoices.CLOSED),
128 )
129 def patient_departed(self):
130 from .services import PatientCommunication, PractitionerCommunication
132 for practitioner in self.owner_practitioners():
133 pc = PractitionerCommunication(practitioner=practitioner).send_patient_left(self)
134 """
135 PatientCommunication(patient=self.patient).send_payments_email
136 """
138 patcom1 = PatientCommunication(patient=self.patient).send_thankyou_message(self)
139 patcom2 = PatientCommunication(patient=self.patient).send_payments_summary(self)
141 return self.StatusChoices.OVERDUE if self.pending_amount.amount > 0 else self.StatusChoices.CLOSED
143 @transition(field=status, source=StatusChoices.OVERDUE, target=StatusChoices.CLOSED)
144 def close_appointment(self):
145 pass
147 @transition(field=status, source=StatusChoices.CLOSED, target=StatusChoices.OVERDUE)
148 def overdue_appointment(self):
149 pass
151 @cached_property
152 def total_price(self):
153 agg = self.charges.aggregate(total_price=Sum(F("price") * F("quantity"))).get("total_price") or 0
154 return Money(agg or 0, "MXN")
156 @cached_property
157 def total_shared_cost(self):
158 agg = (
159 self.charges.aggregate(total_shared_cost=Sum(F("shared_cost") * F("quantity"))).get("total_shared_cost")
160 or 0
161 )
162 return Money(agg or 0, "MXN")
164 @cached_property
165 def total_hard_cost(self):
166 agg = self.charges.aggregate(total_hard_cost=Sum(F("hard_cost") * F("quantity"))).get("total_hard_cost") or 0
167 return Money(agg or 0, "MXN")
169 @cached_property
170 def paid_amount(self):
171 agg = (
172 PaymentAllocation.objects.filter(appointment_charge__appointment=self)
173 .aggregate(allocated_amount=Sum("amount"))
174 .get("allocated_amount")
175 or 0
176 )
177 return Money(agg or 0, "MXN")
179 @cached_property
180 def pending_amount(self):
181 return self.total_price - self.paid_amount
184class AppointmentPractitioner(RandomSlugModel, TimeStampedModel):
185 practitioner = models.ForeignKey(
186 "practitioners.Practitioner", on_delete=models.CASCADE, related_name="appointments"
187 )
188 appointment = models.ForeignKey("appointments.Appointment", on_delete=models.CASCADE, related_name="practitioners")
190 is_owner = models.BooleanField(default=False)
191 is_accepted = models.BooleanField(default=False)
193 class Meta:
194 unique_together = ["practitioner", "appointment"]
195 ordering = ["appointment", "created_at"]
198class GoogleCalendarEvent(models.Model):
199 appointment = models.OneToOneField(
200 "appointments.Appointment", on_delete=models.CASCADE, null=True, blank=True, related_name="googleevent"
201 )
202 calendar = models.ForeignKey(
203 "practitioners.PractitionerGoogleCalendar", on_delete=models.CASCADE, null=True, blank=True
204 )
205 event_id = models.CharField(max_length=128, blank=True, null=True)
207 def get_summary(self):
208 status_mapping = {
209 Appointment.StatusChoices.SCHEDULED: "⚪️",
210 Appointment.StatusChoices.CONFIRMED: "🟩",
211 Appointment.StatusChoices.WAITING: "🟧",
212 Appointment.StatusChoices.ONGOING: "🟨",
213 Appointment.StatusChoices.FINISHED: "🟦",
214 Appointment.StatusChoices.OVERDUE: "🚨",
215 Appointment.StatusChoices.CLOSED: "✅",
216 Appointment.StatusChoices.CANCELED: "⛔️",
217 "default": "",
218 }
219 icon = status_mapping.get(self.appointment.status, "default")
220 type_of = self.appointment.type_of or ""
221 string = f"{icon} {self.appointment.patient.full_name.upper()} ({type_of})"
222 return string
224 def get_description(self):
225 charges_list = self.appointment.charges.all()
226 # payments_list = Payment.objects.filter(appointment=self.appointment)
227 payments_list = appointment.payments.all()
228 data = {
229 "charges": charges_list,
230 "payments": payments_list,
231 "object": self,
232 "appointment": self.appointment,
233 }
234 # html = render_to_string('appointments/appointment_body.html', data)
235 html = render_to_string("appointments/appointment_body.txt", data)
237 return html
239 def get_start(self):
240 if self.appointment.start_timestamp:
241 value = self.appointment.start_timestamp
242 else:
243 _date = self.appointment.date
244 _time = self.appointment.start_time or time(9, 0, 0)
245 tz = pytz.timezone(self.calendar.timezone)
246 value = tz.localize(datetime.combine(_date, _time))
247 return value
249 def get_end(self):
250 if self.appointment.end_timestamp:
251 value = self.appointment.end_timestamp
252 elif self.appointment.end_time:
253 _date = self.appointment.date
254 _time = self.appointment.end_time
255 tz = pytz.timezone(self.calendar.timezone)
256 value = tz.localize(datetime.combine(_date, _time))
257 else:
258 duration = getattr(self.appointment.type_of, "duration", None)
259 if duration:
260 value = self.get_start() + duration
261 else:
262 value = self.get_start() + timedelta(hours=1)
263 value = max(self.get_start() + timedelta(minutes=10), value)
264 return value
266 def get_attendees(self):
267 """
268 ej. [ {'email': algo@gmial.com},
269 {'email': algo@gmial.com},
270 {'email': algo@gmial.com},
271 {'email': algo@gmial.com},
272 {'email': algo@gmial.com}, ]
273 """
274 email_list = []
275 qs = (
276 self.appointment.practitioners.all()
277 .exclude(practitioner=self.calendar.practitioner)
278 .values_list("practitioner__user__email", flat=True)
279 )
280 email_list = [{"email": item} for item in qs]
281 return email_list
283 def __str__(self):
284 return super().__str__()
286 def get_event_dict(self):
287 data_dict = {
288 "summary": self.get_summary(),
289 # 'location': '800 Howard St., San Francisco, CA 94103',
290 "description": self.get_description(),
291 "start": {
292 "dateTime": self.get_start().isoformat(),
293 "timeZone": self.calendar.timezone,
294 },
295 "end": {
296 "dateTime": self.get_end().isoformat(),
297 "timeZone": self.calendar.timezone,
298 },
299 }
300 if self.get_attendees():
301 data_dict["attendees"] = self.get_attendees()
303 print("data dict", data_dict)
304 return data_dict
307class AppointmentCharge(PositiveMovement):
308 """
309 Model to represent the appointment of patients
310 """
312 appointment = models.ForeignKey("appointments.Appointment", on_delete=models.CASCADE, related_name="charges")
313 product = models.ForeignKey("products.Product", on_delete=models.PROTECT, related_name="charges")
314 created_by = models.ForeignKey("users.User", on_delete=models.PROTECT, null=True)
316 hard_cost = MoneyField(max_digits=14, decimal_places=2, default_currency="MXN", default=0)
317 shared_cost = MoneyField(max_digits=14, decimal_places=2, default_currency="MXN", default=0)
318 price = MoneyField(max_digits=14, decimal_places=2, default_currency="MXN", default=0)
319 quantity = models.DecimalField(decimal_places=2, max_digits=9, default=1)
321 @cached_property
322 def paid_amount(self):
323 agg = self.payments.aggregate(allocated_amount=models.Sum("amount")).get("allocated_amount") or 0
324 return Money(agg or 0, "MXN")
326 @cached_property
327 def total_price(self):
328 return self.price * self.quantity
330 @cached_property
331 def total_shared_cost(self):
332 return self.shared_cost * self.quantity
334 @cached_property
335 def total_hard_cost(self):
336 return self.hard_cost * self.quantity
338 @cached_property
339 def pending_amount(self):
340 return self.total_price - self.paid_amount
342 class Meta:
343 ordering = ["created_at"]
344 verbose_name = "Cargo de Cita"
345 verbose_name_plural = "Cargos de Citas"
347 def __str__(self):
348 return f"Cargo {self.product} en {self.appointment}"
351class AppointmentExport(RandomSlugModel, TimeStampedModel):
352 """
353 Model for Appointment export "Reporte de Ventas"
354 """
356 organization = models.ForeignKey("organizations.Organization", on_delete=models.CASCADE)
357 practitioner = models.ForeignKey("practitioners.Practitioner", on_delete=models.CASCADE, blank=True, null=True)
359 from_date = models.DateField()
360 to_date = models.DateField()
361 export_file = models.FileField(upload_to="appointment/exports/", blank=True, null=True)
363 class Meta:
364 ordering = ["organization", "practitioner", "-created_at"]
365 verbose_name = "Reporte de Ingresos"
366 verbose_name_plural = "Reportes de Ingresos"
369class AppointmentChargeExport(RandomSlugModel, TimeStampedModel):
370 """
371 Model for Appointment Charge export "Reporte de Adeudos"
372 """
374 organization = models.ForeignKey("organizations.Organization", on_delete=models.CASCADE)
375 practitioner = models.ForeignKey("practitioners.Practitioner", on_delete=models.CASCADE, blank=True, null=True)
377 # from_date = models.DateField()
378 # to_date = models.DateField()
379 export_file = models.FileField(upload_to="appointmentcharge/exports/", blank=True, null=True)
381 class Meta:
382 ordering = ["organization", "practitioner", "-created_at"]
383 verbose_name = "Reporte de Adeudos"
384 verbose_name_plural = "Reportes de Adeudos"