before merge

This commit is contained in:
gitea 2024-12-17 13:33:59 +00:00
parent 9e0824de6d
commit 0d16ef4520
40 changed files with 4105 additions and 2363 deletions

1
.gitignore vendored
View File

@ -5,6 +5,7 @@
__pycache__
db.sqlite3
media
./car_inventory/settings.py
# Backup files #
*.bak

View File

@ -1533,7 +1533,7 @@ def decode_vin(vin):
# VR3USHNLWRJ521303
# KNARH81E8P5194005
# Example usage
vin_number = 'LGWEE4A53SK607775'
vin_number = 'KNARH81E8P5194005'
decoded_vin = decode_vin(vin_number)
print(decoded_vin)

View File

@ -7,6 +7,7 @@ admin.site.register(models.Vendor)
admin.site.register(models.Customer)
admin.site.register(models.SaleQuotation)
admin.site.register(models.SaleQuotationCar)
admin.site.register(models.SalesOrder)
admin.site.register(models.Car)
admin.site.register(models.CarFinance)
admin.site.register(models.CarColors)
@ -15,6 +16,9 @@ admin.site.register(models.CustomCard)
admin.site.register(models.CarSpecificationValue)
admin.site.register(models.ExteriorColors)
admin.site.register(models.InteriorColors)
admin.site.register(models.Subscription)
admin.site.register(models.SubscriptionPlan)
admin.site.register(models.SubscriptionUser)
@admin.register(models.CarMake)
class CarMakeAdmin(admin.ModelAdmin):
@ -55,18 +59,18 @@ class CarSeriesAdmin(admin.ModelAdmin):
verbose_name = "Car Series"
@admin.register(models.CarTrim)
class CarTrimAdmin(admin.ModelAdmin):
list_display = ('name',
'id_car_serie__name',
'id_car_serie__id_car_model__name',
'id_car_serie__id_car_model__id_car_make__name')
search_fields = ('name', 'arabic_name', 'id_car_serie__id_car_model__name')
list_filter = ('id_car_serie__id_car_model__id_car_make__is_sa_import',
'id_car_serie__id_car_model__id_car_make__name')
# @admin.register(models.CarTrim)
# class CarTrimAdmin(admin.ModelAdmin):
# list_display = ('name',
# 'id_car_serie__name',
# 'id_car_serie__id_car_model__name',
# 'id_car_serie__id_car_model__id_car_make__name')
# search_fields = ('name', 'arabic_name', 'id_car_serie__id_car_model__name')
# list_filter = ('id_car_serie__id_car_model__id_car_make__is_sa_import',
# 'id_car_serie__id_car_model__id_car_make__name')
class Meta:
verbose_name = "Car Trim"
# class Meta:
# verbose_name = "Car Trim"
@admin.register(models.CarSpecification)

View File

@ -19,6 +19,12 @@ from django.forms import ModelMultipleChoiceField
from django.utils.translation import gettext_lazy as _
class UserForm(forms.ModelForm):
class Meta:
model = Dealer
fields = ['name', 'arabic_name', 'phone_number', 'address','dealer_type']
# Dealer Form
class DealerForm(forms.ModelForm):
class Meta:

View File

@ -0,0 +1,349 @@
# Generated by Django 4.2.17 on 2024-12-12 07:40
from decimal import Decimal
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
import inventory.mixins
import phonenumber_field.modelfields
class Migration(migrations.Migration):
replaces = [('inventory', '0001_initial'), ('inventory', '0002_remove_salequotationcar_financial_details_and_more'), ('inventory', '0003_remove_salequotationcar_administration_fee_and_more'), ('inventory', '0004_remove_carfinance_administration_vat_amount_and_more'), ('inventory', '0005_alter_carfinance_options_alter_carfinance_total'), ('inventory', '0006_alter_car_status'), ('inventory', '0007_salequotation_amount_salequotation_dealer_and_more'), ('inventory', '0008_carfinance_vat_amount'), ('inventory', '0009_alter_salequotation_amount'), ('inventory', '0010_alter_salequotation_dealer'), ('inventory', '0011_remove_salequotationcar_dealer'), ('inventory', '0012_remove_salequotationcar_price'), ('inventory', '0005_alter_carfinance_options_remove_carfinance_total'), ('inventory', '0013_merge_20241211_1620')]
initial = True
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='Car',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('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')], 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')),
],
options={
'verbose_name': 'Car',
'verbose_name_plural': 'Cars',
},
),
migrations.CreateModel(
name='CarMake',
fields=[
('id_car_make', models.AutoField(primary_key=True, serialize=False)),
('name', models.CharField(max_length=255)),
('arabic_name', models.CharField(max_length=255)),
('logo', models.ImageField(blank=True, null=True, upload_to='car_make', verbose_name='logo')),
('is_sa_import', models.BooleanField(default=False)),
],
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='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')),
('profit_margin', models.DecimalField(decimal_places=2, editable=False, max_digits=14, verbose_name='Profit Margin')),
('discount_amount', models.DecimalField(decimal_places=2, default=Decimal('0.00'), max_digits=14, verbose_name='Discount Amount')),
('registration_fee', models.DecimalField(decimal_places=2, default=Decimal('0.00'), max_digits=14, verbose_name='Registration Fee')),
('administration_fee', models.DecimalField(decimal_places=2, default=Decimal('0.00'), max_digits=14, verbose_name='Administration Fee')),
('transportation_fee', models.DecimalField(decimal_places=2, default=Decimal('0.00'), max_digits=14, verbose_name='Transportation Fee')),
('custom_card_fee', models.DecimalField(decimal_places=2, default=Decimal('0.00'), max_digits=14, verbose_name='Custom Card Fee')),
('vat_rate', models.DecimalField(decimal_places=2, default=Decimal('0.15'), max_digits=14, verbose_name='VAT Rate')),
('car', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='finances', to='inventory.car')),
('vat_amount', models.DecimalField(decimal_places=2, default=2300, editable=False, max_digits=14, verbose_name='Vat Amount')),
],
options={
'verbose_name': 'Car Financial Details',
},
),
migrations.AddField(
model_name='car',
name='id_car_make',
field=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'),
),
migrations.CreateModel(
name='CarModel',
fields=[
('id_car_model', models.AutoField(primary_key=True, serialize=False)),
('name', models.CharField(max_length=255)),
('arabic_name', models.CharField(max_length=255)),
('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='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(max_length=1, verbose_name='Text 2')),
('text3', models.CharField(max_length=1, verbose_name='Text 3')),
('registration_date', models.DateTimeField(verbose_name='Registration Date')),
('car', models.ForeignKey(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(max_length=255)),
('arabic_name', models.CharField(max_length=255)),
('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)),
('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(max_length=255)),
('arabic_name', models.CharField(max_length=255)),
('start_production_year', models.IntegerField(blank=True, null=True)),
('end_production_year', models.IntegerField(blank=True, null=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='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.ForeignKey(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(max_length=10, verbose_name='Commercial Registration Number')),
('vrn', models.CharField(max_length=15, 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')),
('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),
),
migrations.CreateModel(
name='Customer',
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')),
('middle_name', models.CharField(blank=True, max_length=50, null=True, verbose_name='Middle Name')),
('last_name', models.CharField(max_length=50, verbose_name='Last Name')),
('email', models.EmailField(max_length=254, unique=True, verbose_name='Email')),
('national_id', models.CharField(max_length=10, 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')),
('created', models.DateTimeField(auto_now_add=True, verbose_name='Created')),
('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.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.CreateModel(
name='SaleQuotation',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('remarks', models.TextField(blank=True, null=True, verbose_name='Remarks')),
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')),
('customer', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='quotations', to='inventory.customer', verbose_name='Customer')),
('status', models.CharField(choices=[('DRAFT', 'Draft'), ('CONFIRMED', 'Confirmed'), ('CANCELED', 'Canceled')], default='DRAFT', max_length=10, verbose_name='Status')),
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Updated At')),
('amount', models.DecimalField(decimal_places=2, default=Decimal('0.00'), max_digits=10, verbose_name='Amount')),
('dealer', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='sales', to='inventory.dealer')),
],
),
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')),
('address', models.CharField(blank=True, max_length=200, null=True, verbose_name='Address')),
('logo', models.ImageField(blank=True, null=True, upload_to='logos/vendors', verbose_name='Logo')),
('dealer', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='vendors', to='inventory.dealer')),
],
options={
'verbose_name': 'Vendor',
'verbose_name_plural': 'Vendors',
},
bases=(models.Model, inventory.mixins.LocalizedNameMixin),
),
migrations.AddField(
model_name='car',
name='vendor',
field=models.ForeignKey(blank=True, null=True, 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)),
('reserved_until', models.DateTimeField()),
('car', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='reservations', to='inventory.car')),
('reserved_by', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
],
options={
'ordering': ['-reserved_at'],
'unique_together': {('car', 'reserved_until')},
},
),
migrations.CreateModel(
name='CarColors',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('car', models.ForeignKey(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='SalesOrder',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')),
('total_amount', models.DecimalField(decimal_places=2, max_digits=14, verbose_name='Total Amount')),
('quotation', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='sales_order', to='inventory.salequotation', verbose_name='Quotation')),
],
),
migrations.AlterField(
model_name='car',
name='status',
field=models.CharField(choices=[('available', 'Available'), ('sold', 'Sold'), ('hold', 'Hold'), ('damaged', 'Damaged'), ('reserved', 'Reserved')], default='available', max_length=10, verbose_name='Status'),
),
migrations.CreateModel(
name='SaleQuotationCar',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('car', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='inventory.car', verbose_name='Car')),
('quotation', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='quotation_cars', to='inventory.salequotation', verbose_name='Quotation')),
('quantity', models.PositiveIntegerField(default=1, verbose_name='Quantity')),
],
),
]

View File

@ -0,0 +1,14 @@
# Generated by Django 4.2.17 on 2024-12-11 13:20
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('inventory', '0005_alter_carfinance_options_remove_carfinance_total'),
('inventory', '0012_remove_salequotationcar_price'),
]
operations = [
]

