integrate image gen with haikal

This commit is contained in:
ismail 2025-08-25 17:23:33 +03:00
parent 6a2911cea4
commit 6a70c762f0
15 changed files with 415 additions and 39 deletions

View File

@ -1,3 +1,4 @@
import logging
import uuid
from datetime import datetime
from django.conf import settings
@ -51,6 +52,10 @@ from imagekit.processors import ResizeToFill
# from simple_history.models import HistoricalRecords
from plans.models import Invoice
logger = logging.getLogger(__name__)
logging.basicConfig(level=logging.INFO)
class Base(models.Model):
id = models.UUIDField(
unique=True,
@ -3720,4 +3725,32 @@ class Ticket(models.Model):
return ", ".join(parts)
def __str__(self):
return f"#{self.id} - {self.subject} ({self.status})"
return f"#{self.id} - {self.subject} ({self.status})"
class CarImage(models.Model):
car = models.OneToOneField('Car', on_delete=models.CASCADE, related_name='generated_image')
image_hash = models.CharField(max_length=64, unique=True)
image = models.ImageField(upload_to='car_images/', null=True, blank=True)
is_generating = models.BooleanField(default=False)
created_at = models.DateTimeField(auto_now_add=True)
def generate_hash(self):
"""Simple hash generation"""
car = self.car
hash_string = f"{car.id_car_make.name if car.id_car_make else ''}-{car.id_car_model.name if car.id_car_model else ''}-{car.year}-{getattr(car, 'color', 'default')}"
return hashlib.sha256(hash_string.encode()).hexdigest()
def schedule_generation(self):
"""Schedule image generation"""
from django_q.tasks import async_task
from inventory.tasks import generate_car_image_task
self.is_generating = True
self.save()
async_task(
generate_car_image_task,
self.id,
task_name=f"generate_car_image_{self.car.vin}"
)

View File

@ -413,24 +413,26 @@ def create_item_model(sender, instance, created, **kwargs):
# quotation.save()
@receiver(post_save, sender=models.CarColors)
def update_car_when_color_changed(sender, instance, **kwargs):
"""
Signal receiver to handle updates to a car instance when its related
CarColors instance is modified. Triggered by the `post_save` signal
for the `CarColors` model. Ensures that the associated `Car` instance
is saved, propagating changes effectively.
# @receiver(post_save, sender=models.CarColors)
# def update_car_hash_when_color_changed(sender, instance, **kwargs):
# """
# Signal receiver to handle updates to a car instance when its related
# CarColors instance is modified. Triggered by the `post_save` signal
# for the `CarColors` model. Ensures that the associated `Car` instance
# is saved, propagating changes effectively.
:param sender: The model class (`CarColors`) that was saved.
:type sender: Type[models.CarColors]
:param instance: The specific instance of `CarColors` that was saved.
:type instance: models.CarColors
:param kwargs: Additional keyword arguments passed by the signal.
:type kwargs: dict
:return: None
"""
car = instance.car
car.save()
# :param sender: The model class (`CarColors`) that was saved.
# :type sender: Type[models.CarColors]
# :param instance: The specific instance of `CarColors` that was saved.
# :type instance: models.CarColors
# :param kwargs: Additional keyword arguments passed by the signal.
# :type kwargs: dict
# :return: None
# """
# car = instance.car
# car.hash = car.get_hash
# car.save()
@receiver(post_save, sender=models.Opportunity)
@ -1260,4 +1262,41 @@ def send_ticket_notification(sender, instance, created, **kwargs):
kwargs={"dealer_slug": instance.dealer.slug, "ticket_id": instance.pk},
),
),
)
)
@receiver(post_save, sender=models.CarColors)
def handle_car_image(sender, instance, created, **kwargs):
"""
Simple handler for car image generation
"""
try:
# Create or get car image record
car = instance.car
car_image, created = models.CarImage.objects.get_or_create(car=car, defaults={'image_hash': car.get_hash})
# Check for existing image with same hash
existing = models.CarImage.objects.filter(
image_hash=car_image.image_hash,
image__isnull=False
).exclude(car=car).first()
if existing:
# Copy existing image
car_image.image.save(
existing.image.name,
existing.image.file,
save=True
)
logger.info(f"Reused image for car {car.vin}")
else:
# Schedule async generation
async_task(
'inventory.tasks.generate_car_image_task',
car_image.id,
task_name=f"generate_car_image_{car.vin}"
)
logger.info(f"Scheduled image generation for car {car.vin}")
except Exception as e:
logger.error(f"Error handling car image for {car.vin}: {e}")

