update estimate creation
This commit is contained in:
parent
903c90e8d7
commit
83a00f6d05
@ -2264,4 +2264,12 @@ class CarDealershipRegistrationForm(forms.ModelForm):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = UserRegistration
|
model = UserRegistration
|
||||||
fields = ("name","arabic_name", "email","phone_number", "crn", "vrn", "address")
|
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"}),
|
||||||
|
)
|
||||||
@ -910,6 +910,16 @@ class Car(Base):
|
|||||||
)
|
)
|
||||||
self.save()
|
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
|
@property
|
||||||
def logo(self):
|
def logo(self):
|
||||||
return self.id_car_make.logo.url if self.id_car_make.logo else None
|
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
|
# max_length=255, unique=True, editable=False, null=True, blank=True,allow_unicode=True
|
||||||
# )
|
# )
|
||||||
slug = RandomCharField(length=8, unique=True)
|
slug = RandomCharField(length=8, unique=True)
|
||||||
|
|
||||||
objects = StaffUserManager()
|
objects = StaffUserManager()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@ -2487,12 +2497,14 @@ class Opportunity(models.Model):
|
|||||||
]
|
]
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
if self.customer:
|
try:
|
||||||
return (
|
if self.customer:
|
||||||
f"Opportunity for {self.customer.first_name} {self.customer.last_name}"
|
return (
|
||||||
)
|
f"Opportunity for {self.customer.first_name} {self.customer.last_name}"
|
||||||
return f"Opportunity for {self.organization.name}"
|
)
|
||||||
|
return f"Opportunity for {self.organization.name}"
|
||||||
|
except Exception:
|
||||||
|
return f"Opportunity for car :{self.car}"
|
||||||
|
|
||||||
class Notes(models.Model):
|
class Notes(models.Model):
|
||||||
dealer = models.ForeignKey(Dealer, on_delete=models.CASCADE, related_name="notes")
|
dealer = models.ForeignKey(Dealer, on_delete=models.CASCADE, related_name="notes")
|
||||||
|
|||||||
@ -342,6 +342,11 @@ urlpatterns = [
|
|||||||
views.CarDetailView.as_view(),
|
views.CarDetailView.as_view(),
|
||||||
name="car_detail",
|
name="car_detail",
|
||||||
),
|
),
|
||||||
|
path(
|
||||||
|
"<slug:dealer_slug>/cars/<slug:slug>/estimate/",
|
||||||
|
views.create_estimate_for_car,
|
||||||
|
name="create_estimate_for_car",
|
||||||
|
),
|
||||||
path("cars/<slug:slug>/history/", views.car_history, name="car_history"),
|
path("cars/<slug:slug>/history/", views.car_history, name="car_history"),
|
||||||
path(
|
path(
|
||||||
"<slug:dealer_slug>/cars/<slug:slug>/update/",
|
"<slug:dealer_slug>/cars/<slug:slug>/update/",
|
||||||
@ -789,7 +794,7 @@ urlpatterns = [
|
|||||||
),
|
),
|
||||||
path(
|
path(
|
||||||
"<slug:dealer_slug>/sales/estimates/create/<slug:slug>/",
|
"<slug:dealer_slug>/sales/estimates/create/<slug:slug>/",
|
||||||
views.create_estimate,
|
views.estimate_create_from_opportunity,
|
||||||
name="estimate_create_from_opportunity",
|
name="estimate_create_from_opportunity",
|
||||||
),
|
),
|
||||||
path(
|
path(
|
||||||
@ -938,7 +943,7 @@ urlpatterns = [
|
|||||||
views.ItemServiceUpdateView.as_view(),
|
views.ItemServiceUpdateView.as_view(),
|
||||||
name="item_service_update",
|
name="item_service_update",
|
||||||
),
|
),
|
||||||
|
|
||||||
path(
|
path(
|
||||||
"<slug:dealer_slug>/items/services/<int:pk>/detail/",
|
"<slug:dealer_slug>/items/services/<int:pk>/detail/",
|
||||||
views.ItemServiceDetailView.as_view(),
|
views.ItemServiceDetailView.as_view(),
|
||||||
@ -962,7 +967,7 @@ urlpatterns = [
|
|||||||
),
|
),
|
||||||
path(
|
path(
|
||||||
"<slug:dealer_slug>/items/expeneses/<uuid:pk>/detail/",
|
"<slug:dealer_slug>/items/expeneses/<uuid:pk>/detail/",
|
||||||
views.ItemExpenseDetailView.as_view(),
|
views.ItemExpenseDetailView.as_view(),
|
||||||
name="item_expense_detail",
|
name="item_expense_detail",
|
||||||
),
|
),
|
||||||
# Bills
|
# Bills
|
||||||
|
|||||||
@ -2815,3 +2815,36 @@ def generate_car_image_simple(car):
|
|||||||
error_msg = f"Image processing failed: {e}"
|
error_msg = f"Image processing failed: {e}"
|
||||||
logger.error(error_msg)
|
logger.error(error_msg)
|
||||||
return {"success": False, "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
|
||||||
@ -204,6 +204,7 @@ from .services import (
|
|||||||
)
|
)
|
||||||
from .utils import (
|
from .utils import (
|
||||||
CarFinanceCalculator,
|
CarFinanceCalculator,
|
||||||
|
create_estimate_,
|
||||||
get_car_finance_data,
|
get_car_finance_data,
|
||||||
get_finance_data,
|
get_finance_data,
|
||||||
get_item_transactions,
|
get_item_transactions,
|
||||||
@ -1737,6 +1738,15 @@ class CarDetailView(LoginRequiredMixin, PermissionRequiredMixin, DetailView):
|
|||||||
context_object_name = "car"
|
context_object_name = "car"
|
||||||
permission_required = ["inventory.view_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):
|
def CarFinanceUpdateView(request,dealer_slug,slug):
|
||||||
car = get_object_or_404(models.Car, slug=slug)
|
car = get_object_or_404(models.Car, slug=slug)
|
||||||
@ -7239,6 +7249,7 @@ class OpportunityCreateView(
|
|||||||
instance = form.save(commit=False)
|
instance = form.save(commit=False)
|
||||||
instance.dealer = dealer
|
instance.dealer = dealer
|
||||||
instance.staff = instance.lead.staff
|
instance.staff = instance.lead.staff
|
||||||
|
instance.customer = instance.lead.customer
|
||||||
instance.save()
|
instance.save()
|
||||||
instance.lead.convert_to_customer()
|
instance.lead.convert_to_customer()
|
||||||
instance.lead.save()
|
instance.lead.save()
|
||||||
@ -12001,3 +12012,70 @@ def payment_result(request):
|
|||||||
if s == "success":
|
if s == "success":
|
||||||
return render(request, 'plans/payment_success.html')
|
return render(request, 'plans/payment_success.html')
|
||||||
return render(request, 'plans/payment_failed.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)
|
||||||
@ -30,7 +30,9 @@
|
|||||||
{% else %}
|
{% else %}
|
||||||
{% if perms.django_ledger.add_estimatemodel %}
|
{% if perms.django_ledger.add_estimatemodel %}
|
||||||
<a class="dropdown-item"
|
<a class="dropdown-item"
|
||||||
href="{% url 'estimate_create_from_opportunity' request.dealer.slug opportunity.slug %}">{{ _("Create Quotation") }}</a>
|
type="button"
|
||||||
|
data-bs-toggle="modal"
|
||||||
|
data-bs-target="#estimateModal">{% trans "Create Estimate" %}</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</li>
|
</li>
|
||||||
@ -854,6 +856,33 @@
|
|||||||
{% include "components/note_modal.html" with content_type="opportunity" slug=opportunity.slug %}
|
{% include "components/note_modal.html" with content_type="opportunity" slug=opportunity.slug %}
|
||||||
<!-- schedule Modal -->
|
<!-- schedule Modal -->
|
||||||
{% include "components/schedule_modal.html" with content_type="opportunity" slug=opportunity.slug %}
|
{% include "components/schedule_modal.html" with content_type="opportunity" slug=opportunity.slug %}
|
||||||
|
|
||||||
|
<div class="modal fade"
|
||||||
|
id="estimateModal"
|
||||||
|
tabindex="-1"
|
||||||
|
aria-labelledby="estimateModalLabel"
|
||||||
|
aria-hidden="true">
|
||||||
|
<div class="modal-dialog modal-sm">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title" id="estimateModalLabel">{% trans 'Create Estimate' %}</h5>
|
||||||
|
<button type="button"
|
||||||
|
class="btn-close"
|
||||||
|
data-bs-dismiss="modal"
|
||||||
|
aria-label="Close"></button>
|
||||||
|
</div>
|
||||||
|
<div id="estimateModalBody" class="main-modal-body" style="padding: 20px;">
|
||||||
|
<form action="{% url 'estimate_create_from_opportunity' request.dealer.slug opportunity.slug %}" method="post">
|
||||||
|
{% csrf_token %}
|
||||||
|
<p>{% trans 'Are you sure you want to create an estimate from this opportunity?' %}</p>
|
||||||
|
<button type="submit" class="btn btn-primary">{% trans 'Create Estimate' %}</button>
|
||||||
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">{% trans 'Cancel' %}</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
{% block customJS %}
|
{% block customJS %}
|
||||||
<script>
|
<script>
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
{% extends 'base.html' %}
|
{% extends 'base.html' %}
|
||||||
{% load i18n static custom_filters %}
|
{% load i18n static custom_filters %}
|
||||||
|
{% load crispy_forms_filters %}
|
||||||
{% load tenhal_tag %}
|
{% load tenhal_tag %}
|
||||||
{% block title %}{{ _("Car Details") }}{% endblock %}
|
{% block title %}{{ _("Car Details") }}{% endblock %}
|
||||||
{% block customCSS %}
|
{% block customCSS %}
|
||||||
@ -74,6 +75,27 @@
|
|||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<!-- Main row -->
|
<!-- Main row -->
|
||||||
|
{% if car.ready %}
|
||||||
|
<div class="d-flex align-items-center">
|
||||||
|
<button type="button"
|
||||||
|
class="btn btn-sm btn-phoenix-success mb-2 me-2"
|
||||||
|
data-bs-toggle="modal"
|
||||||
|
data-bs-target="#estimateModal">
|
||||||
|
{% trans 'Create Estimate' %}
|
||||||
|
</button>
|
||||||
|
<div class="d-flex align-items-center">
|
||||||
|
<span class="badge rounded-10 badge-phoenix-success">{{active_estimates|length}}</span>
|
||||||
|
<div class="d-flex flex-wrap gap-2 ms-2">
|
||||||
|
{% for e in active_estimates %}
|
||||||
|
<div class="d-flex flex-column gap-2">
|
||||||
|
<span class="badge rounded-10 badge-phoenix-success">{{e.content_object.estimate_number}}</span>
|
||||||
|
<span class="badge rounded-10 badge-phoenix-success">{{e.related_object}}</span>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
{% if perms.inventory.view_car %}
|
{% if perms.inventory.view_car %}
|
||||||
<div class="row-fluid {% if car.status == 'sold' %}disabled{% endif %}">
|
<div class="row-fluid {% if car.status == 'sold' %}disabled{% endif %}">
|
||||||
<div class="row g-3 justify-content-between">
|
<div class="row g-3 justify-content-between">
|
||||||
@ -509,6 +531,32 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- Main Modal -->
|
<!-- Main Modal -->
|
||||||
|
<div class="modal fade"
|
||||||
|
id="estimateModal"
|
||||||
|
tabindex="-1"
|
||||||
|
aria-labelledby="estimateModalLabel"
|
||||||
|
aria-hidden="true">
|
||||||
|
<div class="modal-dialog modal-md">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title" id="estimateModalLabel">{% trans 'Create Estimate' %}</h5>
|
||||||
|
<button type="button"
|
||||||
|
class="btn-close"
|
||||||
|
data-bs-dismiss="modal"
|
||||||
|
aria-label="Close"></button>
|
||||||
|
</div>
|
||||||
|
<div id="estimateModalBody" class="main-modal-body" style="padding: 20px;">
|
||||||
|
<form action="{% url 'create_estimate_for_car' request.dealer.slug car.slug %}" method="post">
|
||||||
|
{% csrf_token %}
|
||||||
|
{{estimate_form|crispy}}
|
||||||
|
<button type="submit" class="btn btn-primary">{% trans 'Create Estimate' %}</button>
|
||||||
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">{% trans 'Cancel' %}</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="modal fade"
|
<div class="modal fade"
|
||||||
id="mainModal"
|
id="mainModal"
|
||||||
tabindex="-1"
|
tabindex="-1"
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user