print pdf logic for po #268
@ -2264,4 +2264,12 @@ class CarDealershipRegistrationForm(forms.ModelForm):
|
||||
|
||||
class Meta:
|
||||
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"}),
|
||||
)
|
||||
@ -902,6 +902,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
|
||||
@ -1523,7 +1533,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
|
||||
@ -2479,12 +2489,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")
|
||||
|
||||
@ -342,6 +342,11 @@ urlpatterns = [
|
||||
views.CarDetailView.as_view(),
|
||||
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(
|
||||
"<slug:dealer_slug>/cars/<slug:slug>/update/",
|
||||
@ -789,7 +794,7 @@ urlpatterns = [
|
||||
),
|
||||
path(
|
||||
"<slug:dealer_slug>/sales/estimates/create/<slug:slug>/",
|
||||
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(
|
||||
"<slug:dealer_slug>/items/services/<int:pk>/detail/",
|
||||
views.ItemServiceDetailView.as_view(),
|
||||
@ -962,7 +967,7 @@ urlpatterns = [
|
||||
),
|
||||
path(
|
||||
"<slug:dealer_slug>/items/expeneses/<uuid:pk>/detail/",
|
||||
views.ItemExpenseDetailView.as_view(),
|
||||
views.ItemExpenseDetailView.as_view(),
|
||||
name="item_expense_detail",
|
||||
),
|
||||
# Bills
|
||||
|
||||
@ -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
|
||||
@ -206,6 +206,7 @@ from .services import (
|
||||
)
|
||||
from .utils import (
|
||||
CarFinanceCalculator,
|
||||
create_estimate_,
|
||||
get_car_finance_data,
|
||||
get_finance_data,
|
||||
get_item_transactions,
|
||||
@ -1739,6 +1740,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)
|
||||
@ -7241,6 +7251,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()
|
||||
@ -12023,3 +12034,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)
|
||||
@ -30,7 +30,9 @@
|
||||
{% else %}
|
||||
{% if perms.django_ledger.add_estimatemodel %}
|
||||
<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 %}
|
||||
</li>
|
||||
@ -854,6 +856,33 @@
|
||||
{% include "components/note_modal.html" with content_type="opportunity" slug=opportunity.slug %}
|
||||
<!-- schedule Modal -->
|
||||
{% 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 %}
|
||||
{% block customJS %}
|
||||
<script>
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
{% extends 'base.html' %}
|
||||
{% load i18n static custom_filters %}
|
||||
{% load crispy_forms_filters %}
|
||||
{% load tenhal_tag %}
|
||||
{% block title %}{{ _("Car Details") }}{% endblock %}
|
||||
{% block customCSS %}
|
||||
@ -74,6 +75,27 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
<!-- 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 %}
|
||||
<div class="row-fluid {% if car.status == 'sold' %}disabled{% endif %}">
|
||||
<div class="row g-3 justify-content-between">
|
||||
@ -509,6 +531,32 @@
|
||||
</div>
|
||||
</div>
|
||||
<!-- 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"
|
||||
id="mainModal"
|
||||
tabindex="-1"
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user