View File

@ -26,6 +26,10 @@ class Migration(migrations.Migration):
model_name='carfinance',
name='vat_rate',
),
# migrations.RemoveField(
# model_name='carfinance',
# name='total',
# ),
migrations.AddField(
model_name='carfinance',
name='total',

View File

@ -0,0 +1,23 @@
# Generated by Django 4.2.17 on 2024-12-11 13:26
from decimal import Decimal
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('inventory', '0013_merge_20241211_1620'),
]
operations = [
# migrations.RemoveField(
# model_name='carfinance',
# name='total',
# ),
migrations.AddField(
model_name='carfinance',
name='total',
field=models.DecimalField(blank=True, decimal_places=2, default=Decimal('0.00'), max_digits=14, null=True),
),
]

View File

@ -0,0 +1,14 @@
# Generated by Django 4.2.17 on 2024-12-12 07:35
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('inventory', '0014_carfinance_total'),
('inventory', '0016_alter_carfinance_car_alter_customcard_car'),
]
operations = [
]

View File

@ -0,0 +1,68 @@
# Generated by Django 4.2.17 on 2024-12-15 13:20
from decimal import Decimal
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('inventory', '0017_merge_20241212_1035'),
]
operations = [
migrations.AlterModelOptions(
name='carfinance',
options={'verbose_name': 'Car Financial Details'},
),
migrations.AlterModelOptions(
name='carreservation',
options={'ordering': ['-reserved_at']},
),
migrations.AddField(
model_name='carfinance',
name='vat_rate',
field=models.DecimalField(decimal_places=2, default=Decimal('0.15'), max_digits=14, verbose_name='VAT Rate'),
),
migrations.AlterField(
model_name='carfinance',
name='car',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='finances', to='inventory.car'),
),
migrations.AlterField(
model_name='carfinance',
name='vat_amount',
field=models.DecimalField(decimal_places=2, editable=False, max_digits=14, verbose_name='Vat Amount'),
),
migrations.AlterField(
model_name='carreservation',
name='car',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='reservations', to='inventory.car'),
),
migrations.AlterField(
model_name='carreservation',
name='reserved_at',
field=models.DateTimeField(auto_now_add=True),
),
migrations.AlterField(
model_name='carreservation',
name='reserved_by',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL),
),
migrations.AlterField(
model_name='carreservation',
name='reserved_until',
field=models.DateTimeField(),
),
migrations.AlterField(
model_name='customcard',
name='car',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='custom_cards', to='inventory.car', verbose_name='Car'),
),
migrations.DeleteModel(
name='CarLocation',
),
]

View File

@ -0,0 +1,32 @@
# Generated by Django 4.2.17 on 2024-12-15 14:47
from django.db import migrations, models
import django.db.models.deletion
import inventory.mixins
import phonenumber_field.modelfields
class Migration(migrations.Migration):
dependencies = [
('inventory', '0018_alter_carfinance_options_and_more'),
]
operations = [
migrations.CreateModel(
name='SubDealer',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('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')),
('dealer', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='subdealers', to='inventory.dealer')),
],
options={
'verbose_name': 'SubDealer',
'verbose_name_plural': 'SubDealers',
},
bases=(models.Model, inventory.mixins.LocalizedNameMixin),
),
]

View File

@ -0,0 +1,18 @@
# Generated by Django 4.2.17 on 2024-12-15 14:53
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('inventory', '0019_subdealer'),
]
operations = [
migrations.AddField(
model_name='subdealer',
name='dealer_type',
field=models.CharField(choices=[('Inventory', 'Inventory'), ('Accountent', 'Accountent'), ('sales', 'Sales')], default='Inventory', max_length=255, verbose_name='Dealer Type'),
),
]

View File

@ -0,0 +1,22 @@
# Generated by Django 4.2.17 on 2024-12-16 09:02
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('inventory', '0020_subdealer_dealer_type'),
]
operations = [
migrations.AddField(
model_name='subdealer',
name='user',
field=models.OneToOneField(default=1, on_delete=django.db.models.deletion.CASCADE, related_name='subdealer', to=settings.AUTH_USER_MODEL),
preserve_default=False,
),
]

View File

@ -0,0 +1,27 @@
# Generated by Django 4.2.17 on 2024-12-16 09:31
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('inventory', '0021_subdealer_user'),
]
operations = [
migrations.AddField(
model_name='dealer',
name='dealer_type',
field=models.CharField(choices=[('Inventory', 'Inventory'), ('Accountent', 'Accountent'), ('sales', 'Sales')], default='Inventory', max_length=255, verbose_name='Dealer Type'),
),
migrations.AddField(
model_name='dealer',
name='parent_dealer',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='inventory.dealer'),
),
migrations.DeleteModel(
name='SubDealer',
),
]

View File

@ -0,0 +1,24 @@
# Generated by Django 4.2.17 on 2024-12-16 09:33
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('inventory', '0022_dealer_dealer_type_dealer_parent_dealer_and_more'),
]
operations = [
migrations.AlterField(
model_name='dealer',
name='dealer_type',
field=models.CharField(choices=[('Owner', 'Owner'), ('Inventory', 'Inventory'), ('Accountent', 'Accountent'), ('sales', 'Sales')], default='Owner', max_length=255, verbose_name='Dealer Type'),
),
migrations.AlterField(
model_name='dealer',
name='parent_dealer',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='inventory.dealer', verbose_name='Parent Dealer'),
),
]

View File

@ -0,0 +1,29 @@
# Generated by Django 4.2.17 on 2024-12-16 11:31
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('inventory', '0023_alter_dealer_dealer_type_alter_dealer_parent_dealer'),
]
operations = [
migrations.AlterField(
model_name='dealer',
name='crn',
field=models.CharField(blank=True, max_length=10, null=True, verbose_name='Commercial Registration Number'),
),
migrations.AlterField(
model_name='dealer',
name='parent_dealer',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='sub_dealers', to='inventory.dealer', verbose_name='Parent Dealer'),
),
migrations.AlterField(
model_name='dealer',
name='vrn',
field=models.CharField(blank=True, max_length=15, null=True, verbose_name='VAT Registration Number'),
),
]

View File

@ -0,0 +1,49 @@
# Generated by Django 4.2.17 on 2024-12-16 15:02
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('inventory', '0024_alter_dealer_crn_alter_dealer_parent_dealer_and_more'),
]
operations = [
migrations.CreateModel(
name='Subscription',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('plan', models.CharField(max_length=255)),
('start_date', models.DateField()),
('end_date', models.DateField()),
('max_users', models.IntegerField()),
],
),
migrations.CreateModel(
name='SubscriptionPlan',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=255)),
('description', models.TextField()),
('price', models.DecimalField(decimal_places=2, max_digits=10)),
('max_users', models.IntegerField()),
],
),
migrations.CreateModel(
name='SubscriptionUser',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('subscription', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='inventory.subscription')),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
],
),
migrations.AddField(
model_name='subscription',
name='users',
field=models.ManyToManyField(through='inventory.SubscriptionUser', to=settings.AUTH_USER_MODEL),
),
]

View File

@ -0,0 +1,18 @@
# Generated by Django 4.2.17 on 2024-12-16 15:22
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('inventory', '0025_subscription_subscriptionplan_subscriptionuser_and_more'),
]
operations = [
migrations.AddField(
model_name='subscription',
name='is_active',
field=models.BooleanField(default=True),
),
]

View File

@ -0,0 +1,17 @@
# Generated by Django 4.2.17 on 2024-12-17 09:45
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('inventory', '0026_subscription_is_active'),
]
operations = [
migrations.AlterModelOptions(
name='dealer',
options={'permissions': [('can_edit_dealer_type', 'Can edit dealer type')], 'verbose_name': 'Dealer', 'verbose_name_plural': 'Dealers'},
),
]

View File

@ -0,0 +1,17 @@
# Generated by Django 4.2.17 on 2024-12-17 09:53
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('inventory', '0027_alter_dealer_options'),
]
operations = [
migrations.AlterModelOptions(
name='dealer',
options={'permissions': [('change_dealer_type', 'Can change dealer type')], 'verbose_name': 'Dealer', 'verbose_name_plural': 'Dealers'},
),
]

View File

@ -32,3 +32,12 @@ class LocalizedNameMixin:
return getattr(self, 'arabic_name', None)
return getattr(self, 'name', None)
class AddDealerInstanceMixin:
def form_valid(self, form):
if form.is_valid():
form.instance.dealer = self.request.user.dealer.get_parent_or_self
form.save()
return super().form_valid(form)
else:
return form.errors

View File

