Merge branch 'main' of http://10.10.1.120:3000/tenhal_admin/haikal
This commit is contained in:
commit
28ac0c99d8
@ -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.site.register(models.CarLocation)
|
||||
admin.site.register(models.CarReservation)
|
||||
admin.site.register(models.Organization)
|
||||
@ -59,18 +63,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)
|
||||
|
||||
@ -26,6 +26,12 @@ import django_tables2 as tables
|
||||
from django.forms import formset_factory
|
||||
|
||||
|
||||
|
||||
|
||||
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:
|
||||
|
||||
349
inventory/migrations/0001_squashed_0013_merge_20241211_1620.py
Normal file
349
inventory/migrations/0001_squashed_0013_merge_20241211_1620.py
Normal 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')),
|
||||
],
|
||||
),
|
||||
]
|
||||
14
inventory/migrations/0013_merge_20241211_1620.py
Normal file
14
inventory/migrations/0013_merge_20241211_1620.py
Normal 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 = [
|
||||
]
|
||||
@ -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',
|
||||
|
||||
23
inventory/migrations/0014_carfinance_total.py
Normal file
23
inventory/migrations/0014_carfinance_total.py
Normal 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),
|
||||
),
|
||||
]
|
||||
14
inventory/migrations/0017_merge_20241212_1035.py
Normal file
14
inventory/migrations/0017_merge_20241212_1035.py
Normal 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 = [
|
||||
]
|
||||
@ -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',
|
||||
),
|
||||
]
|
||||
32
inventory/migrations/0019_subdealer.py
Normal file
32
inventory/migrations/0019_subdealer.py
Normal 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),
|
||||
),
|
||||
]
|
||||
18
inventory/migrations/0020_subdealer_dealer_type.py
Normal file
18
inventory/migrations/0020_subdealer_dealer_type.py
Normal 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'),
|
||||
),
|
||||
]
|
||||
22
inventory/migrations/0021_subdealer_user.py
Normal file
22
inventory/migrations/0021_subdealer_user.py
Normal 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,
|
||||
),
|
||||
]
|
||||
@ -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',
|
||||
),
|
||||
]
|
||||
@ -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'),
|
||||
),
|
||||
]
|
||||
@ -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'),
|
||||
),
|
||||
]
|
||||
@ -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),
|
||||
),
|
||||
]
|
||||
18
inventory/migrations/0026_subscription_is_active.py
Normal file
18
inventory/migrations/0026_subscription_is_active.py
Normal 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),
|
||||
),
|
||||
]
|
||||
17
inventory/migrations/0027_alter_dealer_options.py
Normal file
17
inventory/migrations/0027_alter_dealer_options.py
Normal 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'},
|
||||
),
|
||||
]
|
||||
17
inventory/migrations/0028_alter_dealer_options.py
Normal file
17
inventory/migrations/0028_alter_dealer_options.py
Normal 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'},
|
||||
),
|
||||
]
|
||||
@ -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
|
||||
@ -15,7 +15,6 @@ from django_ledger.models import (
|
||||
UnitOfMeasureModel,
|
||||
CustomerModel,
|
||||
ItemModelQuerySet,
|
||||
|
||||
)
|
||||
from django.db.models import Sum
|
||||
from decimal import Decimal, InvalidOperation
|
||||
@ -31,7 +30,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):
|
||||
@ -43,7 +42,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)
|
||||
|
||||
@ -56,7 +55,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)
|
||||
year_begin = models.IntegerField(blank=True, null=True)
|
||||
@ -71,7 +72,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)
|
||||
@ -89,7 +92,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
|
||||
@ -100,8 +105,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)
|
||||
|
||||
@ -114,25 +121,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(
|
||||
@ -140,53 +151,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"))
|
||||
@ -404,14 +415,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})"
|
||||
@ -482,7 +497,12 @@ class CarLocation(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"))
|
||||
@ -505,37 +525,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")
|
||||
@ -547,14 +638,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:
|
||||
@ -562,7 +663,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
|
||||
@ -610,11 +711,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"))
|
||||
|
||||
@ -646,7 +761,7 @@ class SaleQuotationCar(models.Model):
|
||||
SaleQuotation,
|
||||
on_delete=models.CASCADE,
|
||||
related_name="quotation_cars",
|
||||
verbose_name=_("Quotation")
|
||||
verbose_name=_("Quotation"),
|
||||
)
|
||||
car = models.ForeignKey(
|
||||
Car,
|
||||
@ -690,11 +805,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}"
|
||||
|
||||
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
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,
|
||||
@ -15,6 +15,11 @@ 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.Car)
|
||||
def create_car_location(sender, instance, created, **kwargs):
|
||||
"""
|
||||
|
||||
@ -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
|
||||
@ -70,7 +73,13 @@ 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'),
|
||||
# Organization URLs
|
||||
path('organizations/', views.OrganizationListView.as_view(), name='organization_list'),
|
||||
path('organizations/<int:pk>/', views.OrganizationDetailView.as_view(), name='organization_detail'),
|
||||
@ -84,7 +93,14 @@ urlpatterns = [
|
||||
path('representatives/create/', views.RepresentativeCreateView.as_view(), name='representative_create'),
|
||||
path('representatives/<int:pk>/update/', views.RepresentativeUpdateView.as_view(), name='representative_update'),
|
||||
path('representatives/<int:pk>/delete/', views.RepresentativeDeleteView.as_view(), name='representative_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'
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@ -26,11 +26,19 @@ 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 .services import elm, decodevin, get_make, get_model, normalize_name, get_ledger_data
|
||||
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)
|
||||
@ -68,7 +76,7 @@ class HomeView(LoginRequiredMixin, TemplateView):
|
||||
|
||||
|
||||
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)
|
||||
@ -111,7 +119,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)
|
||||
@ -232,7 +240,6 @@ class AjaxHandlerView(LoginRequiredMixin, View):
|
||||
]
|
||||
return JsonResponse(serialized_specs, safe=False)
|
||||
|
||||
|
||||
class CarInventory(LoginRequiredMixin, ListView):
|
||||
model = models.Car
|
||||
home_label = _('inventory')
|
||||
@ -246,8 +253,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')
|
||||
@ -290,7 +298,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'),
|
||||
@ -364,7 +372,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
|
||||
@ -388,33 +395,27 @@ 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')
|
||||
@ -527,48 +528,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(
|
||||
@ -583,44 +586,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):
|
||||
@ -630,49 +616,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):
|
||||
@ -682,10 +654,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()
|
||||
@ -702,11 +676,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")
|
||||
@ -716,10 +692,37 @@ 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',)
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
quotation = self.object
|
||||
|
||||
# Totals Calculation
|
||||
context['vat_rate'] = settings.VAT_RATE
|
||||
context['total_sales_before_vat'] = sum(item.car.selling_price * item.quantity for item in quotation.quotation_cars.all())
|
||||
context['vat_amount'] = sum(item.car.vat_amount * item.quantity for item in quotation.quotation_cars.all())
|
||||
context['total_sales_after_vat'] = context['total_sales_before_vat'] + context['vat_amount']
|
||||
|
||||
# Additional Costs
|
||||
total_quantity = quotation.total_quantity
|
||||
context['administration_fee'] = sum(item.car.administration_fee for item in quotation.quotation_cars.all())
|
||||
context['transportation_fee'] = sum(item.car.transportation_fee for item in quotation.quotation_cars.all())
|
||||
context['custom_card_fee'] = sum(item.car.custom_card_fee for item in quotation.quotation_cars.all())
|
||||
context['registration_fee'] = sum(item.car.registration_fee for item in quotation.quotation_cars.all())
|
||||
context['administration_fee_vat'] = sum(item.car.administration_fee_vat for item in quotation.quotation_cars.all())
|
||||
context['transportation_fee_vat'] = sum(item.car.transportation_fee_vat for item in quotation.quotation_cars.all())
|
||||
context['custom_card_fee_vat'] = sum(item.car.custom_card_fee_vat for item in quotation.quotation_cars.all())
|
||||
context['administration_fee_total'] = sum(item.car.administration_fee + context['administration_fee_vat'] for item in quotation.quotation_cars.all())
|
||||
context['transportation_fee_total'] = sum(item.car.transportation_fee + context['transportation_fee_vat'] for item in quotation.quotation_cars.all())
|
||||
context['custom_card_fee_total'] = sum(item.car.custom_card_fee + context['custom_card_fee_vat'] for item in quotation.quotation_cars.all())
|
||||
context['registration_fee_total'] = sum(item.car.registration_fee * total_quantity for item in quotation.quotation_cars.all())
|
||||
|
||||
return context
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
@ -763,10 +766,116 @@ 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"
|
||||
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", {})
|
||||
|
||||
|
||||
class OrganizationListView(LoginRequiredMixin, ListView):
|
||||
|
||||
Binary file not shown.
File diff suppressed because it is too large
Load Diff
@ -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
48
templates/errors/400.html
Normal 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
48
templates/errors/403.html
Normal 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
48
templates/errors/404.html
Normal 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
48
templates/errors/500.html
Normal 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 %}
|
||||
@ -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="customerDropdown"
|
||||
@ -155,6 +160,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 %}
|
||||
@ -163,7 +197,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>
|
||||
|
||||
@ -212,15 +212,18 @@
|
||||
<div class="col-lg-6 col-xl-6">
|
||||
<div class="card rounded shadow">
|
||||
<p class="card-header bg-primary text-white rounded-top fw-bold">{% trans 'Financial Details' %}</p>
|
||||
|
||||
<div class="card-body">
|
||||
|
||||
{% if car.finances %}
|
||||
|
||||
<table class="table table-sm table-responsive align-middle">
|
||||
<table class="table table-sm table-responsive align-middle">
|
||||
{% if perms.inventory.view_carfinance %}
|
||||
<tr>
|
||||
<th>{% trans "Cost Price" %}</th>
|
||||
<td>{{ car.finances.cost_price }}</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
<tr>
|
||||
<th>{% trans "Selling Price" %}</th>
|
||||
<td>{{ car.finances.selling_price }}</td>
|
||||
@ -253,12 +256,14 @@
|
||||
<th>{% trans "Total" %}</th>
|
||||
<td>{{ car.finances.total }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<tr>
|
||||
<td colspan="2">
|
||||
{% if perms.inventory.change_carfinance %}
|
||||
<a href="{% url 'car_finance_update' car.finances.pk %}"
|
||||
class="btn btn-warning btn-sm mb-3">
|
||||
{% trans "Edit Finance Details" %}
|
||||
</a>
|
||||
</a>
|
||||
{% endif %}
|
||||
|
||||
{% else %}
|
||||
<p>{% trans "No finance details available." %}</p>
|
||||
@ -267,7 +272,7 @@
|
||||
{% trans "Add Finance Details" %}
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</tr>
|
||||
{% endif %}
|
||||
</table>
|
||||
</div>
|
||||
@ -366,25 +371,29 @@
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<tr>
|
||||
<td>
|
||||
<a class="btn btn-sm btn-success me-1"
|
||||
data-bs-toggle="modal"
|
||||
data-bs-target="#reserveModal">
|
||||
{{ _("Reserve") }}
|
||||
</a>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
{% else %}
|
||||
<tr>
|
||||
<td>
|
||||
<form method="POST" action="{% url 'reserve_car' car.id %}" class="d-inline">
|
||||
{% csrf_token %}
|
||||
<button type="submit" class="btn btn-success btn-sm">{% trans "Reserve" %}</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row g-4">
|
||||
<div class="d-grid gap-2">
|
||||
<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>
|
||||
|
||||
<a href="{% url 'inventory_stats' %}" class="btn btn-secondary btn-sm">{% trans "Back to List" %}</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -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 %}
|
||||
|
||||
75
templates/users/user_detail.html
Normal file
75
templates/users/user_detail.html
Normal 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 %}
|
||||
39
templates/users/user_form.html
Normal file
39
templates/users/user_form.html
Normal 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 %}
|
||||
115
templates/users/user_list.html
Normal file
115
templates/users/user_list.html
Normal 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">«</span>
|
||||
</a>
|
||||
</li>
|
||||
{% else %}
|
||||
<li class="page-item disabled">
|
||||
<a class="page-link" href="#" aria-label="Previous">
|
||||
<span aria-hidden="true">«</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">»</span>
|
||||
</a>
|
||||
</li>
|
||||
{% else %}
|
||||
<li class="page-item disabled">
|
||||
<a class="page-link" href="#" aria-label="Next">
|
||||
<span aria-hidden="true">»</span>
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</nav>
|
||||
{% endif %}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
3
templates/vendors/vendors_list.html
vendored
3
templates/vendors/vendors_list.html
vendored
@ -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>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user