add history + some fixes
This commit is contained in:
parent
7a116cbaba
commit
e34109fd3e
29
inventory/migrations/0026_carhistory.py
Normal file
29
inventory/migrations/0026_carhistory.py
Normal file
@ -0,0 +1,29 @@
|
||||
# Generated by Django 4.2.17 on 2025-02-17 08:54
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('inventory', '0025_email_status'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='CarHistory',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('event_date', models.DateField()),
|
||||
('event_type', models.CharField(choices=[('PURCHASE', 'Purchase'), ('SALE', 'Sale'), ('TRANSFER', 'Transfer'), ('ACCIDENT', 'Accident'), ('MAINTENANCE', 'Maintenance'), ('SERVICE', 'Service'), ('OTHER', 'Other')], max_length=50)),
|
||||
('description', models.TextField(blank=True, null=True)),
|
||||
('mileage', models.IntegerField(blank=True, null=True)),
|
||||
('cost', models.DecimalField(blank=True, decimal_places=2, max_digits=10, null=True)),
|
||||
('car', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='history', to='inventory.car')),
|
||||
],
|
||||
options={
|
||||
'ordering': ['-event_date'],
|
||||
},
|
||||
),
|
||||
]
|
||||
20
inventory/migrations/0027_carhistory_dealer.py
Normal file
20
inventory/migrations/0027_carhistory_dealer.py
Normal file
@ -0,0 +1,20 @@
|
||||
# Generated by Django 4.2.17 on 2025-02-17 08:57
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('inventory', '0026_carhistory'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='carhistory',
|
||||
name='dealer',
|
||||
field=models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, related_name='history', to='inventory.dealer'),
|
||||
preserve_default=False,
|
||||
),
|
||||
]
|
||||
18
inventory/migrations/0028_carhistory_additional_info.py
Normal file
18
inventory/migrations/0028_carhistory_additional_info.py
Normal file
@ -0,0 +1,18 @@
|
||||
# Generated by Django 4.2.17 on 2025-02-17 09:01
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('inventory', '0027_carhistory_dealer'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='carhistory',
|
||||
name='additional_info',
|
||||
field=models.JSONField(blank=True, default=dict, null=True, verbose_name='Car History Additional Info'),
|
||||
),
|
||||
]
|
||||
@ -0,0 +1,21 @@
|
||||
# Generated by Django 4.2.17 on 2025-02-17 09:03
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('inventory', '0028_carhistory_additional_info'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name='carhistory',
|
||||
name='cost',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='carhistory',
|
||||
name='mileage',
|
||||
),
|
||||
]
|
||||
@ -0,0 +1,21 @@
|
||||
# Generated by Django 4.2.17 on 2025-02-17 09:50
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('inventory', '0029_remove_carhistory_cost_remove_carhistory_mileage'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='activity',
|
||||
name='activity_type',
|
||||
field=models.CharField(choices=[('call', 'Call'), ('sms', 'SMS'), ('email', 'Email'), ('whatsapp', 'WhatsApp'), ('visit', 'Visit'), ('add_car', 'Add Car'), ('sale_car', 'Sale Car'), ('reserve_car', 'Reserve Car'), ('transfer_car', 'Transfer Car'), ('remove_car', 'Remove Car'), ('create_quotation', 'Create Quotation'), ('cancel_quotation', 'Cancel Quotation'), ('create_order', 'Create Order'), ('cancel_order', 'Cancel Order'), ('create_invoice', 'Create Invoice'), ('cancel_invoice', 'Cancel Invoice')], max_length=50, verbose_name='Activity Type'),
|
||||
),
|
||||
migrations.DeleteModel(
|
||||
name='CarHistory',
|
||||
),
|
||||
]
|
||||
@ -436,10 +436,11 @@ class Car(models.Model):
|
||||
hash_object.update(f"{self.id_car_make.name}{self.id_car_model.name}{self.year}{self.id_car_serie.name}{self.id_car_trim.name}{color}".encode('utf-8'))
|
||||
return hash_object.hexdigest()
|
||||
|
||||
def mark_as_sold(self):
|
||||
def mark_as_sold(self,user):
|
||||
self.cancel_reservation()
|
||||
self.status = CarStatusChoices.SOLD
|
||||
self.status = CarStatusChoices.SOLD
|
||||
self.save()
|
||||
Activity.objects.create(content_object=self, notes="Car Sold",created_by=user,activity_type=ActionChoices.SALE_CAR)
|
||||
|
||||
def cancel_reservation(self):
|
||||
if self.reservations.exists():
|
||||
@ -996,7 +997,9 @@ class ActionChoices(models.TextChoices):
|
||||
WHATSAPP = "whatsapp", _("WhatsApp")
|
||||
VISIT = "visit", _("Visit")
|
||||
ADD_CAR = "add_car", _("Add Car")
|
||||
SALE_CAR = "sale_car", _("Sale Car")
|
||||
RESERVE_CAR = "reserve_car", _("Reserve Car")
|
||||
TRANSFER_CAR = "transfer_car", _("Transfer Car")
|
||||
REMOVE_CAR = "remove_car", _("Remove Car")
|
||||
CREATE_QUOTATION = "create_quotation", _("Create Quotation")
|
||||
CANCEL_QUOTATION = "cancel_quotation", _("Cancel Quotation")
|
||||
|
||||
@ -647,9 +647,11 @@ def create_customer_user(sender, instance, created, **kwargs):
|
||||
user = User.objects.create(
|
||||
username=instance.email,
|
||||
email=instance.email,
|
||||
first_name=instance.additional_info["customer_info"].get("first_name", ""),
|
||||
last_name=instance.additional_info["customer_info"].get("last_name", ""),
|
||||
)
|
||||
customer_info = instance.additional_info.get("customer_info",None)
|
||||
user.first_name = customer_info.get("first_name", None) if customer_info else ""
|
||||
user.last_name = customer_info.get("last_name", None) if customer_info else ""
|
||||
|
||||
user.set_unusable_password()
|
||||
user.save()
|
||||
instance.user = user
|
||||
|
||||
@ -199,6 +199,7 @@ urlpatterns = [
|
||||
path("cars/inventory/stats", views.inventory_stats_view, name="inventory_stats"),
|
||||
path("cars/inventory/list", views.CarListView.as_view(), name="car_list"),
|
||||
path("cars/<int:pk>/", views.CarDetailView.as_view(), name="car_detail"),
|
||||
path("cars/<int:pk>/history/", views.car_history, name="car_history"),
|
||||
path("cars/<int:pk>/update/", views.CarUpdateView.as_view(), name="car_update"),
|
||||
path("cars/<int:pk>/delete/", views.CarDeleteView.as_view(), name="car_delete"),
|
||||
path(
|
||||
|
||||
@ -456,7 +456,7 @@ class CarTransfer:
|
||||
|
||||
self._add_car_item_to_invoice()
|
||||
|
||||
def _add_car_item_to_invoice(self):
|
||||
def _add_car_item_to_invoice(self):
|
||||
self.item = self.from_dealer.entity.get_items_products().filter(name=self.car.vin).first()
|
||||
if not self.item:
|
||||
return
|
||||
@ -474,9 +474,10 @@ class CarTransfer:
|
||||
commit=True,
|
||||
operation=InvoiceModel.ITEMIZE_APPEND,
|
||||
)
|
||||
self.invoice.save()
|
||||
self.invoice.mark_as_review()
|
||||
self.invoice.mark_as_approved(self.from_dealer.entity.slug, self.from_dealer.entity.admin)
|
||||
|
||||
if self.invoice.can_review():
|
||||
self.invoice.mark_as_review()
|
||||
self.invoice.mark_as_approved(self.from_dealer.entity.slug, self.from_dealer.entity.admin)
|
||||
self.invoice.save()
|
||||
|
||||
def _create_product_in_receiver_ledger(self):
|
||||
@ -488,6 +489,7 @@ class CarTransfer:
|
||||
coa_model=self.to_dealer.entity.get_default_coa(),
|
||||
)
|
||||
|
||||
self.product.additional_info = {}
|
||||
self.product.additional_info.update({"car_info": self.car.to_dict()})
|
||||
self.product.save()
|
||||
|
||||
|
||||
@ -405,6 +405,10 @@ class CarCreateView(LoginRequiredMixin, CreateView):
|
||||
messages.success(self.request, "Car saved successfully.")
|
||||
return super().form_valid(form)
|
||||
|
||||
def car_history(request,pk):
|
||||
car = get_object_or_404(models.Car, pk=pk)
|
||||
activities = models.Activity.objects.filter(content_type__model="car", object_id=car.id)
|
||||
return render(request,'inventory/car_history.html',{"car":car,"activities":activities})
|
||||
|
||||
class AjaxHandlerView(LoginRequiredMixin, View):
|
||||
def get(self, request, *args, **kwargs):
|
||||
@ -1035,6 +1039,8 @@ def car_transfer_accept_reject(request, car_pk, transfer_pk):
|
||||
# success = CarTransfer(car, transfer)
|
||||
if success:
|
||||
messages.success(request, _("Car Transfer Completed successfully."))
|
||||
models.Activity.objects.create(content_object=car,notes=f"Transfered from {transfer.from_dealer} to {transfer.to_dealer}",created_by=request.user)
|
||||
|
||||
models.Notification.objects.create(
|
||||
user=transfer.from_dealer.user,
|
||||
message=f"Car transfer request from {transfer.to_dealer} is completed.",
|
||||
@ -2693,7 +2699,7 @@ def create_sale_order(request, pk):
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
models.Car.objects.get(vin=item.item_model.name).mark_as_sold()
|
||||
models.Car.objects.get(vin=item.item_model.name).mark_as_sold(user=request.user)
|
||||
|
||||
messages.success(request, "Sale Order created successfully")
|
||||
return redirect("estimate_detail", pk=pk)
|
||||
|
||||
@ -6,21 +6,21 @@
|
||||
|
||||
{% if not car.ready %}
|
||||
<div class="alert alert-outline-warning d-flex align-items-center" role="alert">
|
||||
<svg class="svg-inline--fa fa-circle-info text-warning fs-5 me-3" aria-hidden="true" focusable="false" data-prefix="fas" data-icon="circle-info" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" data-fa-i2svg=""><path fill="currentColor" d="M256 512A256 256 0 1 0 256 0a256 256 0 1 0 0 512zM216 336h24V272H216c-13.3 0-24-10.7-24-24s10.7-24 24-24h48c13.3 0 24 10.7 24 24v88h8c13.3 0 24 10.7 24 24s-10.7 24-24 24H216c-13.3 0-24-10.7-24-24s10.7-24 24-24zm40-208a32 32 0 1 1 0 64 32 32 0 1 1 0-64z"></path></svg><!-- <span class="fas fa-info-circle text-warning fs-5 me-3"></span> Font Awesome fontawesome.com -->
|
||||
<i class="fa-solid fa-circle-info fs-6"></i>
|
||||
<p class="mb-0 flex-1">{{ _("This car information is not complete , please add colors and finances before making it ready for sale .") }}</p>
|
||||
<button class="btn-close" type="button" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if car.get_transfer %}
|
||||
<div class="alert alert-outline-warning d-flex align-items-center" role="alert">
|
||||
<svg class="svg-inline--fa fa-circle-info text-warning fs-5 me-3" aria-hidden="true" focusable="false" data-prefix="fas" data-icon="circle-info" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" data-fa-i2svg=""><path fill="currentColor" d="M256 512A256 256 0 1 0 256 0a256 256 0 1 0 0 512zM216 336h24V272H216c-13.3 0-24-10.7-24-24s10.7-24 24-24h48c13.3 0 24 10.7 24 24v88h8c13.3 0 24 10.7 24 24s-10.7 24-24 24H216c-13.3 0-24-10.7-24-24s10.7-24 24-24zm40-208a32 32 0 1 1 0 64 32 32 0 1 1 0-64z"></path></svg><!-- <span class="fas fa-info-circle text-warning fs-5 me-3"></span> Font Awesome fontawesome.com -->
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if car.get_transfer %}
|
||||
<div class="alert alert-outline-warning d-flex align-items-center" role="alert">
|
||||
<i class="fa-solid fa-circle-info fs-6"></i>
|
||||
<p class="mb-0 flex-1">{{ _("This car is in transfer process to another dealer, please wait for the acceptance .") }}</p>
|
||||
<button class="btn-close" type="button" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if car.is_reserved %}
|
||||
<div class="alert alert-outline-warning d-flex align-items-center" role="alert">
|
||||
<svg class="svg-inline--fa fa-circle-info text-warning fs-5 me-3" aria-hidden="true" focusable="false" data-prefix="fas" data-icon="circle-info" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" data-fa-i2svg=""><path fill="currentColor" d="M256 512A256 256 0 1 0 256 0a256 256 0 1 0 0 512zM216 336h24V272H216c-13.3 0-24-10.7-24-24s10.7-24 24-24h48c13.3 0 24 10.7 24 24v88h8c13.3 0 24 10.7 24 24s-10.7 24-24 24H216c-13.3 0-24-10.7-24-24s10.7-24 24-24zm40-208a32 32 0 1 1 0 64 32 32 0 1 1 0-64z"></path></svg><!-- <span class="fas fa-info-circle text-warning fs-5 me-3"></span> Font Awesome fontawesome.com -->
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if car.is_reserved %}
|
||||
<div class="alert alert-outline-warning d-flex align-items-center" role="alert">
|
||||
<i class="fa-solid fa-circle-info fs-6"></i>
|
||||
<p class="mb-0 flex-1">{{ _("This car is reserved until ") }}{{ car.get_reservation.reserved_until }}</p>
|
||||
<button class="btn-close" type="button" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||
</div>
|
||||
|
||||
47
templates/inventory/car_history.html
Normal file
47
templates/inventory/car_history.html
Normal file
@ -0,0 +1,47 @@
|
||||
{% extends 'base.html' %}
|
||||
{% load i18n static custom_filters %}
|
||||
{% block title %}
|
||||
{{ _('Car Details') }}
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<div class="tab-content" id="myTabContent">
|
||||
<div class="tab-pane fade active show" id="tab-activity" role="tabpanel" aria-labelledby="activity-tab">
|
||||
<div class="row gx-xl-8 gx-xxl-11">
|
||||
<div class="col-xl-5 p-xxl-7">
|
||||
<div class="ms-xxl-3 d-none d-xl-block position-sticky" style="top: 30%"><img class="d-dark-none img-fluid" src="../assets/img/spot-illustrations/timeline.png" alt="" /><img class="d-light-none img-fluid" src="../assets/img/spot-illustrations/timeline-dark.png" alt="" /></div>
|
||||
</div>
|
||||
<div class="col-xl-12 scrollbar">
|
||||
<div>
|
||||
<h4 class="py-3 border-y mb-5 ms-8">{{_('History')}}</h4>
|
||||
<div class="timeline-basic mb-9">
|
||||
{% for activity in activities %}
|
||||
<div class="timeline-item">
|
||||
<div class="row g-3">
|
||||
<div class="col-auto">
|
||||
<div class="timeline-item-bar position-relative">
|
||||
<div class="icon-item icon-item-md rounded-7 border border-translucent"><span class="fa-solid fa-clipboard text-success fs-9"></span></div><span class="timeline-bar border-end border-dashed"></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col">
|
||||
<div class="d-flex justify-content-between">
|
||||
<div class="d-flex mb-2">
|
||||
<h6 class="lh-sm mb-0 me-2 text-body-secondary timeline-item-title">{{activity.content_object}}</h6>
|
||||
</div>
|
||||
<p class="text-body-quaternary fs-9 mb-0 text-nowrap timeline-time"><span class="fa-regular fa-clock me-1"></span>{{activity.created}}</p>
|
||||
</div>
|
||||
<h6 class="fs-10 fw-normal mb-3">by <a class="fw-semibold" href="#!">{{activity.created_by}}</a></h6>
|
||||
<p class="fs-9 text-body-secondary w-sm-60 mb-5">{{activity.notes}}.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@ -19,6 +19,8 @@
|
||||
<select class="form-control item" name="item[]" required>
|
||||
{% for item in items %}
|
||||
<option style="background-color: rgb({{ item.color }});" value="{{ item.hash }}">{{ item.make }} {{item.model}} {{item.serie}} {{item.trim}} {{item.color_name}}</option>
|
||||
{% empty %}
|
||||
<option disabled>{% trans "No Cars Found" %}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user