category va elonlanri olish optimalashtrildi
This commit is contained in:
0
core/apps/vendors/management/__init__.py
vendored
Normal file
0
core/apps/vendors/management/__init__.py
vendored
Normal file
0
core/apps/vendors/management/commands/__init__.py
vendored
Normal file
0
core/apps/vendors/management/commands/__init__.py
vendored
Normal file
183
core/apps/vendors/management/commands/sync_firebase_products.py
vendored
Normal file
183
core/apps/vendors/management/commands/sync_firebase_products.py
vendored
Normal file
@@ -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="")
|
||||||
16
core/apps/vendors/migrations/0005_vendorproductmodel_product_specification.py
vendored
Normal file
16
core/apps/vendors/migrations/0005_vendorproductmodel_product_specification.py
vendored
Normal file
@@ -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'),
|
||||||
|
),
|
||||||
|
]
|
||||||
1
core/apps/vendors/models/vendor_product.py
vendored
1
core/apps/vendors/models/vendor_product.py
vendored
@@ -17,6 +17,7 @@ class VendorproductModel(AbstractBaseModel):
|
|||||||
is_publish = models.BooleanField(verbose_name=_("is publish"), default=True)
|
is_publish = models.BooleanField(verbose_name=_("is publish"), default=True)
|
||||||
image = models.ImageField(verbose_name=_("image"), upload_to="products/", null=True, blank=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)
|
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):
|
def __str__(self):
|
||||||
return self.name
|
return self.name
|
||||||
|
|||||||
199
sync_firebase_products.py
Normal file
199
sync_firebase_products.py
Normal file
@@ -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)
|
||||||
Reference in New Issue
Block a user