Files
ACORE/apps/mail_connector/signals.py
T
lokses 3be444b05e Carbonio CE: API-слой синхронизации ящиков аптек
Серверная часть интеграции по эталону AD-коннектора (драйвер — Pharmacy):

- carbonio_service.py: клиент admin API Carbonio/Zimbra (JSON-SOAP на 6071):
  auth + get/create/modify(status)/delete account, ping для read-only проверки
- services/sync_service.py: sync_pharmacy_to_carbonio (идемпотентно),
  deactivate_pharmacy_carbonio; логирование в CarbonioSyncLog
- signals.py: post_save/pre_save на Pharmacy — создание ящика при включённой
  синхронизации, закрытие/открытие при смене статуса аптеки; защита от рекурсии
  (account_id гасит повторный вызов), sync_enabled=False по умолчанию не трогает прод
- tasks.py: Celery-обёртки; CarbonioSyncLog + read-only админка
- команда sync_pharmacies_to_carbonio (dry-run по умолчанию, --apply)
- Carbonio-поля в админке Pharmacy; убран конфликт models.py vs models/

Без живого сервера деградирует мягко: CarbonioConfigError не роняет сохранение.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-24 10:02:35 +03:00

65 lines
3.4 KiB
Python

"""Сигналы синхронизации почтовых ящиков аптек с Carbonio CE.
Драйвер — модель Pharmacy (в отличие от AD/Mattermost, где драйвер Employee):
почта заводится на аптеку, а не на сотрудника. По умолчанию
Pharmacy.carbonio_sync_enabled=False, поэтому обычные сохранения аптек
сигналы не трогают — прод дёргается только для явно включённых аптек.
"""
import logging
from django.db.models.signals import post_save, pre_save
from django.dispatch import receiver
from apps.locations.models import Pharmacy
logger = logging.getLogger(__name__)
def _queue(task, pharmacy, action: str) -> None:
"""Ставим Celery-задачу; недоступный брокер не должен ронять сохранение."""
try:
task.delay(pharmacy.pk)
logger.info(f'Carbonio: поставлена задача {action} для аптеки {pharmacy.pk}')
except Exception as e:
logger.error(f'Carbonio: не удалось поставить задачу {action} для аптеки {pharmacy.pk}: {e}')
@receiver(pre_save, sender=Pharmacy)
def cache_pharmacy_carbonio_state(sender, instance, **kwargs):
"""Запоминаем прежний статус, чтобы в post_save увидеть закрытие/открытие."""
if not instance.pk:
instance._carbonio_pre_save_status = None
return
try:
old = Pharmacy.objects.values('status').get(pk=instance.pk)
instance._carbonio_pre_save_status = old['status']
except Pharmacy.DoesNotExist:
instance._carbonio_pre_save_status = None
@receiver(post_save, sender=Pharmacy)
def create_pharmacy_carbonio(sender, instance, created, **kwargs):
"""Заводим ящик при новой аптеке с включённой синхронизацией или когда
синхронизацию включили, а аккаунта ещё нет. Наличие account_id гасит
повторный вызов (после save() в sync_service) — рекурсии нет."""
if not instance.carbonio_sync_enabled or instance.carbonio_account_id:
return
from apps.mail_connector.tasks import sync_pharmacy_to_carbonio_task
_queue(sync_pharmacy_to_carbonio_task, instance, 'создания ящика')
@receiver(post_save, sender=Pharmacy)
def toggle_pharmacy_carbonio_on_status(sender, instance, created, **kwargs):
"""Закрываем ящик при закрытии аптеки, снова открываем — при возврате в open."""
if created or not instance.carbonio_account_id:
return
old_status = getattr(instance, '_carbonio_pre_save_status', None)
if old_status is None:
return
if old_status == 'open' and instance.status != 'open':
from apps.mail_connector.tasks import deactivate_pharmacy_carbonio_task
_queue(deactivate_pharmacy_carbonio_task, instance, 'закрытия ящика')
elif old_status != 'open' and instance.status == 'open' and instance.carbonio_sync_enabled:
from apps.mail_connector.tasks import sync_pharmacy_to_carbonio_task
_queue(sync_pharmacy_to_carbonio_task, instance, 'открытия ящика')