diff --git a/core/apps/vendors/management/__init__.py b/core/apps/vendors/management/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/core/apps/vendors/management/commands/__init__.py b/core/apps/vendors/management/commands/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/core/apps/vendors/management/commands/sync_firebase_products.py b/core/apps/vendors/management/commands/sync_firebase_products.py new file mode 100644 index 0000000..ea5659f --- /dev/null +++ b/core/apps/vendors/management/commands/sync_firebase_products.py @@ -0,0 +1,183 @@ +""" +Django management command: Firebase → DB mahsulot sync. + +Ishlatish: + python manage.py sync_firebase_products + python manage.py sync_firebase_products --dry-run + python manage.py sync_firebase_products --update + +Docker orqali: + docker compose exec web python manage.py sync_firebase_products +""" + +import os +import json +from decimal import Decimal, InvalidOperation + +import firebase_admin +from firebase_admin import credentials, firestore +from django.core.management.base import BaseCommand + +from core.apps.vendors.models import VendorproductModel, CategoryModel, SectionModel + +CERT_PATH = os.path.join( + os.path.dirname(os.path.abspath(__file__)), + "../../../../..", # project root + "fondexuzb-firebase-adminsdk-fbsvc-7b0e2d6200.json", +) + + +def _init_firebase(): + if firebase_admin._apps: + return firestore.client() + cert = os.path.normpath(CERT_PATH) + if not os.path.exists(cert): + raise FileNotFoundError(f"Firebase credentials topilmadi: {cert}") + cred = credentials.Certificate(cert) + firebase_admin.initialize_app(cred) + return firestore.client() + + +def _to_decimal(value, default=Decimal("0")): + try: + return Decimal(str(value)).quantize(Decimal("0.01")) + except (InvalidOperation, TypeError): + return default + + +def _to_int(value, default=-1): + try: + return int(value) + except (TypeError, ValueError): + return default + + +def _parse_spec(spec): + """product_specification ni dict'ga parse qiladi.""" + if isinstance(spec, dict) and spec: + return spec + if isinstance(spec, str): + try: + parsed = json.loads(spec) + if isinstance(parsed, dict) and parsed: + return parsed + except Exception: + pass + return None + + +class Command(BaseCommand): + help = "Firebase vendor_products → Django DB sync" + + def add_arguments(self, parser): + parser.add_argument( + "--dry-run", + action="store_true", + help="Haqiqatda DB'ga yozmaydi", + ) + parser.add_argument( + "--update", + action="store_true", + help="Mavjud mahsulotlarni ham yangilaydi", + ) + + def handle(self, *args, **options): + dry_run = options["dry_run"] + update = options["update"] + + try: + db = _init_firebase() + except FileNotFoundError as e: + self.stderr.write(self.style.ERROR(str(e))) + return + + self.stdout.write("Firebase vendor_products o'qilmoqda…") + docs = list(db.collection("vendor_products").stream()) + self.stdout.write(f"Jami Firebase'da: {len(docs)} ta mahsulot\n") + + created = updated = skipped = errors = 0 + total = len(docs) + + for i, doc in enumerate(docs, start=1): + doc_id = doc.id + data = doc.to_dict() or {} + + if not data.get("name"): + skipped += 1 + self._print_progress(i, total, created, updated, skipped, errors) + continue + + try: + name = (data.get("name") or "").strip() or "—" + spec = _parse_spec(data.get("product_specification")) + + product_fields = dict( + vendor=data.get("vendorID") or data.get("vendor") or "", + name=name, + description=data.get("description") or "", + price=_to_decimal(data.get("price", 0)), + discount_price=_to_decimal(data.get("disPrice", 0)), + quantity=_to_int(data.get("quantity", -1)), + is_publish=bool(data.get("publish", True)), + photos_json=data.get("photos") or None, + product_specification=spec, + ) + + category = CategoryModel.objects.filter( + firestore_id=data.get("categoryID", "") + ).first() + section = SectionModel.objects.filter( + firestore_id=data.get("section_id", "") + ).first() + + existing = VendorproductModel.objects.filter(firestore_id=doc_id).first() + + if existing and not update: + skipped += 1 + self._print_progress(i, total, created, updated, skipped, errors) + continue + + if existing: + for field, val in product_fields.items(): + setattr(existing, field, val) + existing.category = category + existing.section = section + if not dry_run: + existing.save() + updated += 1 + else: + if not dry_run: + VendorproductModel.objects.create( + firestore_id=doc_id, + category=category, + section=section, + **product_fields, + ) + created += 1 + + except Exception as e: + self.stderr.write(f"\n [XATO] {doc_id}: {e}") + errors += 1 + + self._print_progress(i, total, created, updated, skipped, errors) + + self.stdout.write("\n\n" + "=" * 50) + self.stdout.write(self.style.SUCCESS(f"Yaratildi : {created}")) + if update: + self.stdout.write(self.style.SUCCESS(f"Yangilandi: {updated}")) + self.stdout.write(f"O'tkazildi: {skipped}") + if errors: + self.stdout.write(self.style.ERROR(f"Xatolar : {errors}")) + if dry_run: + self.stdout.write(self.style.WARNING("\n[DRY-RUN] Hech narsa saqlanmadi.")) + + def _print_progress(self, current, total, created, updated, skipped, errors): + percent = int(current / total * 100) if total else 0 + bar_filled = percent // 5 + bar = "█" * bar_filled + "░" * (20 - bar_filled) + line = ( + f"\r [{bar}] {percent:3d}% " + f"{current}/{total} | " + f"✓ {created} ↑ {updated} — {skipped} ✗ {errors}" + ) + self.stdout.write(line, ending="") diff --git a/core/apps/vendors/migrations/0005_vendorproductmodel_product_specification.py b/core/apps/vendors/migrations/0005_vendorproductmodel_product_specification.py new file mode 100644 index 0000000..543c577 --- /dev/null +++ b/core/apps/vendors/migrations/0005_vendorproductmodel_product_specification.py @@ -0,0 +1,16 @@ +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('vendors', '0004_rename_vendor_id_str_vendorproductmodel_vendor'), + ] + + operations = [ + migrations.AddField( + model_name='vendorproductmodel', + name='product_specification', + field=models.JSONField(blank=True, null=True, verbose_name='product specification'), + ), + ] diff --git a/core/apps/vendors/models/vendor_product.py b/core/apps/vendors/models/vendor_product.py index bd3ff03..1d9b690 100644 --- a/core/apps/vendors/models/vendor_product.py +++ b/core/apps/vendors/models/vendor_product.py @@ -17,6 +17,7 @@ class VendorproductModel(AbstractBaseModel): is_publish = models.BooleanField(verbose_name=_("is publish"), default=True) image = models.ImageField(verbose_name=_("image"), upload_to="products/", null=True, blank=True) photos_json = models.JSONField(verbose_name=_("photos url list"), null=True, blank=True) + product_specification = models.JSONField(verbose_name=_("product specification"), null=True, blank=True) def __str__(self): return self.name diff --git a/sync_firebase_products.py b/sync_firebase_products.py new file mode 100644 index 0000000..004e5be --- /dev/null +++ b/sync_firebase_products.py @@ -0,0 +1,199 @@ +""" +Firebase → Django mahsulotlarni sinxronlashtirish skripti. + +Ishlatish: + python sync_firebase_products.py + python sync_firebase_products.py --dry-run # haqiqatda saqlamaydi + python sync_firebase_products.py --update # mavjudlarni ham yangilaydi + +Logika: + - firestore_id bo'yicha tekshiradi + - Mavjud bo'lsa — o'tkazib ketadi (--update bilan yangilaydi) + - Yangi bo'lsa — create qiladi +""" + +import os +import sys +import argparse +import django +import firebase_admin +from firebase_admin import credentials, firestore +from decimal import Decimal, InvalidOperation + +# ── Django sozlash ─────────────────────────────────────────────────────────── +BASE_DIR = os.path.dirname(os.path.abspath(__file__)) +sys.path.insert(0, BASE_DIR) +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings.local") + +try: + django.setup() +except Exception as e: + print(f"[XATO] Django setup: {e}") + sys.exit(1) + +from core.apps.vendors.models import ( + VendorproductModel, + CategoryModel, + SectionModel, +) + +# ── Firebase ulanish ───────────────────────────────────────────────────────── +CERT_PATH = os.path.join(BASE_DIR, "fondexuzb-firebase-adminsdk-fbsvc-7b0e2d6200.json") + +if not os.path.exists(CERT_PATH): + print(f"[XATO] Firebase credentials topilmadi: {CERT_PATH}") + sys.exit(1) + +if not firebase_admin._apps: + cred = credentials.Certificate(CERT_PATH) + firebase_admin.initialize_app(cred) + +db = firestore.client() + + +# ── Yordamchi funksiyalar ──────────────────────────────────────────────────── + +def to_decimal(value, default=Decimal("0")): + """Har qanday qiymatni Decimal'ga o'tkazadi.""" + try: + return Decimal(str(value)).quantize(Decimal("0.01")) + except (InvalidOperation, TypeError): + return default + + +def to_int(value, default=-1): + try: + return int(value) + except (TypeError, ValueError): + return default + + +def get_category(firestore_id: str): + """CategoryModel'ni firestore_id bo'yicha topadi.""" + if not firestore_id: + return None + return CategoryModel.objects.filter(firestore_id=firestore_id).first() + + +def get_section(firestore_id: str): + """SectionModel'ni firestore_id bo'yicha topadi.""" + if not firestore_id: + return None + return SectionModel.objects.filter(firestore_id=firestore_id).first() + + +def build_product_data(doc_id: str, data: dict) -> dict: + """Firebase hujjatidan Django model uchun dict yasaydi.""" + + # product_specification: {"Og'irlik": "1 kg", "Hajm": "500 ml"} kabi + spec = data.get("product_specification") + if isinstance(spec, str): + # Ba'zida JSON string sifatida keladi + import json + try: + spec = json.loads(spec) + except Exception: + spec = None + if not isinstance(spec, dict) or not spec: + spec = None + + return dict( + firestore_id=doc_id, + vendor=data.get("vendorID") or data.get("vendor") or "", + name=(data.get("name") or "").strip() or "—", + description=data.get("description") or "", + price=to_decimal(data.get("price", 0)), + discount_price=to_decimal(data.get("disPrice", 0)), + quantity=to_int(data.get("quantity", -1)), + is_publish=bool(data.get("publish", True)), + photos_json=data.get("photos") or None, + product_specification=spec, + # ForeignKey'lar ayrida to'ldiriladi + _category_id=data.get("categoryID") or "", + _section_id=data.get("section_id") or "", + ) + + +def sync_products(dry_run: bool = False, update: bool = False): + print("Firebase vendor_products kolleksiyasi o'qilmoqda…") + docs = list(db.collection("vendor_products").stream()) + print(f"Jami Firebase'da: {len(docs)} ta mahsulot\n") + + created = 0 + updated = 0 + skipped = 0 + errors = 0 + + for doc in docs: + doc_id = doc.id + data = doc.to_dict() or {} + + if not data.get("name"): + print(f" [O'TKAZILDI] {doc_id} — nomi yo'q") + skipped += 1 + continue + + try: + product_data = build_product_data(doc_id, data) + category_fid = product_data.pop("_category_id") + section_fid = product_data.pop("_section_id") + + exists = VendorproductModel.objects.filter(firestore_id=doc_id).first() + + if exists and not update: + skipped += 1 + continue + + # ForeignKey topish + category = get_category(category_fid) + section = get_section(section_fid) + + if exists and update: + for field, val in product_data.items(): + setattr(exists, field, val) + exists.category = category + exists.section = section + if not dry_run: + exists.save() + print(f" [YANGILANDI] {doc_id} — {product_data['name']}") + updated += 1 + else: + if not dry_run: + VendorproductModel.objects.create( + **product_data, + category=category, + section=section, + ) + print(f" [YARATILDI] {doc_id} — {product_data['name']}") + created += 1 + + except Exception as e: + print(f" [XATO] {doc_id}: {e}") + errors += 1 + + print("\n" + "=" * 50) + print(f"Yaratildi : {created}") + print(f"Yangilandi: {updated}") + print(f"O'tkazildi: {skipped}") + print(f"Xatolar : {errors}") + if dry_run: + print("\n[DRY-RUN] Hech narsa saqlanmadi.") + + +# ── Entry point ─────────────────────────────────────────────────────────────── + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description="Firebase → Django mahsulot sync") + parser.add_argument( + "--dry-run", + action="store_true", + help="Haqiqatda DB'ga yozmaydi, faqat natijalari ko'rsatadi", + ) + parser.add_argument( + "--update", + action="store_true", + help="Mavjud mahsulotlarni ham yangilaydi (default: o'tkazib ketadi)", + ) + args = parser.parse_args() + + sync_products(dry_run=args.dry_run, update=args.update)