diff --git a/inventory/forms.py b/inventory/forms.py index 3c88aed5..d5a34498 100644 --- a/inventory/forms.py +++ b/inventory/forms.py @@ -2264,4 +2264,12 @@ class CarDealershipRegistrationForm(forms.ModelForm): class Meta: model = UserRegistration - fields = ("name","arabic_name", "email","phone_number", "crn", "vrn", "address") \ No newline at end of file + fields = ("name","arabic_name", "email","phone_number", "crn", "vrn", "address") + +class CarDetailsEstimateCreate(forms.Form): + customer = forms.ModelChoiceField( + queryset=Customer.objects.all(), + required=True, + label="Customer", + widget=forms.Select(attrs={"class": "form-control"}), + ) \ No newline at end of file diff --git a/inventory/models.py b/inventory/models.py index e3d70112..69db6cf8 100644 --- a/inventory/models.py +++ b/inventory/models.py @@ -910,6 +910,16 @@ class Car(Base): ) self.save() + def get_active_estimates(self): + qs = self.item_model.itemtransactionmodel_set.exclude(ce_model__status="canceled") + + data = [] + for item in qs: + x = ExtraInfo.objects.filter(object_id=item.ce_model.pk,content_type=ContentType.objects.get_for_model(EstimateModel)).first() + if x: + data.append(x) + return data + @property def logo(self): return self.id_car_make.logo.url if self.id_car_make.logo else None @@ -1531,7 +1541,7 @@ class Staff(models.Model): # max_length=255, unique=True, editable=False, null=True, blank=True,allow_unicode=True # ) slug = RandomCharField(length=8, unique=True) - + objects = StaffUserManager() @property @@ -2487,12 +2497,14 @@ class Opportunity(models.Model): ] def __str__(self): - if self.customer: - return ( - f"Opportunity for {self.customer.first_name} {self.customer.last_name}" - ) - return f"Opportunity for {self.organization.name}" - + try: + if self.customer: + return ( + f"Opportunity for {self.customer.first_name} {self.customer.last_name}" + ) + return f"Opportunity for {self.organization.name}" + except Exception: + return f"Opportunity for car :{self.car}" class Notes(models.Model): dealer = models.ForeignKey(Dealer, on_delete=models.CASCADE, related_name="notes") diff --git a/inventory/urls.py b/inventory/urls.py index f8348b41..e2d76dcf 100644 --- a/inventory/urls.py +++ b/inventory/urls.py @@ -342,6 +342,11 @@ urlpatterns = [ views.CarDetailView.as_view(), name="car_detail", ), + path( + "/cars//estimate/", + views.create_estimate_for_car, + name="create_estimate_for_car", + ), path("cars//history/", views.car_history, name="car_history"), path( "/cars//update/", @@ -789,7 +794,7 @@ urlpatterns = [ ), path( "/sales/estimates/create//", - views.create_estimate, + views.estimate_create_from_opportunity, name="estimate_create_from_opportunity", ), path( @@ -938,7 +943,7 @@ urlpatterns = [ views.ItemServiceUpdateView.as_view(), name="item_service_update", ), - + path( "/items/services//detail/", views.ItemServiceDetailView.as_view(), @@ -962,7 +967,7 @@ urlpatterns = [ ), path( "/items/expeneses//detail/", - views.ItemExpenseDetailView.as_view(), + views.ItemExpenseDetailView.as_view(), name="item_expense_detail", ), # Bills diff --git a/inventory/utils.py b/inventory/utils.py index ee70bdfe..4c8caf36 100644 --- a/inventory/utils.py +++ b/inventory/utils.py @@ -2815,3 +2815,36 @@ def generate_car_image_simple(car): error_msg = f"Image processing failed: {e}" logger.error(error_msg) return {"success": False, "error": error_msg} + + + + +def create_estimate_(dealer,car,customer): + entity = dealer.entity + title = f"Estimate for {car.vin}-{car.id_car_make.name}-{car.id_car_model.name}-{car.year} for customer {customer.first_name} {customer.last_name}" + estimate = entity.create_estimate( + estimate_title=title, + customer_model=customer.customer_model, + contract_terms="fixed", + ) + + estimate_itemtxs = { + car.item_model.item_number: { + "unit_cost": round(float(car.marked_price)), + "unit_revenue": round(float(car.marked_price)), + "quantity": 1, + "total_amount": round(float(car.final_price_plus_vat)), + } + } + + try: + estimate.migrate_itemtxs( + itemtxs=estimate_itemtxs, + commit=True, + operation=EstimateModel.ITEMIZE_APPEND, + ) + except Exception as e: + estimate.delete() + raise e + + return estimate \ No newline at end of file diff --git a/inventory/views.py b/inventory/views.py index 15f16d12..1db91551 100644 --- a/inventory/views.py +++ b/inventory/views.py @@ -204,6 +204,7 @@ from .services import ( ) from .utils import ( CarFinanceCalculator, + create_estimate_, get_car_finance_data, get_finance_data, get_item_transactions, @@ -1737,6 +1738,15 @@ class CarDetailView(LoginRequiredMixin, PermissionRequiredMixin, DetailView): context_object_name = "car" permission_required = ["inventory.view_car"] + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + form = forms.CarDetailsEstimateCreate() + form.fields["customer"].queryset = form.fields["customer"].queryset.filter(dealer=self.request.dealer) + context["estimate_form"] = form + context["active_estimates"] = self.object.get_active_estimates() + + return context + def CarFinanceUpdateView(request,dealer_slug,slug): car = get_object_or_404(models.Car, slug=slug) @@ -7239,6 +7249,7 @@ class OpportunityCreateView( instance = form.save(commit=False) instance.dealer = dealer instance.staff = instance.lead.staff + instance.customer = instance.lead.customer instance.save() instance.lead.convert_to_customer() instance.lead.save() @@ -12001,3 +12012,70 @@ def payment_result(request): if s == "success": return render(request, 'plans/payment_success.html') return render(request, 'plans/payment_failed.html') + + +@require_POST +def create_estimate_for_car(request,dealer_slug,slug): + car = get_object_or_404(models.Car, slug=slug) + dealer = get_object_or_404(models.Dealer, slug=dealer_slug) + + if request.method == 'POST': + form = forms.CarDetailsEstimateCreate(request.POST) + if form.is_valid(): + customer = form.cleaned_data['customer'] + estimate = create_estimate_(dealer, car, customer) + + if request.is_staff: + models.ExtraInfo.objects.create( + dealer=dealer, + content_object=estimate, + related_object=request.staff, + created_by=request.user, + data={"vat_rate": dealer.vat_rate, "discount": 0}, + ) + else: + models.ExtraInfo.objects.create( + dealer=dealer, + content_object=estimate, + related_object=request.user, + created_by=request.user, + data={"vat_rate": dealer.vat_rate, "discount": 0}, + ) + + messages.success(request, "Estimate created successfully.") + return redirect("estimate_detail", dealer_slug=dealer.slug, pk=estimate.pk) + else: + messages.error(request, "Please correct the errors below.") + return redirect("car_detail", dealer_slug=dealer.slug, slug=car.slug) +@require_POST +def estimate_create_from_opportunity(request,dealer_slug,slug): + opportunity = get_object_or_404(models.Opportunity, slug=slug) + dealer = get_object_or_404(models.Dealer, slug=dealer_slug) + car = opportunity.car + customer = opportunity.customer + # TODO: set safe guard, so it doesnt recreate it + if not all([dealer,car,customer]): + messages.error(request, "Please correct the errors below.") + return redirect("opportunity_detail", dealer_slug=dealer.slug, slug=opportunity.slug) + + estimate = create_estimate_(dealer, car, customer) + + if request.is_staff: + models.ExtraInfo.objects.create( + dealer=dealer, + content_object=estimate, + related_object=request.staff, + created_by=request.user, + data={"vat_rate": dealer.vat_rate, "discount": 0}, + ) + else: + models.ExtraInfo.objects.create( + dealer=dealer, + content_object=estimate, + related_object=request.user, + created_by=request.user, + data={"vat_rate": dealer.vat_rate, "discount": 0}, + ) + + messages.success(request, "Estimate created successfully.") + return redirect("estimate_detail", dealer_slug=dealer.slug, pk=estimate.pk) \ No newline at end of file diff --git a/templates/crm/opportunities/opportunity_detail.html b/templates/crm/opportunities/opportunity_detail.html index 5198269b..8c80c9ee 100644 --- a/templates/crm/opportunities/opportunity_detail.html +++ b/templates/crm/opportunities/opportunity_detail.html @@ -30,7 +30,9 @@ {% else %} {% if perms.django_ledger.add_estimatemodel %} {{ _("Create Quotation") }} + type="button" + data-bs-toggle="modal" + data-bs-target="#estimateModal">{% trans "Create Estimate" %} {% endif %} {% endif %} @@ -854,6 +856,33 @@ {% include "components/note_modal.html" with content_type="opportunity" slug=opportunity.slug %} {% include "components/schedule_modal.html" with content_type="opportunity" slug=opportunity.slug %} + + + {% endblock %} {% block customJS %}