Merge pull request 'Audit log changes' (#70) from frontend into main

Reviewed-on: #70
This commit is contained in:
ismail 2025-06-12 17:20:17 +03:00
commit cba8a39d1b
45 changed files with 1976 additions and 1537 deletions

View File

@ -1,10 +1,12 @@
#!/bin/sh
echo "Delete Old Migrations"
find ./inventory -type f -iname "00*.py" -delete
find ./haikalbot -type f -iname "00*.py" -delete
echo "Delete Old Cache"
find ./car_inventory -type d -iname "__pycache__"|xargs rm -rf
find ./inventory -type d -iname "__pycache__"|xargs rm -rf
find ./haikalbot -type d -iname "__pycache__"|xargs rm -rf
echo "Apply Base Migrate"

View File

@ -1,23 +0,0 @@
# Generated by Django 5.2.1 on 2025-05-25 23:01
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
]
operations = [
migrations.CreateModel(
name='ChatLog',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('user_message', models.TextField()),
('chatbot_response', models.TextField()),
('timestamp', models.DateTimeField(auto_now_add=True)),
],
),
]

View File

@ -1,22 +0,0 @@
# Generated by Django 5.2.1 on 2025-05-25 23:01
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
('haikalbot', '0001_initial'),
('inventory', '0001_initial'),
]
operations = [
migrations.AddField(
model_name='chatlog',
name='dealer',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='chatlogs', to='inventory.dealer'),
),
]

View File

@ -1,33 +0,0 @@
# Generated by Django 5.2.1 on 2025-05-26 00:28
import django.db.models.deletion
import django.utils.timezone
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('haikalbot', '0002_initial'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='AnalysisCache',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('prompt_hash', models.CharField(db_index=True, max_length=64)),
('dealer_id', models.IntegerField(blank=True, db_index=True, null=True)),
('created_at', models.DateTimeField(default=django.utils.timezone.now)),
('updated_at', models.DateTimeField(auto_now=True)),
('expires_at', models.DateTimeField()),
('result', models.JSONField()),
('user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
],
options={
'indexes': [models.Index(fields=['prompt_hash', 'dealer_id'], name='haikalbot_a_prompt__b98e1e_idx'), models.Index(fields=['expires_at'], name='haikalbot_a_expires_e790cd_idx')],
},
),
]

View File

@ -1,36 +0,0 @@
# Generated by Django 5.2.1 on 2025-05-26 08:17
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('haikalbot', '0003_analysiscache'),
('inventory', '0001_initial'),
]
operations = [
migrations.AlterModelOptions(
name='analysiscache',
options={'verbose_name_plural': 'Analysis caches'},
),
migrations.AlterModelOptions(
name='chatlog',
options={'ordering': ['-timestamp']},
),
migrations.AlterField(
model_name='analysiscache',
name='expires_at',
field=models.DateTimeField(db_index=True),
),
migrations.AlterField(
model_name='chatlog',
name='timestamp',
field=models.DateTimeField(auto_now_add=True, db_index=True),
),
migrations.AddIndex(
model_name='chatlog',
index=models.Index(fields=['dealer', 'timestamp'], name='haikalbot_c_dealer__6f8d63_idx'),
),
]

View File