@ -14,7 +14,6 @@ from django_ledger.models import (
UnitOfMeasureModel,
CustomerModel,
ItemModelQuerySet,
)
from decimal import Decimal, InvalidOperation
from django.core.exceptions import ValidationError
@ -28,7 +27,7 @@ class CarMake(models.Model, LocalizedNameMixin):
id_car_make = models.AutoField(primary_key=True)
name = models.CharField(max_length=255)
arabic_name = models.CharField(max_length=255)
logo = models.ImageField(_('logo'), upload_to='car_make', blank=True, null=True)
logo = models.ImageField(_("logo"), upload_to="car_make", blank=True, null=True)
is_sa_import = models.BooleanField(default=False)
def __str__(self):
@ -40,7 +39,7 @@ class CarMake(models.Model, LocalizedNameMixin):
class CarModel(models.Model, LocalizedNameMixin):
id_car_model = models.AutoField(primary_key=True)
id_car_make = models.ForeignKey(CarMake, models.DO_NOTHING, db_column='id_car_make')
id_car_make = models.ForeignKey(CarMake, models.DO_NOTHING, db_column="id_car_make")
name = models.CharField(max_length=255)
arabic_name = models.CharField(max_length=255)
@ -53,7 +52,9 @@ class CarModel(models.Model, LocalizedNameMixin):
class CarSerie(models.Model, LocalizedNameMixin):
id_car_serie = models.AutoField(primary_key=True)
id_car_model = models.ForeignKey(CarModel, models.DO_NOTHING, db_column='id_car_model')
id_car_model = models.ForeignKey(
CarModel, models.DO_NOTHING, db_column="id_car_model"
)
name = models.CharField(max_length=255)
arabic_name = models.CharField(max_length=255)
@ -66,7 +67,9 @@ class CarSerie(models.Model, LocalizedNameMixin):
class CarTrim(models.Model, LocalizedNameMixin):
id_car_trim = models.AutoField(primary_key=True)
id_car_serie = models.ForeignKey(CarSerie, models.DO_NOTHING, db_column='id_car_serie')
id_car_serie = models.ForeignKey(
CarSerie, models.DO_NOTHING, db_column="id_car_serie"
)
name = models.CharField(max_length=255)
arabic_name = models.CharField(max_length=255)
start_production_year = models.IntegerField(blank=True, null=True)
@ -83,7 +86,9 @@ class CarSpecification(models.Model, LocalizedNameMixin):
id_car_specification = models.AutoField(primary_key=True)
name = models.CharField(max_length=255)
arabic_name = models.CharField(max_length=255)
id_parent = models.ForeignKey('self', models.DO_NOTHING, db_column='id_parent', blank=True, null=True)
id_parent = models.ForeignKey(
"self", models.DO_NOTHING, db_column="id_parent", blank=True, null=True
)
def __str__(self):
return self.name
@ -94,8 +99,10 @@ class CarSpecification(models.Model, LocalizedNameMixin):
class CarSpecificationValue(models.Model):
id_car_specification_value = models.AutoField(primary_key=True)
id_car_trim = models.ForeignKey(CarTrim, models.DO_NOTHING, db_column='id_car_trim')
id_car_specification = models.ForeignKey(CarSpecification, models.DO_NOTHING, db_column='id_car_specification')
id_car_trim = models.ForeignKey(CarTrim, models.DO_NOTHING, db_column="id_car_trim")
id_car_specification = models.ForeignKey(
CarSpecification, models.DO_NOTHING, db_column="id_car_specification"
)
value = models.CharField(max_length=500)
unit = models.CharField(max_length=255, blank=True, null=True)
@ -108,25 +115,29 @@ class CarSpecificationValue(models.Model):
# Car Model
class CarStatusChoices(models.TextChoices):
AVAILABLE = 'available', _('Available')
SOLD = 'sold', _('Sold')
HOLD = 'hold', _('Hold')
DAMAGED = 'damaged', _('Damaged')
RESERVED = 'reserved', _('Reserved')
AVAILABLE = "available", _("Available")
SOLD = "sold", _("Sold")
HOLD = "hold", _("Hold")
DAMAGED = "damaged", _("Damaged")
RESERVED = "reserved", _("Reserved")
class CarStockTypeChoices(models.TextChoices):
NEW = 'new', _('New')
USED = 'used', _('Used')
NEW = "new", _("New")
USED = "used", _("Used")
class DEALER_TYPES(models.TextChoices):
Owner = "Owner", _("Owner")
Inventory = "Inventory", _("Inventory")
Accountent = "Accountent", _("Accountent")
Sales = "sales", _("Sales")
class Car(models.Model):
vin = models.CharField(max_length=17, unique=True, verbose_name=_("VIN"))
dealer = models.ForeignKey(
"Dealer",
models.DO_NOTHING,
related_name='cars',
verbose_name=_("Dealer")
"Dealer", models.DO_NOTHING, related_name="cars", verbose_name=_("Dealer")
)
vendor = models.ForeignKey(
@ -134,53 +145,53 @@ class Car(models.Model):
models.DO_NOTHING,
null=True,
blank=True,
related_name='cars',
verbose_name=_("Vendor")
related_name="cars",
verbose_name=_("Vendor"),
)
id_car_make = models.ForeignKey(
CarMake,
models.DO_NOTHING,
db_column='id_car_make',
db_column="id_car_make",
null=True,
blank=True,
verbose_name=_("Make")
verbose_name=_("Make"),
)
id_car_model = models.ForeignKey(
CarModel,
models.DO_NOTHING,
db_column='id_car_model',
db_column="id_car_model",
null=True,
blank=True,
verbose_name=_("Model")
verbose_name=_("Model"),
)
year = models.IntegerField(verbose_name=_("Year"))
id_car_serie = models.ForeignKey(
CarSerie,
models.DO_NOTHING,
db_column='id_car_serie',
db_column="id_car_serie",
null=True,
blank=True,
verbose_name=_("Series")
verbose_name=_("Series"),
)
id_car_trim = models.ForeignKey(
CarTrim,
models.DO_NOTHING,
db_column='id_car_trim',
db_column="id_car_trim",
null=True,
blank=True,
verbose_name=_("Trim")
verbose_name=_("Trim"),
)
status = models.CharField(
max_length=10,
choices=CarStatusChoices,
choices=CarStatusChoices.choices,
default=CarStatusChoices.AVAILABLE,
verbose_name=_("Status")
verbose_name=_("Status"),
)
stock_type = models.CharField(
max_length=10,
choices=CarStockTypeChoices,
choices=CarStockTypeChoices.choices,
default=CarStockTypeChoices.NEW,
verbose_name=_("Stock Type")
verbose_name=_("Stock Type"),
)
remarks = models.TextField(blank=True, null=True, verbose_name=_("Remarks"))
mileage = models.IntegerField(blank=True, null=True, verbose_name=_("Mileage"))
@ -203,26 +214,28 @@ class Car(models.Model):
@property
def selling_price(self):
finance = self.finances.first()
return finance.selling_price if finance else Decimal('0.00')
return finance.selling_price if finance else Decimal("0.00")
@property
def discount_amount(self):
finance = self.finances.first()
return finance.discount_amount if finance else Decimal('0.00')
return finance.discount_amount if finance else Decimal("0.00")
@property
def vat_amount(self):
finance = self.finances.first()
return finance.vat_amount if finance else Decimal('0.00')
return finance.vat_amount if finance else Decimal("0.00")
@property
def total(self):
finance = self.finances.first()
return finance.total if finance else Decimal('0.00')
return finance.total if finance else Decimal("0.00")
class CarReservation(models.Model):
car = models.ForeignKey('Car', on_delete=models.CASCADE, related_name='reservations')
car = models.ForeignKey(
"Car", on_delete=models.CASCADE, related_name="reservations"
)
reserved_by = models.ForeignKey(User, on_delete=models.CASCADE)
reserved_at = models.DateTimeField(auto_now_add=True)
reserved_until = models.DateTimeField()
@ -231,35 +244,64 @@ class CarReservation(models.Model):
return self.reserved_until > now()
class Meta:
unique_together = ('car', 'reserved_until')
ordering = ['-reserved_at']
unique_together = ("car", "reserved_until")
ordering = ["-reserved_at"]
# Car Finance Model
class CarFinance(models.Model):
car = models.ForeignKey(Car, on_delete=models.CASCADE, related_name='finances')
cost_price = models.DecimalField(max_digits=14, decimal_places=2, verbose_name=_("Cost Price"))
selling_price = models.DecimalField(max_digits=14, decimal_places=2, verbose_name=_("Selling Price"))
profit_margin = models.DecimalField(max_digits=14,
decimal_places=2,
verbose_name=_("Profit Margin"),
editable=False)
vat_amount = models.DecimalField(max_digits=14,
decimal_places=2,
verbose_name=_("Vat Amount"),
editable=False)
discount_amount = models.DecimalField(max_digits=14, decimal_places=2, verbose_name=_("Discount Amount"),
default=Decimal('0.00'))
registration_fee = models.DecimalField(max_digits=14, decimal_places=2, verbose_name=_("Registration Fee"),
default=Decimal('0.00'))
administration_fee = models.DecimalField(max_digits=14, decimal_places=2, verbose_name=_("Administration Fee"),
default=Decimal('0.00'))
transportation_fee = models.DecimalField(max_digits=14, decimal_places=2, verbose_name=_("Transportation Fee"),
default=Decimal('0.00'))
custom_card_fee = models.DecimalField(max_digits=14, decimal_places=2, verbose_name=_("Custom Card Fee"),
default=Decimal('0.00'))
vat_rate = models.DecimalField(max_digits=14, decimal_places=2, default=Decimal('0.15'), verbose_name=_("VAT Rate"),)
total = models.DecimalField(max_digits=14, decimal_places=2, default=Decimal('0.00'), null=True, blank=True)
car = models.ForeignKey(Car, on_delete=models.CASCADE, related_name="finances")
cost_price = models.DecimalField(
max_digits=14, decimal_places=2, verbose_name=_("Cost Price")
)
selling_price = models.DecimalField(
max_digits=14, decimal_places=2, verbose_name=_("Selling Price")
)
profit_margin = models.DecimalField(
max_digits=14, decimal_places=2, verbose_name=_("Profit Margin"), editable=False
)
vat_amount = models.DecimalField(
max_digits=14, decimal_places=2, verbose_name=_("Vat Amount"), editable=False
)
discount_amount = models.DecimalField(
max_digits=14,
decimal_places=2,
verbose_name=_("Discount Amount"),
default=Decimal("0.00"),
)
registration_fee = models.DecimalField(
max_digits=14,
decimal_places=2,
verbose_name=_("Registration Fee"),
default=Decimal("0.00"),
)
administration_fee = models.DecimalField(
max_digits=14,
decimal_places=2,
verbose_name=_("Administration Fee"),
default=Decimal("0.00"),
)
transportation_fee = models.DecimalField(
max_digits=14,
decimal_places=2,
verbose_name=_("Transportation Fee"),
default=Decimal("0.00"),
)
custom_card_fee = models.DecimalField(
max_digits=14,
decimal_places=2,
verbose_name=_("Custom Card Fee"),
default=Decimal("0.00"),
)
vat_rate = models.DecimalField(
max_digits=14,
decimal_places=2,
default=Decimal("0.15"),
verbose_name=_("VAT Rate"),
)
total = models.DecimalField(
max_digits=14, decimal_places=2, default=Decimal("0.00"), null=True, blank=True
)
def __str__(self):
return f"{self.selling_price}"
@ -267,12 +309,24 @@ class CarFinance(models.Model):
def save(self, *args, **kwargs):
self.full_clean()
try:
services = self.administration_fee + self.transportation_fee + self.custom_card_fee
services = (
self.administration_fee + self.transportation_fee + self.custom_card_fee
)
price_after_discount = self.selling_price - self.discount_amount
total_vat_amount = (price_after_discount + services) * self.vat_rate
self.vat_amount = self.selling_price * self.vat_rate
self.profit_margin = self.selling_price - self.cost_price - self.discount_amount - self.registration_fee
self.total = price_after_discount + services + total_vat_amount + self.registration_fee
self.profit_margin = (
self.selling_price
- self.cost_price
- self.discount_amount
- self.registration_fee
)
self.total = (
price_after_discount
+ services
+ total_vat_amount
+ self.registration_fee
)
except InvalidOperation as e:
raise ValidationError(_("Invalid decimal operation: %s") % str(e))
@ -284,7 +338,9 @@ class CarFinance(models.Model):
@property
def total_vat_amount(self):
services = self.administration_fee + self.transportation_fee + self.custom_card_fee
services = (
self.administration_fee + self.transportation_fee + self.custom_card_fee
)
price_after_discount = self.selling_price - self.discount_amount
return (price_after_discount + services) * self.vat_rate
@ -317,14 +373,18 @@ class InteriorColors(models.Model, LocalizedNameMixin):
# Colors Model
class CarColors(models.Model):
car = models.ForeignKey('Car', on_delete=models.CASCADE, related_name='colors')
exterior = models.ForeignKey('ExteriorColors', on_delete=models.CASCADE, related_name='colors')
interior = models.ForeignKey('InteriorColors', on_delete=models.CASCADE, related_name='colors')
car = models.ForeignKey("Car", on_delete=models.CASCADE, related_name="colors")
exterior = models.ForeignKey(
"ExteriorColors", on_delete=models.CASCADE, related_name="colors"
)
interior = models.ForeignKey(
"InteriorColors", on_delete=models.CASCADE, related_name="colors"
)
class Meta:
verbose_name = _("Color")
verbose_name_plural = _("Colors")
unique_together = ('car', 'exterior', 'interior')
unique_together = ("car", "exterior", "interior")
def __str__(self):
return f"{self.car} ({self.exterior.name}) ({self.interior.name})"
@ -332,7 +392,12 @@ class CarColors(models.Model):
# Custom Card Model
class CustomCard(models.Model):
car = models.ForeignKey(Car, on_delete=models.CASCADE, related_name='custom_cards', verbose_name=_("Car"))
car = models.ForeignKey(
Car,
on_delete=models.CASCADE,
related_name="custom_cards",
verbose_name=_("Car"),
)
custom_number = models.CharField(max_length=255, verbose_name=_("Custom Number"))
custom_date = models.DateField(verbose_name=_("Custom Date"))
@ -346,7 +411,12 @@ class CustomCard(models.Model):
# Car Registration Model
class CarRegistration(models.Model):
car = models.ForeignKey(Car, on_delete=models.CASCADE, related_name='registrations', verbose_name=_("Car"))
car = models.ForeignKey(
Car,
on_delete=models.CASCADE,
related_name="registrations",
verbose_name=_("Car"),
)
plate_number = models.IntegerField(verbose_name=_("Plate Number"))
text1 = models.CharField(max_length=1, verbose_name=_("Text 1"))
text2 = models.CharField(max_length=1, verbose_name=_("Text 2"))
@ -369,37 +439,108 @@ class TimestampedModel(models.Model):
class Meta:
abstract = True
#subscription
class Subscription(models.Model):
plan = models.CharField(max_length=255) # e.g. "basic", "premium"
start_date = models.DateField()
end_date = models.DateField()
max_users = models.IntegerField() # maximum number of users per account
users = models.ManyToManyField(User, through='SubscriptionUser') # many-to-many relationship with User model
is_active = models.BooleanField(default=True)
def __str__(self):
return self.plan
class SubscriptionUser(models.Model):
subscription = models.ForeignKey(Subscription, on_delete=models.CASCADE)
user = models.ForeignKey(User, on_delete=models.CASCADE)
def __str__(self):
return f"{self.subscription} - {self.user}"
class SubscriptionPlan(models.Model):
name = models.CharField(max_length=255)
description = models.TextField()
price = models.DecimalField(max_digits=10, decimal_places=2)
max_users = models.IntegerField() # maximum number of users per account
def __str__(self):
return f"{self.name} - {self.price}"
# Dealer Model
class Dealer(models.Model, LocalizedNameMixin):
user = models.OneToOneField(User, on_delete=models.CASCADE, related_name='dealer')
crn = models.CharField(max_length=10, verbose_name=_("Commercial Registration Number"))
vrn = models.CharField(max_length=15, verbose_name=_("VAT Registration Number"))
user = models.OneToOneField(User, on_delete=models.CASCADE, related_name="dealer")
crn = models.CharField(
max_length=10, verbose_name=_("Commercial Registration Number"),null=True,blank=True
)
vrn = models.CharField(max_length=15, verbose_name=_("VAT Registration Number"),null=True,blank=True)
arabic_name = models.CharField(max_length=255, verbose_name=_("Arabic Name"))
name = models.CharField(max_length=255, verbose_name=_("English Name"))
phone_number = PhoneNumberField(region='SA', verbose_name=_("Phone Number"))
address = models.CharField(max_length=200, blank=True, null=True, verbose_name=_("Address"))
logo = models.ImageField(upload_to="logos/users", blank=True, null=True, verbose_name=_("Logo"))
phone_number = PhoneNumberField(region="SA", verbose_name=_("Phone Number"))
address = models.CharField(
max_length=200, blank=True, null=True, verbose_name=_("Address")
)
logo = models.ImageField(
upload_to="logos/users", blank=True, null=True, verbose_name=_("Logo")
)
parent_dealer = models.ForeignKey(
"self",
on_delete=models.SET_NULL,
blank=True,
null=True,
verbose_name=_("Parent Dealer"),
related_name="sub_dealers",
)
dealer_type = models.CharField(
max_length=255,
choices=DEALER_TYPES.choices,
verbose_name=_("Dealer Type"),
default=DEALER_TYPES.Owner,
)
@property
def get_active_plan(self):
try:
return self.user.subscription_set.filter(is_active=True).first()
except SubscriptionPlan.DoesNotExist:
return None
class Meta:
verbose_name = _("Dealer")
verbose_name_plural = _("Dealers")
verbose_name_plural = _("Dealers")
permissions = [
('change_dealer_type', 'Can change dealer type'),
]
def __str__(self):
return self.name
@property
def get_sub_dealers(self):
if self.dealer_type == "Owner":
return self.sub_dealers.all()
return None
@property
def is_parent(self):
return self.dealer_type == "Owner"
@property
def get_parent_or_self(self):
return self.parent_dealer if self.parent_dealer else self
# Vendor Model
class Vendor(models.Model, LocalizedNameMixin):
dealer = models.ForeignKey(Dealer, on_delete=models.CASCADE, related_name='vendors')
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"))
dealer = models.ForeignKey(Dealer, on_delete=models.CASCADE, related_name="vendors")
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 = PhoneNumberField(region='SA', verbose_name=_("Phone Number"))
address = models.CharField(max_length=200, blank=True, null=True, verbose_name=_("Address"))
logo = models.ImageField(upload_to="logos/vendors", blank=True, null=True, verbose_name=_("Logo"))
phone_number = PhoneNumberField(region="SA", verbose_name=_("Phone Number"))
address = models.CharField(
max_length=200, blank=True, null=True, verbose_name=_("Address")
)
logo = models.ImageField(
upload_to="logos/vendors", blank=True, null=True, verbose_name=_("Logo")
)
class Meta:
verbose_name = _("Vendor")
@ -411,14 +552,24 @@ class Vendor(models.Model, LocalizedNameMixin):
# Customer Model
class Customer(models.Model):
dealer = models.ForeignKey(Dealer, on_delete=models.CASCADE, related_name='customers')
dealer = models.ForeignKey(
Dealer, on_delete=models.CASCADE, related_name="customers"
)
first_name = models.CharField(max_length=50, verbose_name=_("First Name"))
middle_name = models.CharField(max_length=50, blank=True, null=True, verbose_name=_("Middle Name"))
middle_name = models.CharField(
max_length=50, blank=True, null=True, verbose_name=_("Middle Name")
)
last_name = models.CharField(max_length=50, verbose_name=_("Last Name"))
email = models.EmailField(unique=True, verbose_name=_("Email"))
national_id = models.CharField(max_length=10, unique=True, verbose_name=_("National ID"))
phone_number = PhoneNumberField(region='SA', unique=True, verbose_name=_("Phone Number"))
address = models.CharField(max_length=200, blank=True, null=True, verbose_name=_("Address"))
national_id = models.CharField(
max_length=10, unique=True, verbose_name=_("National ID")
)
phone_number = PhoneNumberField(
region="SA", unique=True, verbose_name=_("Phone Number")
)
address = models.CharField(
max_length=200, blank=True, null=True, verbose_name=_("Address")
)
created = models.DateTimeField(auto_now_add=True, verbose_name=_("Created"))
class Meta:
@ -426,7 +577,7 @@ class Customer(models.Model):
verbose_name_plural = _("Customers")
def __str__(self):
middle = f" {self.middle_name}" if self.middle_name else ''
middle = f" {self.middle_name}" if self.middle_name else ""
return f"{self.first_name}{middle} {self.last_name}"
@property
@ -440,11 +591,25 @@ class SaleQuotation(models.Model):
("CONFIRMED", _("Confirmed")),
("CANCELED", _("Canceled")),
]
dealer = models.ForeignKey(Dealer, on_delete=models.CASCADE, related_name='sales', null=True)
customer = models.ForeignKey(Customer, on_delete=models.CASCADE, related_name="quotations", verbose_name=_("Customer"))
amount = models.DecimalField(decimal_places=2, default=Decimal('0.00'), max_digits=10, verbose_name=_("Amount"))
dealer = models.ForeignKey(
Dealer, on_delete=models.CASCADE, related_name="sales", null=True
)
customer = models.ForeignKey(
Customer,
on_delete=models.CASCADE,
related_name="quotations",
verbose_name=_("Customer"),
)
amount = models.DecimalField(
decimal_places=2,
default=Decimal("0.00"),
max_digits=10,
verbose_name=_("Amount"),
)
remarks = models.TextField(blank=True, null=True, verbose_name=_("Remarks"))
status = models.CharField(max_length=10, choices=STATUS_CHOICES, default="DRAFT", verbose_name=_("Status"))
status = models.CharField(
max_length=10, choices=STATUS_CHOICES, default="DRAFT", verbose_name=_("Status")
)
created_at = models.DateTimeField(auto_now_add=True, verbose_name=_("Created At"))
updated_at = models.DateTimeField(auto_now=True, verbose_name=_("Updated At"))
@ -471,19 +636,13 @@ class SaleQuotationCar(models.Model):
SaleQuotation,
on_delete=models.CASCADE,
related_name="quotation_cars",
verbose_name=_("Quotation")
)
car = models.ForeignKey(
Car,
on_delete=models.CASCADE,
verbose_name=_("Car")
verbose_name=_("Quotation"),
)
car = models.ForeignKey(Car, on_delete=models.CASCADE, verbose_name=_("Car"))
# dealer = models.ForeignKey(Dealer, on_delete=models.CASCADE, related_name="sale_cars", verbose_name=_("Dealer"))
quantity = models.PositiveIntegerField(default=1, verbose_name=_("Quantity"))
# price = models.DecimalField(decimal_places=2, max_digits=10, verbose_name=_("Price"), editable=False)
def get_financial_details(self):
"""Retrieve financial details dynamically from CarFinance."""
car_finance = self.car.finances.first()
@ -506,11 +665,16 @@ class SaleQuotationCar(models.Model):
class SalesOrder(models.Model):
quotation = models.OneToOneField(SaleQuotation, on_delete=models.CASCADE, related_name="sales_order", verbose_name=_("Quotation"))
quotation = models.OneToOneField(
SaleQuotation,
on_delete=models.CASCADE,
related_name="sales_order",
verbose_name=_("Quotation"),
)
created_at = models.DateTimeField(auto_now_add=True, verbose_name=_("Created At"))
total_amount = models.DecimalField(max_digits=14, decimal_places=2, verbose_name=_("Total Amount"))
total_amount = models.DecimalField(
max_digits=14, decimal_places=2, verbose_name=_("Total Amount")
)
def __str__(self):
return f"Sales Order #{self.id} from Quotation #{self.quotation.id}"