View File

@ -1,5 +1,8 @@
import base64
import logging
import requests
from PIL import Image
from io import BytesIO
from plans.models import Plan
from django.urls import reverse
from django.conf import settings
@ -18,7 +21,8 @@ from .utils import get_accounts_data,create_account
from django.template.loader import render_to_string
from django.utils.translation import gettext_lazy as _
from django.contrib.auth.models import User, Group, Permission
from inventory.models import DealerSettings, Dealer,Schedule,Notification,CarReservation,CarStatusChoices
from inventory.models import DealerSettings, Dealer,Schedule,Notification,CarReservation,CarStatusChoices,CarImage
logger = logging.getLogger(__name__)
logging.basicConfig(level=logging.INFO)
@ -928,4 +932,30 @@ def remove_reservation_by_id(reservation_id):
logger.error(f"Error removing reservation with ID {reservation_id}: {e}")
def test_task(**kwargs):
print("TASK : ",kwargs.get("dealer"))
print("TASK : ",kwargs.get("dealer"))
def generate_car_image_task(car_image_id):
"""
Simple async task to generate car image
"""
from inventory.utils import generate_car_image_simple
try:
car_image = CarImage.objects.get(id=car_image_id)
result = generate_car_image_simple(car_image)
return {
'success': result.get('success', False),
'car_image_id': car_image_id,
'error': result.get('error'),
'message': 'Image generated' if result.get('success') else 'Generation failed'
}
except CarImage.DoesNotExist:
error_msg = f"CarImage with id {car_image_id} not found"
logger.error(error_msg)
return {'success': False, 'error': error_msg}
except Exception as e:
error_msg = f"Unexpected error: {e}"
logger.error(error_msg)
return {'success': False, 'error': error_msg}

View File