@ -1,845 +0,0 @@
# Generated by Django 5.2.1 on 2025-05-25 23:01
import datetime
import django.core.validators
import django.db.models.deletion
import django.utils.timezone
import inventory.mixins
import inventory.models
import phonenumber_field.modelfields
import uuid
from decimal import Decimal
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
('appointment', '0001_initial'),
('auth', '0012_alter_user_first_name_max_length'),
('contenttypes', '0002_remove_content_type_name'),
('django_ledger', '0001_initial'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='CarEquipment',
fields=[
('id_car_equipment', models.AutoField(primary_key=True, serialize=False)),
('name', models.CharField(blank=True, max_length=255, null=True)),
('arabic_name', models.CharField(blank=True, max_length=255, null=True)),
('year_begin', models.IntegerField(blank=True, null=True)),
('slug', models.SlugField(blank=True, max_length=255, null=True, unique=True)),
],
options={
'verbose_name': 'Equipment',
},
bases=(models.Model, inventory.mixins.LocalizedNameMixin),
),
migrations.CreateModel(
name='CarMake',
fields=[
('id_car_make', models.AutoField(primary_key=True, serialize=False)),
('name', models.CharField(blank=True, max_length=255, null=True)),
('slug', models.SlugField(blank=True, max_length=255, null=True, unique=True)),
('arabic_name', models.CharField(blank=True, max_length=255, null=True)),
('logo', models.ImageField(blank=True, null=True, upload_to='car_make', verbose_name='logo')),
('is_sa_import', models.BooleanField(default=False)),
('car_type', models.SmallIntegerField(blank=True, choices=[(1, 'Car'), (2, 'Light Commercial'), (3, 'Heavy-Duty Tractors'), (4, 'Trailers'), (5, 'Medium Trucks'), (6, 'Buses'), (20, 'Motorcycles'), (21, 'Buggy'), (22, 'Moto ATV'), (23, 'Scooters'), (24, 'Karting'), (25, 'ATV'), (26, 'Snowmobiles')], null=True)),
],
options={
'verbose_name': 'Make',
},
bases=(models.Model, inventory.mixins.LocalizedNameMixin),
),
migrations.CreateModel(
name='ExteriorColors',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=255, verbose_name='Name')),
('arabic_name', models.CharField(max_length=255, verbose_name='Arabic Name')),
('rgb', models.CharField(blank=True, max_length=24, null=True, verbose_name='RGB')),
],
options={
'verbose_name': 'Exterior Colors',
'verbose_name_plural': 'Exterior Colors',
},
bases=(models.Model, inventory.mixins.LocalizedNameMixin),
),
migrations.CreateModel(
name='InteriorColors',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=255, verbose_name='Name')),
('arabic_name', models.CharField(max_length=255, verbose_name='Arabic Name')),
('rgb', models.CharField(blank=True, max_length=24, null=True, verbose_name='RGB')),
],
options={
'verbose_name': 'Interior Colors',
'verbose_name_plural': 'Interior Colors',
},
bases=(models.Model, inventory.mixins.LocalizedNameMixin),
),
migrations.CreateModel(
name='Payment',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('amount', models.DecimalField(decimal_places=2, max_digits=10, verbose_name='amount')),
('payment_method', models.CharField(choices=[('cash', 'cash'), ('credit', 'credit'), ('transfer', 'transfer'), ('debit', 'debit'), ('sadad', 'SADAD')], max_length=50, verbose_name='method')),
('reference_number', models.CharField(blank=True, max_length=100, null=True, verbose_name='reference number')),
('payment_date', models.DateField(auto_now_add=True, verbose_name='date')),
],
options={
'verbose_name': 'payment',
'verbose_name_plural': 'payments',
},
),
migrations.CreateModel(
name='VatRate',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('rate', models.DecimalField(decimal_places=2, default=Decimal('0.15'), max_digits=5)),
('is_active', models.BooleanField(default=True)),
('created_at', models.DateTimeField(auto_now_add=True)),
],
),
migrations.CreateModel(
name='AdditionalServices',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=255, verbose_name='Name')),
('arabic_name', models.CharField(max_length=255, verbose_name='Arabic Name')),
('description', models.TextField(verbose_name='Description')),
('price', models.DecimalField(decimal_places=2, max_digits=14, verbose_name='Price')),
('taxable', models.BooleanField(default=False, verbose_name='taxable')),
('uom', models.CharField(choices=[('EA', 'Each'), ('PR', 'Pair'), ('SET', 'Set'), ('GAL', 'Gallon'), ('L', 'Liter'), ('M', 'Meter'), ('KG', 'Kilogram'), ('HR', 'Hour'), ('BX', 'Box'), ('RL', 'Roll'), ('PKG', 'Package'), ('DZ', 'Dozen'), ('SQ_M', 'Square Meter'), ('PC', 'Piece'), ('BDL', 'Bundle')], max_length=10, verbose_name='Unit of Measurement')),
('item', models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='django_ledger.itemmodel', verbose_name='Item')),
],
options={
'verbose_name': 'Additional Services',
'verbose_name_plural': 'Additional Services',
},
bases=(models.Model, inventory.mixins.LocalizedNameMixin),
),
migrations.CreateModel(
name='Car',
fields=[
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True, verbose_name='Primary Key')),
('slug', models.SlugField(blank=True, help_text='Slug for the object. If not provided, it will be generated automatically.', null=True, unique=True, verbose_name='Slug')),
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')),
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Updated At')),
('vin', models.CharField(max_length=17, unique=True, verbose_name='VIN')),
('year', models.IntegerField(verbose_name='Year')),
('status', models.CharField(choices=[('available', 'Available'), ('sold', 'Sold'), ('hold', 'Hold'), ('damaged', 'Damaged'), ('reserved', 'Reserved'), ('transfer', 'Transfer')], default='available', max_length=10, verbose_name='Status')),
('stock_type', models.CharField(choices=[('new', 'New'), ('used', 'Used')], default='new', max_length=10, verbose_name='Stock Type')),
('remarks', models.TextField(blank=True, null=True, verbose_name='Remarks')),
('mileage', models.IntegerField(blank=True, null=True, verbose_name='Mileage')),
('receiving_date', models.DateTimeField(verbose_name='Receiving Date')),
('hash', models.CharField(blank=True, max_length=64, null=True, verbose_name='Hash')),
('item_model', models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.DO_NOTHING, to='django_ledger.itemmodel', verbose_name='Item Model')),
('id_car_make', models.ForeignKey(blank=True, db_column='id_car_make', null=True, on_delete=django.db.models.deletion.DO_NOTHING, to='inventory.carmake', verbose_name='Make')),
],
options={
'verbose_name': 'Car',
'verbose_name_plural': 'Cars',
},
),
migrations.CreateModel(
name='CarFinance',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('cost_price', models.DecimalField(decimal_places=2, max_digits=14, verbose_name='Cost Price')),
('selling_price', models.DecimalField(decimal_places=2, max_digits=14, verbose_name='Selling Price')),
('discount_amount', models.DecimalField(decimal_places=2, default=Decimal('0.00'), max_digits=14, verbose_name='Discount Amount')),
('additional_services', models.ManyToManyField(blank=True, related_name='additional_finances', to='inventory.additionalservices')),
('car', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='finances', to='inventory.car')),
],
options={
'verbose_name': 'Car Financial Details',
'verbose_name_plural': 'Car Financial Details',
},
),
migrations.CreateModel(
name='CarModel',
fields=[
('id_car_model', models.AutoField(primary_key=True, serialize=False)),
('name', models.CharField(blank=True, max_length=255, null=True)),
('arabic_name', models.CharField(blank=True, max_length=255, null=True)),
('slug', models.SlugField(blank=True, max_length=255, null=True, unique=True)),
('id_car_make', models.ForeignKey(db_column='id_car_make', on_delete=django.db.models.deletion.DO_NOTHING, to='inventory.carmake')),
],
options={
'verbose_name': 'Model',
},
bases=(models.Model, inventory.mixins.LocalizedNameMixin),
),
migrations.AddField(
model_name='car',
name='id_car_model',
field=models.ForeignKey(blank=True, db_column='id_car_model', null=True, on_delete=django.db.models.deletion.DO_NOTHING, to='inventory.carmodel', verbose_name='Model'),
),
migrations.CreateModel(
name='CarOption',
fields=[
('id_car_option', models.AutoField(primary_key=True, serialize=False)),
('name', models.CharField(blank=True, max_length=255, null=True)),
('arabic_name', models.CharField(blank=True, max_length=255, null=True)),
('slug', models.SlugField(blank=True, max_length=255, null=True, unique=True)),
('id_parent', models.ForeignKey(blank=True, db_column='id_parent', null=True, on_delete=django.db.models.deletion.DO_NOTHING, to='inventory.caroption')),
],
options={
'verbose_name': 'Option',
},
bases=(models.Model, inventory.mixins.LocalizedNameMixin),
),
migrations.CreateModel(
name='CarOptionValue',
fields=[
('id_car_option_value', models.AutoField(primary_key=True, serialize=False)),
('value', models.CharField(max_length=500)),
('unit', models.CharField(blank=True, max_length=255, null=True)),
('is_base', models.IntegerField()),
('id_car_equipment', models.ForeignKey(db_column='id_car_equipment', on_delete=django.db.models.deletion.DO_NOTHING, to='inventory.carequipment')),
('id_car_option', models.ForeignKey(db_column='id_car_option', on_delete=django.db.models.deletion.DO_NOTHING, to='inventory.caroption')),
],
options={
'verbose_name': 'Option Value',
},
),
migrations.CreateModel(
name='CarRegistration',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('plate_number', models.IntegerField(verbose_name='Plate Number')),
('text1', models.CharField(max_length=1, verbose_name='Text 1')),
('text2', models.CharField(blank=True, max_length=1, null=True, verbose_name='Text 2')),
('text3', models.CharField(blank=True, max_length=1, null=True, verbose_name='Text 3')),
('registration_date', models.DateTimeField(verbose_name='Registration Date')),
('car', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='registrations', to='inventory.car', verbose_name='Car')),
],
options={
'verbose_name': 'Registration',
'verbose_name_plural': 'Registrations',
},
),
migrations.CreateModel(
name='CarSerie',
fields=[
('id_car_serie', models.AutoField(primary_key=True, serialize=False)),
('name', models.CharField(blank=True, max_length=255, null=True)),
('arabic_name', models.CharField(blank=True, max_length=255, null=True)),
('year_begin', models.IntegerField(blank=True, null=True)),
('year_end', models.IntegerField(blank=True, null=True)),
('generation_name', models.CharField(blank=True, max_length=255, null=True)),
('slug', models.SlugField(blank=True, max_length=255, null=True, unique=True)),
('id_car_model', models.ForeignKey(db_column='id_car_model', on_delete=django.db.models.deletion.DO_NOTHING, to='inventory.carmodel')),
],
options={
'verbose_name': 'Series',
},
bases=(models.Model, inventory.mixins.LocalizedNameMixin),
),
migrations.AddField(
model_name='car',
name='id_car_serie',
field=models.ForeignKey(blank=True, db_column='id_car_serie', null=True, on_delete=django.db.models.deletion.DO_NOTHING, to='inventory.carserie', verbose_name='Series'),
),
migrations.CreateModel(
name='CarSpecification',
fields=[
('id_car_specification', models.AutoField(primary_key=True, serialize=False)),
('name', models.CharField(max_length=255)),
('arabic_name', models.CharField(max_length=255)),
('slug', models.SlugField(blank=True, max_length=255, null=True, unique=True)),
('id_parent', models.ForeignKey(blank=True, db_column='id_parent', null=True, on_delete=django.db.models.deletion.DO_NOTHING, to='inventory.carspecification')),
],
options={
'verbose_name': 'Specification',
},
bases=(models.Model, inventory.mixins.LocalizedNameMixin),
),
migrations.CreateModel(
name='CarTrim',
fields=[
('id_car_trim', models.AutoField(primary_key=True, serialize=False)),
('name', models.CharField(blank=True, max_length=255, null=True)),
('arabic_name', models.CharField(blank=True, max_length=255, null=True)),
('start_production_year', models.IntegerField(blank=True, null=True)),
('end_production_year', models.IntegerField(blank=True, null=True)),
('slug', models.SlugField(blank=True, max_length=255, null=True, unique=True)),
('id_car_serie', models.ForeignKey(db_column='id_car_serie', on_delete=django.db.models.deletion.DO_NOTHING, to='inventory.carserie')),
],
options={
'verbose_name': 'Trim',
},
bases=(models.Model, inventory.mixins.LocalizedNameMixin),
),
migrations.CreateModel(
name='CarSpecificationValue',
fields=[
('id_car_specification_value', models.AutoField(primary_key=True, serialize=False)),
('value', models.CharField(max_length=500)),
('unit', models.CharField(blank=True, max_length=255, null=True)),
('id_car_specification', models.ForeignKey(db_column='id_car_specification', on_delete=django.db.models.deletion.DO_NOTHING, to='inventory.carspecification')),
('id_car_trim', models.ForeignKey(db_column='id_car_trim', on_delete=django.db.models.deletion.DO_NOTHING, to='inventory.cartrim')),
],
options={
'verbose_name': 'Specification Value',
},
),
migrations.AddField(
model_name='carequipment',
name='id_car_trim',
field=models.ForeignKey(db_column='id_car_trim', on_delete=django.db.models.deletion.DO_NOTHING, to='inventory.cartrim'),
),
migrations.AddField(
model_name='car',
name='id_car_trim',
field=models.ForeignKey(blank=True, db_column='id_car_trim', null=True, on_delete=django.db.models.deletion.DO_NOTHING, to='inventory.cartrim', verbose_name='Trim'),
),
migrations.CreateModel(
name='CustomCard',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('custom_number', models.CharField(max_length=255, verbose_name='Custom Number')),
('custom_date', models.DateField(verbose_name='Custom Date')),
('car', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='custom_cards', to='inventory.car', verbose_name='Car')),
],
options={
'verbose_name': 'Custom Card',
'verbose_name_plural': 'Custom Cards',
},
),
migrations.CreateModel(
name='Dealer',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('crn', models.CharField(blank=True, max_length=10, null=True, verbose_name='Commercial Registration Number')),
('vrn', models.CharField(blank=True, max_length=15, null=True, verbose_name='VAT Registration Number')),
('arabic_name', models.CharField(max_length=255, verbose_name='Arabic Name')),
('name', models.CharField(max_length=255, verbose_name='English Name')),
('phone_number', phonenumber_field.modelfields.PhoneNumberField(max_length=128, region='SA', verbose_name='Phone Number')),
('address', models.CharField(blank=True, max_length=200, null=True, verbose_name='Address')),
('logo', models.ImageField(blank=True, null=True, upload_to='logos/users', verbose_name='Logo')),
('joined_at', models.DateTimeField(auto_now_add=True, verbose_name='Joined At')),
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Updated At')),
('slug', models.SlugField(blank=True, max_length=255, null=True, unique=True)),
('entity', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='django_ledger.entitymodel')),
('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='dealer', to=settings.AUTH_USER_MODEL)),
],
options={
'verbose_name': 'Dealer',
'verbose_name_plural': 'Dealers',
},
bases=(models.Model, inventory.mixins.LocalizedNameMixin),
managers=[
('objects', inventory.models.DealerUserManager()),
],
),
migrations.CreateModel(
name='CustomGroup',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=100)),
('group', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to='auth.group', verbose_name='Group')),
('dealer', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='groups', to='inventory.dealer')),
],
),
migrations.CreateModel(
name='Customer',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('title', models.CharField(choices=[('mr', 'Mr'), ('mrs', 'Mrs'), ('ms', 'Ms'), ('miss', 'Miss'), ('dr', 'Dr'), ('prof', 'Prof'), ('prince', 'Prince'), ('princess', 'Princess'), ('company', 'Company'), ('na', 'N/A')], default='na', max_length=10, verbose_name='Title')),
('first_name', models.CharField(max_length=50, verbose_name='First Name')),
('last_name', models.CharField(max_length=50, verbose_name='Last Name')),
('gender', models.CharField(choices=[('m', 'Male'), ('f', 'Female')], max_length=1, verbose_name='Gender')),
('dob', models.DateField(blank=True, null=True, verbose_name='Date of Birth')),
('email', models.EmailField(max_length=254, unique=True, verbose_name='Email')),
('national_id', models.CharField(blank=True, max_length=10, null=True, unique=True, verbose_name='National ID')),
('phone_number', phonenumber_field.modelfields.PhoneNumberField(max_length=128, region='SA', unique=True, verbose_name='Phone Number')),
('address', models.CharField(blank=True, max_length=200, null=True, verbose_name='Address')),
('active', models.BooleanField(default=True, verbose_name='Active')),
('image', models.ImageField(blank=True, null=True, upload_to='customers/', verbose_name='Image')),
('created', models.DateTimeField(auto_now_add=True, verbose_name='Created')),
('updated', models.DateTimeField(auto_now=True, verbose_name='Updated')),
('slug', models.SlugField(blank=True, editable=False, max_length=255, null=True, unique=True)),
('customer_model', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='django_ledger.customermodel')),
('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='customer_profile', to=settings.AUTH_USER_MODEL)),
('dealer', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='customers', to='inventory.dealer')),
],
options={
'verbose_name': 'Customer',
'verbose_name_plural': 'Customers',
},
),
migrations.CreateModel(
name='CarTransfer',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('transfer_date', models.DateTimeField(auto_now_add=True, verbose_name='Transfer Date')),
('quantity', models.IntegerField(default=1, verbose_name='Quantity')),
('remarks', models.TextField(blank=True, null=True, verbose_name='Remarks')),
('status', models.CharField(default='draft', max_length=10, verbose_name=[('draft', 'Draft'), ('approved', 'Approved'), ('pending', 'Pending'), ('accepted', 'Accepted'), ('success', 'Success'), ('reject', 'Reject'), ('cancelled', 'Cancelled')])),
('is_approved', models.BooleanField(default=False)),
('active', models.BooleanField(default=True)),
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')),
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Updated At')),
('car', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='transfer_logs', to='inventory.car', verbose_name='Car')),
('from_dealer', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='transfers_out', to='inventory.dealer', verbose_name='From Dealer')),
('to_dealer', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='transfers_in', to='inventory.dealer', verbose_name='To Dealer')),
],
options={
'verbose_name': 'Car Transfer Log',
'verbose_name_plural': 'Car Transfer Logs',
},
),
migrations.CreateModel(
name='CarLocation',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('description', models.TextField(blank=True, help_text='Optional description about the showroom placement.', null=True, verbose_name='Description')),
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')),
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Last Updated')),
('car', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='location', to='inventory.car', verbose_name='Car')),
('owner', models.ForeignKey(help_text='Dealer who owns the car.', on_delete=django.db.models.deletion.CASCADE, related_name='owned_cars', to='inventory.dealer', verbose_name='Owner')),
('showroom', models.ForeignKey(help_text='Dealer where the car is displayed (can be the owner).', on_delete=django.db.models.deletion.CASCADE, related_name='showroom_cars', to='inventory.dealer', verbose_name='Showroom')),
],
options={
'verbose_name': 'Car Location',
'verbose_name_plural': 'Car Locations',
},
),
migrations.AddField(
model_name='car',
name='dealer',
field=models.ForeignKey(on_delete=django.db.models.deletion.DO_NOTHING, related_name='cars', to='inventory.dealer', verbose_name='Dealer'),
),
migrations.AddField(
model_name='additionalservices',
name='dealer',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='inventory.dealer', verbose_name='Dealer'),
),
migrations.CreateModel(
name='Activity',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('object_id', models.PositiveIntegerField()),
('activity_type', models.CharField(choices=[('call', 'Call'), ('sms', 'SMS'), ('email', 'Email'), ('meeting', 'Meeting'), ('whatsapp', 'WhatsApp'), ('visit', 'Visit'), ('negotiation', 'Negotiation'), ('follow_up', 'Follow Up'), ('won', 'Won'), ('lost', 'Lost'), ('closed', 'Closed'), ('converted', 'Converted'), ('transfer', 'Transfer'), ('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')),
('notes', models.TextField(blank=True, null=True, verbose_name='Notes')),
('created', models.DateTimeField(auto_now_add=True, verbose_name='Created')),
('updated', models.DateTimeField(auto_now=True, verbose_name='Updated')),
('content_type', models.ForeignKey(on_delete=django.db.models.deletion.DO_NOTHING, to='contenttypes.contenttype')),
('created_by', models.ForeignKey(on_delete=django.db.models.deletion.DO_NOTHING, related_name='activities_created_by', to=settings.AUTH_USER_MODEL)),
('dealer', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='activities', to='inventory.dealer')),
],
options={
'verbose_name': 'Activity',
'verbose_name_plural': 'Activities',
},
),
migrations.CreateModel(
name='DealerSettings',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('additional_info', models.JSONField(blank=True, default=dict, null=True)),
('bill_cash_account', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='bill_cash', to='django_ledger.accountmodel')),
('bill_prepaid_account', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='bill_prepaid', to='django_ledger.accountmodel')),
('bill_unearned_account', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='bill_unearned', to='django_ledger.accountmodel')),
('dealer', models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='settings', to='inventory.dealer')),
('invoice_cash_account', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='invoice_cash', to='django_ledger.accountmodel')),
('invoice_prepaid_account', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='invoice_prepaid', to='django_ledger.accountmodel')),
('invoice_unearned_account', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='invoice_unearned', to='django_ledger.accountmodel')),
],
),
migrations.CreateModel(
name='Email',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('object_id', models.UUIDField()),
('from_email', models.TextField(blank=True, null=True, verbose_name='From Email')),
('to_email', models.TextField(blank=True, null=True, verbose_name='To Email')),
('subject', models.TextField(blank=True, null=True, verbose_name='Subject')),
('message', models.TextField(blank=True, null=True, verbose_name='Message')),
('status', models.CharField(choices=[('SENT', 'Sent'), ('FAILED', 'Failed'), ('DELIVERED', 'Delivered'), ('OPEN', 'Open'), ('DRAFT', 'Draft')], default='OPEN', max_length=20, verbose_name='Status')),
('created', models.DateTimeField(auto_now_add=True, verbose_name='Created')),
('updated', models.DateTimeField(auto_now=True, verbose_name='Updated')),
('content_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='contenttypes.contenttype')),
('created_by', models.ForeignKey(on_delete=django.db.models.deletion.DO_NOTHING, related_name='emails_created', to=settings.AUTH_USER_MODEL)),
],
options={
'verbose_name': 'Email',
'verbose_name_plural': 'Emails',
},
),
migrations.CreateModel(
name='Notes',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('object_id', models.UUIDField()),
('note', models.TextField(verbose_name='Note')),
('created', models.DateTimeField(auto_now_add=True, verbose_name='Created')),
('updated', models.DateTimeField(auto_now=True, verbose_name='Updated')),
('content_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='contenttypes.contenttype')),
('created_by', models.ForeignKey(on_delete=django.db.models.deletion.DO_NOTHING, related_name='notes_created', to=settings.AUTH_USER_MODEL)),
('dealer', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='notes', to='inventory.dealer')),
],
options={
'verbose_name': 'Note',
'verbose_name_plural': 'Notes',
},
),
migrations.CreateModel(
name='Notification',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('message', models.CharField(max_length=255, verbose_name='Message')),
('is_read', models.BooleanField(default=False, verbose_name='Is Read')),
('created', models.DateTimeField(auto_now_add=True, verbose_name='Created')),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='notifications', to=settings.AUTH_USER_MODEL)),
],
options={
'verbose_name': 'Notification',
'verbose_name_plural': 'Notifications',
'ordering': ['-created'],
},
),
migrations.CreateModel(
name='Organization',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=255, verbose_name='Name')),
('arabic_name', models.CharField(max_length=255, verbose_name='Arabic Name')),
('crn', models.CharField(max_length=15, verbose_name='Commercial Registration Number')),
('vrn', models.CharField(max_length=15, verbose_name='VAT Registration Number')),
('email', models.EmailField(max_length=254, verbose_name='Email')),
('phone_number', phonenumber_field.modelfields.PhoneNumberField(max_length=128, region='SA', verbose_name='Phone Number')),
('address', models.CharField(blank=True, max_length=200, null=True, verbose_name='Address')),
('logo', models.ImageField(blank=True, null=True, upload_to='logos', verbose_name='Logo')),
('active', models.BooleanField(default=True, verbose_name='Active')),
('created', models.DateTimeField(auto_now_add=True, verbose_name='Created')),
('updated', models.DateTimeField(auto_now=True, verbose_name='Updated')),
('slug', models.SlugField(blank=True, editable=False, max_length=255, null=True, unique=True)),
('customer_model', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='django_ledger.customermodel')),
('dealer', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='organizations', to='inventory.dealer')),
('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='organization_profile', to=settings.AUTH_USER_MODEL)),
],
options={
'verbose_name': 'Organization',
'verbose_name_plural': 'Organizations',
},
bases=(models.Model, inventory.mixins.LocalizedNameMixin),
),
migrations.CreateModel(
name='Lead',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('first_name', models.CharField(max_length=50, verbose_name='First Name')),
('last_name', models.CharField(max_length=50, verbose_name='Last Name')),
('email', models.EmailField(max_length=254, verbose_name='Email')),
('phone_number', phonenumber_field.modelfields.PhoneNumberField(max_length=128, region='SA', verbose_name='Phone Number')),
('lead_type', models.CharField(choices=[('customer', 'Customer'), ('organization', 'Organization')], default='customer', max_length=50, verbose_name='Lead Type')),
('year', models.PositiveSmallIntegerField(blank=True, null=True, verbose_name='Year')),
('source', models.CharField(choices=[('referrals', 'Referrals'), ('whatsapp', 'WhatsApp'), ('showroom', 'Showroom'), ('tiktok', 'TikTok'), ('instagram', 'Instagram'), ('x', 'X'), ('facebook', 'Facebook'), ('motory', 'Motory'), ('influencers', 'Influencers'), ('youtube', 'Youtube'), ('campaign', 'Campaign')], max_length=50, verbose_name='Source')),
('channel', models.CharField(choices=[('walk_in', 'Walk In'), ('toll_free', 'Toll Free'), ('website', 'Website'), ('email', 'Email'), ('form', 'Form')], max_length=50, verbose_name='Channel')),
('crn', models.CharField(blank=True, max_length=10, null=True, unique=True, verbose_name='Commercial Registration Number')),
('vrn', models.CharField(blank=True, max_length=15, null=True, unique=True, verbose_name='VAT Registration Number')),
('address', models.CharField(max_length=50, verbose_name='address')),
('priority', models.CharField(choices=[('low', 'Low'), ('medium', 'Medium'), ('high', 'High')], default='medium', max_length=10, verbose_name='Priority')),
('status', models.CharField(choices=[('new', 'New'), ('follow_up', 'Follow-up'), ('negotiation', 'Negotiation'), ('won', 'Won'), ('lost', 'Lost'), ('closed', 'Closed')], db_index=True, default='new', max_length=50, verbose_name='Status')),
('next_action', models.CharField(blank=True, max_length=255, null=True, verbose_name='Next Action')),
('next_action_date', models.DateTimeField(blank=True, null=True, verbose_name='Next Action Date')),
('is_converted', models.BooleanField(default=False)),
('converted_at', models.DateTimeField(blank=True, null=True)),
('salary', models.PositiveIntegerField(blank=True, null=True, verbose_name='Salary')),
('created', models.DateTimeField(auto_now_add=True, db_index=True, verbose_name='Created')),
('updated', models.DateTimeField(auto_now=True, verbose_name='Updated')),
('slug', models.SlugField(blank=True, null=True, unique=True)),
('customer', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='customer_leads', to='inventory.customer')),
('dealer', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='leads', to='inventory.dealer')),
('id_car_make', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.DO_NOTHING, to='inventory.carmake', verbose_name='Make')),
('id_car_model', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.DO_NOTHING, to='inventory.carmodel', verbose_name='Model')),
('organization', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='organization_leads', to='inventory.organization')),
],
options={
'verbose_name': 'Lead',
'verbose_name_plural': 'Leads',
},
),
migrations.CreateModel(
name='Refund',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('amount', models.DecimalField(decimal_places=2, max_digits=10, verbose_name='amount')),
('reason', models.TextField(blank=True, verbose_name='reason')),
('refund_date', models.DateField(auto_now_add=True, verbose_name='refund date')),
('payment', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='refund', to='inventory.payment')),
],
options={
'verbose_name': 'refund',
'verbose_name_plural': 'refunds',
},
),
migrations.CreateModel(
name='Representative',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=255, verbose_name='Name')),
('arabic_name', models.CharField(max_length=255, verbose_name='Arabic Name')),
('id_number', models.CharField(max_length=10, unique=True, verbose_name='ID Number')),
('phone_number', phonenumber_field.modelfields.PhoneNumberField(max_length=128, region='SA', verbose_name='Phone Number')),
('email', models.EmailField(max_length=255, verbose_name='Email Address')),
('address', models.CharField(blank=True, max_length=200, null=True, verbose_name='Address')),
('dealer', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='representatives', to='inventory.dealer')),
('organization', models.ManyToManyField(related_name='representatives', to='inventory.organization')),
],
options={
'verbose_name': 'Representative',
'verbose_name_plural': 'Representatives',
},
bases=(models.Model, inventory.mixins.LocalizedNameMixin),
),
migrations.CreateModel(
name='SaleOrder',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('payment_method', models.CharField(choices=[('cash', 'Cash'), ('finance', 'Finance'), ('lease', 'Lease'), ('credit_card', 'Credit Card'), ('bank_transfer', 'Bank Transfer'), ('sadad', 'SADAD')], max_length=20)),
('comments', models.TextField(blank=True, null=True)),
('formatted_order_id', models.CharField(editable=False, max_length=10, unique=True)),
('created', models.DateTimeField(auto_now_add=True)),
('estimate', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='sale_orders', to='django_ledger.estimatemodel', verbose_name='Estimate')),
('invoice', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='sale_orders', to='django_ledger.invoicemodel', verbose_name='Invoice')),
],
options={
'ordering': ['-created'],
},
),
migrations.CreateModel(
name='Schedule',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('purpose', models.CharField(choices=[('product_demo', 'Product Demo'), ('follow_up_call', 'Follow-Up Call'), ('contract_discussion', 'Contract Discussion'), ('sales_meeting', 'Sales Meeting'), ('support_call', 'Support Call'), ('other', 'Other')], max_length=200)),
('scheduled_at', models.DateTimeField()),
('scheduled_type', models.CharField(choices=[('call', 'Call'), ('meeting', 'Meeting'), ('email', 'Email')], default='Call', max_length=200)),
('duration', models.DurationField(default=datetime.timedelta(seconds=300))),
('notes', models.TextField(blank=True, null=True)),
('status', models.CharField(choices=[('scheduled', 'Scheduled'), ('completed', 'Completed'), ('canceled', 'Canceled')], default='Scheduled', max_length=200)),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('customer', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='schedules', to='django_ledger.customermodel')),
('lead', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='schedules', to='inventory.lead')),
('scheduled_by', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
],
options={
'ordering': ['-scheduled_at'],
},
),
migrations.CreateModel(
name='Staff',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=255, verbose_name='Name')),
('arabic_name', models.CharField(max_length=255, verbose_name='Arabic Name')),
('phone_number', phonenumber_field.modelfields.PhoneNumberField(max_length=128, region='SA', verbose_name='Phone Number')),
('staff_type', models.CharField(choices=[('inventory', 'Inventory'), ('accountant', 'Accountant'), ('sales', 'Sales')], max_length=255, verbose_name='Staff Type')),
('active', models.BooleanField(default=True, verbose_name='Active')),
('created', models.DateTimeField(auto_now_add=True, verbose_name='Created')),
('updated', models.DateTimeField(auto_now=True, verbose_name='Updated')),
('slug', models.SlugField(blank=True, editable=False, max_length=255, null=True, unique=True)),
('dealer', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='staff', to='inventory.dealer')),
('staff_member', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='staff', to='appointment.staffmember')),
],
options={
'verbose_name': 'Staff',
'verbose_name_plural': 'Staff',
'permissions': [],
},
bases=(models.Model, inventory.mixins.LocalizedNameMixin),
managers=[
('objects', inventory.models.StaffUserManager()),
],
),
migrations.CreateModel(
name='Opportunity',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('stage', models.CharField(choices=[('discovery', 'Discovery'), ('proposal', 'Proposal'), ('negotiation', 'Negotiation'), ('closed_won', 'Closed Won'), ('closed_lost', 'Closed Lost')], max_length=20, verbose_name='Stage')),
('probability', models.PositiveIntegerField(validators=[inventory.models.validate_probability])),
('expected_revenue', models.DecimalField(decimal_places=2, max_digits=10, verbose_name='Expected Revenue')),
('closing_date', models.DateField(blank=True, null=True, verbose_name='Closing Date')),
('created', models.DateTimeField(auto_now_add=True, verbose_name='Created')),
('updated', models.DateTimeField(auto_now=True, verbose_name='Updated')),
('slug', models.SlugField(blank=True, help_text='Unique slug for the opportunity.', null=True, unique=True, verbose_name='Slug')),
('car', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='inventory.car', verbose_name='Car')),
('customer', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='opportunities', to='inventory.customer')),
('dealer', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='opportunities', to='inventory.dealer')),
('estimate', models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='opportunity', to='django_ledger.estimatemodel')),
('lead', models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='opportunity', to='inventory.lead')),
('staff', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='owner', to='inventory.staff', verbose_name='Owner')),
],
options={
'verbose_name': 'Opportunity',
'verbose_name_plural': 'Opportunities',
},
),
migrations.CreateModel(
name='LeadStatusHistory',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('old_status', models.CharField(choices=[('new', 'New'), ('follow_up', 'Follow-up'), ('negotiation', 'Negotiation'), ('won', 'Won'), ('lost', 'Lost'), ('closed', 'Closed')], max_length=50, verbose_name='Old Status')),
('new_status', models.CharField(choices=[('new', 'New'), ('follow_up', 'Follow-up'), ('negotiation', 'Negotiation'), ('won', 'Won'), ('lost', 'Lost'), ('closed', 'Closed')], max_length=50, verbose_name='New Status')),
('changed_at', models.DateTimeField(auto_now_add=True, verbose_name='Changed At')),
('lead', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='status_history', to='inventory.lead')),
('changed_by', models.ForeignKey(on_delete=django.db.models.deletion.DO_NOTHING, related_name='status_changes', to='inventory.staff')),
],
options={
'verbose_name': 'Lead Status History',
'verbose_name_plural': 'Lead Status Histories',
},
),
migrations.AddField(
model_name='lead',
name='staff',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='assigned', to='inventory.staff', verbose_name='Assigned'),
),
migrations.CreateModel(
name='Tasks',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('object_id', models.UUIDField()),
('title', models.CharField(max_length=255, verbose_name='Title')),
('description', models.TextField(blank=True, null=True, verbose_name='Description')),
('due_date', models.DateField(verbose_name='Due Date')),
('completed', models.BooleanField(default=False, verbose_name='Completed')),
('created', models.DateTimeField(auto_now_add=True, verbose_name='Created')),
('updated', models.DateTimeField(auto_now=True, verbose_name='Updated')),
('assigned_to', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='tasks_assigned', to=settings.AUTH_USER_MODEL)),
('content_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='contenttypes.contenttype')),
('created_by', models.ForeignKey(on_delete=django.db.models.deletion.DO_NOTHING, related_name='tasks_created', to=settings.AUTH_USER_MODEL)),
('dealer', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='tasks', to='inventory.dealer')),
],
options={
'verbose_name': 'Task',
'verbose_name_plural': 'Tasks',
},
),
migrations.CreateModel(
name='UserActivityLog',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('action', models.TextField()),
('timestamp', models.DateTimeField(auto_now_add=True)),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
],
options={
'verbose_name': 'User Activity Log',
'verbose_name_plural': 'User Activity Logs',
'ordering': ['-timestamp'],
},
),
migrations.CreateModel(
name='Vendor',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('crn', models.CharField(max_length=10, unique=True, verbose_name='Commercial Registration Number')),
('vrn', models.CharField(max_length=15, unique=True, verbose_name='VAT Registration Number')),
('arabic_name', models.CharField(max_length=255, verbose_name='Arabic Name')),
('name', models.CharField(max_length=255, verbose_name='English Name')),
('contact_person', models.CharField(max_length=100, verbose_name='Contact Person')),
('phone_number', phonenumber_field.modelfields.PhoneNumberField(max_length=128, region='SA', verbose_name='Phone Number')),
('email', models.EmailField(max_length=255, verbose_name='Email Address')),
('address', models.CharField(max_length=200, verbose_name='Address')),
('logo', models.ImageField(blank=True, null=True, upload_to='logos/vendors', verbose_name='Logo')),
('active', models.BooleanField(default=True, verbose_name='Active')),
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')),
('slug', models.SlugField(blank=True, max_length=255, null=True, unique=True, verbose_name='Slug')),
('dealer', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='vendors', to='inventory.dealer')),
('vendor_model', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.DO_NOTHING, to='django_ledger.vendormodel', verbose_name='Vendor Model')),
],
options={
'verbose_name': 'Vendor',
'verbose_name_plural': 'Vendors',
},
bases=(models.Model, inventory.mixins.LocalizedNameMixin),
),
migrations.AddField(
model_name='car',
name='vendor',
field=models.ForeignKey(on_delete=django.db.models.deletion.DO_NOTHING, related_name='cars', to='inventory.vendor', verbose_name='Vendor'),
),
migrations.CreateModel(
name='CarReservation',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('reserved_at', models.DateTimeField(auto_now_add=True, verbose_name='Reserved At')),
('reserved_until', models.DateTimeField(verbose_name='Reserved Until')),
('car', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='reservations', to='inventory.car', verbose_name='Car')),
('reserved_by', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='reservations', to=settings.AUTH_USER_MODEL, verbose_name='Reserved By')),
],
options={
'verbose_name': 'Car Reservation',
'verbose_name_plural': 'Car Reservations',
'ordering': ['-reserved_at'],
'unique_together': {('car', 'reserved_until')},
},
),
migrations.CreateModel(
name='DealersMake',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('added_at', models.DateTimeField(auto_now_add=True)),
('car_make', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='car_dealers', to='inventory.carmake')),
('dealer', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='dealer_makes', to='inventory.dealer')),
],
options={
'unique_together': {('dealer', 'car_make')},
},
),
migrations.CreateModel(
name='CarColors',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('car', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='colors', to='inventory.car')),
('exterior', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='colors', to='inventory.exteriorcolors')),
('interior', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='colors', to='inventory.interiorcolors')),
],
options={
'verbose_name': 'Color',
'verbose_name_plural': 'Colors',
'unique_together': {('car', 'exterior', 'interior')},
},
),
migrations.CreateModel(
name='PaymentHistory',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('user_data', models.JSONField(blank=True, null=True)),
('amount', models.DecimalField(decimal_places=2, max_digits=10, validators=[django.core.validators.MinValueValidator(0.01)])),
('currency', models.CharField(default='SAR', max_length=3)),
('payment_date', models.DateTimeField(default=django.utils.timezone.now)),
('status', models.CharField(choices=[('initiated', 'initiated'), ('pending', 'Pending'), ('completed', 'Completed'), ('paid', 'Paid'), ('failed', 'Failed'), ('refunded', 'Refunded'), ('cancelled', 'Cancelled')], default='pending', max_length=10)),
('payment_method', models.CharField(choices=[('credit_card', 'Credit Card'), ('debit_card', 'Debit Card'), ('paypal', 'PayPal'), ('bank_transfer', 'Bank Transfer'), ('crypto', 'Cryptocurrency'), ('other', 'Other')], max_length=20)),
('transaction_id', models.CharField(blank=True, max_length=100, null=True, unique=True)),
('invoice_number', models.CharField(blank=True, max_length=50, null=True)),
('order_reference', models.CharField(blank=True, max_length=100, null=True)),
('gateway_response', models.JSONField(blank=True, null=True)),
('gateway_name', models.CharField(blank=True, max_length=50, null=True)),
('description', models.TextField(blank=True, null=True)),
('is_recurring', models.BooleanField(default=False)),
('billing_email', models.EmailField(blank=True, max_length=254, null=True)),
('billing_address', models.TextField(blank=True, null=True)),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='payments', to=settings.AUTH_USER_MODEL)),
],
options={
'verbose_name': 'Payment History',
'verbose_name_plural': 'Payment Histories',
'ordering': ['-payment_date'],
'indexes': [models.Index(fields=['transaction_id'], name='inventory_p_transac_9469f3_idx'), models.Index(fields=['user'], name='inventory_p_user_id_c31626_idx'), models.Index(fields=['status'], name='inventory_p_status_abcb77_idx'), models.Index(fields=['payment_date'], name='inventory_p_payment_b3068c_idx')],
},
),
]