View File

@ -1,12 +1,18 @@
from random import randint
from django.db.models.signals import post_save, post_delete
from django.db.models.signals import post_save, post_delete,pre_delete
from django.dispatch import receiver
from django_ledger.models import EntityModel, VendorModel, CustomerModel, UnitOfMeasureModel
from django.utils.translation import gettext_lazy as _
from . import models
@receiver(pre_delete, sender=models.Dealer)
def remove_user_account(sender, instance, **kwargs):
user = instance.user
if user:
user.delete()
@receiver(post_save, sender=models.CarReservation)
def update_car_status_on_reservation(sender, instance, created, **kwargs):
if created:

View File

@ -1,6 +1,9 @@
from django.urls import path
from . import views
from allauth.account import views as allauth_views
from django.conf.urls import (
handler400, handler403, handler404, handler500
)
urlpatterns = [
@ -40,7 +43,7 @@ urlpatterns = [
path('vendors/<int:pk>/', views.VendorDetailView.as_view(), name='vendor_detail'),
path('vendors/create/', views.VendorCreateView.as_view(), name='vendor_create'),
path('vendors/<int:pk>/update/', views.VendorUpdateView.as_view(), name='vendor_update'),
path('vendors/<int:pk>/delete/', views.delete_vendor, name='vendor_delete'),
path('vendors/<int:pk>/delete/', views.VendorDetailView.as_view(), name='vendor_delete'),
# Car URLs
@ -68,7 +71,17 @@ urlpatterns = [
path('sales/quotations/', views.QuotationListView.as_view(), name='quotation_list'),
path('sales/quotations/<int:pk>/confirm/', views.confirm_quotation, name='confirm_quotation'),
path('sales/orders/detail/<int:order_id>/', views.SalesOrderDetailView.as_view(), name='order_detail'),
# Users URLs
path('user/create/', views.UserCreateView.as_view(), name='user_create'),
path('user/<int:pk>/update/', views.UserUpdateView.as_view(), name='user_update'),
path('user/<int:pk>/', views.UserDetailView.as_view(), name='user_detail'),
path('user/', views.UserListView.as_view(), name='user_list'),
path('user/<int:pk>/confirm/', views.UserDeleteview, name='user_delete'),
]
handler404 = 'inventory.views.custom_page_not_found_view'
handler500 = 'inventory.views.custom_error_view'
handler403 = 'inventory.views.custom_permission_denied_view'
handler400 = 'inventory.views.custom_bad_request_view'

View File

@ -24,10 +24,18 @@ from django.forms import ChoiceField, ModelForm, RadioSelect
from django.urls import reverse, reverse_lazy
from django.contrib import messages
from django.db.models import Sum, F, Count
from inventory.mixins import AddDealerInstanceMixin
from .services import elm, decodevin,get_make,get_model,normalize_name
from . import models, forms
from django_tables2.export.views import ExportMixin
from django.contrib.auth.mixins import PermissionRequiredMixin
from django.contrib.auth.decorators import user_passes_test
from django.contrib.messages.views import SuccessMessageMixin
from django.contrib.auth.models import Group
from django.contrib.auth import get_user_model
User = get_user_model()
logger = logging.getLogger(__name__)
logging.basicConfig(level=logging.INFO)
@ -64,7 +72,7 @@ class HomeView(LoginRequiredMixin, TemplateView):
template_name = 'index.html'
def dispatch(self, request, *args, **kwargs):
if not hasattr(request.user, 'dealer') or not request.user.is_authenticated:
if not any(hasattr(request.user, attr) for attr in ['dealer', 'subdealer']) or not request.user.is_authenticated:
messages.error(request, _('You are not associated with any dealer.'))
return redirect('welcome')
return super().dispatch(request, *args, **kwargs)
@ -107,7 +115,7 @@ class CarCreateView(LoginRequiredMixin, CreateView):
return reverse('inventory_stats')
def form_valid(self, form):
form.instance.dealer = self.request.user.dealer
form.instance.dealer = self.request.user.dealer.get_parent_or_self
form.save()
messages.success(self.request, 'Car saved successfully.')
return super().form_valid(form)
@ -213,7 +221,6 @@ class AjaxHandlerView(LoginRequiredMixin, View):
]
return JsonResponse(serialized_specs, safe=False)
class CarInventory(LoginRequiredMixin, ListView):
model = models.Car
home_label = _('inventory')
@ -227,8 +234,9 @@ class CarInventory(LoginRequiredMixin, ListView):
make_id = self.kwargs['make_id']
model_id = self.kwargs['model_id']
trim_id = self.kwargs['trim_id']
cars = models.Car.objects.filter(
dealer__user=self.request.user,
dealer=self.request.user.dealer.get_parent_or_self,
id_car_make=make_id,
id_car_model=model_id,
id_car_trim=trim_id,).order_by('receiving_date')
@ -271,7 +279,7 @@ def inventory_stats_view(request):
# Annotate total cars by make, model, and trim
cars = (
models.Car.objects.filter(dealer=dealer)
models.Car.objects.filter(dealer=dealer.get_parent_or_self)
.select_related('id_car_make', 'id_car_model', 'id_car_trim')
.annotate(
make_total=Count('id_car_make'),
@ -345,7 +353,6 @@ class CarDetailView(LoginRequiredMixin, DetailView):
template_name = 'inventory/car_detail.html'
context_object_name = 'car'
class CarFinanceCreateView(LoginRequiredMixin, CreateView):
model = models.CarFinance
form_class = forms.CarFinanceForm
@ -369,42 +376,31 @@ class CarFinanceCreateView(LoginRequiredMixin, CreateView):
return context
class CarFinanceUpdateView(LoginRequiredMixin, UpdateView):
class CarFinanceUpdateView(LoginRequiredMixin,SuccessMessageMixin, UpdateView):
model = models.CarFinance
form_class = forms.CarFinanceForm
template_name = 'inventory/car_finance_form.html'
def form_valid(self, form):
messages.success(self.request, _('Car finance updated successfully.'))
return super().form_valid(form)
success_message = _('Car finance details updated successfully.')
def get_success_url(self):
return reverse('car_detail', kwargs={'pk': self.object.car.pk})
class CarUpdateView(LoginRequiredMixin, UpdateView):
class CarUpdateView(LoginRequiredMixin, SuccessMessageMixin,UpdateView):
model = models.Car
form_class = forms.CarUpdateForm
template_name = 'inventory/car_edit.html'
def form_valid(self, form):
messages.success(self.request, _('Car updated successfully.'))
return super().form_valid(form)
success_message = _('Car updated successfully.')
def get_success_url(self):
return reverse('car_detail', kwargs={'pk': self.object.pk})
class CarDeleteView(LoginRequiredMixin, DeleteView):
class CarDeleteView(LoginRequiredMixin,SuccessMessageMixin, DeleteView):
model = models.Car
template_name = 'inventory/car_confirm_delete.html'
success_url = reverse_lazy('inventory_stats')
def delete(self, request, *args, **kwargs):
messages.success(request, _('Car deleted successfully.'))
return super().delete(request, *args, **kwargs)
class CustomCardCreateView(LoginRequiredMixin, CreateView):
model = models.CustomCard
form_class = forms.CustomCardForm
@ -483,48 +479,50 @@ class DealerDetailView(LoginRequiredMixin, DetailView):
context_object_name = 'dealer'
class DealerCreateView(LoginRequiredMixin, CreateView):
class DealerCreateView(LoginRequiredMixin, SuccessMessageMixin,CreateView):
model = models.Dealer
form_class = forms.DealerForm
template_name = 'dealer_form.html'
success_url = reverse_lazy('dealer_list')
success_message = _('Dealer created successfully.')
def form_valid(self, form):
messages.success(self.request, _('Dealer created successfully.'))
return super().form_valid(form)
class DealerUpdateView(LoginRequiredMixin, UpdateView):
class DealerUpdateView(LoginRequiredMixin,SuccessMessageMixin, UpdateView):
model = models.Dealer
form_class = forms.DealerForm
template_name = 'dealers/dealer_form.html'
success_url = reverse_lazy('dealer_detail')
def form_valid(self, form):
messages.success(self.request, _('Dealer updated successfully.'))
return super().form_valid(form)
class DealerDeleteView(LoginRequiredMixin, DeleteView):
success_message = _('Dealer updated successfully.')
def get_success_url(self):
return reverse('dealer_detail', kwargs={'pk': self.object.pk})
def get_form(self, form_class=None):
form = super().get_form(form_class)
form.fields.pop('dealer_type')
return form
def get_form_class(self):
if self.request.user.dealer.is_parent:
return forms.DealerForm
else:
return forms.UserForm
class DealerDeleteView(LoginRequiredMixin,SuccessMessageMixin, DeleteView):
model = models.Dealer
template_name = 'dealer_confirm_delete.html'
success_url = reverse_lazy('dealer_list')
success_message = _('Dealer deleted successfully.')
def delete(self, request, *args, **kwargs):
messages.success(request, _('Dealer deleted successfully.'))
return super().delete(request, *args, **kwargs)
class CustomerListView(LoginRequiredMixin, ListView):
class CustomerListView(LoginRequiredMixin,PermissionRequiredMixin, ListView):
model = models.Customer
home_label = _('customers')
context_object_name = 'customers'
paginate_by = 10
template_name = "customers/customer_list.html"
permission_required = ('inventory.view_customer',)
def get_queryset(self):
query = self.request.GET.get('q')
customers = models.Customer.objects.filter(dealer__user=self.request.user)
customers = models.Customer.objects.filter(dealer=self.request.user.dealer.get_parent_or_self)
if query:
customers = customers.filter(
@ -539,44 +537,27 @@ class CustomerListView(LoginRequiredMixin, ListView):
context['query'] = self.request.GET.get('q', '')
return context
class CustomerDetailView(LoginRequiredMixin, DetailView):
class CustomerDetailView(LoginRequiredMixin,PermissionRequiredMixin, DetailView):
model = models.Customer
template_name = 'customers/view_customer.html'
context_object_name = 'customer'
permission_required = ('inventory.view_customer',)
class CustomerCreateView(LoginRequiredMixin, CreateView):
class CustomerCreateView(LoginRequiredMixin,PermissionRequiredMixin,SuccessMessageMixin,AddDealerInstanceMixin, CreateView):
model = models.Customer
form_class = forms.CustomerForm
template_name = 'customers/customer_form.html'
success_url = reverse_lazy('customer_list')
permission_required = ('inventory.add_customer',)
success_message = _('Customer created successfully.')
def form_valid(self, form):
if form.is_valid():
form.instance.dealer = self.request.user.dealer
form.save()
messages.success(self.request, _('Customer created successfully.'))
return super().form_valid(form)
else:
return form.errors
class CustomerUpdateView(LoginRequiredMixin, UpdateView):
class CustomerUpdateView(LoginRequiredMixin,PermissionRequiredMixin,SuccessMessageMixin,AddDealerInstanceMixin, UpdateView):
model = models.Customer
form_class = forms.CustomerForm
template_name = 'customers/customer_form.html'
success_url = reverse_lazy('customer_list')
def form_valid(self, form):
if form.is_valid():
form.instance.dealer = self.request.user.dealer
form.save()
messages.success(self.request, _('Customer updated successfully.'))
return super().form_valid(form)
else:
return form.errors
permission_required = ('inventory.change_customer',)
success_message = _('Customer updated successfully.')
@login_required
def delete_customer(request, pk):
@ -586,49 +567,35 @@ def delete_customer(request, pk):
return redirect('customer_list')
class VendorListView(LoginRequiredMixin, ListView):
class VendorListView(LoginRequiredMixin,PermissionRequiredMixin, ListView):
model = models.Vendor
context_object_name = 'vendors'
paginate_by = 10
template_name = "vendors/vendors_list.html"
permission_required = ('inventory.view_vendor',)
class VendorDetailView(LoginRequiredMixin, DetailView):
class VendorDetailView(LoginRequiredMixin,PermissionRequiredMixin, DetailView):
model = models.Vendor
template_name = "vendors/view_vendor.html"
permission_required = ('inventory.view_vendor',)
class VendorCreateView(LoginRequiredMixin, CreateView):
class VendorCreateView(LoginRequiredMixin,PermissionRequiredMixin,SuccessMessageMixin,AddDealerInstanceMixin, CreateView):
model = models.Vendor
form_class = forms.VendorForm
template_name = 'vendors/vendor_form.html'
success_url = reverse_lazy('vendor_list')
def form_valid(self, form):
if form.is_valid():
form.instance.dealer = self.request.user.dealer
form.save()
messages.success(self.request, _('Vendor created successfully.'))
return super().form_valid(form)
else:
return form.errors
class VendorUpdateView(LoginRequiredMixin, UpdateView):
permission_required = ('inventory.add_vendor',)
success_message = _('Vendor created successfully.')
class VendorUpdateView(LoginRequiredMixin,PermissionRequiredMixin,SuccessMessageMixin,AddDealerInstanceMixin, UpdateView):
model = models.Vendor
form_class = forms.VendorForm
template_name = 'vendors/vendor_form.html'
success_url = reverse_lazy('vendor_list')
def form_valid(self, form):
if form.is_valid():
form.instance.dealer = self.request.user.dealer
form.save()
messages.success(self.request, _('Vendor updated successfully.'))
return super().form_valid(form)
else:
return form.errors
permission_required = ('inventory.change_vendor',)
success_message = _('Vendor updated successfully.')
@login_required
def delete_vendor(request, pk):
@ -638,10 +605,12 @@ def delete_vendor(request, pk):
return redirect('vendor_list')
class QuotationCreateView(LoginRequiredMixin, CreateView):
class QuotationCreateView(LoginRequiredMixin,PermissionRequiredMixin, CreateView):
model = models.SaleQuotation
form_class = forms.QuotationForm
template_name = 'sales/quotation_form.html'
permission_required = ('inventory.add_salequotation',)
def form_valid(self, form):
quotation = form.save()
@ -658,11 +627,13 @@ class QuotationCreateView(LoginRequiredMixin, CreateView):
return redirect('quotation_list')
class QuotationListView(LoginRequiredMixin, ListView):
class QuotationListView(LoginRequiredMixin,PermissionRequiredMixin, ListView):
model = models.SaleQuotation
template_name = "sales/quotation_list.html"
context_object_name = "quotations"
paginate_by = 10
permission_required = ('inventory.view_salequotation',)
def get_queryset(self):
status = self.request.GET.get("status")
@ -672,10 +643,11 @@ class QuotationListView(LoginRequiredMixin, ListView):
return queryset
class QuotationDetailView(LoginRequiredMixin, DetailView):
class QuotationDetailView(LoginRequiredMixin,PermissionRequiredMixin, DetailView):
model = models.SaleQuotation
template_name = "sales/quotation_detail.html"
context_object_name = "quotation"
permission_required = ('inventory.view_salequotation',)
@login_required
@ -693,7 +665,113 @@ def confirm_quotation(request, pk):
return redirect("quotation_detail", pk=pk)
class SalesOrderDetailView(LoginRequiredMixin, DetailView):
class SalesOrderDetailView(LoginRequiredMixin,PermissionRequiredMixin, DetailView):
model = models.SalesOrder
template_name = "sales/sales_order_detail.html"
context_object_name = "sales_order"
context_object_name = "sales_order"
permission_required = ('inventory.view_salequotation',)
slug_field = 'order_id'
slug_url_kwarg = 'order_id'
#Users
class UserListView(LoginRequiredMixin,PermissionRequiredMixin, ListView):
model = models.Dealer
context_object_name = 'users'
paginate_by = 10
template_name = "users/user_list.html"
permission_required = ('inventory.view_dealer',)
def get_queryset(self):
query = self.request.GET.get('q')
users = self.request.user.dealer.sub_dealers
if query:
users = users.filter(
Q(name__icontains=query) |
Q(arabic_name__icontains=query) |
Q(phone_number__icontains=query)|
Q(address__icontains=query)
)
return users.all()
class UserDetailView(LoginRequiredMixin,PermissionRequiredMixin, DetailView):
model = models.Dealer
template_name = "users/user_detail.html"
context_object_name = "user"
permission_required = ('inventory.view_dealer',)
class UserCreateView(LoginRequiredMixin,PermissionRequiredMixin,SuccessMessageMixin,AddDealerInstanceMixin, CreateView):
model = models.Dealer
form_class = forms.UserForm
template_name = 'users/user_form.html'
success_url = reverse_lazy('user_list')
permission_required = ('inventory.add_dealer',)
success_message = _('User created successfully.')
def get_form(self, form_class = None):
form = super().get_form(form_class)
form.fields['dealer_type'].choices = [t for t in form.fields['dealer_type'].choices if t[0] != 'Owner']
return form
def form_valid(self, form):
dealer = self.request.user.dealer.get_parent_or_self
if dealer.sub_dealers.count() >= dealer.get_active_plan.max_users:
messages.error(self.request, _("You have reached the maximum number of users."))
return redirect('user_list')
user = User.objects.create_user(username=form.cleaned_data['name'])
user.set_password("Tenhal@123")
user.save()
form.instance.user = user
form.instance.parent_dealer = dealer
for group in user.groups.all():
group.user_set.remove(user)
Group.objects.get(name=form.cleaned_data['dealer_type'].lower()).user_set.add(user)
form.save()
return super().form_valid(form)
class UserUpdateView(LoginRequiredMixin,PermissionRequiredMixin,SuccessMessageMixin,AddDealerInstanceMixin, UpdateView):
model = models.Dealer
form_class = forms.UserForm
template_name = 'users/user_form.html'
success_url = reverse_lazy('user_list')
permission_required = ('inventory.change_dealer',)
success_message = _('User updated successfully.')
def get_form(self, form_class = None):
form = super().get_form(form_class)
if not self.request.user.has_perms(["inventory.change_dealer_type"]):
field = form.fields['dealer_type']
field.widget = field.hidden_widget()
form.fields['dealer_type'].choices = [t for t in form.fields['dealer_type'].choices if t[0] != 'Owner']
return form
def form_valid(self, form):
user = form.instance.user
for group in user.groups.all():
group.user_set.remove(user)
Group.objects.get(name=form.cleaned_data['dealer_type'].lower()).user_set.add(user)
form.save()
return super().form_valid(form)
def UserDeleteview(request, pk):
user = get_object_or_404(models.Dealer, pk=pk)
user.delete()
messages.success(request, _('User deleted successfully.'))
return redirect('user_list')
#errors
def custom_page_not_found_view(request, exception):
return render(request, "errors/404.html", {})
def custom_error_view(request, exception=None):
return render(request, "errors/500.html", {})
def custom_permission_denied_view(request, exception=None):
return render(request, "errors/403.html", {})
def custom_bad_request_view(request, exception=None):
return render(request, "errors/400.html", {})

Binary file not shown.

File diff suppressed because it is too large Load Diff

View File

@ -26,6 +26,7 @@
<th>{{ _("Name") }}</th>
<td>{{ dealer.get_local_name }}</td>
</tr>
{% if request.user.dealer.is_parent %}
<tr>
<th>{{ _("Commercial Registration Number") }}</th>
<td>{{ dealer.crn }}</td>
@ -34,6 +35,7 @@
<th>{{ _("VAT Registration Number") }}</th>
<td>{{ dealer.vrn }}</td>
</tr>
{% endif %}
<tr>
<th>{{ _("Phone Number") }}</th>
<td>{{ dealer.phone_number }}</td>

48
templates/errors/400.html Normal file
View File

@ -0,0 +1,48 @@
{% extends "base.html" %}
{% load i18n %}
{% block content %}
<style>
.error-page {
text-align: center;
margin-top: 100px;
background-color: #f7f7f7;
padding: 50px;
border-radius: 10px;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
}
.error-code {
font-size: 64px;
font-weight: bold;
color: #333;
margin-bottom: 20px;
}
.error-message {
font-size: 18px;
color: #666;
margin-bottom: 40px;
}
.error-button {
background-color: #4CAF50;
color: #fff;
padding: 10px 20px;
border: none;
border-radius: 5px;
cursor: pointer;
}
.error-button:hover {
background-color: #3e8e41;
}
</style>
<div class="error-page">
<h1 class="error-code">{% trans "400" %}</h1>
<p class="error-message">{% trans "Bad Request" %}</p>
<a href="{% url 'inventory_stats' %}" class="error-button">{% trans "Go Back" %}</a>
</div>
{% endblock content %}

48
templates/errors/403.html Normal file
View File

@ -0,0 +1,48 @@
{% extends "base.html" %}
{% load i18n %}
{% block content %}
<style>
.error-page {
text-align: center;
margin-top: 100px;
background-color: #f7f7f7;
padding: 50px;
border-radius: 10px;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
}
.error-code {
font-size: 64px;
font-weight: bold;
color: #333;
margin-bottom: 20px;
}
.error-message {
font-size: 18px;
color: #666;
margin-bottom: 40px;
}
.error-button {
background-color: #4CAF50;
color: #fff;
padding: 10px 20px;
border: none;
border-radius: 5px;
cursor: pointer;
}
.error-button:hover {
background-color: #3e8e41;
}
</style>
<div class="error-page">
<h1 class="error-code">{% trans "403" %}</h1>
<p class="error-message">{% trans "You do not have permission to view this page." %}</p>
<a href="{% url 'inventory_stats' %}" class="error-button">{% trans "Go Back" %}</a>
</div>
{% endblock content %}

48
templates/errors/404.html Normal file
View File

@ -0,0 +1,48 @@
{% extends "base.html" %}
{% load i18n %}
{% block content %}
<style>
.error-page {
text-align: center;
margin-top: 100px;
background-color: #f7f7f7;
padding: 50px;
border-radius: 10px;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
}
.error-code {
font-size: 64px;
font-weight: bold;
color: #333;
margin-bottom: 20px;
}
.error-message {
font-size: 18px;
color: #666;
margin-bottom: 40px;
}
.error-button {
background-color: #4CAF50;
color: #fff;
padding: 10px 20px;
border: none;
border-radius: 5px;
cursor: pointer;
}
.error-button:hover {
background-color: #3e8e41;
}
</style>
<div class="error-page">
<h1 class="error-code">{% trans "404" %}</h1>
<p class="error-message">{% trans "Page not found" %}</p>
<a href="{% url 'inventory_stats' %}" class="error-button">{% trans "Go Back" %}</a>
</div>
{% endblock content %}

48
templates/errors/500.html Normal file
View File

@ -0,0 +1,48 @@
{% extends "base.html" %}
{% load i18n %}
{% block content %}
<style>
.error-page {
text-align: center;
margin-top: 100px;
background-color: #f7f7f7;
padding: 50px;
border-radius: 10px;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
}
.error-code {
font-size: 64px;
font-weight: bold;
color: #333;
margin-bottom: 20px;
}
.error-message {
font-size: 18px;
color: #666;
margin-bottom: 40px;
}
.error-button {
background-color: #4CAF50;
color: #fff;
padding: 10px 20px;
border: none;
border-radius: 5px;
cursor: pointer;
}
.error-button:hover {
background-color: #3e8e41;
}
</style>
<div class="error-page">
<h1 class="error-code">{% trans "500" %}</h1>
<p class="error-message">{% trans "Internal Server Error" %}</p>
<a href="{% url 'inventory_stats' %}" class="error-button">{% trans "Go Back" %}</a>
</div>
{% endblock content %}

View File

@ -1,6 +1,6 @@
{% load i18n %}
{% load static %}
<nav class="navbar navbar-expand-lg bg-primary" data-bs-theme="dark">
<div class="container-fluid">
<!-- Brand/Logo -->
@ -33,6 +33,7 @@
{% trans 'inventory' %}
</a>
<ul class="dropdown-menu" aria-labelledby="inventoryDropdown">
{% if perms.inventory.add_car %}
<li>
<a href="{% url 'car_add' %}"
class="dropdown-item fw-lighter">
@ -41,6 +42,7 @@
</small>
</a>
</li>
{% endif %}
<li>
<a href="{% url 'inventory_stats' %}"
class="dropdown-item fw-lighter">
@ -50,7 +52,8 @@
</a>
</li>
</ul>
</li>
</li>
{% if perms.inventory.add_customer %}
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle"
href="#" id="customerDropdown"
@ -58,7 +61,7 @@
aria-expanded="false">
{% trans 'customers' %}
</a>
<ul class="dropdown-menu" aria-labelledby="customerDropdown">
<ul class="dropdown-menu" aria-labelledby="customerDropdown">
<li>
<a href="{% url 'customer_create' %}"
class="dropdown-item fw-lighter">
@ -77,6 +80,8 @@
</li>
</ul>
</li>
{% endif %}
{% if perms.inventory.add_vendor %}
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle"
href="#" id="vendorsDropdown"
@ -103,6 +108,35 @@
</li>
</ul>
</li>
{% endif %}
{% if perms.inventory.add_dealer %}
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle"
href="#" id="vendorsDropdown"
role="button" data-bs-toggle="dropdown"
aria-expanded="false">
{% trans 'Users' %}
</a>
<ul class="dropdown-menu" aria-labelledby="vendorsDropdown">
<li>
<a href="{% url 'user_create' %}"
class="dropdown-item fw-lighter">
<small>
{% trans "Add User" %}
</small>
</a>
</li>
<li>
<a href="{% url 'user_list' %}"
class="dropdown-item fw-lighter">
<small>
{% trans "Users" %}
</small>
</a>
</li>
</ul>
</li>
{% endif %}
{% else %}
<li class="nav-item">
{% block welcome %}<a class="nav-link" href="{% url 'welcome' %}">{% trans 'home' %}</a>{% endblock %}
@ -111,7 +145,7 @@
</ul>
<ul class="navbar-nav ms-auto mb-2 mb-lg-0">
{% if user.is_authenticated and user.dealer%}
{% if user.is_authenticated and user.dealer or user.subdealer%}
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" href="#" id="userDropdown" role="button" data-bs-toggle="dropdown" aria-expanded="false">
<i class="bi bi-person-circle"></i>

View File

@ -153,12 +153,15 @@
</div>
<div class="col-lg-6 col-xl-6">
<p class="fs-5">{% trans 'Financial Details' %}</p>
{% if car.finances.exists %} {% for finance in car.finances.all %}
<table class="table table-sm table-responsive mb-3 align-middle">
<table class="table table-sm table-responsive mb-3 align-middle">
{% if perms.inventory.view_carfinance %}
<tr>
<th>{% trans "Cost Price" %}</th>
<td>{{ finance.cost_price }}</td>
</tr>
{% endif %}
<tr>
<th>{% trans "Selling Price" %}</th>
<td>{{ finance.selling_price }}</td>
@ -191,12 +194,14 @@
<th>{% trans "Total" %}</th>
<td>{{ car.total }}</td>
</tr>
<tr>
<tr>
<td colspan="2">
<a href="{% url 'car_finance_update' finance.pk %}"
{% if perms.inventory.change_carfinance %}
<a href="{% url 'car_finance_update' finance.pk %}"
class="btn btn-warning btn-sm mb-3">
{% trans "Edit Finance Details" %}
</a>
</a>
{% endif %}
{% endfor %}
{% else %}
<p>{% trans "No finance details available." %}</p>
@ -205,7 +210,7 @@
{% trans "Add Finance Details" %}
</a>
</td>
</tr>
</tr>
{% endif %}
</table>
<p class="fs-5 mt-2">{% trans 'Colors Details' %}</p>
@ -292,7 +297,9 @@
<td>
<form method="POST" action="{% url 'reserve_car' car.id %}" class="d-inline">
{% csrf_token %}
{% if perms.inventory.car_change %}
<button type="submit" class="btn btn-success btn-sm">{% trans "Reserve" %}</button>
{% endif %}
</form>
{% endif %}
</td>
@ -304,10 +311,11 @@
<div class="row g-4">
<div class="">
<!-- Actions -->
<a href="#" class="btn btn-danger btn-sm">{% trans "transfer" %}</a>
<a href="{% url 'car_update' car.pk %}" class="btn btn-warning btn-sm">{% trans "Edit" %}</a>
{% if perms.inventory.car_change %}
<a href="#" class="btn btn-danger btn-sm">{% trans "transfer" %}</a>
<a href="{% url 'car_update' car.pk %}" class="btn btn-warning btn-sm">{% trans "Edit" %}</a>
{% endif %}
<a href="{% url 'inventory_stats' %}" class="btn btn-secondary btn-sm">{% trans "Back to List" %}</a>
</div>
</div>

View File

@ -21,8 +21,7 @@
<main class="d-grid gap-4 p-1">
<div class="row g-4">
<div class="col-lg-6 col-xl-12">
<div class="container-fluid p-2">
<form method="get">
<div class="input-group input-group-sm">

View File

@ -14,8 +14,7 @@
<strong class="fs-6">{% trans "Total Cars in Inventory" %}</strong>
<strong class="fs-6">{{ inventory.total_cars }}</strong>
</div>
</div>
</div>
<!-- Inventory by Makes -->
<div class="accordion" id="makesAccordion">
{% for make in inventory.makes %}

View File

@ -0,0 +1,75 @@
{% extends "base.html" %}
{% load i18n %}
{% block title %}{{ _("View Customer") }}{% endblock title %}
{% block content %}
<!-- Delete Modal -->
<div class="modal fade" id="deleteModal"
data-bs-backdrop="static"
data-bs-keyboard="false"
tabindex="-1"
aria-labelledby="deleteModalLabel"
aria-hidden="true">
<div class="modal-dialog modal-sm ">
<div class="modal-content rounded">
<div class="modal-body d-flex justify-content-center">
<h1 class="text-danger me-2"><i class="bi bi-exclamation-diamond-fill"></i></h1>
<span class="text-danger">
{% trans "Are you sure you want to delete this user?" %}
</span>
</div>
<div class="btn-group">
<button type="button"
class="btn btn-sm btn-secondary"
data-bs-dismiss="modal">
{% trans 'No' %}
</button>
<a type="button"
class="btn btn-sm btn-danger"
href="{% url 'user_delete' user.id %}">
{% trans 'Yes' %}
</a>
</div>
</div>
</div>
</div>
<div class="container my-5">
<div class="card rounded ">
<div class="card-header bg-primary text-white ">
<p class="mb-0">{{ _("User Details") }}</p>
</div>
<div class="card-body">
<div class="row">
<div class="col-md-6">
<p><strong>{{ _("Name") }}:</strong> {{ user.name }}</p>
<p><strong>{{ _("Arabic Name") }}:</strong> {{ user.arabic_name }}</p>
</div>
<div class="col-md-6">
<p><strong>{{ _("Phone Number") }}:</strong> {{ user.phone_number }}</p>
<p><strong>{{ _("Address") }}:</strong> {{ user.address }}</p>
<p><strong>{{ _("Role") }}:</strong> {{ user.dealer_type }}</p>
</div>
</div>
</div>
<div class="card-footer d-flex ">
<a class="btn btn-sm btn-primary me-1" href="{% url 'user_update' user.id %}">
<!--<i class="bi bi-pencil-square"></i> -->
{{ _("Edit") }}
</a>
<a class="btn btn-sm btn-danger me-1"
data-bs-toggle="modal"
data-bs-target="#deleteModal">
<!--<i class="bi bi-trash-fill"></i>-->
{{ _("Delete") }}
</a>
<a class="btn btn-sm btn-secondary"
href="{% url 'user_list' %}">
<!--<i class="bi bi-arrow-left-square-fill"></i>-->
{% trans "Back to List" %}
</a>
</div>
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,39 @@
{% extends "base.html" %}
{% load i18n %}
{% load crispy_forms_filters %}
{% block title %}{% trans "users" %}{% endblock title %}
{% block content %}
<div class="container my-5">
<!-- Display Form Errors -->
<div class="card shadow rounded">
<div class="card-header bg-primary text-white">
<p class="mb-0">
{% if user.created %}
<!--<i class="bi bi-pencil-square"></i>-->
{{ _("Edit User") }}
{% else %}
<!--<i class="bi bi-person-plus"></i> -->
{{ _("Add User") }}
{% endif %}
</p>
</div>
<div class="card-body">
<form method="post" class="form" novalidate>
{% csrf_token %}
{{ form|crispy }}
{% for error in form.errors %}
<div class="text-danger">{{ error }}</div>
{% endfor %}
<div class="d-flex justify-content-end">
<button class="btn btn-sm btn-success me-1" type="submit">
<!--<i class="bi bi-save"></i> -->
{{ _("Save") }}
</button>
<a href="{{request.META.HTTP_REFERER}}" class="btn btn-sm btn-danger">{% trans "Cancel" %}</a>
</div>
</form>
</div>
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,115 @@
{% extends "base.html" %}
{% load i18n %}
{% load render_table from django_tables2 %}
{% block title %}{% trans "users" %}{% endblock title %}
{% block users %}<a class="nav-link active">{% trans "users"|capfirst %}</a>{% endblock %}
{% block content %}
<div class="d-flex flex-column min-vh-100">
<div class="d-flex flex-column flex-sm-grow-1 ms-sm-14 p-4">
<main class="d-grid gap-4 p-1">
<div class="row g-4">
<div class="col-lg-6 col-xl-12">
<div class="container-fluid p-2">
<form method="get">
<div class="input-group input-group-sm">
<button id="inputGroup-sizing-sm"
class="btn btn-sm btn-secondary rounded-start" type="submit">
{% trans 'search'|capfirst %}
</button>
<input type="text"
name="q"
class="form-control form-control-sm rounded-end"
value="{{ request.GET.q }}"
aria-describedby="inputGroup-sizing-sm"/>
<!-- Clear Button -->
{% if request.GET.q %}
<a href="{% url request.resolver_match.view_name %}"
class="btn btn-sm btn-outline-danger ms-1 rounded">
<i class="bi bi-x-lg"></i>
</a>
{% endif %}
</div>
</form>
</div>
<table class="table table-hover table-responsive-sm">
<thead>
<tr>
<th>{% trans 'name'|capfirst %}</th>
<th>{% trans 'arabic name'|capfirst %}</th>
<th>{% trans 'phone number'|capfirst %}</th>
<th>{% trans 'address'|capfirst %}</th>
<th>{% trans 'role'|capfirst %}</th>
<th>{% trans 'actions'|capfirst %}</th>
</tr>
</thead>
<tbody>
{% for user in users %}
<tr>
<td>{{ user.name }}</td>
<td>{{ user.arabic_name }}</td>
<td>{{ user.phone_number }}</td>
<td>{{ user.address }}</td>
<td>{% trans user.dealer_type %}</td>
<td>
<a class="btn btn-sm btn-success"
href="{% url 'user_detail' user.id %}">
{% trans 'view'|capfirst %}
</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
<!-- Optional: Pagination -->
{% if is_paginated %}
<nav aria-label="Page navigation">
<ul class="pagination pagination-sm justify-content-center">
{% if page_obj.has_previous %}
<li class="page-item py-0">
<a class="page-link" href="?page={{ page_obj.previous_page_number }}" aria-label="Previous">
<span aria-hidden="true">&laquo;</span>
</a>
</li>
{% else %}
<li class="page-item disabled">
<a class="page-link" href="#" aria-label="Previous">
<span aria-hidden="true">&laquo;</span>
</a>
</li>
{% endif %}
{% for num in page_obj.paginator.page_range %}
{% if page_obj.number == num %}
<li class="page-item active"><a class="page-link" href="?page={{ num }}">{{ num }}</a></li>
{% else %}
<li class="page-item"><a class="page-link" href="?page={{ num }}">{{ num }}</a></li>
{% endif %}
{% endfor %} {% if page_obj.has_next %}
<li class="page-item">
<a class="page-link" href="?page={{ page_obj.next_page_number }}" aria-label="Next">
<span aria-hidden="true">&raquo;</span>
</a>
</li>
{% else %}
<li class="page-item disabled">
<a class="page-link" href="#" aria-label="Next">
<span aria-hidden="true">&raquo;</span>
</a>
</li>
{% endif %}
</ul>
</nav>
{% endif %}
</div>
</div>
</main>
</div>
</div>
{% endblock %}

View File

@ -43,11 +43,12 @@
<tr>
<td>{{ vendor.get_local_name }}</td>
<td>
{% if vendor.logo %}
<img src="{{ vendor.logo.url }}"
alt="{{ vendor.get_local_name }}"
class="img-thumbnail"
style="max-width: 100px;">
{% endif %}
</td>
<td>{{ vendor.address }}</td>
<td>