Carbonio CE: почтовый ящик аптеки — поля, генерация адреса, уникальность
Фундамент интеграции Carbonio на уровне модели Pharmacy (без обращений к серверу): - Поля Pharmacy: carbonio_email, carbonio_account_id, carbonio_sync_enabled (default False), carbonio_sync_status, carbonio_last_sync, carbonio_sync_error - Генерация адреса <mail_prefix организации><номер>@домен (a1@, t1@, p1@...); префикс из Directory.extra_data['mail_prefix'], домен из ConnectorConfig(carbonio) с дефолтом aks-apteka.com.ua - Ручной override адреса не перетирается автогенерацией - Уникальность carbonio_email: clean() + частичный unique-констрейнт в БД (пустые не конфликтуют); автоген при коллизии оставляет поле пустым - 10 тестов (apps.locations.tests) — у приложения было 0 API-слой (клиент/sync/сигналы/задачи) — следующим шагом. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -6,6 +6,22 @@ from core.utils.validators import validate_ukrainian_phone
|
||||
from django.utils import timezone
|
||||
from .base import Location
|
||||
|
||||
# Домен почты по умолчанию, если не задан в ConnectorConfig(carbonio).extra_config['mail_domain']
|
||||
CARBONIO_DEFAULT_DOMAIN = 'aks-apteka.com.ua'
|
||||
|
||||
|
||||
def carbonio_mail_domain() -> str:
|
||||
"""Домен ящиков Carbonio: из настроек коннектора, иначе дефолт."""
|
||||
try:
|
||||
from core.models import ConnectorConfig
|
||||
cfg = ConnectorConfig.objects.filter(connector_type='carbonio').first()
|
||||
domain = (cfg.extra_config or {}).get('mail_domain') if cfg else None
|
||||
if domain:
|
||||
return domain
|
||||
except Exception:
|
||||
pass
|
||||
return CARBONIO_DEFAULT_DOMAIN
|
||||
|
||||
class PharmacyManager(models.Manager):
|
||||
|
||||
def get_queryset(self):
|
||||
@@ -128,6 +144,31 @@ class Pharmacy(Location):
|
||||
working_hours = models.CharField(_('Часы работы'), max_length=100,
|
||||
default='08:00-20:00')
|
||||
|
||||
# --- Carbonio CE (почтовый ящик аптеки) ---
|
||||
CARBONIO_STATUS_CHOICES = [
|
||||
('pending', _('Ожидает синхронизации')),
|
||||
('synced', _('Синхронизирован')),
|
||||
('error', _('Ошибка')),
|
||||
('disabled', _('Отключён')),
|
||||
]
|
||||
carbonio_email = models.EmailField(
|
||||
_('Почта Carbonio'), blank=True,
|
||||
help_text=_('Ящик аптеки. Пусто — сгенерируется как '
|
||||
'<префикс организации><номер>@домен. Можно задать вручную.'),
|
||||
)
|
||||
carbonio_account_id = models.CharField(_('ID аккаунта Carbonio'), max_length=100, blank=True)
|
||||
carbonio_sync_enabled = models.BooleanField(
|
||||
_('Синхронизация с Carbonio'), default=False,
|
||||
help_text=_('Управлять почтовым ящиком аптеки в Carbonio'),
|
||||
)
|
||||
carbonio_sync_status = models.CharField(
|
||||
_('Статус синхронизации Carbonio'), max_length=10,
|
||||
choices=CARBONIO_STATUS_CHOICES, blank=True,
|
||||
)
|
||||
carbonio_last_sync = models.DateTimeField(
|
||||
_('Последняя синхронизация Carbonio'), null=True, blank=True)
|
||||
carbonio_sync_error = models.TextField(_('Ошибка синхронизации Carbonio'), blank=True)
|
||||
|
||||
# --- Услуги (через справочник — без хардкода булевых флагов) ---
|
||||
# services = models.ManyToManyField(
|
||||
# 'core.Directory', blank=True,
|
||||
@@ -173,7 +214,13 @@ class Pharmacy(Location):
|
||||
models.UniqueConstraint(
|
||||
fields=['organization', 'pharmacy_number'],
|
||||
name='unique_pharmacy_number_per_organization'
|
||||
)
|
||||
),
|
||||
# Уникальность почты Carbonio среди заполненных (пустые не конфликтуют).
|
||||
models.UniqueConstraint(
|
||||
fields=['carbonio_email'],
|
||||
condition=models.Q(carbonio_email__gt=''),
|
||||
name='unique_carbonio_email',
|
||||
),
|
||||
]
|
||||
|
||||
def __str__(self):
|
||||
@@ -199,6 +246,27 @@ class Pharmacy(Location):
|
||||
raise ValidationError({
|
||||
'responsible_manager': 'Сотрудник должен иметь должность «Відповідальний менеджер»'
|
||||
})
|
||||
# Уникальность адреса Carbonio (и для заданного вручную, и для сгенерированного).
|
||||
if self.carbonio_email and self._carbonio_email_clashes(self.carbonio_email):
|
||||
raise ValidationError({
|
||||
'carbonio_email': _('Такой адрес Carbonio уже используется другой аптекой')
|
||||
})
|
||||
|
||||
def build_carbonio_email(self) -> str:
|
||||
"""Сгенерировать адрес ящика: <mail_prefix организации><номер>@домен.
|
||||
Пусто, если у организации не задан mail_prefix (в extra_data) или нет номера."""
|
||||
prefix = None
|
||||
if self.organization_id and self.organization:
|
||||
prefix = (self.organization.extra_data or {}).get('mail_prefix')
|
||||
if not prefix or not self.pharmacy_number:
|
||||
return ''
|
||||
return f'{prefix}{self.pharmacy_number}@{carbonio_mail_domain()}'
|
||||
|
||||
def _carbonio_email_clashes(self, email: str) -> bool:
|
||||
qs = Pharmacy.objects.filter(carbonio_email__iexact=email)
|
||||
if self.pk:
|
||||
qs = qs.exclude(pk=self.pk)
|
||||
return qs.exists()
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
if not self.location_type:
|
||||
@@ -207,6 +275,12 @@ class Pharmacy(Location):
|
||||
# status: единственный источник правды — статус, иначе аптека может
|
||||
# числиться "закрыта" и одновременно "активна".
|
||||
self.is_active = self.status == 'open'
|
||||
# Автогенерация адреса Carbonio, если синхронизация включена и адрес не
|
||||
# задан вручную. При коллизии оставляем пустым — требуется ручной ввод.
|
||||
if self.carbonio_sync_enabled and not self.carbonio_email:
|
||||
candidate = self.build_carbonio_email()
|
||||
if candidate and not self._carbonio_email_clashes(candidate):
|
||||
self.carbonio_email = candidate
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
@property
|
||||
|
||||
Reference in New Issue
Block a user