Files
ACORE/ext-visit manager/files(1)/attendance_models.py
T
lokses 131cb27d16 Initial commit — ACORE pharmacy network management system
Django project: employees, locations, visit_manager, it_manager,
mattermost_connector and stub connectors (ad, mail, portal).
2026-05-31 14:21:47 +03:00

122 lines
4.5 KiB
Python

# apps/attendance/models.py
import uuid
import os
from django.db import models
from django.utils import timezone
def photo_upload_path(instance, filename):
"""
Путь: photos/YYYY/MM/DD/<employee_id>/<uuid>.jpg
Пример: photos/2026/04/24/42/a3f1bc2d.jpg
"""
ext = filename.split('.')[-1].lower()
ts = instance.timestamp or timezone.now()
return os.path.join(
'photos',
str(ts.year),
f'{ts.month:02d}',
f'{ts.day:02d}',
str(instance.employee_id),
f'{uuid.uuid4().hex}.{ext}'
)
class AttendanceLog(models.Model):
"""
Основная запись посещения сотрудника.
"""
DIRECTION_CHOICES = [
('in', 'Приход'),
('out', 'Уход'),
]
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
employee = models.ForeignKey(
'employees.Employee',
on_delete=models.PROTECT,
related_name='attendance_logs',
verbose_name='Сотрудник',
)
location = models.ForeignKey(
'pharmacies.Pharmacy',
on_delete=models.PROTECT,
related_name='attendance_logs',
verbose_name='Аптека',
)
timestamp = models.DateTimeField(default=timezone.now, db_index=True, verbose_name='Время')
direction = models.CharField(max_length=3, choices=DIRECTION_CHOICES, verbose_name='Направление')
# Оригинал фото — хранится в файловой системе через Django FileField
photo = models.ImageField(
upload_to=photo_upload_path,
verbose_name='Фото',
blank=True,
null=True,
)
# Превью 64x64 JPEG — хранится прямо в БД (bytea ~2-4 КБ)
# Используется для быстрого отображения в списках без чтения ФС
photo_thumb = models.BinaryField(
verbose_name='Превью (bytea)',
blank=True,
null=True,
editable=False,
)
# Метаданные устройства (опционально)
device_id = models.CharField(max_length=64, blank=True, verbose_name='ID устройства')
ip_address = models.GenericIPAddressField(null=True, blank=True, verbose_name='IP')
synced = models.BooleanField(default=False, db_index=True, verbose_name='Синхронизировано')
created_at = models.DateTimeField(auto_now_add=True)
class Meta:
ordering = ['-timestamp']
indexes = [
models.Index(fields=['employee', 'timestamp']),
models.Index(fields=['location', 'timestamp']),
models.Index(fields=['synced']),
]
verbose_name = 'Запись посещения'
verbose_name_plural = 'Записи посещений'
def __str__(self):
return f'{self.employee} — {self.get_direction_display()} — {self.timestamp:%d.%m.%Y %H:%M}'
@property
def photo_thumb_b64(self) -> str | None:
"""Удобно отдавать превью в JSON как data URI."""
if self.photo_thumb:
import base64
return 'data:image/jpeg;base64,' + base64.b64encode(bytes(self.photo_thumb)).decode()
return None
class WorkShift(models.Model):
"""
Рабочая смена — пара приход/уход, вычисляется из AttendanceLog.
Материализуется для отчётов, чтобы не джойнить каждый раз.
"""
employee = models.ForeignKey('employees.Employee', on_delete=models.CASCADE)
location = models.ForeignKey('pharmacies.Pharmacy', on_delete=models.CASCADE)
date = models.DateField(db_index=True)
arrived_at = models.DateTimeField(null=True, blank=True)
left_at = models.DateTimeField(null=True, blank=True)
duration = models.DurationField(null=True, blank=True) # left_at - arrived_at
log_in = models.OneToOneField(
AttendanceLog, null=True, blank=True,
on_delete=models.SET_NULL, related_name='shift_in'
)
log_out = models.OneToOneField(
AttendanceLog, null=True, blank=True,
on_delete=models.SET_NULL, related_name='shift_out'
)
class Meta:
unique_together = [('employee', 'location', 'date')]
ordering = ['-date']
verbose_name = 'Рабочая смена'
verbose_name_plural = 'Рабочие смены'