View File

@ -1,18 +0,0 @@
# Generated by Django 5.1.7 on 2025-05-25 14:19
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('inventory', '0001_initial'),
]
operations = [
migrations.AddField(
model_name='carfinance',
name='is_sold',
field=models.BooleanField(default=False),
),
]

View File

@ -1,125 +0,0 @@
# Generated by Django 5.1.7 on 2025-05-27 14:41
import django.db.models.deletion
import django.utils.timezone
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('inventory', '0002_carfinance_is_sold'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.AlterModelOptions(
name='saleorder',
options={'ordering': ['-order_date'], 'verbose_name': 'Sales Order', 'verbose_name_plural': 'Sales Orders'},
),
migrations.RenameField(
model_name='saleorder',
old_name='created',
new_name='created_at',
),
migrations.AddField(
model_name='saleorder',
name='actual_delivery_date',
field=models.DateTimeField(blank=True, help_text='The actual date and time the vehicle was delivered.', null=True),
),
migrations.AddField(
model_name='saleorder',
name='agreed_price',
field=models.DecimalField(decimal_places=2, default=0, help_text='The final agreed-upon selling price of the vehicle.', max_digits=12),
preserve_default=False,
),
migrations.AddField(
model_name='saleorder',
name='cancellation_reason',
field=models.TextField(blank=True, help_text='Reason for cancellation, if applicable.', null=True),
),
migrations.AddField(
model_name='saleorder',
name='cancelled_date',
field=models.DateTimeField(blank=True, help_text='The date and time the order was cancelled, if applicable.', null=True),
),
migrations.AddField(
model_name='saleorder',
name='car',
field=models.ForeignKey(default=1, help_text='The specific vehicle (VIN) being sold.', on_delete=django.db.models.deletion.PROTECT, related_name='sales_orders', to='inventory.car'),
preserve_default=False,
),
migrations.AddField(
model_name='saleorder',
name='created_by',
field=models.ForeignKey(help_text='The user who created this sales order.', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='created_sales_orders', to=settings.AUTH_USER_MODEL),
),
migrations.AddField(
model_name='saleorder',
name='customer',
field=models.ForeignKey(default=1, help_text='The customer making the purchase.', on_delete=django.db.models.deletion.PROTECT, related_name='sales_orders', to='inventory.customer'),
preserve_default=False,
),
migrations.AddField(
model_name='saleorder',
name='down_payment_amount',
field=models.DecimalField(decimal_places=2, default=0.0, help_text='The initial payment made by the customer.', max_digits=12),
),
migrations.AddField(
model_name='saleorder',
name='expected_delivery_date',
field=models.DateField(blank=True, help_text='The planned date for vehicle delivery.', null=True),
),
migrations.AddField(
model_name='saleorder',
name='last_modified_by',
field=models.ForeignKey(help_text='The user who last modified this sales order.', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='modified_sales_orders', to=settings.AUTH_USER_MODEL),
),
migrations.AddField(
model_name='saleorder',
name='loan_amount',
field=models.DecimalField(decimal_places=2, default=0.0, help_text='The amount financed by a bank or third-party lender.', max_digits=12),
),
migrations.AddField(
model_name='saleorder',
name='opportunity',
field=models.OneToOneField(default=1, help_text='The associated sales opportunity for this order.', on_delete=django.db.models.deletion.CASCADE, related_name='sales_order', to='inventory.opportunity'),
preserve_default=False,
),
migrations.AddField(
model_name='saleorder',
name='order_date',
field=models.DateTimeField(default=django.utils.timezone.now, help_text='The date and time the sales order was created.'),
),
migrations.AddField(
model_name='saleorder',
name='remaining_balance',
field=models.DecimalField(decimal_places=2, default=0.0, help_text='The remaining amount due from the customer or financing.', max_digits=12),
),
migrations.AddField(
model_name='saleorder',
name='status',
field=models.CharField(choices=[('PENDING_APPROVAL', 'Pending Approval'), ('APPROVED', 'Approved'), ('IN_FINANCING', 'In Financing'), ('PARTIALLY_PAID', 'Partially Paid'), ('FULLY_PAID', 'Fully Paid'), ('PENDING_DELIVERY', 'Pending Delivery'), ('DELIVERED', 'Delivered'), ('CANCELLED', 'Cancelled')], default='PENDING_APPROVAL', help_text='Current status of the sales order.', max_length=20),
),
migrations.AddField(
model_name='saleorder',
name='total_paid_amount',
field=models.DecimalField(decimal_places=2, default=0.0, help_text='Sum of down payment, trade-in value, and loan amount received so far.', max_digits=12),
),
migrations.AddField(
model_name='saleorder',
name='trade_in_value',
field=models.DecimalField(decimal_places=2, default=0.0, help_text='The value of any vehicle traded in by the customer.', max_digits=12),
),
migrations.AddField(
model_name='saleorder',
name='trade_in_vehicle',
field=models.ForeignKey(blank=True, help_text='The vehicle traded in by the customer, if any.', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='traded_in_on_orders', to='inventory.car'),
),
migrations.AddField(
model_name='saleorder',
name='updated_at',
field=models.DateTimeField(auto_now=True),
),
]

