From 9ff796cf098470ea425694e4e1dec5542b71b80a Mon Sep 17 00:00:00 2001 From: gitea Date: Mon, 6 Jan 2025 12:34:19 +0000 Subject: [PATCH] validate workflow --- inventory/models.py | 47 ++- inventory/signals.py | 111 +++---- inventory/views.py | 308 +++++++++++------- templates/inventory/car_detail.html | 6 +- .../sales/estimates/estimate_detail.html | 6 +- templates/sales/estimates/estimate_form.html | 4 +- .../sales/estimates/estimate_preview.html | 6 +- 7 files changed, 272 insertions(+), 216 deletions(-) diff --git a/inventory/models.py b/inventory/models.py index 5a1fae58..c112ad6d 100644 --- a/inventory/models.py +++ b/inventory/models.py @@ -38,18 +38,22 @@ class DealerUserManager(UserManager): return user - -UNIT_CHOICES = ( - ("Unit", _("Unit")), - ("Kg", _("Kg")), - ("L", _("L")), - ("m", _("m")), - ("cm", _("cm")), - ("m2", _("m2")), - ("m3", _("m3")), - ("m3", _("m3")), -) - +class UnitOfMeasure(models.TextChoices): + EACH = 'EA', 'Each' + PAIR = 'PR', 'Pair' + SET = 'SET', 'Set' + GALLON = 'GAL', 'Gallon' + LITER = 'L', 'Liter' + METER = 'M', 'Meter' + KILOGRAM = 'KG', 'Kilogram' + HOUR = 'HR', 'Hour' + BOX = 'BX', 'Box' + ROLL = 'RL', 'Roll' + PACKAGE = 'PKG', 'Package' + DOZEN = 'DZ', 'Dozen' + SQUARE_METER = 'SQ_M', 'Square Meter' + PIECE = 'PC', 'Piece' + BUNDLE = 'BDL', 'Bundle' class VatRate(models.Model): rate = models.DecimalField(max_digits=5, decimal_places=2, default=Decimal('0.15')) is_active = models.BooleanField(default=True) @@ -173,7 +177,7 @@ class AdditionalServices(models.Model, LocalizedNameMixin): description = models.TextField(verbose_name=_("Description")) price = models.DecimalField(max_digits=14, decimal_places=2, verbose_name=_("Price")) taxable = models.BooleanField(default=False, verbose_name=_("taxable")) - uom = models.CharField(max_length=10, choices=UNIT_CHOICES, verbose_name=_("Unit of Measurement")) + 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")) class Meta: @@ -334,8 +338,7 @@ class CarFinance(models.Model): # custom_card_fee = models.DecimalField(max_digits=14, decimal_places=2, verbose_name=_("Custom Card Fee"), # default=Decimal('0.00')) @property - def total(self): - vat = VatRate.objects.filter(is_active=True).first() + def total(self): total = 0 if self.additional_services.count() != 0: total_additional_services = sum(x.default_amount for x in self.additional_services.all()) @@ -344,9 +347,19 @@ class CarFinance(models.Model): total = self.selling_price if self.discount_amount != 0: total = total - self.discount_amount - total = (total * vat.vat_rate).quantize(Decimal('0.01')) + total return total - + + @property + def vat_amount(self): + vat = VatRate.objects.filter(is_active=True).first() + return (self.total * vat.vat_rate).quantize(Decimal('0.01')) + + @property + def total_vat(self): + return self.total + self.vat_amount + + + def __str__(self): return f"Car: {self.car}, Selling Price: {self.selling_price}" diff --git a/inventory/signals.py b/inventory/signals.py index 26bb4ecf..45a56640 100644 --- a/inventory/signals.py +++ b/inventory/signals.py @@ -44,20 +44,22 @@ User = get_user_model() # instance.save() -@receiver(post_save, sender=models.Car) + +# check with marwan +@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. """ try: if created: - if instance.user.dealer is None: + if instance.dealer is None: raise ValueError(f"Cannot create CarLocation for car {instance.vin}: dealer is missing.") - + models.CarLocation.objects.create( car=instance, - owner=instance.user.dealer, - showroom=instance.user.dealer, + owner=instance.dealer, + showroom=instance.dealer, description=f"Initial location set for car {instance.vin}." ) print("Car Location created") @@ -84,6 +86,7 @@ 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( @@ -103,7 +106,16 @@ 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 Cash Account + + try: + # Create unit of measures + for uom in models.UnitOfMeasure.choices: + entity.create_uom( + name=uom[0], + unit_abbr=uom[1]) + except Exception as e: + print(e) + # Create Cash Account asset_ca_cash = entity.create_account( coa_model=coa, code="1010", @@ -206,7 +218,15 @@ def create_ledger_entity(sender, instance, created, **kwargs): balance_type="debit", active=True, ) - + #Create Deferred Revenue Account + entity.create_account( + coa_model=coa, + code="2060", + role=roles.LIABILITY_CL_DEFERRED_REVENUE, + name=_("Deferred Revenue"), + balance_type="credit", + active=True, + ) # Create Vendor @receiver(post_save, sender=models.Vendor) @@ -260,65 +280,28 @@ def create_customer(sender, instance, created, **kwargs): # Create Item @receiver(post_save, sender=models.Car) def create_item_model(sender, instance, created, **kwargs): - # item_name = f"{instance.year} - {instance.id_car_make} - {instance.id_car_model} - {instance.id_car_trim}" - # uom_name = _("Car") - # unit_abbr = _("C") - - # uom, uom_created = UnitOfMeasureModel.objects.get_or_create( - # name=uom_name, - # unit_abbr=unit_abbr - # ) - - # if uom_created: - # print(f"UOM created: {uom_name}") - # else: - # print(f"Using existing UOM: {uom_name}") name = instance.dealer.name entity = EntityModel.objects.filter(name=name).first() - uom = entity.get_uom_all().first() - coa = entity.get_default_coa() - - entity.get_items_all().filter(item_id=instance.vin) - entity.create_item_product( - name=f"{instance.vin}", - item_type=ItemModel.ITEM_TYPE_OTHER, - uom_model=uom, - coa_model=coa - ) - entity.create_item_inventory( - name=f"{instance.vin}", - item_type=ItemModel.ITEM_TYPE_OTHER, - uom_model=uom, - coa_model=coa - ) - - # item = ItemModel.objects.create( - # entity=entity, - # uom=uom, - # name=item_name, - # item_role=ItemModelAbstract.ITEM_ROLE_INVENTORY, - # item_type=ItemModelAbstract.ITEM_TYPE_MATERIAL, - # item_id=instance.vin, - # sold_as_unit=True, - # inventory_received=1.00, - # inventory_received_value=0.00, - # inventory_account=inventory_account, - # for_inventory=True, - # is_product_or_service=True, - # cogs_account=cogs_account, - # earnings_account=earnings_account, - # is_active=True, - # additional_info={ - # "remarks": instance.remarks, - # "status": instance.status, - # "stock_type": instance.stock_type, - # "mileage": instance.mileage, - # }, - # ) - - # print(f"ItemModel {'created' if created else 'updated'} for Car: {item.name}") -# -# + if created: + coa = entity.get_default_coa() + uom = entity.get_uom_all().get(name=models.UnitOfMeasure.KILOGRAM.value) + + if not entity.get_items_all().filter(name=instance.vin).first(): + entity.create_item_product( + name=f"{instance.vin}", + item_type=ItemModel.ITEM_TYPE_OTHER, + uom_model=uom, + coa_model=coa + ) + entity.create_item_inventory( + name=f"{instance.vin}", + item_type=ItemModel.ITEM_TYPE_OTHER, + uom_model=uom, + coa_model=coa + ) + + + # # update price - CarFinance @receiver(post_save, sender=models.CarFinance) def update_item_model_cost(sender, instance, created, **kwargs): diff --git a/inventory/views.py b/inventory/views.py index b7101fdc..7c2cbc59 100644 --- a/inventory/views.py +++ b/inventory/views.py @@ -136,7 +136,7 @@ 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.set_password(password) user.save() @@ -150,11 +150,13 @@ def dealer_signup(request, *args, **kwargs): address=address, ) user = authenticate(request, username=username, password=password) - if user is not None: - return JsonResponse({"message": "User created successfully."}, status=200) + if user is not None: + return JsonResponse( + {"message": "User created successfully."}, status=200 + ) else: return JsonResponse({"error": "User creation failed."}, status=400) - + except Exception as e: return JsonResponse({"error": str(e)}, status=400) @@ -181,7 +183,6 @@ class TestView(TemplateView): template_name = "test.html" - class AccountingDashboard(LoginRequiredMixin, TemplateView): template_name = "dashboards/accounting.html" @@ -266,7 +267,9 @@ class AjaxHandlerView(LoginRequiredMixin, View): return JsonResponse({"error": _("VIN number exists")}, status=400) if not vin_no or len(vin_no.strip()) != 17: - return JsonResponse({"success": False, "error": "Invalid VIN number provided."}, status=400) + return JsonResponse( + {"success": False, "error": "Invalid VIN number provided."}, status=400 + ) vin_no = vin_no.strip() vin_data = {} @@ -492,25 +495,25 @@ def inventory_stats_view(request): except Exception as e: print(e) result = { - "total_cars": cars.count(), - "makes": [ - { - "make_id": make_data["make_id"], - "make_name": make_data["make_name"], - "total_cars": make_data["total_cars"], - "models": [ - { - "model_id": model_data["model_id"], - "model_name": model_data["model_name"], - "total_cars": model_data["total_cars"], - "trims": list(model_data["trims"].values()), - } - for model_data in make_data["models"].values() - ], - } - for make_data in inventory.values() - ], - } + "total_cars": cars.count(), + "makes": [ + { + "make_id": make_data["make_id"], + "make_name": make_data["make_name"], + "total_cars": make_data["total_cars"], + "models": [ + { + "model_id": model_data["model_id"], + "model_name": model_data["model_name"], + "total_cars": model_data["total_cars"], + "trims": list(model_data["trims"].values()), + } + for model_data in make_data["models"].values() + ], + } + for make_data in inventory.values() + ], + } return render(request, "inventory/inventory_stats.html", {"inventory": result}) @@ -644,6 +647,8 @@ def reserve_car_view(request, car_id): models.CarReservation.objects.create( car=car, reserved_by=request.user, reserved_until=reserved_until ) + car.status = models.CarStatusChoices.RESERVED + car.save() messages.success(request, _("Car reserved successfully.")) except Exception as e: messages.error(request, f"Error reserving car: {e}") @@ -737,9 +742,9 @@ class CustomerListView(LoginRequiredMixin, ListView): if query: customers = customers.filter( - Q(national_id__icontains=query) | - Q(first_name__icontains=query) | - Q(last_name__icontains=query) + Q(national_id__icontains=query) + | Q(first_name__icontains=query) + | Q(last_name__icontains=query) ) return customers @@ -1708,6 +1713,7 @@ class EstimateListView(LoginRequiredMixin, ListView): entity = self.request.user.dealer.entity return entity.get_estimates() + # class EstimateCreateView(LoginRequiredMixin, SuccessMessageMixin, CreateView): # model = EstimateModel # form_class = EstimateModelCreateForm @@ -1743,99 +1749,143 @@ class EstimateListView(LoginRequiredMixin, ListView): def create_estimate(request): dealer = get_user_type(request) entity = dealer.entity + if request.method == "POST": - try: - data = json.loads(request.body) - title = data.get("title") - customer_id = data.get("customer") - terms = data.get("terms") - customer = entity.get_customers().filter(pk=customer_id).first() + # try: + data = json.loads(request.body) + title = data.get("title") + customer_id = data.get("customer") + terms = data.get("terms") + customer = entity.get_customers().filter(pk=customer_id).first() - estimate = entity.create_estimate( - estimate_title=title, customer_model=customer, contract_terms=terms - ) - - items = data.get("item", []) - quantities = data.get("quantity", []) - - if items and quantities: - if isinstance(items, list): - items_list = [ - {"item_id": items[i], "quantity": quantities[i]} - for i in range(len(items)) - ] - items_txs = [] - for item in items_list: - item_instance = ItemModel.objects.get(pk=item.get("item_id")) - car_instace = models.Car.objects.get(vin=item_instance.name) - items_txs.append({ - "item_number": item_instance.item_number, - "quantity": float(item.get("quantity")), - "unit_cost": car_instace.finances.cost_price, - "unit_revenue": car_instace.finances.selling_price, - "total_amount": car_instace.finances.cost_price * int(item.get("quantity")), - }) - - estimate_itemtxs = { - item.get("item_number"): { - "unit_cost": item.get("unit_cost"), - "unit_revenue": item.get("unit_revenue"), - "quantity": item.get("quantity"), - "total_amount": item.get("total_amount"), - } - for item in items_txs - } - else: - item = entity.get_items_all().filter(pk=items).first() - - instance = models.Car.objects.get(vin=item.name) - estimate_itemtxs = { - item.item_number: { - "unit_cost": instance.finances.cost_price, - "unit_revenue": instance.finances.selling_price, - "quantity": float(quantities), - "total_amount": instance.finances.total * int(quantities), - } - } - estimate.migrate_itemtxs( - itemtxs=estimate_itemtxs, - commit=True, - operation=EstimateModel.ITEMIZE_APPEND, - ) - - if isinstance(items, list): - for item in items: - instance = models.Car.objects.get(vin=item) - instance.status = models.CarStatusChoices.RESERVED - instance.save() - else: - instance = models.Car.objects.get(vin=items) - instance.status = models.CarStatusChoices.RESERVED - instance.save() - url = reverse("estimate_detail", kwargs={"pk": estimate.pk}) + items = data.get("item", []) + quantities = data.get("quantity", []) + + if not all([items, quantities]): return JsonResponse( - { - "status": "success", - "message": "Estimate created successfully!", - "url": url, - }, - status=200, - ) - except Exception as e: - return JsonResponse( + {"status": "error", "message": "Items and Quantities are required"}, + status=400, + ) + if isinstance(quantities, list): + if '0' in quantities: + return JsonResponse( + { + "status": "error", + "message": "Quantity must be greater than zero" + }) + else: + if int(quantities) <= 0: + return JsonResponse( { "status": "error", - "message": f"An error occurred while processing the request.{e}", - }, - status=400, - ) + "message": "Quantity must be greater than zero" + } + ) + + estimate = entity.create_estimate( + estimate_title=title, customer_model=customer, contract_terms=terms + ) + if isinstance(items, list): + item_quantity_map = {} + for item, quantity in zip(items, quantities): + if item in item_quantity_map: + item_quantity_map[item] += int(quantity) + else: + item_quantity_map[item] = int(quantity) + item_list = list(item_quantity_map.keys()) + quantity_list = list(item_quantity_map.values()) + + items_list = [ + {"item_id": item_list[i], "quantity": quantity_list[i]} + for i in range(len(item_list)) + ] + items_txs = [] + for item in items_list: + item_instance = ItemModel.objects.get(pk=item.get("item_id")) + car_instance = models.Car.objects.get(vin=item_instance.name) + items_txs.append( + { + "item_number": item_instance.item_number, + "quantity": float(item.get("quantity")), + "unit_cost": car_instance.finances.cost_price, + "unit_revenue": car_instance.finances.selling_price, + "total_amount": car_instance.finances.cost_price + * int(item.get("quantity")), + } + ) + + estimate_itemtxs = { + item.get("item_number"): { + "unit_cost": item.get("unit_cost"), + "unit_revenue": item.get("unit_revenue"), + "quantity": item.get("quantity"), + "total_amount": item.get("total_amount"), + } + for item in items_txs + } + else: + item = entity.get_items_all().filter(pk=items).first() + instance = models.Car.objects.get(vin=item.name) + estimate_itemtxs = { + item.item_number: { + "unit_cost": instance.finances.cost_price, + "unit_revenue": instance.finances.selling_price, + "quantity": float(quantities), + "total_amount": instance.finances.total * int(quantities), + } + } + + estimate.migrate_itemtxs( + itemtxs=estimate_itemtxs, + commit=True, + operation=EstimateModel.ITEMIZE_APPEND, + ) + + if isinstance(items, list): + for item in items: + item_instance = ItemModel.objects.get(pk=item) + instance = models.Car.objects.get(vin=item_instance.name) + instance.status = models.CarStatusChoices.RESERVED + instance.save() + else: + item_instance = ItemModel.objects.get(pk=items) + instance = models.Car.objects.get(vin=item_instance.name) + instance.status = models.CarStatusChoices.RESERVED + instance.save() + + url = reverse("estimate_detail", kwargs={"pk": estimate.pk}) + return JsonResponse( + { + "status": "success", + "message": "Estimate created successfully!", + "url": f"{url}" + }) + + # except Exception as e: + # return JsonResponse( + # { + # "status": "error", + # "message": f"An error occurred while processing the request: {str(e)}", + # }, + # status=400, + # ) form = EstimateModelCreateForm(entity_slug=entity.slug, user_model=entity.admin) - car_list = models.Car.objects.filter(dealer=dealer,finances__selling_price__gt=0).exclude(status='reserved').values_list("vin", flat=True) + car_list = models.Car.objects.filter( + dealer=dealer, finances__selling_price__gt=0 + ).exclude(status="reserved") context = { "form": form, - "items": entity.get_items_all().filter(item_role=ItemModel.ITEM_ROLE_PRODUCT,name__in=[car_list]), + "items": [ + { + "car": x, + "product": entity.get_items_all() + .filter(item_role=ItemModel.ITEM_ROLE_PRODUCT, name=x.vin) + .first(), + } + for x in car_list + ], } return render(request, "sales/estimates/estimate_form.html", context) @@ -1849,12 +1899,17 @@ class EstimateDetailView(LoginRequiredMixin, DetailView): estimate = kwargs.get("object") if estimate.get_itemtxs_data(): total = sum( - x.ce_cost_estimate for x in estimate.get_itemtxs_data()[0].all() + (x.ce_revenue_estimate - models.Car.objects.get(vin=x.item_model.name).finances.discount_amount) for x in estimate.get_itemtxs_data()[0].all() ) vat = models.VatRate.objects.filter(is_active=True).first() - # vat = settings.VAT_RATE - kwargs["vat_amount"] = total * vat.vat_rate - kwargs["total"] = (total * vat.vat_rate) + total + + # Calculate VAT and total with 2 decimal places + vat_amount = round(total * vat.vat_rate, 2) # Round to 2 decimal places + grand_total = round((total * vat.vat_rate) + total, 2) # Round to 2 decimal places + + # Add values to the context + kwargs["vat_amount"] = vat_amount + kwargs["total"] = grand_total kwargs["vat"] = vat.rate kwargs["invoice"] = ( InvoiceModel.objects.all().filter(ce_model=estimate).first() @@ -1910,11 +1965,22 @@ def estimate_mark_as(request, pk): messages.error(request, "Estimate is not ready for approval") return redirect("estimate_detail", pk=estimate.pk) estimate.mark_as_approved() + messages.success(request, "Estimate approved successfully.") + for i in estimate.get_itemtxs_data()[0]: + car = models.Car.objects.get(vin=i.item_model.name) + car.status = models.CarStatusChoices.SOLD + car.save() elif mark == "rejected": if not estimate.can_cancel(): messages.error(request, "Estimate is not ready for rejection") return redirect("estimate_detail", pk=estimate.pk) estimate.mark_as_canceled() + messages.success(request, "Estimate canceled successfully.") + for i in estimate.get_itemtxs_data()[0]: + car = models.Car.objects.get(vin=i.item_model.name) + car.status = models.CarStatusChoices.AVAILABLE + car.save() + elif mark == "completed": if not estimate.can_complete(): messages.error(request, "Estimate is not ready for completion") @@ -2257,7 +2323,7 @@ def create_lead(request, pk): class LeadListView(ListView): model = models.Customer template_name = "crm/lead_list.html" - context_object_name ='customers' + context_object_name = "customers" def get_queryset(self): query = self.request.GET.get("q") @@ -2267,9 +2333,9 @@ class LeadListView(ListView): if query: customers = customers.filter( - Q(national_id__icontains=query) | - Q(first_name__icontains=query) | - Q(last_name__icontains=query) + Q(national_id__icontains=query) + | Q(first_name__icontains=query) + | Q(last_name__icontains=query) ) return customers @@ -2412,5 +2478,5 @@ class ItemServiceListView(ListView): class SubscriptionPlans(ListView): model = models.SubscriptionPlan - template_name = 'subscriptions/subscription_plan.html' - context_object_name = 'plans' + template_name = "subscriptions/subscription_plan.html" + context_object_name = "plans" diff --git a/templates/inventory/car_detail.html b/templates/inventory/car_detail.html index 84483915..f85a4bc4 100644 --- a/templates/inventory/car_detail.html +++ b/templates/inventory/car_detail.html @@ -122,12 +122,10 @@
{% if car.finances %} - {% if perms.inventory.view_carfinance %} - {% endif %} @@ -154,15 +152,13 @@ - + - - + + {% endfor %} @@ -187,7 +187,7 @@ // Run the function on page load - window.onload = calculateTotals; + //window.onload = calculateTotals; function setFormAction(action) { // Get the form element diff --git a/templates/sales/estimates/estimate_form.html b/templates/sales/estimates/estimate_form.html index d19e688c..12d41ad4 100644 --- a/templates/sales/estimates/estimate_form.html +++ b/templates/sales/estimates/estimate_form.html @@ -18,7 +18,7 @@
@@ -68,7 +68,7 @@
diff --git a/templates/sales/estimates/estimate_preview.html b/templates/sales/estimates/estimate_preview.html index 60be3c76..d34cbbc6 100644 --- a/templates/sales/estimates/estimate_preview.html +++ b/templates/sales/estimates/estimate_preview.html @@ -335,14 +335,12 @@ }); document.getElementById('confirmAccept').addEventListener('click', function () { - // Handle the accept action here - alert('Estimate Accepted'); + // Handle the accept action here $('#acceptModal').modal('hide'); }); document.getElementById('confirmReject').addEventListener('click', function () { - // Handle the reject action here - alert('Estimate Rejected'); + // Handle the reject action here $('#rejectModal').modal('hide'); });
{% trans "Cost Price"|capfirst %} {{ car.finances.cost_price }}
{% trans "Selling Price"|capfirst %} {{ car.finances.selling_price }}
{% trans "Total"|capfirst %}{{ car.finances.total }}{{ car.finances.total_vat }}
- {% if perms.inventory.change_carfinance %} {% trans "Edit" %} - {% endif %} {% else %}

{% trans "No finance details available." %}

diff --git a/templates/sales/estimates/estimate_detail.html b/templates/sales/estimates/estimate_detail.html index 068ca883..67ad84c2 100644 --- a/templates/sales/estimates/estimate_detail.html +++ b/templates/sales/estimates/estimate_detail.html @@ -131,8 +131,8 @@
{{forloop.counter}} {{item.item_model.name}} {{item.ce_quantity}}{{item.ce_unit_cost_estimate}}{{item.ce_total_amount}}{{item.ce_unit_revenue_estimate}}{{item.ce_revenue_estimate}}