This commit is contained in:
Marwan Alwali 2024-12-22 18:20:12 +03:00
commit 1396294e4b
14 changed files with 897 additions and 447 deletions

View File

@ -26,7 +26,7 @@ SECRET_KEY = 'django-insecure-gc9bh4*3=b6hihdnaom0edjsbxh$5t)aap@e8p&340r7)*)qb8
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True
ALLOWED_HOSTS = ['10.10.1.109', 'localhost', '127.0.0.1', '192.168.1.109', '172.20.10.4']
ALLOWED_HOSTS = ['10.10.1.120', 'localhost', '127.0.0.1', '192.168.1.135', '172.20.10.4']
# Application definition
INSTALLED_APPS = [
@ -107,9 +107,9 @@ WSGI_APPLICATION = 'car_inventory.wsgi.application'
DATABASES = {
"default": {
"ENGINE": "django_prometheus.db.backends.postgresql",
"NAME": "haikal_app",
"USER": "f95166",
"PASSWORD": "Kfsh&rc9788",
"NAME": "haikal",
"USER": "haikal",
"PASSWORD": "haikal",
"HOST": "localhost",
"PORT": 5432,
}

View File

@ -0,0 +1,19 @@
# Generated by Django 4.2.17 on 2024-12-22 08:39
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('inventory', '0040_additionalservices_display_name'),
]
operations = [
migrations.AddField(
model_name='salequotation',
name='quotation_number',
field=models.CharField(default=1, max_length=10, unique=True),
preserve_default=False,
),
]

View File

@ -0,0 +1,26 @@
# Generated by Django 4.2.17 on 2024-12-22 10:36
from django.db import migrations, models
import django.db.models.deletion
import uuid
class Migration(migrations.Migration):
dependencies = [
('django_ledger', '0017_alter_accountmodel_unique_together_and_more'),
('inventory', '0041_salequotation_quotation_number'),
]
operations = [
migrations.AddField(
model_name='salequotation',
name='entity',
field=models.ForeignKey(default="cb12725d-3b89-4742-8668-05d825b0b1f0", on_delete=django.db.models.deletion.CASCADE, to='django_ledger.entitymodel'),
preserve_default=False,
),
migrations.AddField(
model_name='salequotation',
name='is_approved',
field=models.BooleanField(default=False),
),
]

View File

@ -0,0 +1,17 @@
# Generated by Django 4.2.17 on 2024-12-22 11:48
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('inventory', '0042_salequotation_entity_salequotation_is_approved'),
]
operations = [
migrations.RemoveField(
model_name='salequotation',
name='status',
),
]

View File

@ -36,7 +36,7 @@ class LocalizedNameMixin:
class AddDealerInstanceMixin:
def form_valid(self, form):
if form.is_valid():
form.instance.dealer = self.request.user.dealer.get_parent_or_self
form.instance.dealer = self.request.user.dealer.get_root_dealer
form.save()
return super().form_valid(form)
else:

View File

