print pdf logic for po #268

Merged
ismail merged 10 commits from frontend into main 2025-09-17 14:14:57 +03:00
7 changed files with 225 additions and 12 deletions
Showing only changes of commit d7789704cf - Show all commits

View File

@ -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"}),
)

View File

@ -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")

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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>

View File

@ -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"