View File

@ -1,19 +0,0 @@
# Generated by Django 5.1.7 on 2025-05-27 14:43
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('inventory', '0003_alter_saleorder_options_and_more'),
]
operations = [
migrations.AlterField(
model_name='saleorder',
name='car',
field=models.ForeignKey(blank=True, help_text='The specific vehicle (VIN) being sold.', null=True, on_delete=django.db.models.deletion.PROTECT, related_name='sales_orders', to='inventory.car'),
),
]

View File

@ -1,18 +0,0 @@
# Generated by Django 5.1.7 on 2025-05-28 13:14
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('inventory', '0004_alter_saleorder_car'),
]
operations = [
migrations.AlterField(
model_name='opportunity',
name='stage',
field=models.CharField(choices=[('qualification', 'Qualification'), ('test_drive', 'Test Drive'), ('quotation', 'Quotation'), ('negotiation', 'Negotiation'), ('financing', 'Financing'), ('closed_won', 'Closed Won'), ('closed_lost', 'Closed Lost'), ('on_hold', 'On Hold')], max_length=20, verbose_name='Stage'),
),
]

View File

@ -1,45 +0,0 @@
# Generated by Django 5.1.7 on 2025-05-28 13:16
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('inventory', '0005_alter_opportunity_stage'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.RemoveField(
model_name='opportunity',
name='closing_date',
),
migrations.AddField(
model_name='opportunity',
name='assigned_to',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='assigned_opportunities', to=settings.AUTH_USER_MODEL),
),
migrations.AddField(
model_name='opportunity',
name='expected_close_date',
field=models.DateField(blank=True, null=True),
),
migrations.AddField(
model_name='opportunity',
name='loss_reason',
field=models.CharField(blank=True, max_length=255, null=True),
),
migrations.AddField(
model_name='opportunity',
name='vehicle_of_interest_make',
field=models.CharField(blank=True, max_length=50, null=True),
),
migrations.AddField(
model_name='opportunity',
name='vehicle_of_interest_model',
field=models.CharField(blank=True, max_length=100, null=True),
),
]

