diff --git a/config/settings/common.py b/config/settings/common.py index c5cb632..1aa8d1b 100644 --- a/config/settings/common.py +++ b/config/settings/common.py @@ -49,6 +49,7 @@ INSTALLED_APPS = [ "django.contrib.sessions", "django.contrib.messages", "django.contrib.staticfiles", + "django.contrib.postgres", ] + APPS MODULES = [app for app in MODULES if isinstance(app, str)] diff --git a/core/apps/vendors/migrations/0006_productattributemodel_productvariantmodel.py b/core/apps/vendors/migrations/0006_productattributemodel_productvariantmodel.py new file mode 100644 index 0000000..e6adc97 --- /dev/null +++ b/core/apps/vendors/migrations/0006_productattributemodel_productvariantmodel.py @@ -0,0 +1,63 @@ +# Generated by Django 6.0.4 on 2026-04-13 12:09 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("vendors", "0005_vendorproductmodel_product_specification"), + ] + + operations = [ + migrations.CreateModel( + name="ProductAttributeModel", + fields=[ + ("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), + ("created_at", models.DateTimeField(auto_now_add=True)), + ("updated_at", models.DateTimeField(auto_now=True)), + ( + "firestore_id", + models.CharField(blank=True, max_length=255, null=True, unique=True, verbose_name="firestore id"), + ), + ("name", models.CharField(max_length=255, verbose_name="name")), + ], + options={ + "verbose_name": "Product Attribute", + "verbose_name_plural": "Product Attributes", + "db_table": "product_attributes", + }, + ), + migrations.CreateModel( + name="ProductVariantModel", + fields=[ + ("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), + ("created_at", models.DateTimeField(auto_now_add=True)), + ("updated_at", models.DateTimeField(auto_now=True)), + ( + "firestore_id", + models.CharField(blank=True, max_length=255, null=True, unique=True, verbose_name="firestore id"), + ), + ("price", models.DecimalField(decimal_places=2, default=0, max_digits=12, verbose_name="price")), + ("sku", models.CharField(blank=True, max_length=255, null=True, verbose_name="SKU")), + ("quantity", models.IntegerField(default=-1, verbose_name="quantity")), + ("image_url", models.URLField(blank=True, max_length=1000, null=True, verbose_name="image url")), + ("attribute_data", models.JSONField(blank=True, null=True, verbose_name="attribute data")), + ( + "product", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="variants", + to="vendors.vendorproductmodel", + verbose_name="product", + ), + ), + ], + options={ + "verbose_name": "Product Variant", + "verbose_name_plural": "Product Variants", + "db_table": "product_variants", + }, + ), + ] diff --git a/core/apps/vendors/models/__init__.py b/core/apps/vendors/models/__init__.py index 8dc7430..ef58f97 100644 --- a/core/apps/vendors/models/__init__.py +++ b/core/apps/vendors/models/__init__.py @@ -2,3 +2,5 @@ from .category import * # noqa from .vendor import * # noqa from .vendor_product import * # noqa from .section import * # noqa +from .product_attribute import * # noqa +from .product_variant import * # noqa diff --git a/core/apps/vendors/models/product_attribute.py b/core/apps/vendors/models/product_attribute.py new file mode 100644 index 0000000..5481569 --- /dev/null +++ b/core/apps/vendors/models/product_attribute.py @@ -0,0 +1,16 @@ +from django.db import models +from django.utils.translation import gettext_lazy as _ +from django_core.models import AbstractBaseModel + + +class ProductAttributeModel(AbstractBaseModel): + firestore_id = models.CharField(verbose_name=_("firestore id"), max_length=255, unique=True, null=True, blank=True) + name = models.CharField(verbose_name=_("name"), max_length=255) + + def __str__(self): + return self.name + + class Meta: + db_table = "product_attributes" + verbose_name = _("Product Attribute") + verbose_name_plural = _("Product Attributes") diff --git a/core/apps/vendors/models/product_variant.py b/core/apps/vendors/models/product_variant.py new file mode 100644 index 0000000..20566f8 --- /dev/null +++ b/core/apps/vendors/models/product_variant.py @@ -0,0 +1,26 @@ +from django.db import models +from django.utils.translation import gettext_lazy as _ +from django_core.models import AbstractBaseModel + + +class ProductVariantModel(AbstractBaseModel): + product = models.ForeignKey( + "VendorproductModel", + verbose_name=_("product"), + on_delete=models.CASCADE, + related_name="variants" + ) + firestore_id = models.CharField(verbose_name=_("firestore id"), max_length=255, unique=True, null=True, blank=True) + price = models.DecimalField(verbose_name=_("price"), max_digits=12, decimal_places=2, default=0) + sku = models.CharField(verbose_name=_("SKU"), max_length=255, null=True, blank=True) + quantity = models.IntegerField(verbose_name=_("quantity"), default=-1) + image_url = models.URLField(verbose_name=_("image url"), max_length=1000, null=True, blank=True) + attribute_data = models.JSONField(verbose_name=_("attribute data"), null=True, blank=True) + + def __str__(self): + return f"Variant {self.sku} for {self.product.name}" + + class Meta: + db_table = "product_variants" + verbose_name = _("Product Variant") + verbose_name_plural = _("Product Variants") diff --git a/scratch/inspect_attributes.py b/scratch/inspect_attributes.py new file mode 100644 index 0000000..1860f86 --- /dev/null +++ b/scratch/inspect_attributes.py @@ -0,0 +1,42 @@ +import firebase_admin +from firebase_admin import credentials +from firebase_admin import firestore +import json +import os + +cert_path = "fondexuzb-firebase-adminsdk-fbsvc-7b0e2d6200.json" + +if not os.path.exists(cert_path): + print(f"Xatolik: {cert_path} fayli topilmadi!") + exit(1) + +if not firebase_admin._apps: + cred = credentials.Certificate(cert_path) + firebase_admin.initialize_app(cred) + +db = firestore.client() + +def inspect_products(): + print(f"\n--- vendor_products kolleksiyasini tekshirish ---") + + docs = db.collection('vendor_products').limit(5).stream() + + class DateTimeEncoder(json.JSONEncoder): + def default(self, obj): + if hasattr(obj, 'isoformat'): + return obj.isoformat() + return str(obj) + + for doc in docs: + data = doc.to_dict() + print(f"Hujjat ID: {doc.id}") + # Print only keys that might be relevant to attributes + for key, value in data.items(): + if 'attr' in key.lower() or 'spec' in key.lower(): + print(f" {key}: {value}") + # Print everything for the first one to be sure + print(json.dumps(data, indent=4, ensure_ascii=False, cls=DateTimeEncoder)) + print("-" * 20) + +if __name__ == "__main__": + inspect_products() diff --git a/sync_firebase_products.py b/sync_firebase_products.py index 4543403..08aeb04 100644 --- a/sync_firebase_products.py +++ b/sync_firebase_products.py @@ -40,6 +40,8 @@ from core.apps.vendors.models import ( VendorproductModel, CategoryModel, SectionModel, + ProductAttributeModel, + ProductVariantModel, ) # ── Firebase ulanish ───────────────────────────────────────────────────────── @@ -141,6 +143,23 @@ def print_progress(current, total, created, updated, skipped, errors): print(line, end="", flush=True) +def sync_attributes(): + print("Firebase vendor_attributes kolleksiyasi o'qilmoqda…") + docs = db.collection("vendor_attributes").stream() + count = 0 + for doc in docs: + data = doc.to_dict() + attr_id = data.get("id") or doc.id + name = data.get("title") or "Unnamed" + + attr, created = ProductAttributeModel.objects.update_or_create( + firestore_id=attr_id, + defaults={"name": name} + ) + count += 1 + print(f"Jami {count} ta atribut sinxronlandi.\n") + + # ── Asosiy funksiya ─────────────────────────────────────────────────────────── def sync_products(dry_run: bool = False, update: bool = False, after: datetime | None = None): @@ -193,16 +212,43 @@ def sync_products(dry_run: bool = False, update: bool = False, after: datetime | exists.section = section if not dry_run: exists.save() + product_obj = exists updated += 1 else: + product_obj = None if not dry_run: - VendorproductModel.objects.create( + product_obj = VendorproductModel.objects.create( **product_data, category=category, section=section, ) created += 1 + # ── Variantlarni qayta ishlash ──────────────────────────────────── + + item_attr = data.get("item_attribute") + if item_attr and isinstance(item_attr, dict) and not dry_run: + # Agar product_obj yaratilgan bo'lsa (yoki mavjud bo'lsa) + target_product = product_obj or exists + if target_product: + variants_data = item_attr.get("variants") or [] + for v_data in variants_data: + v_id = v_data.get("variant_id") + if not v_id: + continue + + ProductVariantModel.objects.update_or_create( + firestore_id=v_id, + defaults={ + "product": target_product, + "price": to_decimal(v_data.get("variant_price", 0)), + "sku": v_data.get("variant_sku"), + "quantity": to_int(v_data.get("variant_quantity", -1)), + "image_url": v_data.get("variant_image"), + "attribute_data": item_attr.get("attributes") + } + ) + except Exception as e: print(f"\n [XATO] {doc_id}: {e}") errors += 1 @@ -251,4 +297,5 @@ if __name__ == "__main__": print(f"[XATO] {e}") sys.exit(1) + sync_attributes() sync_products(dry_run=args.dry_run, update=args.update, after=after_dt)