@ -3,11 +3,15 @@ import secrets
import logging
import datetime
import requests
from PIL import Image
from io import BytesIO
from decimal import Decimal
from inventory import models
from django.urls import reverse
from django.conf import settings
from urllib.parse import urljoin
from django.utils import timezone
from django.db import transaction
from django_ledger.io import roles
from django.contrib import messages
from django.shortcuts import redirect
@ -20,16 +24,17 @@ from django_ledger.models import (
BillModel,
VendorModel,
)
from django.core.files.base import ContentFile
from django_ledger.models.items import ItemModel
from django.utils.translation import get_language
from django.core.exceptions import ObjectDoesNotExist
from django.utils.translation import gettext_lazy as _
from django_q.models import Schedule as DjangoQSchedule
from django.contrib.contenttypes.models import ContentType
from django_ledger.models.transactions import TransactionModel
from django_ledger.models.journal_entry import JournalEntryModel
from django.db import transaction
from django_q.models import Schedule as DjangoQSchedule
from django_ledger.io import roles
# from .tasks import generate_car_image_task
logger = logging.getLogger(__name__)
@ -1649,7 +1654,7 @@ def _post_sale_and_cogs(invoice, dealer):
)
TransactionModel.objects.create(
journal_entry=je_sale,
account=vat_acc,
amount=car.get_additional_services_vat,
tx_type='credit',
@ -2348,4 +2353,274 @@ def create_account(entity, coa, account_data):
account.save()
logger.info(f"Created default account: {account}")
except Exception as e:
logger.error(f"Error creating default account: {account_data['code']}, {e}")
logger.error(f"Error creating default account: {account_data['code']}, {e}")
def get_or_generate_car_image(car):
"""
Utility function to get or generate car image asynchronously
"""
try:
car_image, created = models.CarImage.objects.get_or_create(car=car)
if created:
car_image.image_hash = car.get_hash
car_image.save()
if car_image.image:
return car_image.image.url
# Check for existing image with same hash
existing = models.CarImage.objects.filter(
image_hash=car_image.image_hash,
image__isnull=False
).exclude(car=car).first()
if existing:
car_image.image.save(
existing.image.name,
existing.image.file,
save=True
)
return car_image.image.url
# If no image exists and not already generating, schedule generation
if not car_image.is_generating:
car_image.schedule_image_generation()
return None # Return None while image is being generated
except Exception as e:
logger.error(f"Error getting/generating car image: {e}")
return None
def force_regenerate_car_image(car):
"""
Force regeneration of car image (useful for admin actions)
"""
try:
car_image, created = models.CarImage.objects.get_or_create(car=car)
car_image.image_hash = car.get_hash
car_image.image.delete(save=False) # Remove old image
car_image.is_generating = False
car_image.generation_attempts = 0
car_image.last_generation_error = None
car_image.save()
car_image.schedule_image_generation()
return True
except Exception as e:
logger.error(f"Error forcing image regeneration: {e}")
return False
class CarImageAPIClient:
"""Simple client to handle authenticated requests to the car image API"""
BASE_URL = "http://127.0.0.1:8888"
USERNAME = "ismail.mosa.ibrahim@gmail.com"
PASSWORD = "Supremk4!"
def __init__(self):
self.session = None
self.csrf_token = None
def login(self):
"""Login to the API and maintain session"""
try:
# Start fresh session
self.session = requests.Session()
# Step 1: Get CSRF token
response = self.session.get(f"{self.BASE_URL}/login")
response.raise_for_status()
# Get CSRF token from cookies
self.csrf_token = self.session.cookies.get('csrftoken')
if not self.csrf_token:
raise Exception("CSRF token not found in cookies")
# Step 2: Login
login_data = {
"username": self.USERNAME,
"password": self.PASSWORD,
"csrfmiddlewaretoken": self.csrf_token
}
login_response = self.session.post(
f"{self.BASE_URL}/login",
data=login_data
)
if login_response.status_code != 200:
raise Exception(f"Login failed with status {login_response.status_code}")
logger.info("Successfully logged in to car image API")
return True
except Exception as e:
logger.error(f"Login failed: {e}")
self.session = None
self.csrf_token = None
return False
def generate_image(self, payload):
"""Generate car image using authenticated session"""
if not self.session or not self.csrf_token:
if not self.login():
raise Exception("Cannot generate image: Login failed")
try:
headers = {
'X-CSRFToken': self.csrf_token,
'Referer': self.BASE_URL,
}
print(payload)
generate_data = {
"year": payload['year'],
"make": payload['make'],
"model": payload['model'],
"exterior_color": payload['color'],
"angle": "3/4 rear",
"reference_image": ""
}
response = self.session.post(
f"{self.BASE_URL}/generate",
json=generate_data,
headers=headers,
timeout=160
)
response.raise_for_status()
# Parse response
result = response.json()
image_url = result.get('url')
if not image_url:
raise Exception("No image URL in response")
# Download the actual image
image_response = self.session.get(
f"{self.BASE_URL}{image_url}",
timeout=160
)
image_response.raise_for_status()
return image_response.content, None
except requests.RequestException as e:
error_msg = f"API request failed: {e}"
logger.error(error_msg)
return None, error_msg
except Exception as e:
error_msg = f"Unexpected error: {e}"
logger.error(error_msg)
return None, error_msg
# Global client instance
api_client = CarImageAPIClient()
def resize_image(image_data, max_size=(800, 600)):
"""
Resize image to make it smaller while maintaining aspect ratio
Returns resized image data in the same format
"""
try:
img = Image.open(BytesIO(image_data))
original_format = img.format
original_mode = img.mode
# Calculate new size maintaining aspect ratio
img.thumbnail(max_size, Image.Resampling.LANCZOS)
# Save back to bytes in original format
output_buffer = BytesIO()
if original_format and original_format.upper() in ['JPEG', 'JPG']:
img.save(output_buffer, format='JPEG', quality=95, optimize=True)
elif original_format and original_format.upper() == 'PNG':
# Preserve transparency for PNG
if original_mode == 'RGBA':
img.save(output_buffer, format='PNG', optimize=True)
else:
img.save(output_buffer, format='PNG', optimize=True)
else:
# Default to JPEG for other formats
if img.mode in ('RGBA', 'LA', 'P'):
# Convert to RGB if image has transparency
background = Image.new('RGB', img.size, (255, 255, 255))
if img.mode == 'RGBA':
background.paste(img, mask=img.split()[3])
else:
background.paste(img, (0, 0))
img = background
img.save(output_buffer, format='JPEG', quality=95, optimize=True)
resized_data = output_buffer.getvalue()
logger.info(f"Resized image from {len(image_data)} to {len(resized_data)} bytes")
return resized_data, None
except Exception as e:
error_msg = f"Image resizing failed: {e}"
logger.error(error_msg)
return None, error_msg
def generate_car_image_simple(car_image):
"""
Simple function to generate car image with authentication and resizing
"""
car = car_image.car
# Prepare payload
payload = {
'make': car.id_car_make.name if car.id_car_make else '',
'model': car.id_car_model.name if car.id_car_model else '',
'year': car.year,
'color': car.colors.exterior.name
}
logger.info(f"Generating image for car {car.vin}")
# Generate image using API client
image_data, error = api_client.generate_image(payload)
if error:
return {'success': False, 'error': error}
if not image_data:
return {'success': False, 'error': 'No image data received'}
try:
# Resize the image to make it smaller
resized_data, resize_error = resize_image(image_data, max_size=(800, 600))
if resize_error:
# If resizing fails, use original image but log warning
logger.warning(f"Resizing failed, using original image: {resize_error}")
resized_data = image_data
# Determine file extension based on content
try:
img = Image.open(BytesIO(resized_data))
file_extension = img.format.lower() if img.format else 'jpg'
except:
file_extension = 'jpg'
# Save the resized image
car_image.image.save(
f"{car_image.image_hash}.{file_extension}",
ContentFile(resized_data),
save=False
)
logger.info(f"Successfully generated and resized image for car {car.vin}")
return {'success': True}
except Exception as e:
error_msg = f"Image processing failed: {e}"
logger.error(error_msg)
return {'success': False, 'error': error_msg}

Binary file not shown.

After

Width:  |  Height:  |  Size: 401 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 336 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 327 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 431 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 401 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 412 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 422 KiB

View File

@ -26,7 +26,7 @@
</div>
</a>
</li>
{% endif %}
{% if perms.inventory.view_car%}
@ -433,13 +433,13 @@
</a>
</div>
{% if request.user.is_authenticated%}
<div class="navbar-logo">
<div class="d-flex align-items-center">
{% with name_to_display=request.user.first_name|default:request.dealer.name %}
<h6 class="text-info ms-2 d-none d-sm-block fs-7"
data-bs-toggle="tooltip"
data-bs-placement="bottom"
<h6 class="text-info ms-2 d-none d-sm-block fs-7"
data-bs-toggle="tooltip"
data-bs-placement="bottom"
title="{% trans 'Logged in as ' %}{{request.user.username }}">
{% trans 'Hello, ' %}{{ name_to_display }}
</h6>

View File

@ -79,7 +79,7 @@
<div class="col-lg-12 col-xl-6">
<div class="avatar avatar-5xl mb-3">
<img class="rounded h-100 w-100"
src="{% static 'images/cars/' %}{{ car.vin }}.png"
src="{% static 'images/car_images/' %}{{ car.get_hash }}.png"
alt="{{ car.vin }}" />
</div>
<div class="card mb-3 rounded shadow d-flex align-content-center

View File

@ -70,7 +70,7 @@
<div class="card-body bg-primary-subtle">
<label class="label" for="vin">{% trans 'VIN'|capfirst %}:</label>
<div class="input-group">
<input id="vin"
<input id="vin"
name="vin"
type="text"
class="form-control form-control-sm"
@ -180,7 +180,7 @@
</div>
</div>
<!-- Stock Type Card -->
<div class="col-lg-4 col-xl-3">
<div class="col-lg-4 col-xl-3">
<div class="card h-100">
<div class="card-body bg-info-subtle">
<label class="label" for="stock_type">{% trans 'Stock Type'|capfirst %}:</label>

View File

@ -189,17 +189,17 @@
hx-on::before-request="on_before_request()"
hx-on::after-request="on_after_request()"></div>
<div class="w-100 list table-responsive">
{% for car in cars %}
<div class="card border mb-3 py-0 px-0" id="project-list-table-body">
<div class="card-body">
<div class="row align-items-center">
<!-- Vehicle Image/Icon -->
<div class="col-auto">
<div class="avatar avatar-3xl">
<img class="rounded-soft shadow shadow-lg"
src="{% static 'images/cars/' %}{{ car.vin }}.png"
src="{% static 'images/car_images/' %}{{ car.get_hash }}.png"
alt="{{ car.vin }}" />
</div>
</div>
@ -311,5 +311,4 @@
{% endblock %}