View File

@ -1,28 +0,0 @@
# Generated by Django 5.1.7 on 2025-05-28 13:36
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('inventory', '0006_remove_opportunity_closing_date_and_more'),
]
operations = [
migrations.AlterField(
model_name='lead',
name='status',
field=models.CharField(choices=[('new', 'New'), ('contacted', 'Contacted'), ('qualified', 'Qualified'), ('unqualified', 'Unqualified'), ('converted', 'Converted')], db_index=True, default='new', max_length=50, verbose_name='Status'),
),
migrations.AlterField(
model_name='leadstatushistory',
name='new_status',
field=models.CharField(choices=[('new', 'New'), ('contacted', 'Contacted'), ('qualified', 'Qualified'), ('unqualified', 'Unqualified'), ('converted', 'Converted')], max_length=50, verbose_name='New Status'),
),
migrations.AlterField(
model_name='leadstatushistory',
name='old_status',
field=models.CharField(choices=[('new', 'New'), ('contacted', 'Contacted'), ('qualified', 'Qualified'), ('unqualified', 'Unqualified'), ('converted', 'Converted')], max_length=50, verbose_name='Old Status'),
),
]

View File

@ -1,37 +0,0 @@
# Generated by Django 5.1.7 on 2025-05-28 13:44
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('inventory', '0007_alter_lead_status_alter_leadstatushistory_new_status_and_more'),
]
operations = [
migrations.RemoveField(
model_name='lead',
name='address',
),
migrations.RemoveField(
model_name='lead',
name='crn',
),
migrations.RemoveField(
model_name='lead',
name='priority',
),
migrations.RemoveField(
model_name='lead',
name='salary',
),
migrations.RemoveField(
model_name='lead',
name='vrn',
),
migrations.RemoveField(
model_name='lead',
name='year',
),
]

View File

@ -1,38 +0,0 @@
# Generated by Django 5.1.7 on 2025-05-28 13:46
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('inventory', '0008_remove_lead_address_remove_lead_crn_and_more'),
]
operations = [
migrations.AddField(
model_name='lead',
name='address',
field=models.CharField(blank=True, max_length=200, null=True, verbose_name='Address'),
),
migrations.AddField(
model_name='opportunity',
name='crn',
field=models.CharField(blank=True, max_length=20, null=True, verbose_name='CRN'),
),
migrations.AddField(
model_name='opportunity',
name='priority',
field=models.CharField(choices=[('high', 'High'), ('medium', 'Medium'), ('low', 'Low')], default='medium', max_length=20, verbose_name='Priority'),
),
migrations.AddField(
model_name='opportunity',
name='salary',
field=models.DecimalField(blank=True, decimal_places=2, max_digits=10, null=True, verbose_name='Salary'),
),
migrations.AddField(
model_name='opportunity',
name='vrn',
field=models.CharField(blank=True, max_length=20, null=True, verbose_name='VRN'),
),
]

View File

@ -1,19 +0,0 @@
# Generated by Django 5.1.7 on 2025-05-29 15:22
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('inventory', '0009_lead_address_opportunity_crn_opportunity_priority_and_more'),
]
operations = [
migrations.AlterField(
model_name='opportunity',
name='assigned_to',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='assigned_opportunities', to='inventory.staff'),
),
]

View File

@ -1,17 +0,0 @@
# Generated by Django 5.1.7 on 2025-05-29 15:23
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('inventory', '0010_alter_opportunity_assigned_to'),
]
operations = [
migrations.RemoveField(
model_name='opportunity',
name='assigned_to',
),
]

View File

@ -1,19 +0,0 @@
# Generated by Django 5.1.7 on 2025-05-29 16:12
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('inventory', '0011_remove_opportunity_assigned_to'),
]
operations = [
migrations.AddField(
model_name='opportunity',
name='organization',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='inventory.organization', verbose_name='Organization'),
),
]

View File

@ -1,23 +0,0 @@
# Generated by Django 5.1.7 on 2025-05-29 23:00
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('inventory', '0012_opportunity_organization'),
]
operations = [
migrations.AddField(
model_name='opportunity',
name='amount',
field=models.DecimalField(blank=True, decimal_places=2, max_digits=10, null=True, verbose_name='Amount'),
),
migrations.AlterField(
model_name='opportunity',
name='expected_revenue',
field=models.DecimalField(blank=True, decimal_places=2, max_digits=10, null=True, verbose_name='Expected Revenue'),
),
]

View File

@ -1,19 +0,0 @@
# Generated by Django 5.1.7 on 2025-05-29 23:02
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('inventory', '0013_opportunity_amount_and_more'),
]
operations = [
migrations.AlterField(
model_name='opportunity',
name='amount',
field=models.DecimalField(decimal_places=2, default=0, max_digits=10, verbose_name='Amount'),
preserve_default=False,
),
]

View File

