diff --git a/inventory/forms.py b/inventory/forms.py index 943689c7..409c5ad5 100644 --- a/inventory/forms.py +++ b/inventory/forms.py @@ -686,7 +686,7 @@ class ScheduleForm(forms.ModelForm): scheduled_at = forms.DateTimeField(widget=DateTimeInput(attrs={'type': 'datetime-local'})) class Meta: model = Schedule - fields = ['purpose','scheduled_type', 'scheduled_at', 'notes'] + fields = ['purpose','scheduled_type', 'scheduled_at','duration', 'notes'] class NoteForm(forms.ModelForm): diff --git a/inventory/migrations/0015_merge_20250209_1116.py b/inventory/migrations/0015_merge_20250209_1116.py new file mode 100644 index 00000000..a5a709bd --- /dev/null +++ b/inventory/migrations/0015_merge_20250209_1116.py @@ -0,0 +1,14 @@ +# Generated by Django 4.2.17 on 2025-02-09 08:16 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('inventory', '0012_merge_20250206_1308'), + ('inventory', '0014_remove_lead_car_lead_id_car_make_lead_id_car_model_and_more'), + ] + + operations = [ + ] diff --git a/inventory/migrations/0016_schedule_duration.py b/inventory/migrations/0016_schedule_duration.py new file mode 100644 index 00000000..82536c79 --- /dev/null +++ b/inventory/migrations/0016_schedule_duration.py @@ -0,0 +1,19 @@ +# Generated by Django 4.2.17 on 2025-02-09 08:23 + +import datetime +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('inventory', '0015_merge_20250209_1116'), + ] + + operations = [ + migrations.AddField( + model_name='schedule', + name='duration', + field=models.DurationField(default=datetime.timedelta(seconds=300)), + ), + ] diff --git a/inventory/migrations/0017_car_hash.py b/inventory/migrations/0017_car_hash.py new file mode 100644 index 00000000..88457273 --- /dev/null +++ b/inventory/migrations/0017_car_hash.py @@ -0,0 +1,18 @@ +# Generated by Django 4.2.17 on 2025-02-09 11:36 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('inventory', '0016_schedule_duration'), + ] + + operations = [ + migrations.AddField( + model_name='car', + name='hash', + field=models.CharField(blank=True, max_length=64, null=True, verbose_name='Hash'), + ), + ] diff --git a/inventory/models.py b/inventory/models.py index fc68fb95..e867d83f 100644 --- a/inventory/models.py +++ b/inventory/models.py @@ -1,38 +1,21 @@ -# from datetime import timezone -from django.utils import timezone -import itertools -from uuid import uuid4 -from django.conf import settings -from django.db import models, transaction -from django.db.models import Sum, F, Count +from decimal import Decimal +import hashlib +from django.db import models +from datetime import timedelta from django.contrib.auth.models import User, UserManager -from django.db.models.signals import pre_save, post_save -from django.dispatch import receiver from django.utils.translation import gettext_lazy as _ from django_ledger.models import ( VendorModel, EntityModel, - EntityUnitModel, ItemModel, - AccountModel, - ItemModelAbstract, - UnitOfMeasureModel, CustomerModel, - ItemModelQuerySet, ) from django_ledger.io.io_core import get_localdate -from django.db.models import Sum -from decimal import Decimal, InvalidOperation from django.core.exceptions import ValidationError from phonenumber_field.modelfields import PhoneNumberField from django.utils.timezone import now -from sqlalchemy.orm.base import object_state - -from .utilities.financials import get_financial_value, get_total, get_total_financials -from django.db.models import FloatField from .mixins import LocalizedNameMixin from django_ledger.models import EntityModel, ItemModel,EstimateModel,InvoiceModel -from django_countries.fields import CountryField from django.contrib.contenttypes.fields import GenericForeignKey from django.contrib.contenttypes.models import ContentType @@ -403,6 +386,11 @@ class Car(models.Model): remarks = models.TextField(blank=True, null=True, verbose_name=_("Remarks")) mileage = models.IntegerField(blank=True, null=True, verbose_name=_("Mileage")) receiving_date = models.DateTimeField(verbose_name=_("Receiving Date")) + hash = models.CharField(max_length=64, blank=True, null=True, verbose_name=_("Hash")) + + def save(self, *args, **kwargs): + self.hash = self.get_hash + super(Car, self).save(*args, **kwargs) class Meta: verbose_name = _("Car") @@ -424,6 +412,12 @@ class Car(models.Model): def get_car_group(self): return f"{self.id_car_make.get_local_name} {self.id_car_model.get_local_name}" + @property + def get_hash(self): + hash_object = hashlib.sha256() + hash_object.update(f"{self.id_car_make.name}{self.id_car_model.name}".encode('utf-8')) + return hash_object.hexdigest() + def to_dict(self): return { "vin": self.vin, @@ -437,6 +431,7 @@ class Car(models.Model): "remarks": self.remarks, "mileage": self.mileage, "receiving_date": self.receiving_date.strftime('%Y-%m-%d %H:%M:%S'), + 'hash': self.get_hash, "id": self.id, } @@ -1176,7 +1171,8 @@ class Lead(models.Model): def full_name(self): return f"{self.first_name} {self.last_name}" def convert_to_customer(self,entity): - if entity and not entity.get_customers().filter(email=self.email).exists(): + customer = entity.get_customers().filter(email=self.email).first() + if entity and not customer: customer = entity.create_customer( customer_model_kwargs={ "customer_name": self.full_name, @@ -1184,12 +1180,13 @@ class Lead(models.Model): "phone": self.phone_number, "email": self.email, } - ) - customer.additional_info = {} - customer.additional_info.update({"info":self.to_dict()}) - self.customer = customer - customer.save() - self.save() + ) + + customer.additional_info = {} + customer.additional_info.update({"info":self.to_dict()}) + self.customer = customer + customer.save() + self.save() def get_latest_schedule(self): return self.schedules.order_by('-scheduled_at').first() @@ -1219,6 +1216,7 @@ class Schedule(models.Model): purpose = models.CharField(max_length=200, choices=PURPOSE_CHOICES) scheduled_at = models.DateTimeField() scheduled_type = models.CharField(max_length=200, choices=ScheduledType,default='Call') + duration = models.DurationField(default=timedelta(minutes=5)) notes = models.TextField(blank=True, null=True) status = models.CharField(max_length=200, choices=ScheduleStatusChoices, default='Scheduled') created_at = models.DateTimeField(auto_now_add=True) diff --git a/inventory/signals.py b/inventory/signals.py index da7cc19c..e7704cb7 100644 --- a/inventory/signals.py +++ b/inventory/signals.py @@ -694,7 +694,8 @@ def update_item_model_cost(sender, instance, created, **kwargs): product = entity.get_items_all().filter(name=instance.car.vin).first() product.default_amount = instance.selling_price - product.additional_info = {} + if not isinstance(product.additional_info, dict): + product.additional_info = {} product.additional_info.update({"car_finance":instance.to_dict()}) product.additional_info.update({"additional_services": [service.to_dict() for service in instance.additional_services.all()]}) product.save() diff --git a/inventory/views.py b/inventory/views.py index 1472230b..6444587b 100644 --- a/inventory/views.py +++ b/inventory/views.py @@ -1,4 +1,6 @@ -from appointment.models import Appointment +from django.db.models import Func +from appointment.models import Appointment,AppointmentRequest,Service,StaffMember +from datetime import timedelta from calendar import month_name from random import randint from rich import print @@ -116,6 +118,11 @@ logger = logging.getLogger(__name__) logging.basicConfig(level=logging.INFO) + +class Hash(Func): + function = 'get_hash' + + def switch_language(request): language = request.GET.get("language", "en") referer = request.META.get("HTTP_REFERER", "/") @@ -2339,12 +2346,13 @@ def create_estimate(request): items = data.get("item", []) quantities = data.get("quantity", []) - + if not all([items, quantities]): return JsonResponse( {"status": "error", "message": "Items and Quantities are required"}, status=400, - ) + ) + if isinstance(quantities, list): if "0" in quantities: return JsonResponse( @@ -2355,7 +2363,17 @@ def create_estimate(request): return JsonResponse( {"status": "error", "message": "Quantity must be greater than zero"} ) - + if isinstance(items, list): + for item, quantity in zip(items, quantities): + if int(quantity) > models.Car.objects.filter(hash=item).count(): + return JsonResponse( + {"status": "error", "message": "Quantity must be less than or equal to the number of cars in stock"}, + ) + else: + if int(quantities) > models.Car.objects.filter(hash=item).count(): + return JsonResponse( + {"status": "error", "message": "Quantity must be less than or equal to the number of cars in stock"}, + ) estimate = entity.create_estimate( estimate_title=title, customer_model=customer, contract_terms=terms ) @@ -2375,18 +2393,20 @@ def create_estimate(request): ] items_txs = [] for item in items_list: - item_instance = ItemModel.objects.get(pk=item.get("item_id")) - car_instance = models.Car.objects.get(vin=item_instance.name) - items_txs.append( - { - "item_number": item_instance.item_number, - "quantity": Decimal(item.get("quantity")), - "unit_cost": car_instance.finances.selling_price, - "unit_revenue": car_instance.finances.selling_price, - "total_amount": (car_instance.finances.total_vat) - * int(item.get("quantity")), - } - ) + # item_instance = ItemModel.objects.get(pk=item.get("item_id")) + car_instance = ItemModel.objects.filter(additional_info__car_info__hash=item.get("item_id")).all() + + # car_instance = models.Car.objects.get(vin=item_instance.name) + for i in car_instance[:int(quantities[0])]: + items_txs.append( + { + "item_number": i.item_number, + "quantity": 1, + "unit_cost": i.additional_info.get('car_finance').get("selling_price"), + "unit_revenue": i.additional_info.get('car_finance').get("selling_price"), + "total_amount": (i.additional_info.get('car_finance').get("total_vat")) + } + ) estimate_itemtxs = { item.get("item_number"): { @@ -2416,14 +2436,18 @@ def create_estimate(request): ) if isinstance(items, list): - for item in items: - item_instance = ItemModel.objects.get(pk=item) - instance = models.Car.objects.get(vin=item_instance.name) + for item in estimate_itemtxs.keys(): + item_instance = ItemModel.objects.get(item_number=item) + instance = models.Car.objects.get(name=item_instance.name) reserve_car(instance, request) + # for item in items: + # item_instance = ItemModel.objects.filter(additioinal_info__car_info__hash=item).first() + # instance = models.Car.objects.get(hash=item) + # reserve_car(instance, request) else: - item_instance = ItemModel.objects.get(pk=items) - instance = models.Car.objects.get(vin=item_instance.name) + item_instance = ItemModel.objects.get(additioinal_info__car_info__hash=items) + instance = models.Car.objects.get(hash=item) response = reserve_car(instance, request) url = reverse("estimate_detail", kwargs={"pk": estimate.pk}) @@ -2439,22 +2463,21 @@ def create_estimate(request): entity_slug=entity.slug, user_model=entity.admin ) form.fields["customer"].queryset = entity.get_customers().filter(active=True) - car_list = models.Car.objects.filter( - dealer=dealer, finances__selling_price__gt=0 - ).exclude(status="reserved") + car_list = models.Car.objects.filter(dealer=dealer).exclude(status="reserved").values_list( + 'id_car_make__name', 'id_car_model__name','hash').distinct() + context = { "form": form, "items": [ { - "car": x, - "product": entity.get_items_all() - .filter(item_role=ItemModel.ITEM_ROLE_PRODUCT, name=x.vin) - .first(), + 'make':x[0], + 'model':x[1], + 'hash': x[2] } for x in car_list ], } - + return render(request, "sales/estimates/estimate_form.html", context) @@ -3031,12 +3054,14 @@ def lead_convert(request, pk): if lead.is_converted: messages.error(request, "Lead is already converted to customer.") return redirect("opportunity_create",pk=lead.pk) + if not models.Car.objects.filter(id_car_make=lead.id_car_make,id_car_model=lead.id_car_model).first(): + messages.error(request, "Cannot convert lead to customer. Car model not found.") + return redirect("lead_list") lead.convert_to_customer(dealer.entity) messages.success(request, "Lead converted to customer successfully!") return redirect("opportunity_create",pk=lead.pk) - @login_required def schedule_lead(request, pk): lead = get_object_or_404(models.Lead, pk=pk) @@ -3047,8 +3072,33 @@ def schedule_lead(request, pk): instance.lead = lead if hasattr(request.user, "staff"): instance.scheduled_by = request.user.staff + + # Save the Schedule instance instance.save() - messages.success(request, "Lead scheduled successfully!") + + # Create AppointmentRequest + service = Service.objects.filter(name=instance.scheduled_type).first() + if not service: + messages.error(request, "Service not found!") + return redirect("lead_list") + + appointment_request = AppointmentRequest.objects.create( + date=instance.scheduled_at.date(), + start_time=instance.scheduled_at.time(), + end_time=(instance.scheduled_at + instance.duration).time(), + service=service, + staff_member=StaffMember.objects.first() + ) + + # Create Appointment + Appointment.objects.create( + client=request.user, # Replace with the appropriate client + appointment_request=appointment_request, + phone="123-456-7890", # Replace with actual phone number + address="123 Main St", # Replace with actual address + ) + + messages.success(request, "Lead scheduled and appointment created successfully!") return redirect("lead_list") else: messages.error(request, f"Invalid form data: {str(form.errors)}") @@ -3131,9 +3181,9 @@ class OpportunityCreateView(CreateView): def get_initial(self): initial = super().get_initial() if self.kwargs.get('pk',None): - lead = models.Lead.objects.get(pk=self.kwargs.get('pk')) + lead = models.Lead.objects.get(pk=self.kwargs.get('pk')) + initial['customer'] = lead.customer - initial['car'] = lead.car return initial def form_valid(self, form): diff --git a/scripts/run.py b/scripts/run.py index e4cf4eb9..aeb1622e 100644 --- a/scripts/run.py +++ b/scripts/run.py @@ -1,16 +1,17 @@ from django_ledger.models.invoice import InvoiceModel from django_ledger.utils import accruable_net_summary from decimal import Decimal -from django_ledger.models import EstimateModel,EntityModel +from django_ledger.models import EstimateModel,EntityModel,ItemModel from rich import print from datetime import date -from inventory.models import VatRate,Lead,CarMake,CarModel,Schedule +from inventory.models import Car, Dealer, VatRate,Lead,CarMake,CarModel,Schedule from inventory.utils import CarFinanceCalculator from appointment.models import Appointment,AppointmentRequest,Service,StaffMember from django.contrib.auth import get_user_model from django_ledger.io.io_core import get_localdate from datetime import datetime, timedelta - +from django.utils import timezone +import hashlib User = get_user_model() @@ -24,7 +25,7 @@ def run(): # service="Haircut", # date_time="2023-10-15 10:00:00", # status="pending") - make = CarMake.objects.first() + # make = CarMake.objects.first() # Lead.objects.create( # first_name="John", # last_name="Doe", @@ -53,19 +54,43 @@ def run(): # staff="John Doe", # priority="high", # ) - service = Service.objects.first() - appointment_request = AppointmentRequest.objects.create( - date=get_localdate(), - start_time=datetime.now().strftime("%H:%M:%S"), - end_time=datetime.time(datetime.now() + timedelta(minutes=30)).strftime("%H:%M:%S"), - service=service, - staff_member=StaffMember.objects.first(), - ) + # now = timezone.now() + # end_time = now + timedelta(minutes=10) - appointment = Appointment.objects.create( - client=User.objects.first(), - appointment_request=appointment_request, - phone="123-456-7890", - address="123 Main St", - ) - \ No newline at end of file + # service = Service.objects.first() + # appointment_request = AppointmentRequest.objects.create( + # date=now.date(), + # start_time=now.time(), + # end_time=end_time.time(), + # service=service, + # staff_member=StaffMember.objects.first(), + # ) + + # appointment = Appointment.objects.create( + # client=User.objects.first(), + # appointment_request=appointment_request, + # phone="123-456-7890", + # address="123 Main St", + # ) + dealer = Dealer.objects.get(user__email="ismail.mosa.ibrahim@gmail.com") + entity = dealer.entity + + car_list = Car.objects.filter(dealer=dealer).all() + # context = { + # "items": [ + # { + # "car": x, + # "product": entity.get_items_all() + # .filter(item_role=ItemModel.ITEM_ROLE_PRODUCT, name=x.vin) + # .first(), + # } + # for x in car_list + # ], + # } + + for i in car_list: + hash_object = hashlib.sha256() + hash_object.update(f"{i.id_car_make.name}{i.id_car_model.name}".encode('utf-8')) + print(hash_object.hexdigest() , i.id_car_make.name, i.id_car_model.name) + + \ No newline at end of file diff --git a/templates/crm/leads/lead_list.html b/templates/crm/leads/lead_list.html index c2e3f5b4..e1154868 100644 --- a/templates/crm/leads/lead_list.html +++ b/templates/crm/leads/lead_list.html @@ -140,6 +140,7 @@ {{ lead.phone_number }} {% if lead.get_latest_schedule %} + {% if lead.get_latest_schedule.scheduled_type == "Call" %} {{ lead.get_latest_schedule.scheduled_at }} @@ -150,6 +151,7 @@ {{ lead.get_latest_schedule.scheduled_at }} {% endif %} + {% endif %} {{ lead.staff|upper }} diff --git a/templates/sales/estimates/estimate_form.html b/templates/sales/estimates/estimate_form.html index dfd3d0f6..c71daf7e 100644 --- a/templates/sales/estimates/estimate_form.html +++ b/templates/sales/estimates/estimate_form.html @@ -18,7 +18,7 @@