From 3be444b05ef33af8c31fc4425499b756c506daba Mon Sep 17 00:00:00 2001 From: lokses Date: Fri, 24 Jul 2026 10:02:35 +0300 Subject: [PATCH] =?UTF-8?q?Carbonio=20CE:=20API-=D1=81=D0=BB=D0=BE=D0=B9?= =?UTF-8?q?=20=D1=81=D0=B8=D0=BD=D1=85=D1=80=D0=BE=D0=BD=D0=B8=D0=B7=D0=B0?= =?UTF-8?q?=D1=86=D0=B8=D0=B8=20=D1=8F=D1=89=D0=B8=D0=BA=D0=BE=D0=B2=20?= =?UTF-8?q?=D0=B0=D0=BF=D1=82=D0=B5=D0=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Серверная часть интеграции по эталону 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 --- apps/locations/admin.py | 11 ++ apps/mail_connector/admin.py | 20 ++- apps/mail_connector/apps.py | 7 +- apps/mail_connector/carbonio_service.py | 143 ++++++++++++++++++ .../{models => management}/__init__.py | 0 .../management/commands/__init__.py | 0 .../commands/sync_pharmacies_to_carbonio.py | 62 ++++++++ .../mail_connector/migrations/0001_initial.py | 35 +++++ apps/mail_connector/models.py | 34 ++++- apps/mail_connector/services/__init__.py | 0 apps/mail_connector/services/sync_service.py | 111 ++++++++++++++ apps/mail_connector/signals.py | 64 ++++++++ apps/mail_connector/tasks.py | 31 ++++ 13 files changed, 515 insertions(+), 3 deletions(-) create mode 100644 apps/mail_connector/carbonio_service.py rename apps/mail_connector/{models => management}/__init__.py (100%) create mode 100644 apps/mail_connector/management/commands/__init__.py create mode 100644 apps/mail_connector/management/commands/sync_pharmacies_to_carbonio.py create mode 100644 apps/mail_connector/migrations/0001_initial.py create mode 100644 apps/mail_connector/services/__init__.py create mode 100644 apps/mail_connector/services/sync_service.py create mode 100644 apps/mail_connector/signals.py create mode 100644 apps/mail_connector/tasks.py diff --git a/apps/locations/admin.py b/apps/locations/admin.py index 919843d..91c9b5e 100644 --- a/apps/locations/admin.py +++ b/apps/locations/admin.py @@ -266,6 +266,8 @@ class PharmacyAdmin(ModelAdmin, ImportExportModelAdmin): readonly_fields = [ 'address', 'employees_count_display', 'license_status_display', 'is_active', 'created_at', 'updated_at', + 'carbonio_account_id', 'carbonio_sync_status', 'carbonio_last_sync', + 'carbonio_sync_error', ] fieldsets = ( @@ -304,6 +306,15 @@ class PharmacyAdmin(ModelAdmin, ImportExportModelAdmin): 'pharmacists_count', 'employees_count_display', ), }), + (_('Почта Carbonio'), { + 'classes': ('tab',), + 'fields': ( + 'carbonio_sync_enabled', 'carbonio_email', 'carbonio_account_id', + 'carbonio_sync_status', 'carbonio_last_sync', 'carbonio_sync_error', + ), + 'description': _('Пусто в «Почта Carbonio» — адрес сгенерируется как ' + '<префикс организации><номер>@домен. Можно задать вручную.'), + }), (_('Примечания'), { 'classes': ('tab',), 'fields': ('comments', 'created_at', 'updated_at'), diff --git a/apps/mail_connector/admin.py b/apps/mail_connector/admin.py index 8c38f3f..71268b1 100644 --- a/apps/mail_connector/admin.py +++ b/apps/mail_connector/admin.py @@ -1,3 +1,21 @@ from django.contrib import admin +from unfold.admin import ModelAdmin -# Register your models here. +from .models import CarbonioSyncLog + + +@admin.register(CarbonioSyncLog) +class CarbonioSyncLogAdmin(ModelAdmin): + list_display = ('pharmacy', 'action', 'status', 'created_at') + list_filter = ('action', 'status') + search_fields = ('pharmacy__pharmacy_number', 'pharmacy__carbonio_email', 'message') + date_hierarchy = 'created_at' + + def has_add_permission(self, request): + return False + + def has_change_permission(self, request, obj=None): + return False + + def has_delete_permission(self, request, obj=None): + return False diff --git a/apps/mail_connector/apps.py b/apps/mail_connector/apps.py index 5874113..8df255b 100644 --- a/apps/mail_connector/apps.py +++ b/apps/mail_connector/apps.py @@ -2,5 +2,10 @@ from django.apps import AppConfig class MailConnectorConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' name = 'apps.mail_connector' - verbose_name = 'Carbonio CE Connector' \ No newline at end of file + verbose_name = 'Carbonio CE Connector' + + def ready(self): + """Подключаем сигналы синхронизации Carbonio при инициализации приложения.""" + import apps.mail_connector.signals # noqa diff --git a/apps/mail_connector/carbonio_service.py b/apps/mail_connector/carbonio_service.py new file mode 100644 index 0000000..6f5d882 --- /dev/null +++ b/apps/mail_connector/carbonio_service.py @@ -0,0 +1,143 @@ +"""Клиент административного API Carbonio CE. + +Carbonio CE — форк Zimbra, у него совместимый Admin SOAP API на порту 6071 +(`/service/admin/soap`), понимающий JSON-конверты. Схема: AuthRequest → токен, +далее Create/Get/Modify/Delete AccountRequest с токеном в заголовке. + +Настройки берутся из ConnectorConfig(connector_type='carbonio'): +host, port (6071), use_ssl, username/password (admin, пароль шифруется), +extra_config: {"mail_domain": "...", "verify_ssl": false, "timeout": 30}. +""" +import logging + +import httpx + +logger = logging.getLogger(__name__) + + +class CarbonioConfigError(Exception): + """Коннектор Carbonio не настроен или отключён.""" + + +class CarbonioError(Exception): + """Ошибка вызова admin API Carbonio (SOAP Fault / сеть).""" + + +class CarbonioService: + ADMIN_NS = 'urn:zimbraAdmin' + CTX_NS = 'urn:zimbra' + + def __init__(self): + from core.models import ConnectorConfig + cfg = ConnectorConfig.objects.filter(connector_type='carbonio').first() + + if not cfg: + raise CarbonioConfigError( + 'Настройки коннектора Carbonio не найдены. ' + 'Добавьте запись в Core → Настройки коннекторов.' + ) + if not cfg.is_enabled: + raise CarbonioConfigError('Коннектор Carbonio отключён.') + if not cfg.host or not cfg.username or not cfg.password: + raise CarbonioConfigError( + 'Для коннектора Carbonio нужны host, username и password admin-аккаунта.' + ) + + extra = cfg.extra_config or {} + self.domain = extra.get('mail_domain', 'aks-apteka.com.ua') + self.username = cfg.username + self.password = cfg.password + + scheme = 'https' if cfg.use_ssl else 'http' + port = cfg.port or 6071 + self.endpoint = f'{scheme}://{cfg.host}:{port}/service/admin/soap' + + # На admin-порту Carbonio часто самоподписанный сертификат — по умолчанию + # проверку выключаем, но её можно включить через extra_config.verify_ssl. + verify = bool(extra.get('verify_ssl', False)) + self._client = httpx.Client(verify=verify, timeout=extra.get('timeout', 30)) + self._token = None + + # --- низкоуровневый вызов --- + + def _call(self, request: str, params: dict, authed: bool = True) -> dict: + envelope = {'Body': {request: {'_jsns': self.ADMIN_NS, **params}}} + if authed: + envelope['Header'] = { + 'context': {'_jsns': self.CTX_NS, 'authToken': self._auth_token()} + } + try: + resp = self._client.post(self.endpoint, json=envelope) + data = resp.json() + except (httpx.HTTPError, ValueError) as e: + logger.error(f'Carbonio API {request}: сетевая ошибка — {e}') + raise CarbonioError(str(e)) from e + + body = data.get('Body', {}) + if 'Fault' in body: + reason = (body['Fault'].get('Reason') or {}).get('Text', 'неизвестная ошибка') + raise CarbonioError(f'{request}: {reason}') + return body.get(request.replace('Request', 'Response'), {}) + + def _auth_token(self) -> str: + if self._token: + return self._token + resp = self._call( + 'AuthRequest', + {'name': self.username, 'password': self.password}, + authed=False, + ) + token = resp.get('authToken') + if isinstance(token, list): + token = token[0].get('_content') + elif isinstance(token, dict): + token = token.get('_content') + if not token: + raise CarbonioError('AuthRequest не вернул authToken') + self._token = token + return token + + # --- высокоуровневые операции --- + + def ping(self) -> bool: + """Read-only проверка коннекта и кредов (только авторизация).""" + return bool(self._auth_token()) + + def get_account(self, email: str) -> dict | None: + """Аккаунт по адресу или None, если не существует.""" + try: + resp = self._call('GetAccountRequest', + {'account': {'by': 'name', '_content': email}}) + except CarbonioError: + return None + accounts = resp.get('account') + if isinstance(accounts, list): + return accounts[0] if accounts else None + return accounts + + def create_account(self, email: str, password: str, display_name: str = '') -> str: + """Создать почтовый ящик, вернуть id аккаунта.""" + attrs = [] + if display_name: + attrs.append({'n': 'displayName', '_content': display_name}) + resp = self._call('CreateAccountRequest', + {'name': email, 'password': password, 'a': attrs}) + account = resp.get('account') + if isinstance(account, list): + account = account[0] if account else {} + account_id = (account or {}).get('id') + if not account_id: + raise CarbonioError('CreateAccountRequest не вернул id аккаунта') + return account_id + + def set_account_status(self, account_id: str, status: str) -> None: + """zimbraAccountStatus: active / closed / locked / maintenance.""" + self._call('ModifyAccountRequest', + {'id': account_id, + 'a': [{'n': 'zimbraAccountStatus', '_content': status}]}) + + def set_password(self, account_id: str, password: str) -> None: + self._call('SetPasswordRequest', {'id': account_id, 'newPassword': password}) + + def delete_account(self, account_id: str) -> None: + self._call('DeleteAccountRequest', {'id': account_id}) diff --git a/apps/mail_connector/models/__init__.py b/apps/mail_connector/management/__init__.py similarity index 100% rename from apps/mail_connector/models/__init__.py rename to apps/mail_connector/management/__init__.py diff --git a/apps/mail_connector/management/commands/__init__.py b/apps/mail_connector/management/commands/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/apps/mail_connector/management/commands/sync_pharmacies_to_carbonio.py b/apps/mail_connector/management/commands/sync_pharmacies_to_carbonio.py new file mode 100644 index 0000000..9fd7388 --- /dev/null +++ b/apps/mail_connector/management/commands/sync_pharmacies_to_carbonio.py @@ -0,0 +1,62 @@ +"""Массовая синхронизация почтовых ящиков аптек с Carbonio CE. + +По умолчанию dry-run: показывает, что будет сделано, ничего не меняя. +Реальная синхронизация — только с флагом --apply. +""" +from django.core.management.base import BaseCommand, CommandError + + +class Command(BaseCommand): + help = 'Синхронизировать почтовые ящики аптек с Carbonio (по умолчанию dry-run)' + + def add_arguments(self, parser): + parser.add_argument('--pharmacy-id', type=int, help='Только одна аптека по ID') + parser.add_argument('--all', action='store_true', + help='Все аптеки с включённой синхронизацией Carbonio') + parser.add_argument('--apply', action='store_true', + help='Реально выполнить (без флага — dry-run)') + + def handle(self, *args, **options): + from apps.locations.models import Pharmacy + from apps.mail_connector.carbonio_service import CarbonioConfigError, CarbonioService + from apps.mail_connector.services.sync_service import sync_pharmacy_to_carbonio + + if options['pharmacy_id']: + qs = Pharmacy.objects.filter(pk=options['pharmacy_id']) + elif options['all']: + qs = Pharmacy.objects.filter(carbonio_sync_enabled=True) + else: + raise CommandError('Укажите --pharmacy-id или --all') + + total = qs.count() + if not total: + self.stdout.write(self.style.WARNING('Нет аптек, подходящих под условие')) + return + + apply = options['apply'] + self.stdout.write( + f'{"ПРИМЕНЕНИЕ" if apply else "DRY-RUN"}: аптек к обработке — {total}') + + if apply: + # Ранняя проверка коннекта, чтобы не сыпать ошибками по каждой аптеке. + try: + CarbonioService().ping() + except CarbonioConfigError as e: + raise CommandError(str(e)) + + ok = failed = 0 + for pharmacy in qs.iterator(): + target = pharmacy.carbonio_email or pharmacy.build_carbonio_email() or '(нет адреса)' + if not apply: + self.stdout.write(f' [dry-run] аптека {pharmacy.pk} → {target}') + continue + if sync_pharmacy_to_carbonio(pharmacy): + ok += 1 + self.stdout.write(self.style.SUCCESS(f' ✓ {pharmacy.pk} → {pharmacy.carbonio_email}')) + else: + failed += 1 + self.stdout.write(self.style.ERROR( + f' ✗ {pharmacy.pk} → {pharmacy.carbonio_sync_error or "ошибка"}')) + + if apply: + self.stdout.write(f'Готово: успешно {ok}, ошибок {failed}') diff --git a/apps/mail_connector/migrations/0001_initial.py b/apps/mail_connector/migrations/0001_initial.py new file mode 100644 index 0000000..94cb3bd --- /dev/null +++ b/apps/mail_connector/migrations/0001_initial.py @@ -0,0 +1,35 @@ +# Generated by Django 6.0.3 on 2026-07-24 06:59 + +import django.db.models.deletion +import uuid +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('locations', '0008_pharmacy_carbonio_account_id_pharmacy_carbonio_email_and_more'), + ] + + operations = [ + migrations.CreateModel( + name='CarbonioSyncLog', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('uuid', models.UUIDField(default=uuid.uuid4, editable=False, unique=True)), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('action', models.CharField(choices=[('create', 'Создание'), ('update', 'Обновление'), ('activate', 'Включение'), ('deactivate', 'Отключение')], max_length=20, verbose_name='Действие')), + ('status', models.CharField(choices=[('success', 'Успех'), ('error', 'Ошибка')], max_length=10, verbose_name='Статус')), + ('message', models.TextField(blank=True, verbose_name='Сообщение')), + ('pharmacy', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='carbonio_sync_logs', to='locations.pharmacy', verbose_name='Аптека')), + ], + options={ + 'verbose_name': 'Запись синхронизации Carbonio', + 'verbose_name_plural': 'Журнал синхронизации Carbonio', + 'ordering': ['-created_at'], + }, + ), + ] diff --git a/apps/mail_connector/models.py b/apps/mail_connector/models.py index 71a8362..d2bcfdb 100644 --- a/apps/mail_connector/models.py +++ b/apps/mail_connector/models.py @@ -1,3 +1,35 @@ from django.db import models +from django.utils.translation import gettext_lazy as _ -# Create your models here. +from core.models import BaseModel + + +class CarbonioSyncLog(BaseModel): + """Журнал операций синхронизации почтовых ящиков аптек с Carbonio CE.""" + + pharmacy = models.ForeignKey( + 'locations.Pharmacy', on_delete=models.CASCADE, + related_name='carbonio_sync_logs', verbose_name=_('Аптека'), + ) + action = models.CharField( + _('Действие'), max_length=20, + choices=[ + ('create', _('Создание')), + ('update', _('Обновление')), + ('activate', _('Включение')), + ('deactivate', _('Отключение')), + ], + ) + status = models.CharField( + _('Статус'), max_length=10, + choices=[('success', _('Успех')), ('error', _('Ошибка'))], + ) + message = models.TextField(_('Сообщение'), blank=True) + + class Meta: + verbose_name = _('Запись синхронизации Carbonio') + verbose_name_plural = _('Журнал синхронизации Carbonio') + ordering = ['-created_at'] + + def __str__(self): + return f'{self.pharmacy} — {self.get_action_display()} — {self.get_status_display()}' diff --git a/apps/mail_connector/services/__init__.py b/apps/mail_connector/services/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/apps/mail_connector/services/sync_service.py b/apps/mail_connector/services/sync_service.py new file mode 100644 index 0000000..c2ab61c --- /dev/null +++ b/apps/mail_connector/services/sync_service.py @@ -0,0 +1,111 @@ +import logging +import secrets +import string + +from django.utils import timezone + +from apps.mail_connector.carbonio_service import ( + CarbonioService, CarbonioConfigError, CarbonioError, +) + +logger = logging.getLogger(__name__) + + +def generate_random_password(length: int = 16) -> str: + alphabet = string.ascii_letters + string.digits + '!@#$%*' + return ''.join(secrets.choice(alphabet) for _ in range(length)) + + +def _log(pharmacy, action: str, status: str, message: str = '') -> None: + from apps.mail_connector.models import CarbonioSyncLog + CarbonioSyncLog.objects.create( + pharmacy=pharmacy, action=action, status=status, message=message) + + +def _display_name(pharmacy) -> str: + org = pharmacy.organization.name if pharmacy.organization_id else '' + return f'Аптека №{pharmacy.pharmacy_number} {org}'.strip() + + +def _mark_error(pharmacy, message: str) -> None: + pharmacy.carbonio_sync_status = 'error' + pharmacy.carbonio_sync_error = message + pharmacy.carbonio_last_sync = timezone.now() + pharmacy.save(update_fields=[ + 'carbonio_sync_status', 'carbonio_sync_error', 'carbonio_last_sync']) + + +def sync_pharmacy_to_carbonio(pharmacy, password: str | None = None) -> bool: + """Создать (или переактивировать) почтовый ящик аптеки в Carbonio. + + Требует carbonio_email; если он пуст — пробуем сгенерировать из префикса + организации. Идемпотентна: если аккаунт уже есть — просто активируем и + сохраняем его id, повторно не создаём. + """ + if not pharmacy.carbonio_email: + pharmacy.carbonio_email = pharmacy.build_carbonio_email() + if not pharmacy.carbonio_email: + _mark_error(pharmacy, + 'Не задан адрес Carbonio и не удалось сгенерировать ' + '(нет mail_prefix у организации).') + return False + + try: + service = CarbonioService() + except CarbonioConfigError as e: + logger.error(f'Carbonio не настроен — синхронизация аптеки {pharmacy.pk} пропущена: {e}') + return False + + try: + existing = service.get_account(pharmacy.carbonio_email) + if existing: + account_id = existing['id'] + service.set_account_status(account_id, 'active') + action = 'update' + else: + account_id = service.create_account( + pharmacy.carbonio_email, + password or generate_random_password(), + _display_name(pharmacy), + ) + action = 'create' + + pharmacy.carbonio_account_id = account_id + pharmacy.carbonio_sync_status = 'synced' + pharmacy.carbonio_sync_error = '' + pharmacy.carbonio_last_sync = timezone.now() + pharmacy.save(update_fields=[ + 'carbonio_email', 'carbonio_account_id', 'carbonio_sync_status', + 'carbonio_sync_error', 'carbonio_last_sync']) + _log(pharmacy, action, 'success', f'{pharmacy.carbonio_email} → {account_id}') + logger.info(f'Carbonio: ящик {pharmacy.carbonio_email} синхронизирован ({action})') + return True + except CarbonioError as e: + _mark_error(pharmacy, str(e)) + _log(pharmacy, 'create', 'error', str(e)) + logger.error(f'Carbonio: ошибка синхронизации аптеки {pharmacy.pk}: {e}') + return False + + +def deactivate_pharmacy_carbonio(pharmacy) -> bool: + """Закрыть ящик аптеки (zimbraAccountStatus=closed) — например при закрытии аптеки.""" + if not pharmacy.carbonio_account_id: + return False + try: + service = CarbonioService() + except CarbonioConfigError as e: + logger.error(f'Carbonio не настроен — деактивация аптеки {pharmacy.pk} пропущена: {e}') + return False + + try: + service.set_account_status(pharmacy.carbonio_account_id, 'closed') + pharmacy.carbonio_sync_status = 'disabled' + pharmacy.carbonio_last_sync = timezone.now() + pharmacy.save(update_fields=['carbonio_sync_status', 'carbonio_last_sync']) + _log(pharmacy, 'deactivate', 'success', '') + logger.info(f'Carbonio: ящик аптеки {pharmacy.pk} закрыт') + return True + except CarbonioError as e: + _log(pharmacy, 'deactivate', 'error', str(e)) + logger.error(f'Carbonio: ошибка деактивации аптеки {pharmacy.pk}: {e}') + return False diff --git a/apps/mail_connector/signals.py b/apps/mail_connector/signals.py new file mode 100644 index 0000000..058117c --- /dev/null +++ b/apps/mail_connector/signals.py @@ -0,0 +1,64 @@ +"""Сигналы синхронизации почтовых ящиков аптек с 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, 'открытия ящика') diff --git a/apps/mail_connector/tasks.py b/apps/mail_connector/tasks.py new file mode 100644 index 0000000..4fe03c4 --- /dev/null +++ b/apps/mail_connector/tasks.py @@ -0,0 +1,31 @@ +import logging + +from celery import shared_task + +logger = logging.getLogger(__name__) + + +@shared_task +def sync_pharmacy_to_carbonio_task(pharmacy_id): + """Асинхронная синхронизация почтового ящика аптеки с Carbonio.""" + from apps.locations.models import Pharmacy + from apps.mail_connector.services.sync_service import sync_pharmacy_to_carbonio + try: + pharmacy = Pharmacy.objects.get(pk=pharmacy_id) + except Pharmacy.DoesNotExist: + logger.error(f'Carbonio: аптека с ID {pharmacy_id} не найдена') + return + sync_pharmacy_to_carbonio(pharmacy) + + +@shared_task +def deactivate_pharmacy_carbonio_task(pharmacy_id): + """Асинхронное закрытие почтового ящика аптеки в Carbonio.""" + from apps.locations.models import Pharmacy + from apps.mail_connector.services.sync_service import deactivate_pharmacy_carbonio + try: + pharmacy = Pharmacy.objects.get(pk=pharmacy_id) + except Pharmacy.DoesNotExist: + logger.error(f'Carbonio: аптека с ID {pharmacy_id} не найдена') + return + deactivate_pharmacy_carbonio(pharmacy)