@ -281,6 +281,7 @@ urlpatterns = [
path(
"cars/<slug:slug>/add-color/", views.CarColorCreate.as_view(), name="add_color"
),
path('car/colors/<slug:slug>/update/', views.CarColorsUpdateView.as_view(), name='car_colors_update'),
path(
"cars/<slug:slug>/location/add/",
views.CarLocationCreateView.as_view(),
@ -855,6 +856,9 @@ path(
path('management/user_management/', views.user_management, name='user_management'),
path('management/<str:content_type>/<slug:slug>/activate_account/', views.activate_account, name='activate_account'),
path('management/<str:content_type>/<slug:slug>/permenant_delete_account/', views.permenant_delete_account, name='permenant_delete_account'),
path('management/audit_log_dashboard/', views.AuditLogDashboardView, name='audit_log_dashboard'),
#########
# Purchase Order
path('purchase_orders/', views.PurchaseOrderListView.as_view(), name='purchase_order_list'),

View File

@ -31,7 +31,7 @@ from django.forms import HiddenInput, ValidationError
from django.shortcuts import HttpResponse
from django.db.models import Sum, F, Count
from django.core.paginator import Paginator
from django.core.paginator import Paginator,EmptyPage, PageNotAnInteger
from django.contrib.auth.models import User
from django.contrib.auth.models import Group
from django.db.models import Value
@ -60,6 +60,7 @@ from django.views.generic import (
ArchiveIndexView,
)
# Django Ledger
from django_ledger.io import roles
from django_ledger.utils import accruable_net_summary
@ -172,6 +173,10 @@ from .utils import (
)
from .tasks import create_accounts_for_make, send_email
#djago easy audit log
from easyaudit.models import RequestEvent, CRUDEvent, LoginEvent
logger = logging.getLogger(__name__)
logging.basicConfig(level=logging.INFO)
@ -967,37 +972,50 @@ class CarColorCreate(LoginRequiredMixin, PermissionRequiredMixin, CreateView):
context["car"] = get_object_or_404(models.Car, slug=self.kwargs["slug"])
return context
class CarColorsUpdateView( LoginRequiredMixin, PermissionRequiredMixin, SuccessMessageMixin, UpdateView):
model = models.CarColors
form_class = forms.CarColorsForm
template_name = "inventory/add_colors.html"
success_message = _("Car finance details updated successfully")
permission_required = ["inventory.change_carfinance"]
success_message = _("Car Colors details updated successfully")
permission_required = ["inventory.change_car"]
def get_object(self, queryset=None):
"""
Retrieves the CarColors instance associated with the Car slug from the URL.
This ensures we are updating the colors for the correct car.
"""
# Get the car_slug from the URL keywords arguments
slug = self.kwargs.get('slug')
# If no car_slug is provided, it's an invalid request
if not slug:
# You might want to raise Http404 or a more specific error here
raise ValueError("Car slug is required to identify the colors to update.")
return get_object_or_404(models.CarColors, car__slug=slug)
def get_success_url(self):
"""
Redirects to the car's detail page using its slug after a successful update.
"""
# self.object refers to the CarColors instance that was just updated.
# self.object.car then refers to the associated Car instance.
return reverse("car_detail", kwargs={"slug": self.object.car.slug})
def get_form_kwargs(self):
kwargs = super().get_form_kwargs()
kwargs["instance"] = self.get_object()
return kwargs
def get_initial(self):
initial = super().get_initial()
instance = self.get_object()
dealer = get_user_type(self.request)
selected_items = instance.additional_services.filter(dealer=dealer)
initial["additional_finances"] = selected_items
return initial
def get_form(self, form_class=None):
form = super().get_form(form_class)
dealer = get_user_type(self.request)
form.fields[
"additional_finances"
].queryset = models.AdditionalServices.objects.filter(dealer=dealer)
return form
def get_context_data(self, **kwargs):
"""
Adds the related Car object to the template context.
"""
context = super().get_context_data(**kwargs)
# self.object is already available here from get_object()
context['car'] = self.object.car
context['page_title'] = _("Update Colors for %(car_name)s") % {'car_name': context['car']}
return context
class CarListView(LoginRequiredMixin, PermissionRequiredMixin, ListView):
"""
@ -8341,6 +8359,123 @@ def user_management(request):
return render(request, "admin_management/user_management.html", context)
def AuditLogDashboardView(request):
"""
Displays audit logs (User Actions, Login Events, Request Events) with pagination.
Log type is determined by the 'q' query parameter (e.g., ?q=userActions).
Pagination page number is passed as a query parameter (e.g., ?page=2).
"""
q = request.GET.get('q') # Get the log type from the 'q' query parameter
current_pagination_page = request.GET.get('page', 1)
context = {}
template_name = None
logs_per_page = 30 # Define logs per page once
# --- Determine Data Source and Template based on 'q' parameter ---
if q=='userRequests': # This block handles cases where 'q' is 'requestEvents', None, or any other invalid value.
# It defaults to Request Logs if 'q' is not 'userActions' or 'loginEvents'.
template_name = 'admin_management/request_logs.html'
context['title'] = 'Request Logs Dashboard'
request_events = RequestEvent.objects.all().order_by('-datetime')
paginator = Paginator(request_events, logs_per_page)
try:
page_obj = paginator.page(current_pagination_page)
except PageNotAnInteger:
page_obj = paginator.page(1)
except EmptyPage:
page_obj = paginator.page(paginator.num_pages)
elif q == 'loginEvents':
template_name = 'admin_management/auth_logs.html'
context['title'] = 'Login Events Dashboard'
auth_events = LoginEvent.objects.all().order_by('-datetime')
paginator = Paginator(auth_events, logs_per_page)
try:
page_obj = paginator.page(current_pagination_page)
except PageNotAnInteger:
page_obj = paginator.page(1)
except EmptyPage:
page_obj = paginator.page(paginator.num_pages)
else:
template_name = 'admin_management/model_logs.html'
context['title'] = 'User Actions Dashboard'
# OPTIMIZATION: Get the QuerySet but don't evaluate it yet
model_events_queryset = CRUDEvent.objects.all().order_by('-datetime')
# 1. Paginate the raw QuerySet FIRST
paginator = Paginator(model_events_queryset, logs_per_page)
try:
# Get the page object, which contains only the raw QuerySet objects for the current page
page_obj_raw = paginator.page(current_pagination_page)
except PageNotAnInteger:
page_obj_raw = paginator.page(1)
except EmptyPage:
page_obj_raw = paginator.page(paginator.num_pages)
# 2. Now, process 'field_changes' ONLY for the events on the current page
processed_model_events_for_page = []
for event in page_obj_raw.object_list: # Loop only through the current page's items
event_data = {
'datetime': event.datetime,
'user': event.user,
'event_type_display': event.get_event_type_display(),
'model_name': event.content_type.model,
'object_id': event.object_id,
'object_repr': event.object_repr,
'field_changes': []
}
if event.changed_fields:
try:
changes = json.loads(event.changed_fields)
if isinstance(changes, dict):
for field_name, values in changes.items():
old_value = values[0] if isinstance(values, list) and len(values) > 0 else None
new_value = values[1] if isinstance(values, list) and len(values) > 1 else None
event_data['field_changes'].append({
'field': field_name,
'old': old_value,
'new': new_value
})
elif changes is None:
event_data['field_changes'].append({
'field': 'Info',
'old': '',
'new': 'No specific field changes recorded (JSON was null)'
})
else: # Handle valid JSON but not a dictionary (e.g., "[]", 123)
event_data['field_changes'].append({
'field': 'Error',
'old': '',
'new': f'Unexpected JSON format: {type(changes).__name__}'
})
except json.JSONDecodeError:
# Handle invalid JSON; you might log this error
event_data['field_changes'].append({
'field': 'Error',
'old': '',
'new': 'Invalid JSON in changed_fields'
})
processed_model_events_for_page.append(event_data)
# 3. Replace the object_list of the original page_obj with the processed data
# This keeps all pagination properties (has_next, number, etc.) intact.
page_obj_raw.object_list = processed_model_events_for_page
page_obj = page_obj_raw # This will be passed to the context
# Pass the final page object to the context
context['page_obj'] = page_obj
return render(request, template_name, context)
def activate_account(request, content_type, slug):
try:
model = apps.get_model(f"inventory.{content_type}")

View File

@ -94,3 +94,4 @@ urllib3==2.3.0
wcwidth==0.2.13
langchain
langchain_ollama
django-easy-audit==1.3.7

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 MiB

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 MiB

View File

@ -0,0 +1,75 @@
{% extends "base.html" %}
{% load i18n custom_filters %}
{% block title %}{% trans "Accounts" %}{% endblock title %}
{% block accounts %}
<a class="nav-link active fw-bold">
{% trans "Accounts"|capfirst %}
<span class="visually-hidden">(current)</span>
</a>
{% endblock %}
{% block content %}
<div class="row mt-4">
<div class="d-flex justify-content-between mb-2">
<h3 class=""><i class="fa-solid fa-book"></i> {% trans "Audit Log Dashboard" %}</h3>
</div>
<!-- Log Type Tabs -->
<div class="mb-4">
{% include 'admin_management/nav.html' %}
<div class="tab-content p-3 border border-top-0 rounded-bottom" id="accountTypeTabsContent">
<!-- modellogs Tab -->
{% if page_obj %}
<div class="table-responsive px-1 scrollbar mt-3">
<table class= "table align-items-center table-flush table-hover">
<thead>
<tr class="bg-body-highlight">
<th class="sort white-space-nowrap align-middle" scope="col">{{ _("Timestamp") |capfirst }}</th>
<th class="sort white-space-nowrap align-middle" scope="col">{{ _("User") |capfirst }}</th>
<th class="sort white-space-nowrap align-middle" scope="col">{{ _("Event Type") }}</th>
<th class="sort white-space-nowrap align-middle" scope="col">{{ _("username") |capfirst }}</th>
<th class="sort white-space-nowrap align-middle"scope="col">{{ _("IP Address") |capfirst }}</th>
</tr>
</thead>
<tbody class="list">
{% for event in page_obj.object_list %}
<tr class="hover-actions-trigger btn-reveal-trigger position-static">
<td class="align-middle product white-space-nowrap">{{event.datetime}}</td>
<td class="align-middle product white-space-nowrap">{{ event.user.username|default:"N/A" }}</td>
<td class="align-middle product white-space-nowrap">{{ event.get_login_type_display}}</td>
<td class="align-middle product white-space-nowrap">{{ event.username}}</td>
<td class="align-middle product white-space-nowrap">{{ event.remote_ip}}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<div class="d-flex justify-content-end mt-3">
<div class="d-flex">
{% if is_paginated %}
{% include 'partials/pagination.html' %}
{% endif %}
</div>
</div>
{% include 'partials/pagination_audit.html' with q='loginEvents' %}
{% else %}
<p>No authentication audit events found.</p>
{% endif %}
</div>
</div>
</div>
{% endblock %}

View File

@ -2,17 +2,28 @@
{% load i18n %}
{%block title%} {%trans 'Admin Management' %} {%endblock%}
{% block content %}
<h1 class="mt-4"><i class="fas fa-tools me-2"> </i>{{ _("Admin Management")}}</h1>
<div class="row row-cols-1 row-cols-md-4 g-4 mt-10">
<div class="row row-cols-1 row-cols-sm-2 row-cols-md-4 g-4 mt-10">
<div class="col">
<a href="{% url 'user_management' %}">
<div class="card h-100">
<div class="card-header text-center">
<h5 class="card-title">{{ _("User Management")}}</h5>
<span class="me-2"><i class="fas fa-user fa-2x"></i></span>
</div>
</div>
</a>
<a href="{% url 'user_management' %}">
<div class="card h-100">
<div class="card-header text-center">
<h5 class="card-title">{{ _("User Management")}}</h5>
<span class="me-2"><i class="fas fa-user fa-2x"></i></span>
</div>
</div>
</a>
</div>
</div>
<div class="col">
<a href="{% url 'audit_log_dashboard' %}">
<div class="card h-100">
<div class="card-header text-center">
<h5 class="card-title">{{ _("Audit Log Dashboard")}}</h5>
<span class="me-2"><i class="fas fa-user fa-2x"></i></span>
</div>
</div>
</a>
</div>
</div>
{% endblock content %}

View File

@ -0,0 +1,128 @@
{% extends "base.html" %}
{% load i18n custom_filters %}
{% block title %}{% trans "Accounts" %}{% endblock title %}
{% block accounts %}
<a class="nav-link active fw-bold">
{% trans "Accounts"|capfirst %}
<span class="visually-hidden">(current)</span>
</a>
{% endblock %}
{% block content %}
<div class="row mt-4">
<div class="d-flex justify-content-between mb-2">
<h3 class=""><i class="fa-solid fa-book"></i> {% trans "Audit Log Dashboard" %}</h3>
</div>
<!-- Log Type Tabs -->
<div class="mb-4">
{% include 'admin_management/nav.html' %}
<div class="tab-content p-3 border border-top-0 rounded-bottom" id="accountTypeTabsContent">
<!-- modellogs Tab -->
{% if page_obj %}
<div class="table-responsive px-1 scrollbar mt-3">
<table class="table align-items-center table-flush table-hover mt-3">
<thead>
<tr class="bg-body-highlight">
<th>{% trans "Timestamp" %}</th>
<th>{% trans "User" %}</th>
<th>{% trans "Action" %}</th>
<th>{% trans "Model" %}</th>
<th>{% trans "Object ID" %}</th>
<th>{% trans "Object Representation" %}</th>
<th>{% trans "Field" %}</th> {# Dedicated column for field name #}
<th>{% trans "Old Value" %}</th> {# Dedicated column for old value #}
<th>{% trans "New Value" %}</th> {# Dedicated column for new value #}
</tr>
</thead>
<tbody>
{% for event in page_obj.object_list %}
{% if event.field_changes %}
{# Loop through each individual field change for this event #}
{% for change in event.field_changes %}
<tr>
{# Display common event details using rowspan for the first change #}
{% if forloop.first %}
<td rowspan="{{ event.field_changes|length }}">
{{ event.datetime|date:"Y-m-d H:i:s" }}
</td>
<td rowspan="{{ event.field_changes|length }}">
{{ event.user.username|default:"Anonymous" }}
</td>
<td rowspan="{{ event.field_changes|length }}">
{{ event.event_type_display }}
</td>
<td rowspan="{{ event.field_changes|length }}">
{{ event.model_name|title }}
</td>
<td rowspan="{{ event.field_changes|length }}">
{{ event.object_id }}
</td>
<td rowspan="{{ event.field_changes|length }}">
{{ event.object_repr }}
</td>
{% endif %}
{# Display the specific field change details in their own columns #}
<td><strong>{{ change.field }}</strong></td>
<td>
{% if change.old is not None %}
<pre style="white-space: pre-wrap; word-break: break-all; font-size: 0.85em; background-color: #f8f9fa; padding: 5px; border-radius: 3px;">{{ change.old }}</pre>
{% else %}
(None)
{% endif %}
</td>
<td>
{% if change.new is not None %}
<pre style="white-space: pre-wrap; word-break: break-all; font-size: 0.85em; background-color: #f8f9fa; padding: 5px; border-radius: 3px;">{{ change.new }}</pre>
{% else %}
(None)
{% endif %}
</td>
</tr>
{% endfor %}
{% else %}
{# Fallback for events with no specific field changes (e.g., CREATE, DELETE) #}
<tr>
<td>{{ event.datetime|date:"Y-m-d H:i:s" }}</td>
<td>{{ event.user.username|default:"Anonymous" }}</td>
<td>{{ event.event_type_display }}</td>
<td>{{ event.model_name|title }}</td>
<td>{{ event.object_id }}</td>
<td>{{ event.object_repr }}</td>
{# Span the 'Field', 'Old Value', 'New Value' columns #}
<td>
{% if event.event_type_display == "Create" %}
{% trans "Object created." %}
{% elif event.event_type_display == "Delete" %}
{% trans "Object deleted." %}
{% else %}
{% trans "No specific field changes recorded." %}
{% endif %}
</td>
</tr>
{% endif %}
{% endfor %}
</tbody>
</table>
</div>
{% include 'partials/pagination_audit.html' with q='userActions' %}
{% else %}
<p>{% trans "No model change audit events found." %}</p>
{% endif %}
</div>
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,19 @@
{% load i18n %}
<ul class="nav nav-tabs" id="accountTypeTabs" role="tablist">
<li class="nav-item me-3" role="presentation">
<a href="{% url 'audit_log_dashboard' %}?q=userActions">
<i class="fas fa-history me-2"></i>{% trans "User Actions" %}
</a>
</li>
<li class="nav-item me-3" role="presentation">
<a href="{% url 'audit_log_dashboard' %}?q=loginEvents">
<i class="fas fa-right-to-bracket me-2"></i>{% trans "User Login Events" %}
</a>
</li>
<li class="nav-item" role="presentation">
<a href="{% url 'audit_log_dashboard' %}?q=userRequests">
<i class="fas fa-file-alt me-2"></i>{% trans "User Page Requests" %}
</a>
</li>
</ul>

View File

@ -0,0 +1,64 @@
{% extends "base.html" %}
{% load i18n custom_filters %}
{% block title %}{% trans "Accounts" %}{% endblock title %}
{% block accounts %}
<a class="nav-link active fw-bold">
{% trans "Accounts"|capfirst %}
<span class="visually-hidden">(current)</span>
</a>
{% endblock %}
{% block content %}
<div class="row mt-4">
<div class="d-flex justify-content-between mb-2">
<h3 class=""><i class="fa-solid fa-book"></i> {% trans "Audit Log Dashboard" %}</h3>
</div>
<!-- Log Type Tabs -->
<div class="mb-4">
{% include 'admin_management/nav.html' %}
<div class="tab-content p-3 border border-top-0 rounded-bottom" id="accountTypeTabsContent">
<!-- modellogs Tab -->
{% if page_obj %}
<div class="table-responsive px-1 scrollbar mt-3">
<table class= "table align-items-center table-flush table-hover">
<thead>
<tr class="bg-body-highlight">
<th class="sort white-space-nowrap align-middle" scope="col">{{ _("Timestamp") |capfirst }}</th>
<th class="sort white-space-nowrap align-middle" scope="col">{{ _("User") |capfirst }}</th>
<th class="sort white-space-nowrap align-middle" scope="col">{{ _("URL") }}</th>
<th class="sort white-space-nowrap align-middle" scope="col">{{ _("Method") |capfirst }}</th>
<th class="sort white-space-nowrap align-middle"scope="col">{{ _("IP Address") |capfirst }}</th>
</tr>
</thead>
<tbody class="list">
{% for event in page_obj.object_list %}
<tr class="hover-actions-trigger btn-reveal-trigger position-static">
<td class="align-middle product white-space-nowrap">{{event.datetime}}</td>
<td class="align-middle product white-space-nowrap">{{ event.user.username|default:"Anonymous" }}</td>
<td class="align-middle product white-space-nowrap">{{ event.url }}</td>
<td class="align-middle product white-space-nowrap">{{ event.method}}</td>
<td class="align-middle product white-space-nowrap">{{ event.remote_ip}}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% include 'partials/pagination_audit.html' with q='userRequests' %}
{% else %}
<p>No request audit events found.</p>
{% endif %}
</div>
</div>
</div>
{% endblock %}

View File

@ -1,14 +1,6 @@
{% extends 'base.html' %}
{% load i18n static humanize %}
{% block title %}{{ _("Opportunity Detail") }}{% endblock title %}
{% block customCSS %}
<style>
.completed-task {
text-decoration: line-through;
opacity: 0.7;
}
</style>
{% endblock customCSS %}
{% block content %}
<div class="row align-items-center justify-content-between g-3 mb-4">
<div class="col-12 col-md-auto">
@ -87,12 +79,12 @@
<div class="col-12 overflow-auto" style="max-height: 200px;">
<ul class="list-group list-group-flush">
{% for event in opportunity.get_schedules %}
<li class="list-group-item d-flex justify-content-between ">
<li class="list-group-item d-flex justify-content-between align-items-center">
<div class="d-flex align-items-center">
<span class="badge rounded-pill bg-phoenix-primary text-primary me-2 fs-9">{{ event.scheduled_type|capfirst }}</span>
<div class="fs-9 d-flex flex-column">
<span class="fs-9">{{ event.purpose|capfirst }}</span>
<span><span class="badge rounded-pill bg-phoenix-primary text-secondary"><span class="d-inline-block lh-sm me-1" data-feather="clock" style="height:16px;width:16px;"></span> {{ event.scheduled_at|naturaltime|capfirst }}</span></span>
</div>
<span class="fs-9">{{ event.purpose }}</span>
</div>
<div class="fs-9">{{ event.scheduled_at|naturaltime|capfirst }}</div>
</li>
{% empty %}
<li class="list-group-item text-center fs-9">{{ _("No upcoming events") }}</li>
@ -107,22 +99,12 @@
<h4 class="mb-5 d-flex align-items-center"><span class="d-inline-block lh-sm me-1" data-feather="link" style="height:16px;width:16px;"></span> {{ _("Related Records")}}</h4>
<div class="row g-3">
<div class="col-12">
<div class="mb-4">
<div class="d-flex flex-wrap justify-content-between mb-2">
<h5 class="mb-0 text-body-highlight me-2">{{ _("Lead") }}</h5>
</div>
{% if opportunity.lead %}
<span class="badge rounded-pill bg-phoenix-primary text-primary"><a class="dropdown-item d-flex align-items-center" href="{% url 'lead_detail' opportunity.lead.slug %}" target="_blank">{{ _("View Lead")}}&nbsp <span class="d-inline-block lh-sm me-2" data-feather="external-link" style="height:16px;width:16px;"></span></a></span>
{% else %}
<p>{{ _("No Lead") }}</p>
{% endif %}
</div>
<div class="mb-4">
<div class="d-flex flex-wrap justify-content-between mb-2">
<h5 class="mb-0 text-body-highlight me-2">{{ _("Estimate") }}</h5>
</div>
{% if opportunity.estimate %}
<span class="badge rounded-pill bg-phoenix-primary text-primary"><a class="dropdown-item" href="{% url 'estimate_detail' opportunity.estimate.pk %}">{{ _("View Quotation")}}</a></span>
<a class="dropdown-item" href="{% url 'estimate_detail' opportunity.estimate.pk %}">{{ _("View Quotation")}}</a>
{% else %}
<p>{{ _("No Estimate") }}</p>
{% endif %}
@ -132,7 +114,7 @@
<h5 class="mb-0 text-body-highlight me-2">{{ _("Invoice") }}</h5>
</div>
{% if opportunity.estimate.invoice %}
<span class="badge rounded-pill bg-phoenix-primary text-primary"><a class="dropdown-item" href="{% url 'invoice_detail' opportunity.estimate.invoice.pk %}">{{ _("View Invoice")}}</a></span>
<a class="dropdown-item" href="{% url 'invoice_detail' opportunity.estimate.invoice.pk %}">{{ _("View Invoice")}}</a>
{% else %}
<p>{{ _("No Invoice") }}</p>
{% endif %}
@ -252,7 +234,7 @@
</div>
</td>
<td class="py-2 d-none d-sm-block pe-sm-2">:</td>
<td class="py-2"><a class="ps-6 ps-sm-0 fw-semibold mb-0 pb-3 pb-sm-0 text-body" href="tel:{{ opportunity.customer.phone_number }}">{{ opportunity.lead.phone_number }}</a></td>
<td class="py-2"><a class="ps-6 ps-sm-0 fw-semibold mb-0 pb-3 pb-sm-0 text-body" href="tel:{{ opportunity.customer.phone_number }}">{{ opportunity.customer.phone_number }}</a></td>
</tr>
<tr>
<td class="py-2">
@ -262,7 +244,7 @@
</div>
</td>
<td class="py-2 d-none d-sm-block pe-sm-2">:</td>
<td class="py-2"><a class="ps-6 ps-sm-0 fw-semibold mb-0 text-body" href="mailto:{{ opportunity.customer.email}}">{{ opportunity.lead.email}}</a></td>
<td class="py-2"><a class="ps-6 ps-sm-0 fw-semibold mb-0 text-body" href="mailto:{{ opportunity.customer.email}}">{{ opportunity.customer.email}}</a></td>
</tr>
</table>
</div>
@ -450,7 +432,7 @@
</div>
</div>
<div class="row g-3">
{% for metting in opportunity.get_meetings %}
{% for metting in opportunity.lead.get_meetings %}
<div class="col-xxl-6">
<div class="card h-100">
<div class="card-body">
@ -472,10 +454,12 @@
<div class="col-auto d-flex flex-1">
<h2 class="mb-0">Call</h2>
</div>
<div class="col-auto">
<a href="{% url 'schedule_lead' opportunity.lead.slug %}" class="btn btn-primary"><span class="fa-solid fa-plus me-2"></span>Add Call</a>
</div>
</div>
<pre>{{opportunity.get_all_notes}}</pre>
<div class="border-top border-bottom border-translucent" id="leadDetailsTable" data-list='{"valueNames":["name","description","create_date","create_by","last_activity"],"page":5,"pagination":true}'>
<div class="table-responsive scrollbar mx-n1 px-1">
<table class="table fs-9 mb-0">
@ -487,7 +471,7 @@
</tr>
</thead>
<tbody class="list" id="lead-details-table-body">
{% for call in opportunity.get_calls %}
{% for call in opportunity.lead.get_calls %}
<tr class="hover-actions-trigger btn-reveal-trigger position-static">
<td class="description align-middle white-space-nowrap text-start fw-bold text-body-tertiary py-2 pe-6">{{call.purpose}}</td>
<td class="create_date text-end align-middle white-space-nowrap text-body py-2">{{call.scheduled_by}}</td>
@ -559,7 +543,7 @@
</tr>
</thead>
<tbody class="list" id="all-email-table-body">
{% for email in opportunity.get_emails %}
{% for email in opportunity.lead.get_emails %}
<tr class="hover-actions-trigger btn-reveal-trigger position-static">
<td class="fs-9 align-middle px-0 py-3">
<div class="form-check mb-0 fs-8">

View File

@ -416,7 +416,7 @@
<div class="overflow-auto scrollbar" style="height: 10rem;">
<ul class="nav d-flex flex-column mb-2 pb-1">
{% if request.is_dealer %}
<li class="nav-item">
<li class="nav-item">
<a class="nav-link px-3 d-block" href="{% url 'dealer_detail' request.user.dealer.slug %}"> <span class="me-2 text-body align-bottom" data-feather="user"></span><span>{% translate 'profile'|capfirst %}</span></a>
</li>
{% else %}

View File

@ -1,5 +1,6 @@
{% extends "base.html" %}
{% load i18n %}
{%block title%} {%trans 'Add Colors'%} {% endblock%}
{% block content %}
<div class="row mt-4">
<h5 class="text-center">{% trans "Add Colors" %}</h5>
@ -18,7 +19,8 @@
<input class="color-radio"
type="radio"
name="exterior"
value="{{ color.id }}">
value="{{ color.id }}" {% if color.id == form.instance.exterior.id %}checked{% endif %}>
<div class="card-body color-display"
style="background-color: rgb({{ color.rgb }})">
<div class="">
@ -38,7 +40,7 @@
<input class="color-radio"
type="radio"
name="interior"
value="{{ color.id }}">
value="{{ color.id }}" {% if color.id == form.instance.interior.id %}checked{% endif %}>
<div class="card-body color-display"
style="background-color: rgb({{ color.rgb }})">
<div class="">

View File

@ -1,4 +1,4 @@
{% extends 'base.html' %}
{% extends 'base.html' %}
{% load i18n static custom_filters %}
{% block title %}{{ _("Car Details") }}{% endblock %}
{% block customCSS %}
@ -17,14 +17,24 @@
{% endblock customCSS %}
{% block content %}
{% if not car.ready %}
{% if not car.ready and not car.status == 'sold' %}
<div class="alert alert-outline-warning d-flex align-items-center"
role="alert">
<i class="fa-solid fa-circle-info fs-6"></i>
{%if not car.finances and not car.colors%}
<p class="mb-0 flex-1">
{{ _("This car information is not complete , please add colors and finances before making it ready for sale .") }}<a class="ms-3 text-body-primary fs-9"
href="{% url 'add_color' car.slug %}">{{ _("Add Color") }}</a>
{{ _("This car information is not complete , please add colors and finances both before making it ready for sale .") }}
</p>
{% elif car.finances and not car.colors %}
<p class="mb-0 flex-1">
{{ _("This car information is not complete , please add colors before making it ready for sale .") }}
</p>
{%else%}
<p class="mb-0 flex-1">
{{ _("This car information is not complete , please add finances before making it ready for sale .") }}
</p>
{%endif%}
<button class="btn-close"
type="button"
data-bs-dismiss="alert"
@ -237,7 +247,7 @@
</tr>
<tr>
<th>{% trans "Discount Amount"|capfirst %}</th>
<td>{{ car.finances.discount_amount|floatformat:2 }} -</td>
<td>{{ car.finances.discount_amount|floatformat:2 }}</td>
</tr>
<tr>
<th>{% trans "Additional Fee"|capfirst %}</th>
@ -285,6 +295,8 @@
<div class="card-body">
<div class="table-responsive scrollbar mb-3">
<table class="table table-sm fs-9 mb-0 overflow-hidden">
<!--test-->
{% if car.colors %}
<tr>
<th>{% trans 'Exterior' %}</th>
@ -309,32 +321,27 @@
<tr>
<td colspan="2">
{% comment %} {% if not car.get_transfer %}
<a href="{% url 'car_finance_update' car.finances.pk %}"
{% if not car.get_transfer %}
<a href="{% url 'car_colors_update' car.slug %}"
class="btn btn-phoenix-warning btn-sm mb-3">{% trans "Edit" %}</a>
{% else %}
<span class="badge bg-danger">{% trans "Cannot Edit, Car in Transfer." %}</span>
{% endif %}
{% else %}
<p>{% trans "No finance details available." %}</p>
{% if perms.inventory.add_carfinance %}
<a href="{% url 'car_finance_create' car.slug %}"
class="btn btn-phoenix-success btn-sm mb-3">{% trans "Add" %}</a>
{% endif %} {% endcomment %}
</td>
</tr>
{% comment %} <tr>
<td colspan="2">{% trans "No colors available for this car." %}</td>
</tr>
{% else %}
<tr>
<td colspan="2">
{% if perms.inventory.change_carcolors %}
<a href="{% url 'add_color' car.slug %}"
class="btn btn-phoenix-success btn-sm">{% trans "Add" %}</a>
<td colspan="2">
<p>{% trans "No color details available." %}</p>
{% if perms.inventory.add_carcolors %}
<a class="btn btn-phoenix-success btn-sm mb-3" href="{% url 'add_color' car.slug %}">{{ _("Add Color") }}</a>
{% endif %}
</td>
</tr> {% endcomment %}
</tr>
{% endif %}
<!--test-->
</table>
</div>
</div>

View File

@ -0,0 +1,32 @@
{% load i18n static %}
<div class="row align-items-center justify-content-center py-4 pe-0 fs-9">
<div class="col-auto d-flex">
{# Previous Button #}
{% if page_obj.has_previous %}
<a class="page-link" href="?page={{ page_obj.previous_page_number }}{% if q %}&q={{q}}{% endif %}" aria-label="{% trans 'Previous' %}">
<span class="fas fa-chevron-{% if LANGUAGE_CODE == 'ar' %}right{% else %}left{% endif %}" aria-hidden="true"></span>
<span>{% trans "Previous" %}</span>
</a>
{% else %}
<span class="page-link disabled" aria-disabled="true">
<span class="fas fa-chevron-{% if LANGUAGE_CODE == 'ar' %}right{% else %}left{% endif %}" aria-hidden="true"></span>
<span>{% trans "Previous" %}</span>
</span>
{% endif %}
{# Next Button #}
{% if page_obj.has_next %}
<a class="page-link" href="?page={{ page_obj.next_page_number }}{% if q %}&q={{q}}{% endif %}" aria-label="{% trans 'Next' %}">
<span>{% trans "Next" %}</span>
<span class="fas fa-chevron-{% if LANGUAGE_CODE == 'ar' %}left{% else %}right{% endif %}" aria-hidden="true"></span>
</a>
{% else %}
<span class="page-link disabled" aria-disabled="true">
<span>{% trans "Next" %}</span>
<span class="fas fa-chevron-{% if LANGUAGE_CODE == 'ar' %}left{% else %}right{% endif %}" aria-hidden="true"></span>
</span>
{% endif %}
</div>
</div>

View File

@ -15,7 +15,7 @@ document.addEventListener('DOMContentLoaded', function() {
document.getElementById('POModalBody').innerHTML = `
<div class="d-flex justify-content-center gap-3 py-3">
<a class="btn btn-primary px-4" href="${actionUrl}">
<a class="btn btn-outline-primary px-4" href="${actionUrl}">
<i class="fas fa-check-circle me-2"></i>${buttonText}
</a>
<button class="btn btn-outline-secondary" data-bs-dismiss="modal">
@ -45,7 +45,7 @@ document.addEventListener('DOMContentLoaded', function() {
<div class="card-body">
<h3 class="h4 mb-4">
<span class="badge bg-{% if po_model.is_draft %}secondary{% elif po_model.is_approved %}success{% elif po_model.is_fulfilled %}primary{% else %}warning{% endif %}">
<span class="badge bg-{% if po_model.is_draft %}success{% elif po_model.is_approved %}success{% elif po_model.is_fulfilled %}primary{% else %}warning{% endif %}">
{{ po_model.get_po_status_display }}
</span>
</h3>
@ -214,7 +214,7 @@ document.addEventListener('DOMContentLoaded', function() {
{% endif %}
{% if po_model.can_cancel %}
<button class="btn btn-outline-danger"
<button class="btn btn-outline-secondary"
onclick="djLedger.toggleModal('{{ po_model.get_mark_as_canceled_html_id }}')">
<i class="fas fa-window-close me-2"></i>{% trans 'Cancel' %}
</button>

View File

@ -95,7 +95,7 @@
</div>
</div>
<div class="col-12">
<button class="btn btn-primary">{% trans 'Save' %}</button>
<button class="btn btn-outline-success">{% trans 'Save' %}</button>
</div>
</div>
</form>

View File

@ -2,7 +2,7 @@
{% load static i18n crispy_forms_tags %}
{% block content %}
<div class="container-fluid m-0">
<div class="container-fluid mt-4">
<form action="" method="post">
{% csrf_token %}
<div class="form-group">
@ -13,11 +13,11 @@
<label for="account">Account</label>
<select class="form-control" name="account" id="account">
{% for account in inventory_accounts %}
<option value="{{ account.pk }}">{{ account }}"></option>
<option value="{{ account.pk }}">{{ account }}</option>
{% endfor %}
</select>
</div>
<button type="submit" class="btn btn-primary">Add New Item To Inventory</button>
<button type="submit" class="btn btn-primary mt-2">Add New Item To Inventory</button>
</form>
</div>
{% endblock content %}

View File

@ -1,74 +1,76 @@
<!-- po_list.html -->
{% extends "base.html" %}
{% load i18n static %}
{% block title %}
Purchase Orders - {{ block.super }}
{% endblock %}
{% block content %}
<div class="container mt-4">
<h2 class="mb-4">Purchase Orders</h2>
<!-- Success Message -->
{% if messages %}
{% for message in messages %}
<div class="row mt-4">
<!-- Success Message -->
{% if messages %}
{% for message in messages %}
<div class="alert alert-success">{{ message }}</div>
{% endfor %}
{% endif %}
<!-- Add New PO Button -->
<div class="d-flex justify-content-between align-items-center mb-3">
<a href="{% url 'purchase_order_create' %}" class="btn btn-primary">
<i class="bi bi-plus-lg"></i> Create New PO
</a>
</div>
<!-- PO Table -->
<div class="table-responsive">
<table class="table table-striped table-hover align-middle">
<thead class="table-dark">
<tr>
<th scope="col">PO Number</th>
<th scope="col">Description</th>
<th scope="col">Status</th>
<th scope="col">Created At</th>
<th scope="col">Actions</th>
</tr>
</thead>
<tbody>
{% endfor %}
{% endif %}
<!-- Add New PO Button -->
<div class="d-flex justify-content-between mb-2">
<h3 class="">
{{ _("Purchase Orders") |capfirst }}
</h2>
<a href="{% url 'purchase_order_create' %}"
class="btn btn-md btn-phoenix-primary"><i class="fa fa-plus me-2"></i>{{ _("Create New PO") }}</a>
</div>
{% include "partials/search_box.html" %}
<div class="table-responsive px-1 scrollbar mt-3">
<table class= "table align-items-center table-flush table-hover">
<thead>
<tr class="bg-body-highlight">
<th class="sort white-space-nowrap align-middle" scope="col" style="width:15%">PO Number</th>
<th class="sort white-space-nowrap align-middle" scope="col" style="width:40%">Description</th>
<th class="sort white-space-nowrap align-middle" scope="col" style="width:15%">Status</th>
<th class="sort white-space-nowrap align-middle" scope="col" style="width:15%">Created At</th>
<th class="sort white-space-nowrap align-middle" scope="col" style="width:15%">Actions</th>
</tr>
</thead>
<tbody class="list">
{% if purchase_orders %}
{% for po in purchase_orders %}
<tr>
<td>{{ po.po_number }}</td>
<td>{{ po.po_title }}</td>
<td>
<tr class="hover-actions-trigger btn-reveal-trigger position-static">
<td class="align-middle product white-space-nowrap">{{ po.po_number }}</td>
<td class="align-middle product white-space-nowrap">{{ po.po_title }}</td>
<td class="align-middle product white-space-nowrap">
<span class="">
{{ po.po_status|capfirst }}
</span>
</td>
<td>{{ po.created|date:"M d, Y" }}</td>
<td>
<a href="{% url 'purchase_order_detail' po.pk %}" class="btn btn-sm btn-info me-1">
View
</a>
{% comment %} <a href="{% url 'purchase_order_detail' po.id %}" class="btn btn-sm btn-info me-1">
View
<td class="align-middle product white-space-nowrap">{{ po.created|date:"M d, Y" }}</td>
<td class="align-middle product white-space-nowrap">
<a href="{% url 'purchase_order_detail' po.pk %}"
class="btn btn-sm btn-phoenix-success">
<i class="fa-regular fa-eye me-1"></i>
{% trans "view"|capfirst %}
</a>
<a href="{% url 'purchase_order_update' po.id %}" class="btn btn-sm btn-warning me-1">
Edit
</a>
<a href="{% url 'purchase_order_delete' po.id %}" class="btn btn-sm btn-danger">
Delete
</a> {% endcomment %}
</td>
</tr>
{% endfor %}
{% else %}
<tr>
</tr>
{%endfor%}
{% else%}
<tr>
<td colspan="6" class="text-center">No purchase orders found.</td>
</tr>
{% endif %}
</tbody>
</table>
</tbody>
</table>
</div>
<div class="d-flex justify-content-end mt-3">
<div class="d-flex">
{% if is_paginated %}
{% include 'partials/pagination.html' %}
{% endif %}
</div>
</div>
</div>
</div>
{% include 'modal/delete_modal.html' %}
{% endblock %}

View File

@ -24,12 +24,13 @@
{% csrf_token %}
{{ form|crispy }}
<button type="submit"
class="btn btn-primary w-100 my-2">{% trans 'Save PO' %}
class="btn btn-outline-success w-100 my-2">{% trans 'Save PO' %}
</button>
<a href="{% url 'purchase_order_detail' po_model.uuid %}"
class="btn btn-dark w-100 my-2">{% trans 'Back to PO Detail' %}</a>
class="btn btn-outline-secondary w-100 my-2">{% trans 'Back to PO Detail' %}</a>
<a href="{% url 'purchase_order_list' %}"
class="btn btn-info w-100 my-2">{% trans 'PO List' %}</a>
class="btn btn-outline-info
info w-100 my-2">{% trans 'PO List' %}</a>
</div>
</div>
</form>

View File

@ -4,7 +4,7 @@
<div class="table-container">
<table class="table is-fullwidth is-striped is-narrow is-bordered">
<thead>
<tr class="has-text-centered">
<tr class="has-text-centered bg-body-highlight">
<th>{% trans 'Item' %}</th>
<th>{% trans 'Unit Cost' %}</th>
<th>{% trans 'PO Qty' %}</th>