add history + some fixes

This commit is contained in:
gitea 2025-02-17 11:20:55 +00:00
parent 7a116cbaba
commit e34109fd3e
13 changed files with 192 additions and 20 deletions

View 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'],
},
),
]

View 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,
),
]

View 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'),
),
]

View File

@ -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',
),
]

View File

@ -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',
),
]

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View 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 %}

View File

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