from datetime import timedelta from django.utils import timezone from drf_spectacular.utils import extend_schema_field from rest_framework import serializers from commerce.models import ( Cost, IntegrationConnection, IntegrationProvider, IntegrationSyncRun, Product, ProductBinding, ProductImage, Sale, SaleLine, SourceSystem, StockSnapshot, ) from commerce.services.credentials import encrypt_credentials class SourceSystemSerializer(serializers.ModelSerializer): class Meta: model = SourceSystem fields = [ "id", "code", "name", "is_active", "metadata", "created_at", "updated_at", ] read_only_fields = ["id", "created_at", "updated_at"] class IntegrationSyncRunSerializer(serializers.ModelSerializer): class Meta: model = IntegrationSyncRun fields = [ "id", "connection", "status", "task_id", "stage", "stage_processed", "stage_total", "last_progress_at", "processed", "created_products", "matched_products", "stock_snapshots", "images", "image_errors", "sales", "costs", "error", "started_at", "finished_at", "created_at", ] read_only_fields = fields class IntegrationConnectionSerializer(serializers.ModelSerializer): provider_name = serializers.CharField(source="get_provider_display", read_only=True) source = SourceSystemSerializer(read_only=True) credentials_configured = serializers.SerializerMethodField() token = serializers.CharField(write_only=True, required=False, allow_blank=False) login = serializers.CharField(write_only=True, required=False, allow_blank=False) password = serializers.CharField( write_only=True, required=False, allow_blank=False, trim_whitespace=False ) recent_runs = serializers.SerializerMethodField() class Meta: model = IntegrationConnection fields = [ "id", "source", "provider", "provider_name", "auth_type", "credentials_configured", "token", "login", "password", "status", "account_name", "last_error", "last_tested_at", "last_sync_started_at", "last_sync_finished_at", "last_stats", "task_id", "auto_sync", "sync_interval_minutes", "next_sync_at", "recent_runs", "created_at", "updated_at", ] read_only_fields = [ "status", "account_name", "last_error", "last_tested_at", "last_sync_started_at", "last_sync_finished_at", "last_stats", "task_id", "next_sync_at", "created_at", "updated_at", ] validators = [] def get_credentials_configured(self, obj) -> bool: return bool(obj.encrypted_credentials) @extend_schema_field(IntegrationSyncRunSerializer(many=True)) def get_recent_runs(self, obj): return IntegrationSyncRunSerializer(obj.sync_runs.all()[:5], many=True).data def validate_provider(self, value): if value not in { IntegrationProvider.MOYSKLAD, IntegrationProvider.WILDBERRIES, }: raise serializers.ValidationError("Этот сервис пока не поддерживается.") if self.instance and value != self.instance.provider: raise serializers.ValidationError("Провайдер подключения изменить нельзя.") return value def validate(self, attrs): auth_type = attrs.get("auth_type", getattr(self.instance, "auth_type", "token")) provider = attrs.get("provider", getattr(self.instance, "provider", None)) if provider == IntegrationProvider.WILDBERRIES and auth_type != "token": raise serializers.ValidationError( { "auth_type": "Wildberries поддерживает авторизацию только по API-токену." } ) has_new_credentials = any( key in attrs for key in ("token", "login", "password") ) if self.instance is None or has_new_credentials: if auth_type == "token" and not attrs.get("token"): raise serializers.ValidationError({"token": "Укажите токен."}) if auth_type == "basic": errors = {} if not attrs.get("login"): errors["login"] = "Укажите логин." if not attrs.get("password"): errors["password"] = "Укажите пароль." if errors: raise serializers.ValidationError(errors) return attrs def _credentials(self, validated_data): auth_type = validated_data.get( "auth_type", getattr(self.instance, "auth_type", "token") ) if auth_type == "token": token = validated_data.pop("token", None) validated_data.pop("login", None) validated_data.pop("password", None) return {"token": token} if token else None login = validated_data.pop("login", None) password = validated_data.pop("password", None) validated_data.pop("token", None) return {"login": login, "password": password} if login and password else None def create(self, validated_data): credentials = self._credentials(validated_data) provider = validated_data["provider"] if IntegrationConnection.objects.filter(provider=provider).exists(): raise serializers.ValidationError( {"provider": "Этот сервис уже подключён."} ) source, _ = SourceSystem.objects.get_or_create( code=provider, defaults={"name": IntegrationProvider(provider).label}, ) if provider == IntegrationProvider.WILDBERRIES: validated_data.setdefault("sync_interval_minutes", 60) return IntegrationConnection.objects.create( source=source, created_by=self.context["request"].user, encrypted_credentials=encrypt_credentials(credentials or {}), **validated_data, ) def update(self, instance, validated_data): previous_auth_type = instance.auth_type credentials = self._credentials(validated_data) if credentials: instance.encrypted_credentials = encrypt_credentials(credentials) for field, value in validated_data.items(): setattr(instance, field, value) credentials_changed = credentials or instance.auth_type != previous_auth_type if credentials_changed: instance.status = "disconnected" instance.last_error = "" if "auto_sync" in validated_data or "sync_interval_minutes" in validated_data: instance.next_sync_at = ( timezone.now() + timedelta(minutes=instance.sync_interval_minutes) if instance.auto_sync else None ) instance.save() return instance class ProductBindingSerializer(serializers.ModelSerializer): source_code = serializers.CharField(source="source.code", read_only=True) class Meta: model = ProductBinding fields = [ "id", "product", "source", "source_code", "external_id", "external_sku", "metadata", "is_primary", "created_at", "updated_at", ] read_only_fields = ["id", "created_at", "updated_at"] class ProductImageSerializer(serializers.ModelSerializer): url = serializers.ImageField(source="file", read_only=True) thumbnail_url = serializers.ImageField(source="thumbnail", read_only=True) class Meta: model = ProductImage fields = [ "id", "url", "thumbnail_url", "filename", "title", "source_size", "position", "is_primary", "source_updated_at", ] class ProductSerializer(serializers.ModelSerializer): bindings = ProductBindingSerializer(many=True, read_only=True) images = ProductImageSerializer(many=True, read_only=True) variants_count = serializers.IntegerField(read_only=True) class Meta: model = Product fields = [ "id", "parent", "kind", "sku", "name", "barcode", "description", "is_active", "variants_count", "bindings", "images", "created_at", "updated_at", ] read_only_fields = ["id", "created_at", "updated_at"] class ProductMergeSerializer(serializers.Serializer): source_product_ids = serializers.PrimaryKeyRelatedField( queryset=Product.objects.all(), many=True, source="source_products", ) def validate_source_product_ids(self, value): target = self.context["target"] filtered = [product for product in value if product.pk != target.pk] if not filtered: raise serializers.ValidationError("Выберите хотя бы один другой товар.") return filtered class SaleLineSerializer(serializers.ModelSerializer): net_amount = serializers.DecimalField( max_digits=18, decimal_places=2, read_only=True ) class Meta: model = SaleLine fields = [ "id", "sale", "product", "external_id", "external_product_id", "external_sku", "name", "quantity", "gross_amount", "discount_amount", "net_amount", "cost_amount", "metadata", ] def validate(self, attrs): gross = attrs.get("gross_amount", getattr(self.instance, "gross_amount", 0)) discount = attrs.get( "discount_amount", getattr(self.instance, "discount_amount", 0) ) if discount > gross: raise serializers.ValidationError( {"discount_amount": "Скидка не может превышать сумму позиции."} ) return attrs class SaleSerializer(serializers.ModelSerializer): source_code = serializers.CharField(source="source.code", read_only=True) lines = SaleLineSerializer(many=True, read_only=True) def validate(self, attrs): project = attrs.get("project", getattr(self.instance, "project", None)) source = attrs.get("source", getattr(self.instance, "source", None)) if project and source and project.source_id != source.pk: raise serializers.ValidationError( {"project": "Проект относится к другому источнику."} ) return attrs class Meta: model = Sale fields = [ "id", "source", "source_code", "source_record", "external_id", "number", "event_type", "channel", "customer_external_id", "customer_name", "project", "status", "counts_as_sale", "currency", "occurred_at", "source_updated_at", "lines", "created_at", "updated_at", ] class CostSerializer(serializers.ModelSerializer): source_code = serializers.CharField(source="source.code", read_only=True) class Meta: model = Cost fields = [ "id", "source", "source_code", "product", "sale_line", "project", "external_id", "channel", "cost_type", "amount", "currency", "occurred_at", "metadata", "created_at", ] validators = [] def validate(self, attrs): product = attrs.get("product", getattr(self.instance, "product", None)) sale_line = attrs.get("sale_line", getattr(self.instance, "sale_line", None)) project = attrs.get("project", getattr(self.instance, "project", None)) source = attrs.get("source", getattr(self.instance, "source", None)) if project and source and project.source_id != source.pk: raise serializers.ValidationError( {"project": "Проект относится к другому источнику."} ) if ( project and sale_line and sale_line.sale.project_id not in [None, project.pk] ): raise serializers.ValidationError( {"project": "Проект не совпадает с проектом продажи."} ) if product is None and sale_line is None and project is None: raise serializers.ValidationError( "Нужно указать товар, позицию продажи или проект." ) external_id = attrs.get( "external_id", getattr(self.instance, "external_id", "") ) if external_id: source = attrs.get("source", getattr(self.instance, "source", None)) cost_type = attrs.get( "cost_type", getattr(self.instance, "cost_type", None) ) duplicate = Cost.objects.filter( source=source, external_id=external_id, cost_type=cost_type, ) if self.instance: duplicate = duplicate.exclude(pk=self.instance.pk) if duplicate.exists(): raise serializers.ValidationError( {"external_id": "Расход с таким внешним ID уже существует."} ) return attrs class StockSnapshotSerializer(serializers.ModelSerializer): source_code = serializers.CharField(source="source.code", read_only=True) available = serializers.DecimalField( max_digits=18, decimal_places=4, read_only=True ) class Meta: model = StockSnapshot fields = [ "id", "product", "source", "source_code", "warehouse_external_id", "warehouse_name", "quantity", "reserved", "available", "in_transit", "captured_at", "metadata", "created_at", ] class ProductMetricsQuerySerializer(serializers.Serializer): date_from = serializers.DateTimeField(required=False) date_to = serializers.DateTimeField(required=False) def validate(self, attrs): if ( attrs.get("date_from") and attrs.get("date_to") and attrs["date_from"] > attrs["date_to"] ): raise serializers.ValidationError( {"date_to": "Дата окончания должна быть не раньше даты начала."} ) return attrs