@ -1,3 +1,4 @@
import itertools
from uuid import uuid4
from django.conf import settings
from django.db import models, transaction
@ -24,9 +25,9 @@ from phonenumber_field.modelfields import PhoneNumberField
from django.contrib.contenttypes.models import ContentType
from django.utils.timezone import now
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
class CarMake(models.Model, LocalizedNameMixin):
id_car_make = models.AutoField(primary_key=True)
@ -538,7 +539,7 @@ class Dealer(models.Model, LocalizedNameMixin):
def is_parent(self):
return self.dealer_type == "Owner"
@property
def get_parent_or_self(self):
def get_root_dealer(self):
return self.parent_dealer if self.parent_dealer else self
# Vendor Model
@ -602,6 +603,8 @@ class Customer(models.Model):
@property
def get_full_name(self):
return f"{self.first_name} {self.middle_name} {self.last_name}"
class Organization(models.Model, LocalizedNameMixin):
@ -639,6 +642,8 @@ class Representative(models.Model, LocalizedNameMixin):
class SaleQuotation(models.Model):
quotation_number = models.CharField(max_length=10, unique=True)
STATUS_CHOICES = [
("DRAFT", _("Draft")),
("CONFIRMED", _("Confirmed")),
@ -647,6 +652,7 @@ class SaleQuotation(models.Model):
dealer = models.ForeignKey(
Dealer, on_delete=models.CASCADE, related_name="sales", null=True
)
entity = models.ForeignKey(EntityModel, on_delete=models.CASCADE)
customer = models.ForeignKey(
Customer,
on_delete=models.CASCADE,
@ -660,9 +666,10 @@ class SaleQuotation(models.Model):
verbose_name=_("Amount"),
)
remarks = models.TextField(blank=True, null=True, verbose_name=_("Remarks"))
status = models.CharField(
max_length=10, choices=STATUS_CHOICES, default="DRAFT", verbose_name=_("Status")
)
is_approved = models.BooleanField(default=False)
# status = models.CharField(
# max_length=10, choices=STATUS_CHOICES, default="DRAFT", verbose_name=_("Status")
# )
created_at = models.DateTimeField(auto_now_add=True, verbose_name=_("Created At"))
updated_at = models.DateTimeField(auto_now=True, verbose_name=_("Updated At"))
@ -671,30 +678,53 @@ class SaleQuotation(models.Model):
total_quantity = self.quotation_cars.aggregate(total=Sum('quantity'))['total']
return total_quantity or 0
# @property
# def total(self):
# total = self.quotation_cars.aggregate(total_price=Sum(F('car__finances__selling_price') * F('quantity')))
# if total:
# return float(total["total_price"]) * 0.15 + float(total["total_price"])
# return 0
@property
def total(self):
total = self.quotation_cars.aggregate(total_price=Sum(F('car__finances__selling_price') * F('quantity')))
if not total:
return 0
return total["total_price"]
@property
def total_vat(self):
if self.total:
return float(self.total) * 0.15 + float(self.total)
return 0
def confirm(self):
"""Confirm the quotation and lock financial details."""
if self.status != "DRAFT":
raise ValueError(_("Only draft quotations can be confirmed."))
self.status = "CONFIRMED"
self.save()
# def confirm(self):
# """Confirm the quotation and lock financial details."""
# if self.status != "DRAFT":
# raise ValueError(_("Only draft quotations can be confirmed."))
# self.status = "CONFIRMED"
# self.save()
def cancel(self):
"""Cancel the quotation."""
if self.status == "CONFIRMED":
raise ValueError(_("Cannot cancel a confirmed quotation."))
self.status = "CANCELED"
self.save()
# def cancel(self):
# """Cancel the quotation."""
# if self.status == "CONFIRMED":
# raise ValueError(_("Cannot cancel a confirmed quotation."))
# self.status = "CANCELED"
# self.save()
def __str__(self):
return f"Quotation #{self.id} for {self.customer}"
return f"Quotation #{self.quotation_number} for {self.customer}"
@property
def display_quotation_number(self):
return f"QN-{self.quotation_number}"
def save(self, *args, **kwargs):
if not self.quotation_number:
self.quotation_number = str(next(self._get_quotation_number())).zfill(6)
super().save(*args, **kwargs)
@classmethod
def _get_quotation_number(cls):
last_quotation = cls.objects.all().order_by('id').last()
if last_quotation:
last_quotation_number = int(last_quotation.quotation_number)
else:
last_quotation_number = 0
return itertools.count(last_quotation_number + 1)
class SaleQuotationCar(models.Model):
quotation = models.ForeignKey(
@ -733,14 +763,23 @@ class SaleQuotationCar(models.Model):
# "total_amount": car_finance.total,
}
# @property
# def total(self):
# """
# Calculate total price dynamically based on quantity and selling price.
# """
# if not self.car.finances:
# return Decimal("0.00")
# return self.car.finances.selling_price * self.quantity
@property
def total(self):
"""
Calculate total price dynamically based on quantity and selling price.
"""
if not self.car.finances:
return Decimal("0.00")
return self.car.finances.selling_price * self.quantity
@property
def total_vat(self):
"""
Calculate total price dynamically based on quantity and selling price.
"""
if not self.car.finances:
return Decimal("0.00")
price = float(self.car.finances.selling_price * self.quantity)
return (price * 0.15) + price
def __str__(self):
return f"{self.car} - Quotation #{self.quotation.id}"

View File

@ -15,11 +15,19 @@ from django.utils.translation import gettext_lazy as _
from . import models
@receiver(pre_delete, sender=models.Dealer)
def remove_user_account(sender, instance, **kwargs):
user = instance.user
if user:
user.delete()
# @receiver(post_save, sender=models.SaleQuotation)
# def link_quotation_to_entity(sender, instance, created, **kwargs):
# if created:
# # Get the corresponding Django Ledger entity for the dealer
# entity = EntityModel.objects.get(name=instance.dealer.get_root_dealer.name)
# instance.entity = entity
# instance.save()
# @receiver(pre_delete, sender=models.Dealer)
# def remove_user_account(sender, instance, **kwargs):
# user = instance.user
# if user:
# user.delete()
@receiver(post_save, sender=models.Car)
def create_car_location(sender, instance, created, **kwargs):
"""
@ -61,16 +69,23 @@ def update_car_status_on_reservation_delete(sender, instance, **kwargs):
@receiver(post_save, sender=models.Dealer)
def create_ledger_entity(sender, instance, created, **kwargs):
if created:
entity = EntityModel.objects.create(
name=instance.name,
admin=instance.user,
entity, created = EntityModel.objects.get_or_create(
name=instance.get_root_dealer.name,
admin=instance.get_root_dealer.user,
# address_1=instance.address,
accrual_method=False,
fy_start_month=1,
# depth=0,
)
default_coa = entity.create_chart_of_accounts(assign_as_default=True,
print(entity)
if created:
default_coa = entity.create_chart_of_accounts(assign_as_default=True,
commit=True,
coa_name=_("Chart of Accounts"))
if default_coa:
entity.populate_default_coa(activate_accounts=True, coa_model=default_coa)
print(f"Ledger entity created for Dealer: {instance.name}")
# entity.create_account(
# coa_model=coa,
# code=1010,
@ -118,15 +133,12 @@ def create_ledger_entity(sender, instance, created, **kwargs):
# active=True)
if default_coa:
entity.populate_default_coa(activate_accounts=True, coa_model=default_coa)
# uom_name = _("Unit")
# unit_abbr = _("U")
#
# entity.create_uom(uom_name, unit_abbr)
print(f"Ledger entity created for Dealer: {instance.name}")
# Create Vendor
@ -155,21 +167,21 @@ def create_ledger_vendor(sender, instance, created, **kwargs):
@receiver(post_save, sender=models.Customer)
def create_customer(sender, instance, created, **kwargs):
if created:
entity = EntityModel.objects.filter(name=instance.dealer.name).first()
entity = EntityModel.objects.filter(name=instance.dealer.get_root_dealer.name).first()
name = f"{instance.first_name} {instance.middle_name} {instance.last_name}"
entity.create_customer(
customer_name=name,
customer_number=instance.national_id,
address_1=instance.address,
phone=instance.phone_number,
email=instance.email,
sales_tax_rate=0.15,
active=True,
hidden=False,
additional_info={}
entity.create_customer(
customer_model_kwargs={
"customer_name": name,
"address_1": instance.address,
"phone": instance.phone_number,
"email": instance.email,
"sales_tax_rate": 0.15,
"active": True,
"hidden": False,
"additional_info": {}
}
)
print(f"Customer created: {name}")

View File

@ -76,6 +76,9 @@ urlpatterns = [
path('sales/quotations/', views.QuotationListView.as_view(), name='quotation_list'),
path('sales/quotations/<int:pk>/confirm/', views.confirm_quotation, name='confirm_quotation'),
path('sales/orders/detail/<int:order_id>/', views.SalesOrderDetailView.as_view(), name='order_detail'),
path('quotation/<pk>/pdf/', views.quotation_pdf_view, name='quotation_pdf'),
path('generate_invoice/<int:pk>/', views.generate_invoice, name='generate_invoice'),
# Users URLs
path('user/create/', views.UserCreateView.as_view(), name='user_create'),

File diff suppressed because it is too large Load Diff

0
quotation.pdf Normal file
View File

View File

@ -24,6 +24,10 @@
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11.10.5/dist/sweetalert2.all.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jspdf/1.5.3/jspdf.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jspdf-autotable/3.8.4/jspdf.plugin.autotable.min.js" integrity="sha512-PRJxIx+FR3gPzyBBl9cPt62DD7owFXVcfYv0CRNFAcLZeEYfht/PpPNTKHicPs+hQlULFhH2tTWdoxnd1UGu1g==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<script src="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/js/select2.min.js"></script>
<style>
body {
@ -38,8 +42,9 @@ small, .small {
text-transform: uppercase;
border-radius: 5px;
}
</style>
{% block extra_head %}{% endblock extra_head %}
</head>
<body>
{% include 'header.html' %}
@ -102,7 +107,14 @@ small, .small {
});
});
function save_as_pdf(){
const quotationHtml = document.getElementById('quotation-html').outerHTML;
const printWindow = window.open('', '', 'height=500,width=800');
printWindow.document.write(quotationHtml);
printWindow.document.close();
printWindow.print();
printWindow.close();
}
</script>
</body>
</html>

View File

@ -31,9 +31,9 @@
</div>
<div class="container mt-4">
<div class="card">
<div class="card" id="quotation-html">
<div class="card-header">
<h4>{% trans "Quotation Details" %} - {{ quotation.id }}</h4>
<h4>{% trans "Quotation Details" %} - {{ quotation.display_quotation_number }}</h4>
</div>
<div class="card-body">
<div class="row">
@ -47,7 +47,7 @@
</div>
<div class="col-md-6">
<h5>{% trans "Quotation Information" %}</h5>
<p><strong>{% trans "Quotation No" %}:</strong> {{ quotation.id }}</p>
<p><strong>{% trans "Quotation No" %}:</strong> {{ quotation.display_quotation_number }}</p>
<p><strong>{% trans "Date" %}:</strong> {{ quotation.created_at|date }}</p>
<p><strong>{% trans "Remarks" %}:</strong> {{ quotation.remarks }}</p>
</div>
@ -75,7 +75,7 @@
<td>{{ item.quantity }}</td>
<td>{{ item.car.finances.selling_price }}</td>
<td>{{ 0.15 }}</td>
<td>{{ item.total }}</td>
<td>{{ item.total_vat}}</td>
</tr>
{% endfor %}
</tbody>
@ -83,9 +83,9 @@
<tr>
<th colspan="3">{% trans "Totals" %}</th>
<th>{{ quotation.total_quantity }}</th>
<th>{{ total_sales_before_vat }}</th>
<th>{{ vat_amount }}</th>
<th>{{ total_sales_after_vat }}</th>
<th>{{ quotation.total }}</th>
<th>{{ vat_amount }}</th>
<th>{{ quotation.total_vat }}</th>
</tr>
</tfoot>
</table>
@ -114,8 +114,7 @@
<td>{{ total_cost }}</td>
<td>{{ total_vat }}</td>
<td>{{ total_cost_vat }}</td>
</tr>
</tr>
</tbody>
</table>
</div>
@ -123,7 +122,15 @@
<a href="{% url 'quotation_list' %}" class="btn btn-secondary">{% trans "Back to Quotations" %}</a>
{% if perms.inventory.change_carfinance and quotation.status == 'DRAFT' %}
<button class="btn btn-success" data-bs-toggle="modal" data-bs-target="#customCardModal">{% trans "Approve Quotation" %}</button>
{% endif %}
{% endif %}
<a href="{% url 'quotation_pdf' quotation.pk %}" class="btn btn-primary">Download as PDF</a>
{% if not quotation.is_approved %}
<a href="{% url 'confirm_quotation' quotation.pk %}" class="btn btn-success">Approve</a>
{% endif %}
{% if quotation.is_approved %}
<a href="{% url 'generate_invoice' quotation.pk %}" class="btn btn-success">Generate Invoice</a>
{% endif %}
</div>
</div>
</div>

View File

@ -11,6 +11,7 @@
<thead>
<tr>
<th>#</th>
<th>{% trans "Quotation Number" %}</th>
<th>{% trans "Customer" %}</th>
<th>{% trans "Total Cars" %}</th>
<th>{% trans "Total Amount" %}</th>
@ -22,18 +23,15 @@
{% for quotation in quotations %}
<tr>
<td>{{ forloop.counter }}</td>
<td>{{ quotation.quotation_number }}</td>
<td>{{ quotation.customer.get_full_name }}</td>
<td>{{ quotation.quotation_cars.count }}</td>
<td>{{ quotation.quotation_cars.get_financial_details.total_amount }}</td>
<td>{{ quotation.total_vat }}</td>
<td>
{% if quotation.status == 'DRAFT' %}
<span class="badge rounded-pill bg-light">{{ quotation.status }}</span>
{% elif quotation.status == 'PENDING' %}
<span class="badge rounded-pill bg-warning">{{ quotation.status }}</span>
{% elif quotation.status == 'CONFIRMED' %}
<span class="badge rounded-pill bg-success">{{ quotation.status }}</span>
{% elif quotation.status == 'CANCELED' %}
<span class="badge rounded-pill bg-danger">{{ quotation.status }}</span>
{% if quotation.is_approved %}
<span class="badge rounded-pill bg-success">Approved</span>
{% else %}
<span class="badge rounded-pill bg-warning">Pending For Approval</span>
{% endif %}
</td>
<td>{{ quotation.created_at|date:"d/m/Y H:i" }}</td>

View File

@ -0,0 +1,112 @@
{% load static %} {% load i18n %}
<!DOCTYPE html>
{% get_current_language as LANGUAGE_CODE %}
<html lang="{{ LANGUAGE_CODE }}" dir="{% if LANGUAGE_CODE == 'ar' %}rtl{% else %}ltr{% endif %}" data-bs-theme="dark">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="mobile-web-app-capable" content="yes" />
<title>{% block title %}{% trans 'HAIKAL' %}{% endblock %}</title>
<link href="{% static 'css/themes/cosmo/_variables.scss' %}" rel="stylesheet" />
<link href="{% static 'css/custom.css' %}" rel="stylesheet" />
{% if LANGUAGE_CODE == 'ar' %}
<link href="{% static 'css/themes/cosmo/bootstrap.rtl.css' %}" rel="stylesheet" />
{% else %}
<link href="{% static 'css/themes/cosmo/bootstrap.css' %}" rel="stylesheet" />
{% endif %}
<link href="{% static 'css/themes/cosmo/_bootswatch.scss' %}" rel="stylesheet" />
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css" />
</head>
<body>
<div class="container mt-4">
<div class="card" id="quotation-html">
<div class="card-header">
<h4>{% trans "Quotation Details" %} - {{ quotation.quotation_number }}</h4>
</div>
<div class="card-body">
<div class="row">
<div class="col-md-6">
<h5>{% trans "Customer Details" %}</h5>
<p>
<strong>{% trans "Name" %}:</strong>
{{ quotation.customer.get_full_name }}</p>
<p><strong>{% trans "Address" %}:</strong> {{ quotation.customer.address }}</p>
<p><strong>{% trans "VAT No" %}:</strong> {{ quotation.customer.vat_number }}</p>
</div>
<div class="col-md-6">
<h5>{% trans "Quotation Information" %}</h5>
<p><strong>{% trans "Quotation No" %}:</strong> {{ quotation.quotation_number }}</p>
<p><strong>{% trans "Date" %}:</strong> {{ quotation.created_at|date }}</p>
<p><strong>{% trans "Remarks" %}:</strong> {{ quotation.remarks }}</p>
</div>
</div>
<h5 class="mt-4">{% trans "Car Details" %}</h5>
<table class="table table-bordered">
<thead>
<tr>
<th>{% trans "VIN" %}</th>
<th>{% trans "Model" %}</th>
<th>{% trans "Year" %}</th>
<th>{% trans "Quantity" %}</th>
<th>{% trans "Price" %}</th>
<th>{% trans "VAT" %}</th>
<th>{% trans "Total" %}</th>
</tr>
</thead>
<tbody>
{% for item in quotation.quotation_cars.all %}
<tr>
<td>{{ item.car.vin }}</td>
<td>{{ item.car.id_car_model.get_local_name }}</td>
<td>{{ item.car.year }}</td>
<td>{{ item.quantity }}</td>
<td>{{ item.car.finances.selling_price }}</td>
<td>{{ 0.15 }}</td>
<td>{{ item.total_vat}}</td>
</tr>
{% endfor %}
</tbody>
<tfoot>
<tr>
<th colspan="3">{% trans "Totals" %}</th>
<th>{{ quotation.total_quantity }}</th>
<th>{{ quotation.total }}</th>
<th>{{ vat_amount }}</th>
<th>{{ quotation.total_vat }}</th>
</tr>
</tfoot>
</table>
<h5 class="mt-4">{% trans "Additional Costs" %}</h5>
<table class="table table-bordered">
<thead>
<tr>
<th>{% trans "Additions" %}</th>
<th>{% trans "Cost" %}</th>
<th>{% trans "VAT %" %}</th>
<th>{% trans "Total Cost with VAT" %}</th>
</tr>
</thead>
<tbody>
{% for service in services %}
<tr>
<td>{{service.name}}</td>
<td>{{ service.price }}</td>
<td>{{ service.vated }}</td>
<td>{{ service.total_price_vat }}</td>
</tr>
{% endfor %}
<tr>
<td></td>
<td>{{ total_cost }}</td>
<td>{{ total_vat }}</td>
<td>{{ total_cost_vat }}</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</body>
</html>