restore composer.json, add mysqli extension
This commit is contained in:
22
core/apps/vendors/migrations/0007_remove_productvariantmodel_image_url_and_more.py
vendored
Normal file
22
core/apps/vendors/migrations/0007_remove_productvariantmodel_image_url_and_more.py
vendored
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
# Generated by Django 6.0.4 on 2026-04-13 12:28
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("vendors", "0006_productattributemodel_productvariantmodel"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name="productvariantmodel",
|
||||||
|
name="image_url",
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="productvariantmodel",
|
||||||
|
name="image",
|
||||||
|
field=models.ImageField(blank=True, null=True, upload_to="variants/", verbose_name="image"),
|
||||||
|
),
|
||||||
|
]
|
||||||
2
core/apps/vendors/models/product_variant.py
vendored
2
core/apps/vendors/models/product_variant.py
vendored
@@ -14,7 +14,7 @@ class ProductVariantModel(AbstractBaseModel):
|
|||||||
price = models.DecimalField(verbose_name=_("price"), max_digits=12, decimal_places=2, default=0)
|
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)
|
sku = models.CharField(verbose_name=_("SKU"), max_length=255, null=True, blank=True)
|
||||||
quantity = models.IntegerField(verbose_name=_("quantity"), default=-1)
|
quantity = models.IntegerField(verbose_name=_("quantity"), default=-1)
|
||||||
image_url = models.URLField(verbose_name=_("image url"), max_length=1000, null=True, blank=True)
|
image = models.ImageField(verbose_name=_("image"), upload_to="variants/", null=True, blank=True)
|
||||||
attribute_data = models.JSONField(verbose_name=_("attribute data"), null=True, blank=True)
|
attribute_data = models.JSONField(verbose_name=_("attribute data"), null=True, blank=True)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
|
|||||||
@@ -22,12 +22,24 @@ class ProductVariantSerializer(serializers.ModelSerializer):
|
|||||||
"price",
|
"price",
|
||||||
"sku",
|
"sku",
|
||||||
"quantity",
|
"quantity",
|
||||||
"image_url",
|
"image",
|
||||||
"attribute_data",
|
"attribute_data",
|
||||||
]
|
]
|
||||||
|
|
||||||
def to_representation(self, instance):
|
def to_representation(self, instance):
|
||||||
ret = super().to_representation(instance)
|
ret = super().to_representation(instance)
|
||||||
|
|
||||||
|
# Absolute URL for variant image
|
||||||
|
request = self.context.get("request")
|
||||||
|
image = ret.get("image")
|
||||||
|
if image and isinstance(image, str) and not image.startswith("http"):
|
||||||
|
if not image.startswith("/resources/"):
|
||||||
|
image = f"/resources/media/{image.lstrip('/')}"
|
||||||
|
if request:
|
||||||
|
ret["image"] = request.build_absolute_uri(image)
|
||||||
|
else:
|
||||||
|
ret["image"] = image
|
||||||
|
|
||||||
attr_data = ret.get("attribute_data")
|
attr_data = ret.get("attribute_data")
|
||||||
|
|
||||||
if attr_data and isinstance(attr_data, list):
|
if attr_data and isinstance(attr_data, list):
|
||||||
|
|||||||
@@ -24,6 +24,8 @@ import firebase_admin
|
|||||||
from firebase_admin import credentials, firestore
|
from firebase_admin import credentials, firestore
|
||||||
from decimal import Decimal, InvalidOperation
|
from decimal import Decimal, InvalidOperation
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
|
import requests
|
||||||
|
from django.core.files.base import ContentFile
|
||||||
|
|
||||||
# ── Django sozlash ───────────────────────────────────────────────────────────
|
# ── Django sozlash ───────────────────────────────────────────────────────────
|
||||||
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
|
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||||
@@ -131,18 +133,26 @@ def build_product_data(doc_id: str, data: dict) -> dict:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def print_progress(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}"
|
|
||||||
)
|
|
||||||
print(line, end="", flush=True)
|
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():
|
def sync_attributes():
|
||||||
print("Firebase vendor_attributes kolleksiyasi o'qilmoqda…")
|
print("Firebase vendor_attributes kolleksiyasi o'qilmoqda…")
|
||||||
docs = db.collection("vendor_attributes").stream()
|
docs = db.collection("vendor_attributes").stream()
|
||||||
@@ -224,6 +234,21 @@ def sync_products(dry_run: bool = False, update: bool = False, after: datetime |
|
|||||||
)
|
)
|
||||||
created += 1
|
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 ────────────────────────────────────
|
# ── Variantlarni qayta ishlash ────────────────────────────────────
|
||||||
|
|
||||||
item_attr = data.get("item_attribute")
|
item_attr = data.get("item_attribute")
|
||||||
@@ -244,11 +269,22 @@ def sync_products(dry_run: bool = False, update: bool = False, after: datetime |
|
|||||||
"price": to_decimal(v_data.get("variant_price", 0)),
|
"price": to_decimal(v_data.get("variant_price", 0)),
|
||||||
"sku": v_data.get("variant_sku"),
|
"sku": v_data.get("variant_sku"),
|
||||||
"quantity": to_int(v_data.get("variant_quantity", -1)),
|
"quantity": to_int(v_data.get("variant_quantity", -1)),
|
||||||
"image_url": v_data.get("variant_image"),
|
|
||||||
"attribute_data": item_attr.get("attributes")
|
"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:
|
except Exception as e:
|
||||||
print(f"\n [XATO] {doc_id}: {e}")
|
print(f"\n [XATO] {doc_id}: {e}")
|
||||||
errors += 1
|
errors += 1
|
||||||
|
|||||||
Reference in New Issue
Block a user