From 9ccb432d19a1937ef94b630835b0e96438529305 Mon Sep 17 00:00:00 2001 From: gitea Date: Wed, 8 Jan 2025 16:05:38 +0000 Subject: [PATCH] additinal services, and estimate + invoice --- import_data.py | 4 +- inventory/forms.py | 29 +-- .../0004_additionalservices_item_and_more.py | 26 ++ .../0005_alter_additionalservices_item.py | 20 ++ inventory/models.py | 57 +---- inventory/services.py | 2 +- inventory/signals.py | 120 +++++++--- inventory/utils.py | 2 +- inventory/views.py | 225 +++++++++++++++--- scripts/loaddata.py | 154 ++++++++++++ templates/inventory/car_detail.html | 2 +- .../sales/estimates/estimate_detail.html | 34 ++- .../sales/estimates/estimate_preview.html | 17 +- templates/sales/invoices/invoice_detail.html | 32 ++- templates/sales/invoices/invoice_preview.html | 17 +- 15 files changed, 554 insertions(+), 187 deletions(-) create mode 100644 inventory/migrations/0004_additionalservices_item_and_more.py create mode 100644 inventory/migrations/0005_alter_additionalservices_item.py create mode 100644 scripts/loaddata.py diff --git a/import_data.py b/import_data.py index eac58f73..929472c7 100644 --- a/import_data.py +++ b/import_data.py @@ -14,8 +14,8 @@ from inventory.models import ( ) # Set up Django environment -os.environ.setdefault("DJANGO_SETTINGS_MODULE", "your_project.settings") -django.setup() +# os.environ.setdefault("DJANGO_SETTINGS_MODULE", "your_project.settings") +# django.setup() # Load the cleaned JSON data with open("final_car_data.json", "r") as file: diff --git a/inventory/forms.py b/inventory/forms.py index d9ebacc0..5d230a7d 100644 --- a/inventory/forms.py +++ b/inventory/forms.py @@ -190,7 +190,7 @@ class CarUpdateForm(forms.ModelForm, AddClassMixin): class CarFinanceForm(AddClassMixin, forms.ModelForm): additional_finances = forms.ModelMultipleChoiceField( - queryset=ItemModel.objects.filter(item_role=ItemModel.ITEM_ROLE_SERVICE), + queryset=AdditionalServices.objects.all(), widget=forms.CheckboxSelectMultiple(attrs={"class": "form-check-input"}), required=False, ) @@ -202,16 +202,15 @@ class CarFinanceForm(AddClassMixin, forms.ModelForm): "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 __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() @@ -366,19 +365,7 @@ class CarSelectionTable(tables.Table): template_name = "django_tables2/bootstrap4.html" -class WizardForm1(forms.Form): - username = forms.CharField( - widget=forms.TextInput( - attrs={ - "class": "form-control", - "placeholder": "Username", - "required": "required", - } - ), - error_messages={ - "required": _("You must add a username."), - }, - ) +class WizardForm1(forms.Form): email = forms.EmailField( widget=forms.EmailInput( attrs={ diff --git a/inventory/migrations/0004_additionalservices_item_and_more.py b/inventory/migrations/0004_additionalservices_item_and_more.py new file mode 100644 index 00000000..2241b959 --- /dev/null +++ b/inventory/migrations/0004_additionalservices_item_and_more.py @@ -0,0 +1,26 @@ +# Generated by Django 4.2.17 on 2025-01-08 08:42 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('django_ledger', '0017_alter_accountmodel_unique_together_and_more'), + ('inventory', '0003_alter_caroptionvalue_is_base'), + ] + + operations = [ + migrations.AddField( + model_name='additionalservices', + name='item', + field=models.OneToOneField(default='', on_delete=django.db.models.deletion.CASCADE, to='django_ledger.itemmodel', verbose_name='Item'), + preserve_default=False, + ), + 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/0005_alter_additionalservices_item.py b/inventory/migrations/0005_alter_additionalservices_item.py new file mode 100644 index 00000000..bf542c58 --- /dev/null +++ b/inventory/migrations/0005_alter_additionalservices_item.py @@ -0,0 +1,20 @@ +# Generated by Django 4.2.17 on 2025-01-08 08:43 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('django_ledger', '0017_alter_accountmodel_unique_together_and_more'), + ('inventory', '0004_additionalservices_item_and_more'), + ] + + operations = [ + migrations.AlterField( + model_name='additionalservices', + name='item', + field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='django_ledger.itemmodel', verbose_name='Item'), + ), + ] diff --git a/inventory/models.py b/inventory/models.py index 6af7da08..6606a917 100644 --- a/inventory/models.py +++ b/inventory/models.py @@ -28,7 +28,7 @@ from django.utils.timezone import now from .utilities.financials import get_financial_value, get_total, get_total_financials from django.db.models import FloatField from .mixins import LocalizedNameMixin -from django_ledger.models import EntityModel +from django_ledger.models import EntityModel,ItemModel class DealerUserManager(UserManager): @@ -58,10 +58,7 @@ class VatRate(models.Model): rate = models.DecimalField(max_digits=5, decimal_places=2, default=Decimal('0.15')) is_active = models.BooleanField(default=True) created_at = models.DateTimeField(auto_now_add=True) - - @property - def vat_rate(self): - return self.rate / 100 + def __str__(self): return f"Rate: {self.rate}%" @@ -227,6 +224,7 @@ class AdditionalServices(models.Model, LocalizedNameMixin): taxable = models.BooleanField(default=False, verbose_name=_("taxable")) uom = models.CharField(max_length=10, choices=UnitOfMeasure.choices, verbose_name=_("Unit of Measurement")) dealer = models.ForeignKey("Dealer", on_delete=models.CASCADE, verbose_name=_("Dealer")) + item = models.OneToOneField(ItemModel, on_delete=models.CASCADE, verbose_name=_("Item"),null=True, blank=True) class Meta: verbose_name = _("Additional Services") @@ -317,34 +315,6 @@ class Car(models.Model): def get_car_group(self): return f"{self.id_car_make.get_local_name} {self.id_car_model.get_local_name}" - -# class CarData(models.Model): -# vin = models.CharField(max_length=17, unique=True, verbose_name=_("VIN")) -# make = models.CharField(max_length=255, verbose_name=_("Make")) -# make_ar = models.CharField(max_length=255, verbose_name=_("Make Arabic")) -# model = models.CharField(max_length=255, verbose_name=_("Model")) -# model_ar = models.CharField(max_length=255, verbose_name=_("Model Arabic")) -# year = models.IntegerField(verbose_name=_("Year")) -# series = models.CharField(max_length=255,verbose_name=_("Series")) -# trim = models.CharField(max_length=255,verbose_name=_("Trim")) -# specs = models.JsonField -# status = models.CharField( -# max_length=10, -# choices=CarStatusChoices, -# default=CarStatusChoices.AVAILABLE, -# verbose_name=_("Status") -# ) -# stock_type = models.CharField( -# max_length=10, -# choices=CarStockTypeChoices, -# default=CarStockTypeChoices.NEW, -# 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")) - - 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")) @@ -363,7 +333,7 @@ class CarReservation(models.Model): # Car Finance Model class CarFinance(models.Model): - additional_services = models.ManyToManyField(ItemModel, related_name="additional_finances",blank=True) + 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")) @@ -371,19 +341,14 @@ class CarFinance(models.Model): default=Decimal('0.00')) @property - def total(self): - total = self.selling_price - - if self.additional_services.count() != 0: - total_additional_services = sum(x.default_amount for x in self.additional_services.all()) - total += total_additional_services - - return total + def total(self): + if self.additional_services.count() > 0: + return self.selling_price + sum(x.price for x in self.additional_services.all()) + return self.selling_price @property def total_discount(self): - if self.discount_amount != 0: - total = self.total - self.discount_amount - return total + if self.discount_amount > 0: + return self.total - self.discount_amount return self.total @property @@ -395,7 +360,7 @@ class CarFinance(models.Model): def vat_amount(self): vat = VatRate.objects.filter(is_active=True).first() if vat: - return (self.total_discount * Decimal(vat.vat_rate)).quantize(Decimal('0.01')) + return (self.total_discount * Decimal(vat.rate)).quantize(Decimal('0.01')) return Decimal('0.00') diff --git a/inventory/services.py b/inventory/services.py index f4e90bf9..1ec259e9 100644 --- a/inventory/services.py +++ b/inventory/services.py @@ -25,7 +25,7 @@ def get_model(item,make): r = item.split(" ") for i in r: if data:=make.carmodel_set.filter(name__iexact=i).first(): - break + break return data def normalize_name(name): diff --git a/inventory/signals.py b/inventory/signals.py index 6b9d66e1..92c117b3 100644 --- a/inventory/signals.py +++ b/inventory/signals.py @@ -3,7 +3,14 @@ from django.dispatch import receiver from django.utils.translation import gettext_lazy as _ from django.contrib.auth import get_user_model from django_ledger.io import roles -from django_ledger.models import EntityModel,AccountModel,ItemModel,ItemModelAbstract,UnitOfMeasureModel, VendorModel +from django_ledger.models import ( + EntityModel, + AccountModel, + ItemModel, + ItemModelAbstract, + UnitOfMeasureModel, + VendorModel, +) from . import models from .models import OpportunityLog @@ -43,23 +50,24 @@ User = get_user_model() # instance.save() - # check with marwan -@receiver(post_save, sender=models.Car) +@receiver(post_save, sender=models.Car) def create_car_location(sender, instance, created, **kwargs): """ - Signal to create or update the car's location when a car instance is saved. - """ + Signal to create or update the car's location when a car instance is saved. + """ try: if created: if instance.dealer is None: - raise ValueError(f"Cannot create CarLocation for car {instance.vin}: dealer is missing.") - + raise ValueError( + f"Cannot create CarLocation for car {instance.vin}: dealer is missing." + ) + models.CarLocation.objects.create( car=instance, owner=instance.dealer, showroom=instance.dealer, - description=f"Initial location set for car {instance.vin}." + description=f"Initial location set for car {instance.vin}.", ) print("Car Location created") except Exception as e: @@ -85,7 +93,6 @@ def create_car_location(sender, instance, created, **kwargs): # Create Entity @receiver(post_save, sender=models.Dealer) def create_ledger_entity(sender, instance, created, **kwargs): - if created: entity_name = instance.user.dealer.name entity = EntityModel.create_entity( @@ -105,14 +112,11 @@ def create_ledger_entity(sender, instance, created, **kwargs): # entity.populate_default_coa(activate_accounts=True, coa_model=coa) print(f"Ledger entity created for Dealer: {instance.name}") + # Create unit of measures + entity.create_uom(name="Unit", unit_abbr="unit") + for u in models.UnitOfMeasure.choices: + entity.create_uom(name=u[1], unit_abbr=u[0]) - - # Create unit of measures - - entity.create_uom( - name="Unit", - unit_abbr="unit") - # Create Cash Account asset_ca_cash = entity.create_account( coa_model=coa, @@ -157,6 +161,17 @@ def create_ledger_entity(sender, instance, created, **kwargs): balance_type="credit", active=True, ) + + # add Bank + + # asset_ca_accounts_payable = entity.create_account( + # coa_model=coa, + # code="2101", + # role=roles.LIABILITY_CL_ACC_PAYABLE, + # name=_("Accounts Payable"), + # balance_type="credit", + # active=True, + # ) asset_ca_accounts_payable.role_default = True asset_ca_accounts_payable.save() # Create Equity Account @@ -216,8 +231,8 @@ def create_ledger_entity(sender, instance, created, **kwargs): balance_type="debit", active=True, ) - #Create Deferred Revenue Account - entity.create_account( + # Create Deferred Revenue Account + unearned_account = entity.create_account( coa_model=coa, code="2060", role=roles.LIABILITY_CL_DEFERRED_REVENUE, @@ -225,11 +240,13 @@ def create_ledger_entity(sender, instance, created, **kwargs): balance_type="credit", active=True, ) + unearned_account.role_default = True + unearned_account.save() + # Create Vendor @receiver(post_save, sender=models.Vendor) def create_ledger_vendor(sender, instance, created, **kwargs): - if created: entity = EntityModel.objects.filter(name=instance.dealer.name).first() @@ -245,7 +262,7 @@ def create_ledger_vendor(sender, instance, created, **kwargs): "additional_info": { "arabic_name": instance.arabic_name, "contact_person": instance.contact_person, - } + }, } ) @@ -256,7 +273,7 @@ def create_ledger_vendor(sender, instance, created, **kwargs): def create_customer(sender, instance, created, **kwargs): if created: dealer = instance.dealer - entity = dealer.entity + entity = dealer.entity name = f"{instance.first_name} {instance.middle_name} {instance.last_name}" if entity: entity.create_customer( @@ -281,29 +298,27 @@ def create_item_model(sender, instance, created, **kwargs): name = instance.dealer.name entity = EntityModel.objects.filter(name=name).first() if created: - coa = entity.get_default_coa() + coa = entity.get_default_coa() uom = entity.get_uom_all().get(name="Unit") - + if not entity.get_items_all().filter(name=instance.vin).first(): entity.create_item_product( name=f"{instance.vin}", item_type=ItemModel.ITEM_TYPE_MATERIAL, uom_model=uom, - coa_model=coa + coa_model=coa, ) entity.create_item_inventory( name=f"{instance.vin}", item_type=ItemModel.ITEM_TYPE_MATERIAL, uom_model=uom, - coa_model=coa - ) - - - + coa_model=coa, + ) + + # # update price - CarFinance @receiver(post_save, sender=models.CarFinance) def update_item_model_cost(sender, instance, created, **kwargs): - ItemModel.objects.filter(item_id=instance.car.vin).update( inventory_received_value=instance.cost_price, default_amount=instance.cost_price, @@ -311,7 +326,6 @@ def update_item_model_cost(sender, instance, created, **kwargs): print(f"Inventory item updated with CarFinance data for Car: {instance.car}") - # @receiver(pre_save, sender=models.SaleQuotation) # def update_quotation_status(sender, instance, **kwargs): # if instance.valid_until and timezone.now() > instance.valid_until: @@ -333,13 +347,16 @@ def update_item_model_cost(sender, instance, created, **kwargs): # quotation.status = 'pending' # quotation.save() + @receiver(post_save, sender=models.Opportunity) def notify_staff_on_deal_status_change(sender, instance, **kwargs): if instance.pk: previous = models.Opportunity.objects.get(pk=instance.pk) if previous.deal_status != instance.deal_status: message = f"Deal '{instance.deal_name}' status changed from {previous.deal_status} to {instance.deal_status}." - models.Notification.objects.create(staff=instance.created_by, message=message) + models.Notification.objects.create( + staff=instance.created_by, message=message + ) @receiver(post_save, sender=models.Opportunity) @@ -347,9 +364,9 @@ def log_opportunity_creation(sender, instance, created, **kwargs): if created: models.OpportunityLog.objects.create( opportunity=instance, - action='create', + action="create", user=instance.created_by, - details=f"Opportunity '{instance.deal_name}' was created." + details=f"Opportunity '{instance.deal_name}' was created.", ) @@ -360,16 +377,41 @@ def log_opportunity_update(sender, instance, **kwargs): if previous.deal_status != instance.deal_status: models.OpportunityLog.objects.create( opportunity=instance, - action='status_change', + action="status_change", user=instance.created_by, old_status=previous.deal_status, new_status=instance.deal_status, - details=f"Status changed from {previous.deal_status} to {instance.deal_status}." + details=f"Status changed from {previous.deal_status} to {instance.deal_status}.", ) else: models.OpportunityLog.objects.create( opportunity=instance, - action='update', + action="update", user=instance.created_by, - details=f"Opportunity '{instance.deal_name}' was updated." - ) \ No newline at end of file + details=f"Opportunity '{instance.deal_name}' was updated.", + ) + + +@receiver(post_save, sender=models.AdditionalServices) +def create_item_service(sender, instance, created, **kwargs): + if created: + + entity = instance.dealer.entity + uom = entity.get_uom_all().get(unit_abbr=instance.uom) + cogs = entity.get_all_accounts().get(role=roles.COGS) + + # price = (float(instance.price) * float(vat.rate)) + float(instance.price) if instance.taxable else instance.price + service_model = ItemModel.objects.create( + name=instance.name, + uom=uom, + default_amount=instance.price, + entity=entity, + is_product_or_service=True, + sold_as_unit=True, + is_active=True, + item_role="service", + for_inventory=False, + cogs_account=cogs, + ) + instance.item = service_model + instance.save() diff --git a/inventory/utils.py b/inventory/utils.py index 1bdeb3c2..18bd52ad 100644 --- a/inventory/utils.py +++ b/inventory/utils.py @@ -97,5 +97,5 @@ def reserve_car(car,request): def calculate_vat_amount(amount): vat = models.VatRate.objects.filter(is_active=True).first() if vat: - return ((amount * Decimal(vat.vat_rate)).quantize(Decimal('0.01')),vat.vat_rate) + return ((amount * Decimal(vat.rate)).quantize(Decimal('0.01')),vat.rate) return amount \ No newline at end of file diff --git a/inventory/views.py b/inventory/views.py index 1cee636f..38532065 100644 --- a/inventory/views.py +++ b/inventory/views.py @@ -1,3 +1,4 @@ +from decimal import Decimal from django.core.paginator import Paginator from django_ledger.models import ( EntityModel, @@ -118,7 +119,7 @@ def dealer_signup(request, *args, **kwargs): wf1 = data.get("wizardValidationForm1") wf2 = data.get("wizardValidationForm2") wf3 = data.get("wizardValidationForm3") - username = wf1.get("username") + # username = wf1.get("username") email = wf1.get("email") password = wf1.get("password") password_confirm = wf1.get("confirm_password") @@ -134,7 +135,8 @@ def dealer_signup(request, *args, **kwargs): try: with transaction.atomic(): - user = User.objects.create(username=username, email=email) + # user = User.objects.create(username=username, email=email) + user = User.objects.create(username=email, email=email) user.set_password(password) user.save() @@ -147,7 +149,7 @@ def dealer_signup(request, *args, **kwargs): phone_number=phone, address=address, ) - user = authenticate(request, username=username, password=password) + user = authenticate(request, email=email, password=password) if user is not None: return JsonResponse( {"message": "User created successfully."}, status=200 @@ -544,6 +546,22 @@ class CarFinanceCreateView(LoginRequiredMixin, CreateView): context["car"] = self.car return context + def get_form(self, form_class=None): + form = super().get_form(form_class) + dealer = get_user_type(self.request.user.dealer) + form.fields[ + "additional_finances" + ].queryset = models.AdditionalServices.objects.filter(dealer=dealer) + return form + + # def get_initial(self): + # initial = super().get_initial() + # instance = self.get_object() + # dealer = get_user_type(self.request.user.dealer) + # selected_items = instance.additional_services.filter(dealer=dealer) + # initial["additional_finances"] = selected_items + # return initial + class CarFinanceUpdateView(LoginRequiredMixin, SuccessMessageMixin, UpdateView): model = models.CarFinance @@ -562,10 +580,19 @@ class CarFinanceUpdateView(LoginRequiredMixin, SuccessMessageMixin, UpdateView): def get_initial(self): initial = super().get_initial() instance = self.get_object() - selected_items = instance.additional_services.all() + dealer = get_user_type(self.request.user.dealer) + selected_items = instance.additional_services.filter(dealer=dealer) initial["additional_finances"] = selected_items return initial + def get_form(self, form_class=None): + form = super().get_form(form_class) + dealer = get_user_type(self.request.user.dealer) + form.fields[ + "additional_finances" + ].queryset = models.AdditionalServices.objects.filter(dealer=dealer) + return form + class CarUpdateView(LoginRequiredMixin, SuccessMessageMixin, UpdateView): model = models.Car @@ -1634,7 +1661,6 @@ def bank_account_delete(request, pk): # Accounts - class AccountListView(LoginRequiredMixin, ListView): model = AccountModel template_name = "ledger/coa_accounts/account_list.html" @@ -1822,8 +1848,7 @@ def create_estimate(request): "unit_cost": instance.finances.cost_price, "unit_revenue": instance.finances.selling_price, "quantity": float(quantities), - "total_amount": instance.finances.total_vat - * int(quantities), + "total_amount": instance.finances.total_vat * int(quantities), } } @@ -1879,13 +1904,22 @@ class EstimateDetailView(LoginRequiredMixin, DetailView): def get_context_data(self, **kwargs): estimate = kwargs.get("object") + vat = models.VatRate.objects.filter(is_active=True).first() + if estimate.get_itemtxs_data(): - total = sum( - float( - models.Car.objects.get( + car_and_item_info = [ + { + "car": models.Car.objects.get(vin=x.item_model.name), + "total": models.Car.objects.get( vin=x.item_model.name - ).finances.total - ) + ).finances.selling_price + * Decimal(x.ce_quantity), + "itemmodel": x, + } + for x in estimate.get_itemtxs_data()[0].all() + ] + total = sum( + float(models.Car.objects.get(vin=x.item_model.name).finances.total) * float(x.ce_quantity) for x in estimate.get_itemtxs_data()[0].all() ) @@ -1895,14 +1929,26 @@ class EstimateDetailView(LoginRequiredMixin, DetailView): ).discount_amount for i in estimate.get_itemtxs_data()[0].all() ) - vat = models.VatRate.objects.filter(is_active=True).first() + additional_services = [] + for i in estimate.get_itemtxs_data()[0].all(): + cf = models.CarFinance.objects.get(car__vin=i.item_model.name) + if cf.additional_services.exists(): + additional_services.extend( + [ + {"name": x.name, "price": x.price} + for x in cf.additional_services.all() + ] + ) + grand_total = float(total) - float(discount_amount) - vat_amount = round(float(grand_total) * float(vat.vat_rate), 2) - + vat_amount = round(float(grand_total) * float(vat.rate), 2) + kwargs["vat_amount"] = vat_amount + kwargs["car_and_item_info"] = car_and_item_info kwargs["total"] = grand_total + vat_amount kwargs["discount_amount"] = discount_amount kwargs["vat"] = vat.rate + kwargs["additional_services"] = additional_services kwargs["invoice"] = ( InvoiceModel.objects.all().filter(ce_model=estimate).first() ) @@ -1930,14 +1976,51 @@ class EstimatePreviewView(LoginRequiredMixin, DetailView): def get_context_data(self, **kwargs): estimate = kwargs.get("object") + vat = models.VatRate.objects.filter(is_active=True).first() if estimate.get_itemtxs_data(): + car_and_item_info = [ + { + "car": models.Car.objects.get(vin=x.item_model.name), + "total": models.Car.objects.get( + vin=x.item_model.name + ).finances.selling_price + * Decimal(x.ce_quantity), + "itemmodel": x, + } + for x in estimate.get_itemtxs_data()[0].all() + ] total = sum( - x.ce_cost_estimate for x in estimate.get_itemtxs_data()[0].all() + float(models.Car.objects.get(vin=x.item_model.name).finances.total) + * float(x.ce_quantity) + for x in estimate.get_itemtxs_data()[0].all() ) - vat = models.VatRate.objects.filter(is_active=True).first() - kwargs["vat_amount"] = total * vat.vat_rate - kwargs["total"] = (total * vat.vat_rate) + total + discount_amount = sum( + models.CarFinance.objects.get( + car__vin=i.item_model.name + ).discount_amount + for i in estimate.get_itemtxs_data()[0].all() + ) + + additional_services = [] + + for i in estimate.get_itemtxs_data()[0].all(): + cf = models.CarFinance.objects.get(car__vin=i.item_model.name) + if cf.additional_services.exists(): + additional_services.extend( + [ + {"name": x.name, "price": x.price} + for x in cf.additional_services.all() + ] + ) + + grand_total = float(total) - float(discount_amount) + vat_amount = round(float(grand_total) * float(vat.rate), 2) + + kwargs["vat_amount"] = vat_amount + kwargs["total"] = grand_total + vat_amount kwargs["vat"] = vat.rate + kwargs["car_and_item_info"] = car_and_item_info + kwargs["additional_services"] = additional_services return super().get_context_data(**kwargs) @@ -1997,9 +2080,21 @@ class InvoiceDetailView(LoginRequiredMixin, DetailView): vat = models.VatRate.objects.filter(is_active=True).first() if invoice.get_itemtxs_data(): + car_and_item_info = [ + { + "car": models.Car.objects.get(vin=x.item_model.name), + "total": models.Car.objects.get( + vin=x.item_model.name + ).finances.selling_price + * Decimal(x.quantity), + "itemmodel": x, + } + for x in invoice.get_itemtxs_data()[0].all() + ] total = sum( - float(x.ce_revenue_estimate) * float(x.ce_quantity) - for x in invoice.ce_model.get_itemtxs_data()[0].all() + float(models.Car.objects.get(vin=x.item_model.name).finances.total) + * float(x.quantity) + for x in invoice.get_itemtxs_data()[0].all() ) discount_amount = sum( models.CarFinance.objects.get( @@ -2007,15 +2102,28 @@ class InvoiceDetailView(LoginRequiredMixin, DetailView): ).discount_amount for i in invoice.get_itemtxs_data()[0].all() ) - + + additional_services = [] + + for i in invoice.get_itemtxs_data()[0].all(): + cf = models.CarFinance.objects.get(car__vin=i.item_model.name) + if cf.additional_services.exists(): + additional_services.extend( + [ + {"name": x.name, "price": x.price} + for x in cf.additional_services.all() + ] + ) + grand_total = float(total) - float(discount_amount) - vat_amount = round(float(grand_total) * float(vat.vat_rate), 2) - - - kwargs["vat_amount"] = vat_amount + vat_amount = round(float(grand_total) * float(vat.rate), 2) + + kwargs["vat_amount"] = vat_amount kwargs["total"] = grand_total + vat_amount kwargs["discount_amount"] = discount_amount kwargs["vat"] = vat.rate + kwargs["car_and_item_info"] = car_and_item_info + kwargs["additional_services"] = additional_services kwargs["payments"] = JournalEntryModel.objects.filter( ledger=invoice.ledger ).all() @@ -2124,15 +2232,51 @@ class InvoicePreviewView(LoginRequiredMixin, DetailView): def get_context_data(self, **kwargs): invoice = kwargs.get("object") + vat = models.VatRate.objects.filter(is_active=True).first() if invoice.get_itemtxs_data(): + car_and_item_info = [ + { + "car": models.Car.objects.get(vin=x.item_model.name), + "total": models.Car.objects.get( + vin=x.item_model.name + ).finances.selling_price + * Decimal(x.ce_quantity), + "itemmodel": x, + } + for x in invoice.ce_model.get_itemtxs_data()[0].all() + ] total = sum( - x.unit_cost * x.quantity for x in invoice.get_itemtxs_data()[0].all() + float(models.Car.objects.get(vin=x.item_model.name).finances.total) + * float(x.ce_quantity) + for x in invoice.ce_model.get_itemtxs_data()[0].all() ) - total = int(total) - vat = models.VatRate.objects.filter(is_active=True).first() - kwargs["vat_amount"] = total * vat.vat_rate - kwargs["total"] = (total * vat.vat_rate) + total + discount_amount = sum( + models.CarFinance.objects.get( + car__vin=i.item_model.name + ).discount_amount + for i in invoice.ce_model.get_itemtxs_data()[0].all() + ) + + additional_services = [] + + for i in invoice.ce_model.get_itemtxs_data()[0].all(): + cf = models.CarFinance.objects.get(car__vin=i.item_model.name) + if cf.additional_services.exists(): + additional_services.extend( + [ + {"name": x.name, "price": x.price} + for x in cf.additional_services.all() + ] + ) + + grand_total = float(total) - float(discount_amount) + vat_amount = round(float(grand_total) * float(vat.rate), 2) + + kwargs["vat_amount"] = vat_amount + kwargs["total"] = grand_total + vat_amount kwargs["vat"] = vat.rate + kwargs["car_and_item_info"] = car_and_item_info + kwargs["additional_services"] = additional_services return super().get_context_data(**kwargs) @@ -2383,19 +2527,22 @@ def fetch_notifications(request): class ItemServiceCreateView(CreateView): - model = ItemModel - form_class = ServiceCreateForm + model = models.AdditionalServices + form_class = forms.AdditionalServiceForm template_name = "items/service/service_create.html" success_url = reverse_lazy("item_service_list") - def get_form_kwargs(self): - kwargs = super().get_form_kwargs() - kwargs["entity_slug"] = self.request.user.dealer.entity.slug - kwargs["user_model"] = self.request.user.dealer.entity.admin - return kwargs + # def get_form_kwargs(self): + # kwargs = super().get_form_kwargs() + # kwargs["entity_slug"] = self.request.user.dealer.entity.slug + # kwargs["user_model"] = self.request.user.dealer.entity.admin + # return kwargs def form_valid(self, form): - form.instance.entity = self.request.user.dealer.entity + vat = models.VatRate.objects.get(is_active=True) + form.instance.dealer = get_user_type(self.request.user.dealer) + if form.instance.taxable: + form.instance.price = (form.instance.price * vat.rate) + form.instance.price return super().form_valid(form) diff --git a/scripts/loaddata.py b/scripts/loaddata.py new file mode 100644 index 00000000..80717703 --- /dev/null +++ b/scripts/loaddata.py @@ -0,0 +1,154 @@ +import json +from tqdm import tqdm +from inventory.models import ( + CarMake, CarModel, CarSerie, CarTrim, CarEquipment, + CarSpecification, CarSpecificationValue, CarOption, CarOptionValue +) + +def run(): + with open("final_car_data.json", "r") as file: + data = json.load(file) + + # Step 1: Insert CarMake + for item in tqdm(data["car_make"], desc="Inserting CarMake"): + CarMake.objects.update_or_create( + id_car_make=item["id_car_make"], + defaults={ + "name": item["name"], + "arabic_name": item.get("arabic_name", ""), + "logo": item.get("Logo", ""), + "is_sa_import": item.get("is_sa_import", False), + } + ) + + # Step 2: Insert CarModel + for item in tqdm(data["car_model"], desc="Inserting CarModel"): + CarMake.objects.get(id_car_make=item["id_car_make"]) + CarModel.objects.update_or_create( + id_car_model=item["id_car_model"], + defaults={ + "id_car_make_id": item["id_car_make"], + "name": item["name"], + "arabic_name": item.get("arabic_name", ""), + } + ) + + # Step 3: Insert CarSerie + for item in tqdm(data["car_serie"], desc="Inserting CarSerie"): + CarModel.objects.get(id_car_model=item["id_car_model"]) + CarSerie.objects.update_or_create( + id_car_serie=item["id_car_serie"], + defaults={ + "id_car_model_id": item["id_car_model"], + "name": item["name"], + "arabic_name": item.get("arabic_name", ""), + "year_begin": item.get("year_begin"), + "year_end": item.get("year_end"), + "generation_name": item.get("generation_name", ""), + } + ) + + # Step 4: Insert CarTrim + for item in tqdm(data["car_trim"], desc="Inserting CarTrim"): + CarSerie.objects.get(id_car_serie=item["id_car_serie"]) + CarTrim.objects.update_or_create( + id_car_trim=item["id_car_trim"], + defaults={ + "id_car_serie_id": item["id_car_serie"], + "name": item["name"], + "arabic_name": item.get("arabic_name", ""), + "start_production_year": item["start_production_year"], + "end_production_year": item["end_production_year"], + } + ) + + # Step 5: Insert CarEquipment + for item in tqdm(data["car_equipment"], desc="Inserting CarEquipment"): + CarTrim.objects.get(id_car_trim=item["id_car_trim"]) + CarEquipment.objects.update_or_create( + id_car_equipment=item["id_car_equipment"], + defaults={ + "id_car_trim_id": item["id_car_trim"], + "name": item["name"], + "year_begin": item.get("year"), + } + ) + + # Step 6: Insert CarSpecification (Parent specifications first) + parent_specs = [item for item in data["car_specification"] if item["id_parent"] is None] + child_specs = [item for item in data["car_specification"] if item["id_parent"] is not None] + + for item in tqdm(parent_specs, desc="Inserting Parent CarSpecifications"): + CarSpecification.objects.update_or_create( + id_car_specification=item["id_car_specification"], + defaults={ + "name": item["name"], + "arabic_name": item.get("arabic_name", ""), + "id_parent_id": None + } + ) + + for item in tqdm(child_specs, desc="Inserting Child CarSpecifications"): + CarSpecification.objects.get(id_car_specification=item["id_parent"]) + CarSpecification.objects.update_or_create( + id_car_specification=item["id_car_specification"], + defaults={ + "name": item["name"], + "arabic_name": item.get("arabic_name", ""), + "id_parent_id": item["id_parent"] + } + ) + + # Step 7: Insert CarSpecificationValue + for item in tqdm(data["car_specification_value"], desc="Inserting CarSpecificationValue"): + CarTrim.objects.get(id_car_trim=item["id_car_trim"]) + CarSpecification.objects.get(id_car_specification=item["id_car_specification"]) + CarSpecificationValue.objects.update_or_create( + id_car_specification_value=item["id_car_specification_value"], + defaults={ + "id_car_trim_id": item["id_car_trim"], + "id_car_specification_id": item["id_car_specification"], + "value": item["value"], + "unit": item.get("unit", ""), + } + ) + + # Step 8: Insert CarOption (Parent options first) + parent_options = [item for item in data["car_option"] if item["id_parent"] is None] + child_options = [item for item in data["car_option"] if item["id_parent"] is not None] + + for item in tqdm(parent_options, desc="Inserting Parent CarOptions"): + CarOption.objects.update_or_create( + id_car_option=item["id_car_option"], + defaults={ + "name": item["name"], + "arabic_name": item.get("arabic_name", ""), + "id_parent_id": None + } + ) + + for item in tqdm(child_options, desc="Inserting Child CarOptions"): + CarOption.objects.get(id_car_option=item["id_parent"]) + CarOption.objects.update_or_create( + id_car_option=item["id_car_option"], + defaults={ + "name": item["name"], + "arabic_name": item.get("arabic_name", ""), + "id_parent_id": item["id_parent"] + } + ) + + # Step 9: Insert CarOptionValue + for item in tqdm(data["car_option_value"], desc="Inserting CarOptionValue"): + CarEquipment.objects.get(id_car_equipment=item["id_car_equipment"]) + CarOption.objects.get(id_car_option=item["id_car_option"]) + CarOptionValue.objects.update_or_create( + id_car_option_value=item["id_car_option_value"], + defaults={ + "id_car_option_id": item["id_car_option"], + "id_car_equipment_id": item["id_car_equipment"], + "is_base": item["is_base"], + } + ) + + print("Data population completed successfully.") \ No newline at end of file diff --git a/templates/inventory/car_detail.html b/templates/inventory/car_detail.html index f85a4bc4..dae49368 100644 --- a/templates/inventory/car_detail.html +++ b/templates/inventory/car_detail.html @@ -142,7 +142,7 @@ {% for service in car.finances.additional_services.all %} {{service.name}} - {{ service.default_amount }} + {{ service.price }} {% endfor %} {% endif %} diff --git a/templates/sales/estimates/estimate_detail.html b/templates/sales/estimates/estimate_detail.html index 79bcdbd6..2ca8f77d 100644 --- a/templates/sales/estimates/estimate_detail.html +++ b/templates/sales/estimates/estimate_detail.html @@ -126,30 +126,38 @@ - {% for item in estimate.get_itemtxs_data.0 %} + {% for item in car_and_item_info %} {{forloop.counter}} - {{item.item_model.name}} - {{item.ce_quantity}} - {{item.ce_unit_revenue_estimate}} - {{item.ce_revenue_estimate}} + {{item.car.id_car_model}} + {{item.itemmodel.ce_quantity}} + {{item.car.finances.selling_price}} + {{item.total}} {% endfor %} - {% trans "Vat" %} ({{vat}}%) - - {{vat_amount}} + {% trans "Discount Amount" %} + + - {{discount_amount}} - {% trans "Discount Amount" %} - - {{discount_amount}} + {% trans "Vat" %} ({{vat}}%) + + + {{vat_amount}} - {% trans "Grand Total" %} - + {% trans "Additional Services" %} + + {% for service in additional_services %} + + {{service.name}} - {{service.price}}
+ {% endfor %} + + + + {% trans "Grand Total" %} + {{total}} diff --git a/templates/sales/estimates/estimate_preview.html b/templates/sales/estimates/estimate_preview.html index d34cbbc6..9030caa9 100644 --- a/templates/sales/estimates/estimate_preview.html +++ b/templates/sales/estimates/estimate_preview.html @@ -277,12 +277,12 @@ - {% for item in estimate.get_itemtxs_data.0 %} + {% for item in car_and_item_info %} - {{ item.item_model.name }} - {{ item.ce_quantity }} - {{ item.ce_unit_cost_estimate }} - {{ item.ce_cost_estimate }} + {{ item.car.id_car_model }} + {{ item.itemmodel.ce_quantity }} + {{ item.car.finances.selling_price }} + {{ item.total }} {% endfor %} @@ -292,7 +292,12 @@

