Merge branch 'main' of http://10.10.1.136:3000/ismail/haikal into frontend
@ -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}"
|
||||
)
|
||||
@ -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}")
|
||||
@ -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}
|
||||
@ -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}
|
||||
|
After Width: | Height: | Size: 401 KiB |
|
After Width: | Height: | Size: 336 KiB |
|
After Width: | Height: | Size: 327 KiB |
|
After Width: | Height: | Size: 431 KiB |
|
After Width: | Height: | Size: 401 KiB |
|
After Width: | Height: | Size: 412 KiB |
|
After Width: | Height: | Size: 422 KiB |
@ -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>
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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 %}
|
||||
|
||||
|
||||
|
||||
|
||||