""" Firebase → Django mahsulotlarni sinxronlashtirish skripti. Ishlatish: python sync_firebase_products.py python sync_firebase_products.py --dry-run python sync_firebase_products.py --update python sync_firebase_products.py --after "2024-01-15" python sync_firebase_products.py --after "2024-01-15 10:30:00" Logika: - firestore_id bo'yicha tekshiradi - Mavjud bo'lsa — o'tkazib ketadi (--update bilan yangilaydi) - Yangi bo'lsa — create qiladi - --after bilan faqat o'sha vaqtdan keyin qo'shilganlarni oladi """ import os import sys import json import argparse import django import firebase_admin from firebase_admin import credentials, firestore from decimal import Decimal, InvalidOperation from datetime import datetime, timezone import requests from django.core.files.base import ContentFile # ── 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, ProductAttributeModel, ProductVariantModel, ) # ── 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")): 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_after_date(date_str: str) -> datetime: """'2024-01-15' yoki '2024-01-15 10:30:00' ni timezone-aware datetime'ga o'tkazadi.""" for fmt in ("%Y-%m-%d %H:%M:%S", "%Y-%m-%d"): try: dt = datetime.strptime(date_str, fmt) return dt.replace(tzinfo=timezone.utc) except ValueError: continue raise ValueError( f"Noto'g'ri sana formati: '{date_str}'\n" "To'g'ri formatlar: '2024-01-15' yoki '2024-01-15 10:30:00'" ) def get_category(firestore_id: str): if not firestore_id: return None return CategoryModel.objects.filter(firestore_id=firestore_id).first() def get_section(firestore_id: str): if not firestore_id: return None return SectionModel.objects.filter(firestore_id=firestore_id).first() def parse_spec(spec): """product_specification ni dict'ga o'tkazadi.""" 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 def build_product_data(doc_id: str, data: dict) -> dict: 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=parse_spec(data.get("product_specification")), _category_id=data.get("categoryID") or "", _section_id=data.get("section_id") or "", ) print(line, end="", flush=True) def download_and_save_image(url: str, model_instance, field_name: str, filename: str): """Rasmni yuklab oladi va modelning ko'rsatilgan maydoniga saqlaydi.""" if not url or not url.startswith("http"): return False try: response = requests.get(url, timeout=10) if response.status_code == 200: content = ContentFile(response.content) field = getattr(model_instance, field_name) # S3'da duplicatelarning oldini olish uchun save(name, content, save=True) field.save(filename, content, save=True) return True except Exception as e: print(f"\n [IMG XATO] {url}: {e}") return False 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): print("Firebase vendor_products kolleksiyasi o'qilmoqda…") query = db.collection("vendor_products") if after: # Firestore'da createdAt field bo'yicha server tomonida filter query = query.where("createdAt", ">=", after) print(f"Filter: createdAt >= {after.strftime('%Y-%m-%d %H:%M:%S')} (UTC)") docs = list(query.stream()) total = len(docs) print(f"Jami Firebase'da: {total} ta mahsulot\n") if total == 0: print("Sinxronlash uchun mahsulot topilmadi.") return created = updated = skipped = errors = 0 for i, doc in enumerate(docs, start=1): doc_id = doc.id data = doc.to_dict() or {} if not data.get("name"): skipped += 1 print_progress(i, total, created, updated, skipped, errors) 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 print_progress(i, total, created, updated, skipped, errors) continue 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() product_obj = exists updated += 1 else: product_obj = None if not dry_run: product_obj = VendorproductModel.objects.create( **product_data, category=category, section=section, ) created += 1 # ── Rasmni yuklab olish (Asosiy mahsulot uchun) ──────────────────── if not dry_run and product_obj: # Agar asosiy rasm bo'lmasa, uni yuklab olamiz if not product_obj.image and product_data.get("photos_json"): first_photo = product_data["photos_json"][0] if isinstance(first_photo, str): ext = first_photo.split(".")[-1].split("?")[0] or "jpg" download_and_save_image( first_photo, product_obj, "image", f"product_{doc_id}.{ext}" ) # ── 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)), "attribute_data": item_attr.get("attributes") } ) # Variant rasm yuklash v_obj = ProductVariantModel.objects.filter(firestore_id=v_id).first() if v_obj and not v_obj.image and v_data.get("variant_image"): v_img_url = v_data["variant_image"] ext = v_img_url.split(".")[-1].split("?")[0] or "jpg" download_and_save_image( v_img_url, v_obj, "image", f"variant_{v_id}.{ext}" ) except Exception as e: print(f"\n [XATO] {doc_id}: {e}") errors += 1 print_progress(i, total, created, updated, skipped, errors) print("\n\n" + "=" * 50) print(f"Yaratildi : {created}") if update: print(f"Yangilandi: {updated}") print(f"O'tkazildi: {skipped}") if errors: 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)", ) parser.add_argument( "--after", type=str, default=None, metavar="SANA", help="Faqat shu vaqtdan keyin qo'shilgan mahsulotlar. Misol: '2024-01-15' yoki '2024-01-15 10:30:00'", ) args = parser.parse_args() after_dt: datetime | None = None if args.after: try: after_dt = parse_after_date(args.after) except ValueError as e: print(f"[XATO] {e}") sys.exit(1) sync_attributes() sync_products(dry_run=args.dry_run, update=args.update, after=after_dt)