{% trans "VAT" %} ({{vat}}%): ${{vat_amount}}

-

{% trans "Additional Services" %}: $50.00

+

{% trans "Additional Services" %}: +
+ {% for service in additional_services %} + {{service.name}} - ${{service.price}}
+ {% endfor %} +

diff --git a/templates/sales/invoices/invoice_detail.html b/templates/sales/invoices/invoice_detail.html index 8d588856..ef207ad1 100644 --- a/templates/sales/invoices/invoice_detail.html +++ b/templates/sales/invoices/invoice_detail.html @@ -144,30 +144,38 @@ - {% for item in invoice.ce_model.get_itemtxs_data.0 %} + {% for item in car_and_item_info %} {{forloop.counter}} - {{item.item_model.name}} - {{item.ce_quantity}} - {{item.ce_unit_revenue_estimate}} - {{item.ce_revenue_estimate}} + {{item.car.id_car_model}} + {{item.itemmodel.quantity}} + {{item.car.finances.selling_price}} + {{item.total}} {% endfor %} - {% trans "VAT" %} ({{vat}}%) - - {{vat_amount}} + {% trans "Discount Amount" %} + + - {{discount_amount}} - {% trans "Discount Amount" %} - - {{discount_amount}} + {% trans "VAT" %} ({{vat}}%) + + + {{vat_amount}} - {% trans "Grand Total" %} + {% trans "Additional Services" %} + {% for service in additional_services %} + + {{service.name}} - {{service.price}}
+ {% endfor %} + + + + {% trans "Grand Total" %} + {{total}} diff --git a/templates/sales/invoices/invoice_preview.html b/templates/sales/invoices/invoice_preview.html index f1b18ae2..7808554e 100644 --- a/templates/sales/invoices/invoice_preview.html +++ b/templates/sales/invoices/invoice_preview.html @@ -196,12 +196,12 @@ - {% for item in invoice.get_itemtxs_data.0 %} + {% for item in car_and_item_info %} - {{ item.item_model.name }} - {{ item.quantity }} - {{ item.unit_cost }} - {{ item.total_amount }} + {{ item.car.id_car_model }} + {{ item.itemmodel.ce_quantity }} + {{ item.car.finances.selling_price }} + {{ item.total }} {% endfor %} @@ -211,7 +211,12 @@

VAT ({{vat}}%): ${{vat_amount}}

-

Additional Services: $50.00

+

Additional Services: +
+ {% for service in additional_services %} + {{service.name}} - ${{service.price}}
+ {% endfor %} +