From 645ba6ede072082b7155690d57d506c4a28421db Mon Sep 17 00:00:00 2001 From: gitea Date: Tue, 17 Dec 2024 14:40:11 +0000 Subject: [PATCH 1/3] fix the dealer update form --- car_inventory/settings.py | 11 ++- .../migrations/0029_merge_20241217_1712.py | 14 ++++ .../0030_alter_carfinance_options_and_more.py | 69 +++++++++++++++++++ inventory/views.py | 5 +- templates/inventory/car_inventory.html | 10 +-- 5 files changed, 96 insertions(+), 13 deletions(-) create mode 100644 inventory/migrations/0029_merge_20241217_1712.py create mode 100644 inventory/migrations/0030_alter_carfinance_options_and_more.py diff --git a/car_inventory/settings.py b/car_inventory/settings.py index 8d907648..709620a4 100644 --- a/car_inventory/settings.py +++ b/car_inventory/settings.py @@ -26,7 +26,7 @@ SECRET_KEY = 'django-insecure-gc9bh4*3=b6hihdnaom0edjsbxh$5t)aap@e8p&340r7)*)qb8 # SECURITY WARNING: don't run with debug turned on in production! DEBUG = True -ALLOWED_HOSTS = ['10.10.1.109', 'localhost', '127.0.0.1', '192.168.1.135', '172.20.10.4'] +ALLOWED_HOSTS = ['10.10.1.109','10.10.1.120', 'localhost', '127.0.0.1', '192.168.1.135', '172.20.10.4'] # Application definition @@ -61,7 +61,6 @@ INSTALLED_APPS = [ 'django_ledger', 'djmoney', 'sslserver', - ] SITE_ID = 1 @@ -110,10 +109,10 @@ WSGI_APPLICATION = 'car_inventory.wsgi.application' DATABASES = { "default": { "ENGINE": "django_prometheus.db.backends.postgresql", - "NAME": "haikal_app", - "USER": "f95166", - "PASSWORD": "Kfsh&rc9788", - "HOST": "localhost", + "NAME": "haikal", + "USER": "haikal", + "PASSWORD": "haikal", + "HOST": "10.10.1.120", "PORT": 5432, } } diff --git a/inventory/migrations/0029_merge_20241217_1712.py b/inventory/migrations/0029_merge_20241217_1712.py new file mode 100644 index 00000000..92237cb0 --- /dev/null +++ b/inventory/migrations/0029_merge_20241217_1712.py @@ -0,0 +1,14 @@ +# Generated by Django 4.2.17 on 2024-12-17 14:12 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('inventory', '0023_cartrim_id_car_model'), + ('inventory', '0028_alter_dealer_options'), + ] + + operations = [ + ] diff --git a/inventory/migrations/0030_alter_carfinance_options_and_more.py b/inventory/migrations/0030_alter_carfinance_options_and_more.py new file mode 100644 index 00000000..e66cba04 --- /dev/null +++ b/inventory/migrations/0030_alter_carfinance_options_and_more.py @@ -0,0 +1,69 @@ +# Generated by Django 4.2.17 on 2024-12-17 14:17 + +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', '0029_merge_20241217_1712'), + ] + + operations = [ + migrations.AlterModelOptions( + name='carfinance', + options={'verbose_name': 'Car Financial Details', 'verbose_name_plural': 'Car Financial Details'}, + ), + migrations.AlterModelOptions( + name='carreservation', + options={'ordering': ['-reserved_at'], 'verbose_name': 'Car Reservation', 'verbose_name_plural': 'Car Reservations'}, + ), + migrations.RemoveField( + model_name='carfinance', + name='vat_rate', + ), + migrations.AlterField( + model_name='carreservation', + name='car', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='reservations', to='inventory.car', verbose_name='Car'), + ), + migrations.AlterField( + model_name='carreservation', + name='reserved_at', + field=models.DateTimeField(auto_now_add=True, verbose_name='Reserved At'), + ), + migrations.AlterField( + model_name='carreservation', + name='reserved_by', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='reservations', to=settings.AUTH_USER_MODEL, verbose_name='Reserved By'), + ), + migrations.AlterField( + model_name='carreservation', + name='reserved_until', + field=models.DateTimeField(verbose_name='Reserved Until'), + ), + migrations.AlterField( + model_name='customcard', + name='car', + field=models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='custom_cards', to='inventory.car', verbose_name='Car'), + ), + migrations.CreateModel( + name='CarLocation', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('description', models.TextField(blank=True, help_text='Optional description about the showroom placement.', null=True, verbose_name='Description')), + ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')), + ('updated_at', models.DateTimeField(auto_now=True, verbose_name='Last Updated')), + ('car', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='location', to='inventory.car', verbose_name='Car')), + ('owner', models.ForeignKey(help_text='Dealer who owns the car.', on_delete=django.db.models.deletion.CASCADE, related_name='owned_cars', to='inventory.dealer', verbose_name='Owner')), + ('showroom', models.ForeignKey(help_text='Dealer where the car is displayed (can be the owner).', on_delete=django.db.models.deletion.CASCADE, related_name='showroom_cars', to='inventory.dealer', verbose_name='Showroom')), + ], + options={ + 'verbose_name': 'Car Location', + 'verbose_name_plural': 'Car Locations', + }, + ), + ] diff --git a/inventory/views.py b/inventory/views.py index 7937d6f3..d27fe11d 100644 --- a/inventory/views.py +++ b/inventory/views.py @@ -547,10 +547,11 @@ class DealerUpdateView(LoginRequiredMixin,SuccessMessageMixin, UpdateView): def get_form(self, form_class=None): form = super().get_form(form_class) - form.fields.pop('dealer_type') + if hasattr(form.fields, 'dealer_type'): + form.fields.pop('dealer_type') return form def get_form_class(self): - if self.request.user.dealer.is_parent: + if self.request.user.dealer.dealer_type == 'Owner': return forms.DealerForm else: return forms.UserForm diff --git a/templates/inventory/car_inventory.html b/templates/inventory/car_inventory.html index 608dd226..5a3c6215 100644 --- a/templates/inventory/car_inventory.html +++ b/templates/inventory/car_inventory.html @@ -65,12 +65,12 @@ {% else %} {% trans 'No Color' %} {% trans 'No Color' %} - {% endif %} - {% if car.location.is_owner_showroom %} - {% trans 'Our Showroom' %} - {% else %} + {% endif %} + {% if car.location.is_owner_showroom %} + {% trans 'Our Showroom' %} + {% else %} {{ car.location.showroom.get_local_name }} - {% endif %} + {% endif %} {% trans "view" %} From f0f9dfc65ed973d7fb1286886d4fc1e8c46b76cc Mon Sep 17 00:00:00 2001 From: gitea Date: Wed, 18 Dec 2024 16:08:34 +0000 Subject: [PATCH 2/3] cleanup and refactoring --- haikalna.py | 4 +- inventory/admin.py | 1 + inventory/forms.py | 2 +- .../migrations/0029_merge_20241217_1712.py | 14 --- .../0031_remove_carfinance_vat_amount.py | 17 +++ .../migrations/0032_carfinance_vat_amount.py | 19 +++ .../0033_alter_carfinance_vat_amount.py | 19 +++ .../0034_additionalservices_and_more.py | 59 +++++++++ ..._carfinance_administration_fee_and_more.py | 50 ++++++++ ...remove_car_additional_services_and_more.py | 22 ++++ inventory/models.py | 118 ++++-------------- inventory/services.py | 2 +- inventory/utilities/__init__.py | 0 inventory/utilities/financials.py | 28 +++++ inventory/utils.py | 48 +++++++ inventory/views.py | 97 +++++--------- templates/inventory/car_form.html | 4 +- templates/sales/quotation_detail.html | 33 ++++- templates/sales/quotation_list.html | 14 ++- 19 files changed, 365 insertions(+), 186 deletions(-) delete mode 100644 inventory/migrations/0029_merge_20241217_1712.py create mode 100644 inventory/migrations/0031_remove_carfinance_vat_amount.py create mode 100644 inventory/migrations/0032_carfinance_vat_amount.py create mode 100644 inventory/migrations/0033_alter_carfinance_vat_amount.py create mode 100644 inventory/migrations/0034_additionalservices_and_more.py create mode 100644 inventory/migrations/0035_carfinance_administration_fee_and_more.py create mode 100644 inventory/migrations/0036_remove_car_additional_services_and_more.py create mode 100644 inventory/utilities/__init__.py create mode 100644 inventory/utilities/financials.py diff --git a/haikalna.py b/haikalna.py index db2c893d..49c4504a 100644 --- a/haikalna.py +++ b/haikalna.py @@ -1535,6 +1535,4 @@ def decode_vin(vin): # Example usage vin_number = 'VYFED9HP0SJ519559' decoded_vin = decode_vin(vin_number) -print(decoded_vin) - - +print(decoded_vin) \ No newline at end of file diff --git a/inventory/admin.py b/inventory/admin.py index be161286..0b6321a7 100644 --- a/inventory/admin.py +++ b/inventory/admin.py @@ -23,6 +23,7 @@ admin.site.register(models.CarLocation) admin.site.register(models.CarReservation) admin.site.register(models.Organization) admin.site.register(models.Representative) +admin.site.register(models.CarTrim) @admin.register(models.CarMake) class CarMakeAdmin(admin.ModelAdmin): diff --git a/inventory/forms.py b/inventory/forms.py index fab00634..2e2a4750 100644 --- a/inventory/forms.py +++ b/inventory/forms.py @@ -115,7 +115,7 @@ class CarFinanceForm(AddClassMixin, forms.ModelForm): class Meta: model = CarFinance - exclude = ['car', 'profit_margin', 'vat_amount', 'total', 'vat_rate'] + exclude = ['car', 'profit_margin', 'vat_amount', 'total', 'vat_rate','additional_services'] class CarLocationForm(forms.ModelForm): diff --git a/inventory/migrations/0029_merge_20241217_1712.py b/inventory/migrations/0029_merge_20241217_1712.py deleted file mode 100644 index 92237cb0..00000000 --- a/inventory/migrations/0029_merge_20241217_1712.py +++ /dev/null @@ -1,14 +0,0 @@ -# Generated by Django 4.2.17 on 2024-12-17 14:12 - -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('inventory', '0023_cartrim_id_car_model'), - ('inventory', '0028_alter_dealer_options'), - ] - - operations = [ - ] diff --git a/inventory/migrations/0031_remove_carfinance_vat_amount.py b/inventory/migrations/0031_remove_carfinance_vat_amount.py new file mode 100644 index 00000000..795a77df --- /dev/null +++ b/inventory/migrations/0031_remove_carfinance_vat_amount.py @@ -0,0 +1,17 @@ +# Generated by Django 4.2.17 on 2024-12-18 10:14 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('inventory', '0030_alter_carfinance_options_and_more'), + ] + + operations = [ + migrations.RemoveField( + model_name='carfinance', + name='vat_amount', + ), + ] diff --git a/inventory/migrations/0032_carfinance_vat_amount.py b/inventory/migrations/0032_carfinance_vat_amount.py new file mode 100644 index 00000000..2f14a1aa --- /dev/null +++ b/inventory/migrations/0032_carfinance_vat_amount.py @@ -0,0 +1,19 @@ +# Generated by Django 4.2.17 on 2024-12-18 10:15 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('inventory', '0031_remove_carfinance_vat_amount'), + ] + + operations = [ + migrations.AddField( + model_name='carfinance', + name='vat_amount', + field=models.DecimalField(decimal_places=2, default=0, editable=False, max_digits=14, verbose_name='Vat Amount'), + preserve_default=False, + ), + ] diff --git a/inventory/migrations/0033_alter_carfinance_vat_amount.py b/inventory/migrations/0033_alter_carfinance_vat_amount.py new file mode 100644 index 00000000..94a905e2 --- /dev/null +++ b/inventory/migrations/0033_alter_carfinance_vat_amount.py @@ -0,0 +1,19 @@ +# Generated by Django 4.2.17 on 2024-12-18 10:19 + +from decimal import Decimal +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('inventory', '0032_carfinance_vat_amount'), + ] + + operations = [ + migrations.AlterField( + model_name='carfinance', + name='vat_amount', + field=models.DecimalField(decimal_places=2, default=Decimal('0.00'), editable=False, max_digits=14, verbose_name='Vat Amount'), + ), + ] diff --git a/inventory/migrations/0034_additionalservices_and_more.py b/inventory/migrations/0034_additionalservices_and_more.py new file mode 100644 index 00000000..11148361 --- /dev/null +++ b/inventory/migrations/0034_additionalservices_and_more.py @@ -0,0 +1,59 @@ +# Generated by Django 4.2.17 on 2024-12-18 15:59 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('inventory', '0033_alter_carfinance_vat_amount'), + ] + + operations = [ + migrations.CreateModel( + name='AdditionalServices', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=255, verbose_name='Name')), + ('description', models.TextField(verbose_name='Description')), + ('price', models.DecimalField(decimal_places=2, max_digits=14, verbose_name='Price')), + ], + options={ + 'verbose_name': 'Additional Services', + 'verbose_name_plural': 'Additional Services', + }, + ), + migrations.RemoveField( + model_name='carfinance', + name='administration_fee', + ), + migrations.RemoveField( + model_name='carfinance', + name='custom_card_fee', + ), + migrations.RemoveField( + model_name='carfinance', + name='discount_amount', + ), + migrations.RemoveField( + model_name='carfinance', + name='profit_margin', + ), + migrations.RemoveField( + model_name='carfinance', + name='registration_fee', + ), + migrations.RemoveField( + model_name='carfinance', + name='transportation_fee', + ), + migrations.RemoveField( + model_name='carfinance', + name='vat_amount', + ), + migrations.AddField( + model_name='car', + name='additional_services', + field=models.ManyToManyField(related_name='cars', to='inventory.additionalservices'), + ), + ] diff --git a/inventory/migrations/0035_carfinance_administration_fee_and_more.py b/inventory/migrations/0035_carfinance_administration_fee_and_more.py new file mode 100644 index 00000000..97a498eb --- /dev/null +++ b/inventory/migrations/0035_carfinance_administration_fee_and_more.py @@ -0,0 +1,50 @@ +# Generated by Django 4.2.17 on 2024-12-18 16:00 + +from decimal import Decimal +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('inventory', '0034_additionalservices_and_more'), + ] + + operations = [ + migrations.AddField( + model_name='carfinance', + name='administration_fee', + field=models.DecimalField(decimal_places=2, default=Decimal('0.00'), max_digits=14, verbose_name='Administration Fee'), + ), + migrations.AddField( + model_name='carfinance', + name='custom_card_fee', + field=models.DecimalField(decimal_places=2, default=Decimal('0.00'), max_digits=14, verbose_name='Custom Card Fee'), + ), + migrations.AddField( + model_name='carfinance', + name='discount_amount', + field=models.DecimalField(decimal_places=2, default=Decimal('0.00'), max_digits=14, verbose_name='Discount Amount'), + ), + migrations.AddField( + model_name='carfinance', + name='profit_margin', + field=models.DecimalField(decimal_places=2, default=0, editable=False, max_digits=14, verbose_name='Profit Margin'), + preserve_default=False, + ), + migrations.AddField( + model_name='carfinance', + name='registration_fee', + field=models.DecimalField(decimal_places=2, default=Decimal('0.00'), max_digits=14, verbose_name='Registration Fee'), + ), + migrations.AddField( + model_name='carfinance', + name='transportation_fee', + field=models.DecimalField(decimal_places=2, default=Decimal('0.00'), max_digits=14, verbose_name='Transportation Fee'), + ), + migrations.AddField( + model_name='carfinance', + name='vat_amount', + field=models.DecimalField(decimal_places=2, default=Decimal('0.00'), editable=False, max_digits=14, verbose_name='Vat Amount'), + ), + ] diff --git a/inventory/migrations/0036_remove_car_additional_services_and_more.py b/inventory/migrations/0036_remove_car_additional_services_and_more.py new file mode 100644 index 00000000..fdb57e66 --- /dev/null +++ b/inventory/migrations/0036_remove_car_additional_services_and_more.py @@ -0,0 +1,22 @@ +# Generated by Django 4.2.17 on 2024-12-18 16:02 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('inventory', '0035_carfinance_administration_fee_and_more'), + ] + + operations = [ + migrations.RemoveField( + model_name='car', + name='additional_services', + ), + migrations.AddField( + model_name='carfinance', + name='additional_services', + field=models.ManyToManyField(related_name='cars', to='inventory.additionalservices'), + ), + ] diff --git a/inventory/models.py b/inventory/models.py index 5f8a8e13..0f0a0d66 100644 --- a/inventory/models.py +++ b/inventory/models.py @@ -22,6 +22,7 @@ from django.core.exceptions import ValidationError from phonenumber_field.modelfields import PhoneNumberField from django.contrib.contenttypes.models import ContentType from django.utils.timezone import now +from .utilities.financials import get_financial_value, get_total, get_total_financials from .mixins import LocalizedNameMixin @@ -140,6 +141,18 @@ class DEALER_TYPES(models.TextChoices): Sales = "sales", _("Sales") +class AdditionalServices(models.Model): + name = models.CharField(max_length=255, verbose_name=_("Name")) + description = models.TextField(verbose_name=_("Description")) + price = models.DecimalField(max_digits=14, decimal_places=2, verbose_name=_("Price")) + + class Meta: + verbose_name = _("Additional Services") + verbose_name_plural = _("Additional Services") + + def __str__(self): + return self.name + class Car(models.Model): vin = models.CharField(max_length=17, unique=True, verbose_name=_("VIN")) dealer = models.ForeignKey( @@ -217,90 +230,6 @@ class Car(models.Model): active_reservations = self.reservations.filter(reserved_until__gt=now()) return active_reservations.exists() - @property - def finance(self): - """Fetch the first related CarFinance object, or return None.""" - return self.finances # Assuming a related_name of 'finances' - - def _get_financial_value(self, attribute): - """Reusable method to safely get financial values.""" - finance = self.finance - return getattr(finance, attribute, Decimal('0.00')) if finance else Decimal('0.00') - - def _calculate_vat(self, value): - """Helper to calculate VAT dynamically for a given value.""" - vat_rate = getattr(settings, 'VAT_RATE', Decimal('0.15')) # Default VAT rate - return (value * vat_rate).quantize(Decimal('0.01')) - - @property - def cost_price(self): - return self._get_financial_value('cost_price') - - @property - def selling_price(self): - return self._get_financial_value('selling_price') - - @property - def registration_fee(self): - return self._get_financial_value('registration_fee') - - @property - def administration_fee(self): - return self._get_financial_value('administration_fee') - - @property - def transportation_fee(self): - return self._get_financial_value('transportation_fee') - - @property - def custom_card_fee(self): - return self._get_financial_value('custom_card_fee') - - @property - def discount_amount(self): - return self._get_financial_value('discount_amount') - - @property - def vat_amount(self): - """Dynamically calculate VAT for the selling price after discount.""" - price_after_discount = self.selling_price - self.discount_amount - return self._calculate_vat(price_after_discount) - - @property - def administration_fee_vat(self): - return self._calculate_vat(self.administration_fee) - - @property - def transportation_fee_vat(self): - return self._calculate_vat(self.transportation_fee) - - @property - def custom_card_fee_vat(self): - return self._calculate_vat(self.custom_card_fee) - - @property - def total_vat_amount(self): - """Sum up the VAT for all applicable fields.""" - return ( - self.vat_amount + - self.administration_fee_vat + - self.transportation_fee_vat + - self.custom_card_fee_vat - ) - - @property - def total(self): - """Calculate the total amount including VAT.""" - price_after_discount = self.selling_price - self.discount_amount - subtotal = ( - price_after_discount + - self.registration_fee + - self.administration_fee + - self.transportation_fee + - self.custom_card_fee - ) - return subtotal + self.total_vat_amount - # class CarData(models.Model): # vin = models.CharField(max_length=17, unique=True, verbose_name=_("VIN")) # make = models.CharField(max_length=255, verbose_name=_("Make")) @@ -328,7 +257,6 @@ class Car(models.Model): # receiving_date = models.DateTimeField(verbose_name=_("Receiving Date")) - class CarReservation(models.Model): car = models.ForeignKey('Car', on_delete=models.CASCADE, related_name='reservations', verbose_name=_("Car")) reserved_by = models.ForeignKey(User, on_delete=models.CASCADE, related_name='reservations', verbose_name=_("Reserved By")) @@ -344,9 +272,9 @@ class CarReservation(models.Model): verbose_name = _("Car Reservation") verbose_name_plural = _("Car Reservations") - # Car Finance Model class CarFinance(models.Model): + additional_services = models.ManyToManyField(AdditionalServices, related_name="cars") car = models.OneToOneField(Car, on_delete=models.CASCADE, related_name='finances') cost_price = models.DecimalField(max_digits=14, decimal_places=2, verbose_name=_("Cost Price")) selling_price = models.DecimalField(max_digits=14, decimal_places=2, verbose_name=_("Selling Price")) @@ -357,7 +285,7 @@ class CarFinance(models.Model): vat_amount = models.DecimalField(max_digits=14, decimal_places=2, verbose_name=_("Vat Amount"), - editable=False) + editable=False,default=Decimal('0.00')) discount_amount = models.DecimalField(max_digits=14, decimal_places=2, verbose_name=_("Discount Amount"), default=Decimal('0.00')) registration_fee = models.DecimalField(max_digits=14, decimal_places=2, verbose_name=_("Registration Fee"), @@ -368,16 +296,20 @@ class CarFinance(models.Model): default=Decimal('0.00')) custom_card_fee = models.DecimalField(max_digits=14, decimal_places=2, verbose_name=_("Custom Card Fee"), default=Decimal('0.00')) - + @property + def total(self): + """Calculate the total amount including VAT.""" + return get_total(self) + def __str__(self): return f"Car: {self.car}, Selling Price: {self.selling_price}" - def save(self, *args, **kwargs): - # vat_rate = settings.VAT_RATE + def save(self, *args, **kwargs): self.full_clean() try: price_after_discount = self.selling_price - self.discount_amount self.profit_margin = price_after_discount - self.cost_price + self.vat_amount = settings.VAT_RATE except InvalidOperation as e: raise ValidationError(_("Invalid decimal operation: %s") % str(e)) super().save(*args, **kwargs) @@ -386,7 +318,6 @@ class CarFinance(models.Model): verbose_name = _("Car Financial Details") verbose_name_plural = _("Car Financial Details") - class ExteriorColors(models.Model, LocalizedNameMixin): name = models.CharField(max_length=255, verbose_name=_("Name")) arabic_name = models.CharField(max_length=255, verbose_name=_("Arabic Name")) @@ -795,10 +726,9 @@ class SaleQuotationCar(models.Model): """ Calculate total price dynamically based on quantity and selling price. """ - car_finance = self.car.finances - if not car_finance: + if not self.car.finances: return Decimal("0.00") - return car_finance.selling_price * self.quantity + return self.car.finances.selling_price * self.quantity def __str__(self): return f"{self.car} - Quotation #{self.quotation.id}" diff --git a/inventory/services.py b/inventory/services.py index e2420b2f..daf87be3 100644 --- a/inventory/services.py +++ b/inventory/services.py @@ -7,7 +7,7 @@ import json from django_ledger.models import EntityModel -from .utils import get_jwt_token +from inventory.utils import get_jwt_token from pyvin import VIN from django.conf import settings from openai import OpenAI diff --git a/inventory/utilities/__init__.py b/inventory/utilities/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/inventory/utilities/financials.py b/inventory/utilities/financials.py new file mode 100644 index 00000000..15d529b1 --- /dev/null +++ b/inventory/utilities/financials.py @@ -0,0 +1,28 @@ + +from decimal import Decimal +from django.conf import settings + +def calculate_vat(value): + """Helper to calculate VAT dynamically for a given value.""" + vat_rate = getattr(settings, 'VAT_RATE', Decimal('0.15')) # Default VAT rate + return (value * vat_rate).quantize(Decimal('0.01')) +def get_financial_value(instance,attribute,vat=False): + if vat: + return calculate_vat(getattr(instance, attribute, Decimal('0.00')) if instance else Decimal('0.00')) + return getattr(instance, attribute, Decimal('0.00')) if instance else Decimal('0.00') + +def get_total_financials(instance,vat=False): + price_after_discount = get_financial_value(instance,"selling_price",vat) - get_financial_value(instance,"discount_amount",vat) + subtotal = ( + price_after_discount + + get_financial_value(instance,"registration_fee") + + get_financial_value(instance,"administration_fee",vat) + + get_financial_value(instance,"transportation_fee",vat) + + get_financial_value(instance,"custom_card_fee",vat)) + + return subtotal + +def get_total(instance): + total = get_total_financials(instance) + total_vat = get_total_financials(instance,vat=True) + return total + total_vat \ No newline at end of file diff --git a/inventory/utils.py b/inventory/utils.py index 8fbffcab..b254ef02 100644 --- a/inventory/utils.py +++ b/inventory/utils.py @@ -1,6 +1,9 @@ import requests +from django.conf import settings from django.utils.translation import gettext_lazy as _ +from inventory.utilities.financials import get_financial_value + def get_jwt_token(): url = 'https://carapi.app/api/auth/login' @@ -29,3 +32,48 @@ def localize_some_words(): return None + +def get_calculations(instance,quotation): + context = {} + context['vat_rate'] = settings.VAT_RATE + context['total_sales_before_vat'] = sum(item.car.finances.selling_price * item.quantity for item in quotation.quotation_cars.all()) + context['vat_amount'] = sum(item.car.finances.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.finances.administration_fee for item in quotation.quotation_cars.all()) + context['transportation_fee'] = sum(item.car.finances.transportation_fee for item in quotation.quotation_cars.all()) + context['custom_card_fee'] = sum(item.car.finances.custom_card_fee for item in quotation.quotation_cars.all()) + context['registration_fee'] = sum(item.car.finances.registration_fee for item in quotation.quotation_cars.all()) + context['administration_fee_vat'] = sum(get_financial_value(item.car.finances,"administration_fee",True) for item in quotation.quotation_cars.all()) + context['transportation_fee_vat'] = sum(get_financial_value(item.car.finances,"transportation_fee",True) for item in quotation.quotation_cars.all()) + context['custom_card_fee_vat'] = sum(get_financial_value(item.car.finances,"custom_card_fee",True) for item in quotation.quotation_cars.all()) + context['administration_fee_total'] = sum(item.car.finances.administration_fee + context['administration_fee_vat'] for item in quotation.quotation_cars.all()) + context['transportation_fee_total'] = sum(item.car.finances.transportation_fee + context['transportation_fee_vat'] for item in quotation.quotation_cars.all()) + context['custom_card_fee_total'] = sum(item.car.finances.custom_card_fee + context['custom_card_fee_vat'] for item in quotation.quotation_cars.all()) + context['registration_fee_total'] = sum(item.car.finances.registration_fee * total_quantity for item in quotation.quotation_cars.all()) + + return context +# def get_calculations(quotation): +# context = {} +# 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 \ No newline at end of file diff --git a/inventory/views.py b/inventory/views.py index 54b0d732..a44b39dc 100644 --- a/inventory/views.py +++ b/inventory/views.py @@ -28,6 +28,7 @@ from django.contrib import messages from django.db.models import Sum, F, Count from inventory.mixins import AddDealerInstanceMixin +from inventory.utils import get_calculations 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 @@ -187,6 +188,7 @@ class AjaxHandlerView(LoginRequiredMixin, View): def get_series(self, request): model_id = request.GET.get('model_id') year = request.GET.get('year') + # Validate inputs if not model_id or not year: @@ -206,9 +208,9 @@ class AjaxHandlerView(LoginRequiredMixin, View): def get_trims(self, request): serie_id = request.GET.get('serie_id') - model_id = request.GET.get('model_id') + # model_id = request.GET.get('model_id') trims = models.CarTrim.objects.filter( - id_car_serie=serie_id, id_car_model=model_id).values('id_car_trim', 'name', 'arabic_name') + id_car_serie=serie_id).values('id_car_trim', 'name', 'arabic_name') return JsonResponse(list(trims), safe=False) def get_specifications(self, request): @@ -663,8 +665,10 @@ class QuotationCreateView(LoginRequiredMixin,PermissionRequiredMixin, CreateView def form_valid(self, form): + dealer = self.request.user.dealer.get_parent_or_self + form.instance.dealer = dealer quotation = form.save() - selected_cars = form.cleaned_data.get("cars") + selected_cars = form.cleaned_data.get("cars") for car in selected_cars: car_finance = car.finances if car_finance: @@ -687,7 +691,9 @@ class QuotationListView(LoginRequiredMixin,PermissionRequiredMixin, ListView): def get_queryset(self): status = self.request.GET.get("status") - queryset = models.SaleQuotation.objects.all() + # queryset = models.SaleQuotation.objects.all() + print(self.request.user.dealer.get_parent_or_self.sales.all()) + queryset = self.request.user.dealer.get_parent_or_self.sales.all() if status: queryset = queryset.filter(status=status) return queryset @@ -698,56 +704,14 @@ class QuotationDetailView(LoginRequiredMixin,PermissionRequiredMixin, DetailView 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) - 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()) + + # context_result = get_calculations(quotation) + context_result = get_calculations(self.object,quotation) + context.update(context_result) return context @@ -757,6 +721,8 @@ def confirm_quotation(request, pk): quotation = get_object_or_404(models.SaleQuotation, pk=pk) try: quotation.confirm() + quotation_cars = quotation.quotation_cars.annotate(total_price=F('car__total') * F('quantity')) + total_amount = quotation_cars.aggregate(total=Sum('total_price'))['total'] models.SalesOrder.objects.create( quotation=quotation, total_amount=quotation.quotation_cars.aggregate(Sum("total_amount"))["total_amount__sum"], @@ -891,23 +857,22 @@ class OrganizationDetailView(DetailView): context_object_name = 'organization' -class OrganizationCreateView(LoginRequiredMixin, CreateView): +class OrganizationCreateView(LoginRequiredMixin,SuccessMessageMixin, CreateView): model = models.Organization form_class = forms.OrganizationForm template_name = 'organizations/organization_form.html' success_url = reverse_lazy('organization_list') - + success_message = "Organization created successfully." def form_valid(self, form): if form.is_valid(): - form.instance.dealer = self.request.user.dealer - form.save() - success_message = "Organization created successfully." + form.instance.dealer = self.request.user.dealer.get_parent_or_self + form.save() return super().form_valid(form) else: return form.errors -class OrganizationUpdateView(LoginRequiredMixin, UpdateView): +class OrganizationUpdateView(LoginRequiredMixin,SuccessMessageMixin, UpdateView): model = models.Organization form_class = forms.OrganizationForm template_name = 'organizations/organization_form.html' @@ -915,7 +880,7 @@ class OrganizationUpdateView(LoginRequiredMixin, UpdateView): success_message = "Organization updated successfully." -class OrganizationDeleteView(LoginRequiredMixin, DeleteView): +class OrganizationDeleteView(LoginRequiredMixin,SuccessMessageMixin, DeleteView): model = models.Organization template_name = 'organizations/organization_confirm_delete.html' success_url = reverse_lazy('organization_list') @@ -927,30 +892,28 @@ class RepresentativeListView(LoginRequiredMixin, ListView): template_name = 'representatives/representative_list.html' context_object_name = 'representatives' - class RepresentativeDetailView(DetailView): model = models.Representative template_name = 'representatives/representative_detail.html' context_object_name = 'representative' - -class RepresentativeCreateView(LoginRequiredMixin, CreateView): +class RepresentativeCreateView(LoginRequiredMixin, SuccessMessageMixin,CreateView): model = models.Representative form_class = forms.RepresentativeForm template_name = 'representatives/representative_form.html' success_url = reverse_lazy('representative_list') + success_message = "Representative created successfully." def form_valid(self, form): if form.is_valid(): - form.instance.dealer = self.request.user.dealer + form.instance.dealer = self.request.user.dealer.get_parent_or_self form.save() - success_message = "Representative created successfully." return super().form_valid(form) else: return form.errors -class RepresentativeUpdateView(LoginRequiredMixin, UpdateView): +class RepresentativeUpdateView(LoginRequiredMixin,SuccessMessageMixin, UpdateView): model = models.Representative form_class = forms.RepresentativeForm template_name = 'representatives/representative_form.html' @@ -958,10 +921,8 @@ class RepresentativeUpdateView(LoginRequiredMixin, UpdateView): success_message = "Representative updated successfully." -class RepresentativeDeleteView(LoginRequiredMixin, DeleteView): +class RepresentativeDeleteView(LoginRequiredMixin,SuccessMessageMixin, DeleteView): model = models.Representative template_name = 'representatives/representative_confirm_delete.html' success_url = reverse_lazy('representative_list') - success_message = "Representative deleted successfully." - - + success_message = "Representative deleted successfully." \ No newline at end of file diff --git a/templates/inventory/car_form.html b/templates/inventory/car_form.html index 1b1875f6..5877f9a8 100644 --- a/templates/inventory/car_form.html +++ b/templates/inventory/car_form.html @@ -624,8 +624,8 @@ async function loadSpecifications(trimId){ /*stockTypeSelect.addEventListener('change', checkFormCompletion); mileageInput.addEventListener('input', checkFormCompletion); remarksInput.addEventListener('input', checkFormCompletion);*/ - makeSelect.addEventListener("change", (e) => {loadModels(e.target.value)}) - modelSelect.addEventListener("change", (e) => {loadSeries(e.target.value)}) + makeSelect.addEventListener("change", (e) => {loadModels(e.target.value, modelSelect.value)}) + modelSelect.addEventListener("change", (e) => {loadSeries(e.target.value, yearSelect.value)}) decodeVinBtn.addEventListener('click', decodeVin); }); const Toast = Swal.mixin({ diff --git a/templates/sales/quotation_detail.html b/templates/sales/quotation_detail.html index 534c8505..6cad061f 100644 --- a/templates/sales/quotation_detail.html +++ b/templates/sales/quotation_detail.html @@ -2,6 +2,34 @@ {% load custom_filters %} {% load i18n %} {% block content %} + + + +
diff --git a/templates/sales/quotation_list.html b/templates/sales/quotation_list.html index 8948ce87..5968e347 100644 --- a/templates/sales/quotation_list.html +++ b/templates/sales/quotation_list.html @@ -20,12 +20,22 @@ {% for quotation in quotations %} - - + {{ forloop.counter }} {{ quotation.customer.get_full_name }} {{ quotation.quotation_cars.count }} {{ quotation.quotation_cars.get_financial_details.total_amount }} + + {% if quotation.status == 'DRAFT' %} + {{ quotation.status }} + {% elif quotation.status == 'PENDING' %} + {{ quotation.status }} + {% elif quotation.status == 'CONFIRMED' %} + {{ quotation.status }} + {% elif quotation.status == 'CANCELED' %} + {{ quotation.status }} + {% endif %} + {{ quotation.created_at|date:"d/m/Y H:i" }} From 4fea4e43eb0020f7ddadbca6d36785a89f358541 Mon Sep 17 00:00:00 2001 From: gitea Date: Thu, 19 Dec 2024 17:02:24 +0000 Subject: [PATCH 3/3] more clean and fixes --- inventory/admin.py | 1 + inventory/forms.py | 20 ++++- ...37_alter_carfinance_additional_services.py | 18 ++++ ..._carfinance_administration_fee_and_more.py | 42 ++++++++++ ...39_alter_carfinance_additional_services.py | 18 ++++ .../0040_additionalservices_display_name.py | 19 +++++ inventory/models.py | 84 +++++++++++-------- inventory/utilities/financials.py | 34 +++++--- inventory/utils.py | 60 ++++--------- inventory/views.py | 18 ++-- templates/header.html | 2 +- templates/inventory/car_detail.html | 32 ++++--- templates/sales/quotation_detail.html | 37 ++++---- templates/users/user_detail.html | 17 ++-- templates/users/user_list.html | 3 +- 15 files changed, 251 insertions(+), 154 deletions(-) create mode 100644 inventory/migrations/0037_alter_carfinance_additional_services.py create mode 100644 inventory/migrations/0038_remove_carfinance_administration_fee_and_more.py create mode 100644 inventory/migrations/0039_alter_carfinance_additional_services.py create mode 100644 inventory/migrations/0040_additionalservices_display_name.py diff --git a/inventory/admin.py b/inventory/admin.py index 0b6321a7..c11a3aa6 100644 --- a/inventory/admin.py +++ b/inventory/admin.py @@ -24,6 +24,7 @@ admin.site.register(models.CarReservation) admin.site.register(models.Organization) admin.site.register(models.Representative) admin.site.register(models.CarTrim) +admin.site.register(models.AdditionalServices) @admin.register(models.CarMake) class CarMakeAdmin(admin.ModelAdmin): diff --git a/inventory/forms.py b/inventory/forms.py index 2e2a4750..f953956a 100644 --- a/inventory/forms.py +++ b/inventory/forms.py @@ -17,7 +17,8 @@ from .models import ( CarLocation, Organization, Representative, -SaleQuotationCar +SaleQuotationCar, +AdditionalServices ) from django.forms import ModelMultipleChoiceField @@ -112,11 +113,24 @@ class CarUpdateForm(forms.ModelForm, AddClassMixin): class CarFinanceForm(AddClassMixin, forms.ModelForm): - + additional_finances = forms.ModelMultipleChoiceField( + queryset=AdditionalServices.objects.all(), + widget=forms.CheckboxSelectMultiple(attrs={'class': 'form-check-input'}), + required=False +) class Meta: model = CarFinance - exclude = ['car', 'profit_margin', 'vat_amount', 'total', 'vat_rate','additional_services'] + exclude = ['car', 'additional_finances','profit_margin', 'vat_amount', 'total', 'vat_rate','additional_services'] + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + if self.instance.pk: + self.fields['additional_finances'].initial = self.instance.additional_services.all() + def save(self, commit=True): + instance = super().save() + instance.additional_services.set(self.cleaned_data['additional_finances']) + instance.save() + return instance class CarLocationForm(forms.ModelForm): class Meta: diff --git a/inventory/migrations/0037_alter_carfinance_additional_services.py b/inventory/migrations/0037_alter_carfinance_additional_services.py new file mode 100644 index 00000000..a831bd64 --- /dev/null +++ b/inventory/migrations/0037_alter_carfinance_additional_services.py @@ -0,0 +1,18 @@ +# Generated by Django 4.2.17 on 2024-12-19 09:21 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('inventory', '0036_remove_car_additional_services_and_more'), + ] + + operations = [ + migrations.AlterField( + model_name='carfinance', + name='additional_services', + field=models.ManyToManyField(related_name='additional_finances', to='inventory.additionalservices'), + ), + ] diff --git a/inventory/migrations/0038_remove_carfinance_administration_fee_and_more.py b/inventory/migrations/0038_remove_carfinance_administration_fee_and_more.py new file mode 100644 index 00000000..44eb4122 --- /dev/null +++ b/inventory/migrations/0038_remove_carfinance_administration_fee_and_more.py @@ -0,0 +1,42 @@ +# Generated by Django 4.2.17 on 2024-12-19 09:33 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('inventory', '0037_alter_carfinance_additional_services'), + ] + + operations = [ + migrations.RemoveField( + model_name='carfinance', + name='administration_fee', + ), + migrations.RemoveField( + model_name='carfinance', + name='custom_card_fee', + ), + migrations.RemoveField( + model_name='carfinance', + name='profit_margin', + ), + migrations.RemoveField( + model_name='carfinance', + name='registration_fee', + ), + migrations.RemoveField( + model_name='carfinance', + name='transportation_fee', + ), + migrations.RemoveField( + model_name='carfinance', + name='vat_amount', + ), + migrations.AlterField( + model_name='carfinance', + name='additional_services', + field=models.ManyToManyField(blank=True, null=True, related_name='additional_finances', to='inventory.additionalservices'), + ), + ] diff --git a/inventory/migrations/0039_alter_carfinance_additional_services.py b/inventory/migrations/0039_alter_carfinance_additional_services.py new file mode 100644 index 00000000..89e58402 --- /dev/null +++ b/inventory/migrations/0039_alter_carfinance_additional_services.py @@ -0,0 +1,18 @@ +# Generated by Django 4.2.17 on 2024-12-19 09:33 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('inventory', '0038_remove_carfinance_administration_fee_and_more'), + ] + + operations = [ + migrations.AlterField( + model_name='carfinance', + name='additional_services', + field=models.ManyToManyField(blank=True, related_name='additional_finances', to='inventory.additionalservices'), + ), + ] diff --git a/inventory/migrations/0040_additionalservices_display_name.py b/inventory/migrations/0040_additionalservices_display_name.py new file mode 100644 index 00000000..eb324143 --- /dev/null +++ b/inventory/migrations/0040_additionalservices_display_name.py @@ -0,0 +1,19 @@ +# Generated by Django 4.2.17 on 2024-12-19 12:01 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('inventory', '0039_alter_carfinance_additional_services'), + ] + + operations = [ + migrations.AddField( + model_name='additionalservices', + name='display_name', + field=models.CharField(default=1, max_length=255, verbose_name='Display Name'), + preserve_default=False, + ), + ] diff --git a/inventory/models.py b/inventory/models.py index 0f0a0d66..06d513b9 100644 --- a/inventory/models.py +++ b/inventory/models.py @@ -1,6 +1,7 @@ from uuid import uuid4 from django.conf import settings from django.db import models, transaction +from django.db.models import Sum, F, Count from django.contrib.auth.models import User from django.db.models.signals import pre_save, post_save from django.dispatch import receiver @@ -143,6 +144,7 @@ class DEALER_TYPES(models.TextChoices): class AdditionalServices(models.Model): name = models.CharField(max_length=255, verbose_name=_("Name")) + display_name = models.CharField(max_length=255, verbose_name=_("Display Name")) description = models.TextField(verbose_name=_("Description")) price = models.DecimalField(max_digits=14, decimal_places=2, verbose_name=_("Price")) @@ -151,7 +153,7 @@ class AdditionalServices(models.Model): verbose_name_plural = _("Additional Services") def __str__(self): - return self.name + return self.name + " - " + str(self.price) class Car(models.Model): vin = models.CharField(max_length=17, unique=True, verbose_name=_("VIN")) @@ -274,28 +276,28 @@ class CarReservation(models.Model): # Car Finance Model class CarFinance(models.Model): - additional_services = models.ManyToManyField(AdditionalServices, related_name="cars") + additional_services = models.ManyToManyField(AdditionalServices, related_name="additional_finances",blank=True) car = models.OneToOneField(Car, on_delete=models.CASCADE, related_name='finances') cost_price = models.DecimalField(max_digits=14, decimal_places=2, verbose_name=_("Cost Price")) selling_price = models.DecimalField(max_digits=14, decimal_places=2, verbose_name=_("Selling Price")) - profit_margin = models.DecimalField(max_digits=14, - decimal_places=2, - verbose_name=_("Profit Margin"), - editable=False) - vat_amount = models.DecimalField(max_digits=14, - decimal_places=2, - verbose_name=_("Vat Amount"), - editable=False,default=Decimal('0.00')) discount_amount = models.DecimalField(max_digits=14, decimal_places=2, verbose_name=_("Discount Amount"), default=Decimal('0.00')) - registration_fee = models.DecimalField(max_digits=14, decimal_places=2, verbose_name=_("Registration Fee"), - default=Decimal('0.00')) - administration_fee = models.DecimalField(max_digits=14, decimal_places=2, verbose_name=_("Administration Fee"), - default=Decimal('0.00')) - transportation_fee = models.DecimalField(max_digits=14, decimal_places=2, verbose_name=_("Transportation Fee"), - default=Decimal('0.00')) - custom_card_fee = models.DecimalField(max_digits=14, decimal_places=2, verbose_name=_("Custom Card Fee"), - default=Decimal('0.00')) + # profit_margin = models.DecimalField(max_digits=14, + # decimal_places=2, + # verbose_name=_("Profit Margin"), + # editable=False) + # vat_amount = models.DecimalField(max_digits=14, + # decimal_places=2, + # verbose_name=_("Vat Amount"), + # editable=False,default=Decimal('0.00')) + # registration_fee = models.DecimalField(max_digits=14, decimal_places=2, verbose_name=_("Registration Fee"), + # default=Decimal('0.00')) + # administration_fee = models.DecimalField(max_digits=14, decimal_places=2, verbose_name=_("Administration Fee"), + # default=Decimal('0.00')) + # transportation_fee = models.DecimalField(max_digits=14, decimal_places=2, verbose_name=_("Transportation Fee"), + # default=Decimal('0.00')) + # custom_card_fee = models.DecimalField(max_digits=14, decimal_places=2, verbose_name=_("Custom Card Fee"), + # default=Decimal('0.00')) @property def total(self): """Calculate the total amount including VAT.""" @@ -304,15 +306,15 @@ class CarFinance(models.Model): def __str__(self): return f"Car: {self.car}, Selling Price: {self.selling_price}" - def save(self, *args, **kwargs): - self.full_clean() - try: - price_after_discount = self.selling_price - self.discount_amount - self.profit_margin = price_after_discount - self.cost_price - self.vat_amount = settings.VAT_RATE - except InvalidOperation as e: - raise ValidationError(_("Invalid decimal operation: %s") % str(e)) - super().save(*args, **kwargs) + # def save(self, *args, **kwargs): + # self.full_clean() + # try: + # price_after_discount = self.selling_price - self.discount_amount + # self.profit_margin = price_after_discount - self.cost_price + # self.vat_amount = settings.VAT_RATE + # except InvalidOperation as e: + # raise ValidationError(_("Invalid decimal operation: %s") % str(e)) + # super().save(*args, **kwargs) class Meta: verbose_name = _("Car Financial Details") @@ -669,6 +671,13 @@ class SaleQuotation(models.Model): total_quantity = self.quotation_cars.aggregate(total=Sum('quantity'))['total'] return total_quantity or 0 + # @property + # def total(self): + # total = self.quotation_cars.aggregate(total_price=Sum(F('car__finances__selling_price') * F('quantity'))) + # if total: + # return float(total["total_price"]) * 0.15 + float(total["total_price"]) + # return 0 + def confirm(self): """Confirm the quotation and lock financial details.""" if self.status != "DRAFT": @@ -700,7 +709,10 @@ class SaleQuotationCar(models.Model): verbose_name=_("Car") ) quantity = models.PositiveIntegerField(default=1, verbose_name=_("Quantity")) - + + @property + def finance(self): + return self.car.finances @property def financial_details(self): """ @@ -721,14 +733,14 @@ class SaleQuotationCar(models.Model): # "total_amount": car_finance.total, } - @property - def total_price(self): - """ - Calculate total price dynamically based on quantity and selling price. - """ - if not self.car.finances: - return Decimal("0.00") - return self.car.finances.selling_price * self.quantity + # @property + # def total(self): + # """ + # Calculate total price dynamically based on quantity and selling price. + # """ + # if not self.car.finances: + # return Decimal("0.00") + # return self.car.finances.selling_price * self.quantity def __str__(self): return f"{self.car} - Quotation #{self.quotation.id}" diff --git a/inventory/utilities/financials.py b/inventory/utilities/financials.py index 15d529b1..068f8661 100644 --- a/inventory/utilities/financials.py +++ b/inventory/utilities/financials.py @@ -1,26 +1,36 @@ from decimal import Decimal from django.conf import settings +from inventory import models def calculate_vat(value): """Helper to calculate VAT dynamically for a given value.""" vat_rate = getattr(settings, 'VAT_RATE', Decimal('0.15')) # Default VAT rate return (value * vat_rate).quantize(Decimal('0.01')) -def get_financial_value(instance,attribute,vat=False): +# def get_financial_value(instance,attribute,vat=False): +# if vat: +# return calculate_vat(getattr(instance, attribute, Decimal('0.00')) if instance else Decimal('0.00')) +# return getattr(instance, attribute, Decimal('0.00')) if instance else Decimal('0.00') + +def get_financial_value(name,vat=False): + val = models.AdditionalServices.objects.filter(name=name).first() + if not val: + return 0 if vat: - return calculate_vat(getattr(instance, attribute, Decimal('0.00')) if instance else Decimal('0.00')) - return getattr(instance, attribute, Decimal('0.00')) if instance else Decimal('0.00') + return (val.price * settings.VAT_RATE).quantize(Decimal('0.01')) + return val.price + def get_total_financials(instance,vat=False): - price_after_discount = get_financial_value(instance,"selling_price",vat) - get_financial_value(instance,"discount_amount",vat) - subtotal = ( - price_after_discount + - get_financial_value(instance,"registration_fee") + - get_financial_value(instance,"administration_fee",vat) + - get_financial_value(instance,"transportation_fee",vat) + - get_financial_value(instance,"custom_card_fee",vat)) - - return subtotal + # price_after_discount = get_financial_value(instance,"selling_price",vat) - get_financial_value(instance,"discount_amount",vat) + # subtotal = ( + # price_after_discount + + # get_financial_value("registration_fee") + + # get_financial_value("administration_fee",vat) + + # get_financial_value("transportation_fee",vat) + + # get_financial_value("custom_card_fee",vat)) + + return 1000 def get_total(instance): total = get_total_financials(instance) diff --git a/inventory/utils.py b/inventory/utils.py index b254ef02..9c0a25bf 100644 --- a/inventory/utils.py +++ b/inventory/utils.py @@ -1,7 +1,7 @@ import requests from django.conf import settings from django.utils.translation import gettext_lazy as _ - +from inventory import models from inventory.utilities.financials import get_financial_value @@ -32,48 +32,20 @@ def localize_some_words(): return None - -def get_calculations(instance,quotation): +def get_calculations(quotation): context = {} - context['vat_rate'] = settings.VAT_RATE - context['total_sales_before_vat'] = sum(item.car.finances.selling_price * item.quantity for item in quotation.quotation_cars.all()) - context['vat_amount'] = sum(item.car.finances.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.finances.administration_fee for item in quotation.quotation_cars.all()) - context['transportation_fee'] = sum(item.car.finances.transportation_fee for item in quotation.quotation_cars.all()) - context['custom_card_fee'] = sum(item.car.finances.custom_card_fee for item in quotation.quotation_cars.all()) - context['registration_fee'] = sum(item.car.finances.registration_fee for item in quotation.quotation_cars.all()) - context['administration_fee_vat'] = sum(get_financial_value(item.car.finances,"administration_fee",True) for item in quotation.quotation_cars.all()) - context['transportation_fee_vat'] = sum(get_financial_value(item.car.finances,"transportation_fee",True) for item in quotation.quotation_cars.all()) - context['custom_card_fee_vat'] = sum(get_financial_value(item.car.finances,"custom_card_fee",True) for item in quotation.quotation_cars.all()) - context['administration_fee_total'] = sum(item.car.finances.administration_fee + context['administration_fee_vat'] for item in quotation.quotation_cars.all()) - context['transportation_fee_total'] = sum(item.car.finances.transportation_fee + context['transportation_fee_vat'] for item in quotation.quotation_cars.all()) - context['custom_card_fee_total'] = sum(item.car.finances.custom_card_fee + context['custom_card_fee_vat'] for item in quotation.quotation_cars.all()) - context['registration_fee_total'] = sum(item.car.finances.registration_fee * total_quantity for item in quotation.quotation_cars.all()) + qc_len = quotation.quotation_cars.count() + cars = [x.car for x in quotation.quotation_cars.all()] + finances = models.CarFinance.objects.filter(car__in=cars) - return context -# def get_calculations(quotation): -# context = {} -# 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 \ No newline at end of file + services = models.AdditionalServices.objects.filter(additional_finances__in=finances).all() + data = [{"name":x.name,"price":x.price,"total_price":x.price * qc_len,"vated":float(x.price) * 0.15 * float(qc_len),"total_price_vat":float(x.price) + (float(x.price) * 0.15 * float(qc_len))} for x in services] + context["services"] = data + context["total_cost"] = 0 + context["total_vat"] = 0 + context["total_cost_vat"] = 0 + for k in context["services"]: + context["total_cost"] += k["total_price"] + context["total_vat"] += k["vated"] + context["total_cost_vat"] = float(context["total_cost"])+float(context["total_vat"]) + return context \ No newline at end of file diff --git a/inventory/views.py b/inventory/views.py index a44b39dc..9b064173 100644 --- a/inventory/views.py +++ b/inventory/views.py @@ -28,7 +28,7 @@ from django.contrib import messages from django.db.models import Sum, F, Count from inventory.mixins import AddDealerInstanceMixin -from inventory.utils import get_calculations + 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 @@ -38,7 +38,7 @@ 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 - +from .utils import get_calculations User = get_user_model() logger = logging.getLogger(__name__) @@ -405,6 +405,11 @@ class CarFinanceUpdateView(LoginRequiredMixin,SuccessMessageMixin, UpdateView): def get_success_url(self): return reverse('car_detail', kwargs={'pk': self.object.car.pk}) + + def get_form_kwargs(self): + kwargs = super().get_form_kwargs() + kwargs['instance'] = self.get_object() + return kwargs class CarUpdateView(LoginRequiredMixin, SuccessMessageMixin,UpdateView): @@ -708,9 +713,8 @@ class QuotationDetailView(LoginRequiredMixin,PermissionRequiredMixin, DetailView def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) quotation = self.object - - # context_result = get_calculations(quotation) - context_result = get_calculations(self.object,quotation) + + context_result = get_calculations(quotation) context.update(context_result) return context @@ -767,8 +771,8 @@ class UserListView(LoginRequiredMixin,PermissionRequiredMixin, ListView): class UserDetailView(LoginRequiredMixin,PermissionRequiredMixin, DetailView): model = models.Dealer template_name = "users/user_detail.html" - context_object_name = "user" - permission_required = ('inventory.view_dealer',) + context_object_name = "user_" + permission_required = ('inventory.view_dealer',) class UserCreateView(LoginRequiredMixin,PermissionRequiredMixin,SuccessMessageMixin,AddDealerInstanceMixin, CreateView): model = models.Dealer diff --git a/templates/header.html b/templates/header.html index 2de3f449..e732d55f 100644 --- a/templates/header.html +++ b/templates/header.html @@ -19,7 +19,7 @@