diff --git a/car_inventory/asgi.py b/car_inventory/asgi.py index 206c8884..6eba063c 100644 --- a/car_inventory/asgi.py +++ b/car_inventory/asgi.py @@ -10,16 +10,31 @@ https://docs.djangoproject.com/en/5.0/howto/deployment/asgi/ # asgi.py import os -from django.core.asgi import get_asgi_application +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "car_inventory.settings") + +import django +django.setup() + + +from django.urls import path from channels.routing import ProtocolTypeRouter, URLRouter from channels.auth import AuthMiddlewareStack from api import routing +from inventory.notifications.sse import NotificationSSEApp +from django.urls import re_path +from django.core.asgi import get_asgi_application -os.environ.setdefault("DJANGO_SETTINGS_MODULE", "car_inventory.settings") - -application = ProtocolTypeRouter( - { - "http": get_asgi_application(), - "websocket": AuthMiddlewareStack(URLRouter(routing.websocket_urlpatterns)), - } -) +# application = ProtocolTypeRouter( +# { +# "http": get_asgi_application(), +# # "websocket": AuthMiddlewareStack(URLRouter(routing.websocket_urlpatterns)), +# } +# ) +application = ProtocolTypeRouter({ + "http": AuthMiddlewareStack( + URLRouter([ + path("sse/notifications/", NotificationSSEApp()), + re_path(r"", get_asgi_application()), # All other routes go to Django + ]) + ), +}) \ No newline at end of file diff --git a/car_inventory/urls.py b/car_inventory/urls.py index 906c1c91..1917a67b 100644 --- a/car_inventory/urls.py +++ b/car_inventory/urls.py @@ -4,9 +4,9 @@ from django.conf.urls.static import static from django.conf import settings from django.conf.urls.i18n import i18n_patterns from inventory import views -# from debug_toolbar.toolbar import debug_toolbar_urls - + # from debug_toolbar.toolbar import debug_toolbar_urls +from inventory.notifications.sse import NotificationSSEApp # import debug_toolbar from schema_graph.views import Schema # from two_factor.urls import urlpatterns as tf_urls @@ -30,6 +30,7 @@ urlpatterns += i18n_patterns( path("plans/", include("plans.urls")), path("schema/", Schema.as_view()), path("tours/", include("tours.urls")), + # path('', include(tf_urls)), ) diff --git a/haikalbot/migrations/0001_initial.py b/haikalbot/migrations/0001_initial.py index ea907f56..9f36e19a 100644 --- a/haikalbot/migrations/0001_initial.py +++ b/haikalbot/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 5.2.4 on 2025-07-15 11:38 +# Generated by Django 5.2.4 on 2025-07-22 08:37 import django.db.models.deletion import django.utils.timezone diff --git a/haikalbot/migrations/0002_initial.py b/haikalbot/migrations/0002_initial.py index af1f353e..cee8307b 100644 --- a/haikalbot/migrations/0002_initial.py +++ b/haikalbot/migrations/0002_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 5.2.4 on 2025-07-15 11:38 +# Generated by Django 5.2.4 on 2025-07-22 08:37 import django.db.models.deletion from django.db import migrations, models diff --git a/inventory/forms.py b/inventory/forms.py index e77ea00e..2292b217 100644 --- a/inventory/forms.py +++ b/inventory/forms.py @@ -430,25 +430,22 @@ class CarFinanceForm(forms.ModelForm): additional services associated with a car finance application. """ - additional_finances = forms.ModelMultipleChoiceField( - queryset=AdditionalServices.objects.all(), - widget=forms.CheckboxSelectMultiple(attrs={"class": "form-check-input"}), - required=False, - ) + # 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", - "additional_services", - ] + fields = ["cost_price","marked_price"] def save(self, commit=True): instance = super().save() - instance.additional_services.set(self.cleaned_data["additional_finances"]) + try: + instance.additional_services.set(self.cleaned_data["additional_finances"]) + except KeyError: + pass instance.save() return instance @@ -1362,27 +1359,27 @@ class SaleOrderForm(forms.ModelForm): class Meta: model = SaleOrder fields = [ - "customer", + # "customer", "expected_delivery_date", - "estimate", - "opportunity", + # "estimate", + # "opportunity", "comments", - "order_date", - "status", + # "order_date", + # "status", ] widgets = { "expected_delivery_date": forms.DateInput( attrs={"type": "date", "label": _("Expected Delivery Date")} ), - "order_date": forms.DateInput( - attrs={"type": "date", "label": _("Order Date")} - ), - "customer": forms.Select( - attrs={ - "class": "form-control", - "label": _("Customer"), - } - ), + # "order_date": forms.DateInput( + # attrs={"type": "date", "label": _("Order Date")} + # ), + # "customer": forms.Select( + # attrs={ + # "class": "form-control", + # "label": _("Customer"), + # } + # ), } diff --git a/inventory/management/commands/led.py b/inventory/management/commands/led.py new file mode 100644 index 00000000..0b75e47e --- /dev/null +++ b/inventory/management/commands/led.py @@ -0,0 +1,79 @@ +from decimal import Decimal +import random +from django.core.management.base import BaseCommand +from inventory.models import Car +from django_ledger.models import EntityModel,InvoiceModel,ItemModel +from inventory.utils import CarFinanceCalculator +from rich import print +class Command(BaseCommand): + help = "" + + def handle(self, *args, **options): + e = EntityModel.objects.first() + customer = e.get_customers().first() + admin = e.admin + # estimate = e.get_estimates().first() + # e.create_invoice(coa_model=e.get_default_coa(), customer_model=customer, terms="net_30") + i=InvoiceModel.objects.first() + + calc = CarFinanceCalculator(i) + data = calc.get_finance_data() + for car_data in data['cars']: + car = i.get_itemtxs_data()[0].filter( + item_model__car__vin=car_data['vin'] + ).first().item_model.car + print("car", car) + qty = Decimal(car_data['quantity']) + print("qty", qty) + + # amounts from calculator + net_car_price = Decimal(car_data['total']) # after discount + net_add_price = Decimal(data['total_additionals']) # per car or split however you want + vat_amount = Decimal(data['total_vat_amount']) * qty # prorate if multi-qty + # grand_total = net_car_price + net_add_price + vat_amount + grand_total = Decimal(data['grand_total']) + cost_total = Decimal(car_data['cost_price']) * qty + + print("net_car_price", net_car_price, "net_add_price", net_add_price, "vat_amount", vat_amount, "grand_total", grand_total, "cost_total", cost_total) + + # acc_cars = e.get_coa_accounts().get(name="Inventory (Cars)") + # acc_sales = e.get_coa_accounts().get(name="Car Sales") + # acc_tax = e.get_coa_accounts().get(name="Tax-Payable") + # acc_service = e.get_coa_accounts().get(name="After-Sales Services") + # uom = e.get_uom_all().get(name="Unit") + # car_item = e.get_items_products().get(name='2025 Ford Mustang GT') + # car_item = e.create_item_product( + # name='2025 Ford Mustang GT', + # item_type=ItemModel.ITEM_TYPE_MATERIAL, + # uom_model=uom, + # coa_model=e.get_default_coa(), + # ) + # car_item.earnings_account=acc_sales + # car_item.save() + # service_item = e.get_items_services().get(name='Extended Warranty 5yr/60k') + # service_item = e.create_item_service( + # name='Extended Warranty 5yr/60k', + # uom_model=uom, + # coa_model=e.get_default_coa(), + # ) + # service_item.earnings_account=acc_service + # service_item.save() + # i.invoice_items.add(car_item) + # i.invoice_items.add(service_item) + + # invoices_item_models = i.invoice_items.all() + # invoice_itemtxs = { + # im.item_number: { + # 'unit_cost': Decimal(1500), + # 'unit_revenue': Decimal(1500), + # 'quantity': 1, + # 'total_amount': Decimal(1500), + # } for im in invoices_item_models + # } + # print(invoice_itemtxs) + # invoice_itemtxs = i.migrate_itemtxs(itemtxs=invoice_itemtxs, + # commit=True, + # operation=InvoiceModel.ITEMIZE_APPEND) + # print(i.amount_due) + + # i.save() \ No newline at end of file diff --git a/inventory/management/commands/seed.py b/inventory/management/commands/seed.py new file mode 100644 index 00000000..3c575c06 --- /dev/null +++ b/inventory/management/commands/seed.py @@ -0,0 +1,117 @@ +# /management/commands/seed_dealership.py +import json, random, string, decimal +from django.core.management.base import BaseCommand +from django.test import Client +from django.contrib.auth import get_user_model +from plans.models import Order, PlanPricing, AbstractOrder, UserPlan, BillingInfo,Plan +from inventory.tasks import create_user_dealer +from inventory import models # adjust import to your app +from django_q.tasks import async_task + +User = get_user_model() + +class Command(BaseCommand): + help = "Seed a full dealership via the real signup & downstream views" + + def add_arguments(self, parser): + parser.add_argument('--count', type=int, default=1, help='Number of dealers to seed') + + def handle(self, *args, **opts): + count = opts['count'] + client = Client() # lives inside management command + + for n in range(1, 10): + self.stdout.write(f"🚗 Seeding dealer #{n}") + self._create_dealer(client, n) + # self._create_cars(client, n) + # self._create_customers_and_sales(client, n) + self.stdout.write(self.style.SUCCESS(f"✅ Dealer #{n} ready")) + + # ---------------------------------------------------------- + # 1. Sign-up via the real view + # ---------------------------------------------------------- + def _create_dealer(self, client, n): + payload = { + "email": f"dealer{n}@example.com", + "password": "Password123", + "confirm_password": "Password123", + "name": f"Dealer #{n}", + "arabic_name": f"تاجر {n}", + "phone_number": "+96651234567", + "crn": f"CRN{n}000", + "vrn": f"VRN{n}000", + "address": f"Street {n}, Riyadh", + } + + dealer = create_user_dealer(payload['email'], payload['password'], payload['name'], payload['arabic_name'], payload['phone_number'], payload['crn'], payload['vrn'], payload['address']) + user = dealer.user + self._assign_random_plan(user) + self._services(dealer) + + # resp = client.post( + # "/en/signup/", # adjust URL if necessary + # data=json.dumps(payload), + # content_type="application/json", + # ) + # if resp.status_code != 200: + # raise Exception(f"Signup failed: {resp.content}") + + # # Log in + client.login(email=payload["email"], password=payload["password"]) + + return payload["email"] + + def _assign_random_plan(self,user): + """ + Pick a random Plan and create + initialize a UserPlan for the user. + """ + plans = Plan.objects.all() + if not plans.exists(): + raise ValueError("No plans found – please create at least one Plan record.") + + plan = random.choice(plans) + + user_plan, created = UserPlan.objects.get_or_create( + user=user, + defaults={'plan': plan, 'active': True} + ) + if created: + user_plan.initialize() + return user_plan + + def _services(self,dealer): + additional_services = [ + { + "name": "Vehicle registration transfer assistance", + "arabic_name": "مساعدة في نقل ملكية السيارة", + "price": decimal.Decimal(random.randrange(100, 1000)), + "description": "This is service 1", + }, + { + "name": "Paperwork collection", + "arabic_name": "جمع الأوراق", + "price": decimal.Decimal(random.randrange(100, 1000)), + "description": "This is service 2", + }, + { + "name": "Inspection and test drives", + "arabic_name": "فحص وقيادة تجريبية", + "price": decimal.Decimal(random.randrange(100, 1000)), + "description": "This is service 3", + }, + { + "name": "Shipping and transportation", + "arabic_name": "شحن ونقل", + "price": decimal.Decimal(random.randrange(100, 1000)), + "description": "This is service 4", + }, + ] + + for additional_service in additional_services: + models.AdditionalServices.objects.create( + name=additional_service["name"], + arabic_name=additional_service["arabic_name"], + price=additional_service["price"], + description=additional_service["description"], + dealer=dealer, + ) diff --git a/inventory/management/commands/seed1.py b/inventory/management/commands/seed1.py new file mode 100644 index 00000000..d40198f1 --- /dev/null +++ b/inventory/management/commands/seed1.py @@ -0,0 +1,242 @@ +import datetime +from time import sleep +import json, random, string, decimal +from django.core.management.base import BaseCommand +from django.test import Client +from django.contrib.auth import get_user_model +from plans.models import Order, PlanPricing, AbstractOrder, UserPlan, BillingInfo,Plan +from inventory.services import decodevin +from inventory.tasks import create_user_dealer +from inventory.models import AdditionalServices, Car, CarColors, CarFinance, CarMake, CustomGroup, Customer, Dealer, ExteriorColors, InteriorColors, Lead, UnitOfMeasure,Vendor,Staff +from django_ledger.models import PurchaseOrderModel,ItemTransactionModel,ItemModel,EntityModel +from django_q.tasks import async_task +from faker import Faker +from appointment.models import Appointment, AppointmentRequest, Service, StaffMember + +User = get_user_model() +fake = Faker() + +class Command(BaseCommand): + help = "Seed a full dealership via the real signup & downstream views" + + def add_arguments(self, parser): + parser.add_argument('--count', type=int, default=1, help='Number of dealers to seed') + + def handle(self, *args, **opts): + dealers = Dealer.objects.all() + + for dealer in dealers: + self._create_random_po(dealer) + self._create_random_vendors(dealer) + # self._create_random_staff(dealer) + # self._create_random_cars(dealer) + self._create_random_customers(dealer) + # self._create_randome_services(dealer) + + + # dealer = Dealer.objects.get(name="Dealer #6") + # coa_model = dealer.entity.get_default_coa() + # inventory_account = dealer.entity.get_all_accounts().get(name="Inventory (Cars)") + # uom = dealer.entity.get_uom_all().get(name="Unit") + # item = dealer.entity.create_item_inventory(coa_model=coa_model,inventory_account=inventory_account,uom_model=uom,item_type=ItemModel.ITEM_TYPE_MATERIAL, name=f"Test item {random.randint(1,9999)}") + # item = ItemTransactionModel.objects.create(item_name=f"Test item {random.randint(1,9999)}", entity=dealer.entity) + # po = PurchaseOrderModel.objects.first() + + self.stdout.write(self.style.SUCCESS(f"✅ PO created for {dealers}")) + + def _create_random_po(self, dealer): + for i in range(random.randint(1,70)): + try: + e: EntityModel = dealer.entity + e.create_purchase_order(po_title=f"Test PO {random.randint(1,9999)}-{i}") + except Exception as e: + pass + + def _create_random_vendors(self, dealer): + for i in range(random.randint(1,50)): + try: + name = fake.name() + n = random.randint(1,9999) + phone = f"05678{random.randint(0,9)}{random.randint(0,9)}{random.randint(0,9)}{random.randint(0,9)}{random.randint(0,9)}" + Vendor.objects.create(dealer=dealer, name=f"{name}{n}", arabic_name=f"{name}{n}", email=f"{name}{n}@tenhal.sa", phone_number=phone,crn=f"CRN {n}", vrn=f"VRN {n}", address=f"Address {fake.address()}",contact_person=f"Contact Person {name}{n}") + except Exception as e: + pass + + def _create_random_staff(self, dealer): + for i in range(5): + name = f"{fake.name()}{i}" + email = fake.email() + password = f"{fake.password()}{i}" + user = User.objects.create_user(username=email, email=email, password=password) + user.is_staff = True + user.save() + + staff_member = StaffMember.objects.create(user=user) + services = Service.objects.all() + for service in services: + staff_member.services_offered.add(service) + + staff = Staff.objects.create(dealer=dealer,staff_member=staff_member,name=name,arabic_name=name,phone_number=fake.phone_number(),active=True) + + groups = CustomGroup.objects.filter(dealer=dealer) + random_group = random.choice(list(groups)) + staff.add_group(random_group.group) + # for i in range(random.randint(1,15)): + # n = random.randint(1,9999) + # phone = f"05678{random.randint(1,9999)}{random.randint(1,9999)}{random.randint(1,9999)}{random.randint(1,9999)}{random.randint(1,9999)}" + # Vendor.objects.create(dealer=dealer, name=f"{fake.name}", arabic_name=f"{fake.first_name_female()} {fake.last_name_female()}", email=f"vendor{n}@test.com", phone_number=phone,crn=f"CRN {n}", vrn=f"VRN {n}", address=f"Address {fake.address()}",contact_person=f"Contact Person {n}") + + def _create_random_cars(self,dealer): + vendors = Vendor.objects.filter(dealer=dealer).all() + + vin_list = [ + "1B3ES56C13D120225", + "1GB4KYC86FF131536", + "1HSHXAHR15J136217", + "1G1ZT52845F231124", + "1J4GK48K43W721617", + "JTDBE32K430163717", + "1J4FA69S05P331572", + "2FMGK5D86EBD28496", + "KNADE243696530337", + "1N4AL21EX8N499928", + "1N4AL21E49N400571", + "1G2NW12E54C145398", + ] + for vin in vin_list: + try: + for _ in range(random.randint(1,2)): + vin = f"{vin[:-4]}{random.randint(0, 9)}{random.randint(0, 9)}{random.randint(0, 9)}{random.randint(0, 9)}" + result = decodevin(vin) + make = CarMake.objects.get(name=result["maker"]) + model = make.carmodel_set.filter(name__contains=result["model"]).first() + if not model or model == "": + model = random.choice(make.carmodel_set.all()) + year = result["modelYear"] + serie = random.choice(model.carserie_set.all()) + trim = random.choice(serie.cartrim_set.all()) + vendor = random.choice(vendors) + print(make, model, serie, trim, vendor,vin) + car = Car.objects.create( + vin=vin, + id_car_make=make, + id_car_model=model, + id_car_serie=serie, + id_car_trim=trim, + vendor=vendor, + year=(int(year) or 2025), + receiving_date=datetime.datetime.now(), + dealer=dealer, + mileage=0, + ) + print(car) + CarFinance.objects.create( + car=car, cost_price=random.randint(10000, 100000), selling_price=0,marked_price=random.randint(10000, 100000)+random.randint(2000, 7000) + ) + CarColors.objects.create( + car=car, + interior=random.choice(InteriorColors.objects.all()), + exterior=random.choice(ExteriorColors.objects.all()), + ) + print(make, model, serie, trim) + except Exception as e: + print(e) + + def _create_random_customers(self,dealer): + for i in range(random.randint(1,60)): + try: + c = Customer( + dealer=dealer, + title="MR", + first_name=fake.name(), + last_name=fake.last_name(), + gender="m", + email=fake.email(), + phone_number=fake.phone_number(), + address=fake.address(), + national_id=random.randint(1000000000, 9999999999), + ) + c.create_user_model() + c.create_customer_model() + c.save() + except Exception as e: + pass + + def _create_randome_services(self,dealer): + additional_services = [ + { + "name": "Vehicle registration transfer assistance", + "arabic_name": "مساعدة في نقل ملكية السيارة", + "price": decimal.Decimal(random.randrange(100, 1000)), + "description": "This is service 1", + }, + { + "name": "Paperwork collection", + "arabic_name": "جمع الأوراق", + "price": decimal.Decimal(random.randrange(100, 1000)), + "description": "This is service 2", + }, + { + "name": "Inspection and test drives", + "arabic_name": "فحص وقيادة تجريبية", + "price": decimal.Decimal(random.randrange(100, 1000)), + "description": "This is service 3", + }, + { + "name": "Shipping and transportation", + "arabic_name": "شحن ونقل", + "price": decimal.Decimal(random.randrange(100, 1000)), + "description": "This is service 4", + }, + ] + uom = UnitOfMeasure.EACH + for additional_service in additional_services: + AdditionalServices.objects.create( + name=additional_service["name"], + arabic_name=additional_service["arabic_name"], + price=additional_service["price"], + description=additional_service["description"], + dealer=dealer, + uom=uom + ) + + + def _create_random_lead(self,dealer): + for i in range(random.randint(1,60)): + try: + first_name = fake.name() + last_name = fake.last_name() + email = fake.email() + staff = random.choice(Staff.objects.filter(dealer=dealer)) + + make = random.choice(CarMake.objects.all()) + model = random.choice(make.carmodel_set.all()) + lead = Lead.objects.create( + dealer=dealer, + first_name=first_name, + last_name=last_name, + email=email, + address=fake.address(), + lead_type="customer", + id_car_make=make, + id_car_model=model, + source="website", + channel="website", + staff=staff + ) + c = Customer( + dealer=dealer, + title="MR", + first_name=fake.name(), + last_name=fake.last_name(), + gender="m", + email=fake.email(), + phone_number=fake.phone_number(), + address=fake.address(), + national_id=random.randint(1000000000, 9999999999), + ) + c.create_user_model() + c.create_customer_model() + c.save() + except Exception as e: + pass \ No newline at end of file diff --git a/inventory/middleware.py b/inventory/middleware.py index a7bef21b..dda82b5f 100644 --- a/inventory/middleware.py +++ b/inventory/middleware.py @@ -1,4 +1,5 @@ import logging +import time # from django.http import Http404, HttpResponseForbidden # from django.shortcuts import redirect @@ -44,33 +45,33 @@ logger = logging.getLogger("user_activity") # return request.META.get("REMOTE_ADDR") -class InjectParamsMiddleware: - """ - Middleware to add processed user-related parameters to the request object. +# class InjectParamsMiddleware: +# """ +# Middleware to add processed user-related parameters to the request object. - This middleware processes incoming requests to extract and enhance user - information, specifically linking user context such as `dealer` to the - request. It allows subsequent views and middlewares to access these enriched - request parameters with ease. +# This middleware processes incoming requests to extract and enhance user +# information, specifically linking user context such as `dealer` to the +# request. It allows subsequent views and middlewares to access these enriched +# request parameters with ease. - :ivar get_response: The callable to get the next middleware or view response. - :type get_response: Callable - """ +# :ivar get_response: The callable to get the next middleware or view response. +# :type get_response: Callable +# """ - def __init__(self, get_response): - self.get_response = get_response +# def __init__(self, get_response): +# self.get_response = get_response - def __call__(self, request): - try: - if request.user.is_authenticated: - request.dealer = get_user_type(request) - request.entity = request.dealer.entity - else: - request.dealer = None - except Exception: - pass - response = self.get_response(request) - return response +# def __call__(self, request): +# try: +# if request.user.is_authenticated: +# request.dealer = get_user_type(request) +# request.entity = request.dealer.entity +# else: +# request.dealer = None +# except Exception: +# pass +# response = self.get_response(request) +# return response class InjectDealerMiddleware: @@ -93,6 +94,7 @@ class InjectDealerMiddleware: def __call__(self, request): try: + start = time.time() if request.user.is_authenticated: request.is_dealer = False request.is_staff = False @@ -103,6 +105,7 @@ class InjectDealerMiddleware: if hasattr(request.user, "dealer"): request.is_dealer = True request.dealer = request.user.dealer + elif hasattr(request.user, "staffmember"): request.is_staff = True request.staff = request.user.staffmember.staff @@ -120,6 +123,7 @@ class InjectDealerMiddleware: request.is_inventory = True request.entity = request.dealer.entity request.admin = request.dealer.entity.admin + print("\033[92m⏱ Middleware time:", time.time() - start, "\033[0m") except Exception: pass response = self.get_response(request) diff --git a/inventory/migrations/0001_initial.py b/inventory/migrations/0001_initial.py index 68dd3af2..daaff725 100644 --- a/inventory/migrations/0001_initial.py +++ b/inventory/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 5.2.4 on 2025-07-15 11:38 +# Generated by Django 5.2.4 on 2025-07-22 08:37 import datetime import django.core.serializers.json @@ -48,7 +48,7 @@ class Migration(migrations.Migration): ('name', models.CharField(blank=True, max_length=255, null=True)), ('slug', models.SlugField(blank=True, max_length=255, null=True, unique=True)), ('arabic_name', models.CharField(blank=True, max_length=255, null=True)), - ('logo', models.ImageField(blank=True, null=True, upload_to='car_make', verbose_name='logo')), + ('logo', models.ImageField(blank=True, default='user-logo.png', null=True, upload_to='car_make', verbose_name='logo')), ('is_sa_import', models.BooleanField(default=False)), ('car_type', models.SmallIntegerField(blank=True, choices=[(1, 'Car'), (2, 'Light Commercial'), (3, 'Heavy-Duty Tractors'), (4, 'Trailers'), (5, 'Medium Trucks'), (6, 'Buses'), (20, 'Motorcycles'), (21, 'Buggy'), (22, 'Moto ATV'), (23, 'Scooters'), (24, 'Karting'), (25, 'ATV'), (26, 'Snowmobiles')], null=True)), ], @@ -253,7 +253,7 @@ class Migration(migrations.Migration): ('name', models.CharField(max_length=255, verbose_name='English Name')), ('phone_number', phonenumber_field.modelfields.PhoneNumberField(max_length=128, region='SA', verbose_name='Phone Number')), ('address', models.CharField(blank=True, max_length=200, null=True, verbose_name='Address')), - ('logo', models.ImageField(blank=True, default='logo.png', null=True, upload_to='logos/users', verbose_name='Logo')), + ('logo', models.ImageField(blank=True, default='user-logo.png', null=True, upload_to='logos/users', verbose_name='Logo')), ('joined_at', models.DateTimeField(auto_now_add=True, verbose_name='Joined At')), ('updated_at', models.DateTimeField(auto_now=True, verbose_name='Updated At')), ('slug', models.SlugField(blank=True, max_length=255, null=True, unique=True)), @@ -566,7 +566,7 @@ class Migration(migrations.Migration): ('email', models.EmailField(max_length=254, verbose_name='Email')), ('phone_number', phonenumber_field.modelfields.PhoneNumberField(max_length=128, region='SA', verbose_name='Phone Number')), ('address', models.CharField(blank=True, max_length=200, null=True, verbose_name='Address')), - ('logo', models.ImageField(blank=True, null=True, upload_to='logos', verbose_name='Logo')), + ('logo', models.ImageField(blank=True, default='user-logo.png', null=True, upload_to='logos', verbose_name='Logo')), ('active', models.BooleanField(default=True, verbose_name='Active')), ('created', models.DateTimeField(auto_now_add=True, verbose_name='Created')), ('updated', models.DateTimeField(auto_now=True, verbose_name='Updated')), @@ -772,7 +772,7 @@ class Migration(migrations.Migration): ('phone_number', phonenumber_field.modelfields.PhoneNumberField(max_length=128, region='SA', verbose_name='Phone Number')), ('staff_type', models.CharField(choices=[('inventory', 'Inventory'), ('accountant', 'Accountant'), ('sales', 'Sales')], max_length=255, verbose_name='Staff Type')), ('address', models.CharField(blank=True, max_length=200, null=True, verbose_name='Address')), - ('logo', models.ImageField(blank=True, null=True, upload_to='logos/staff', verbose_name='Image')), + ('logo', models.ImageField(blank=True, default='user-logo.png', null=True, upload_to='logos/staff', verbose_name='Image')), ('active', models.BooleanField(default=True, verbose_name='Active')), ('created', models.DateTimeField(auto_now_add=True, verbose_name='Created')), ('updated', models.DateTimeField(auto_now=True, verbose_name='Updated')), @@ -872,7 +872,7 @@ class Migration(migrations.Migration): ('phone_number', phonenumber_field.modelfields.PhoneNumberField(max_length=128, region='SA', verbose_name='Phone Number')), ('email', models.EmailField(max_length=255, verbose_name='Email Address')), ('address', models.CharField(max_length=200, verbose_name='Address')), - ('logo', models.ImageField(blank=True, null=True, upload_to='logos/vendors', verbose_name='Logo')), + ('logo', models.ImageField(blank=True, default='user-logo.png', null=True, upload_to='logos/vendors', verbose_name='Logo')), ('active', models.BooleanField(default=True, verbose_name='Active')), ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')), ('slug', models.SlugField(blank=True, max_length=255, null=True, unique=True, verbose_name='Slug')), diff --git a/inventory/models.py b/inventory/models.py index df0353d7..c7a5e7c6 100644 --- a/inventory/models.py +++ b/inventory/models.py @@ -223,7 +223,7 @@ class CarMake(models.Model, LocalizedNameMixin): name = models.CharField(max_length=255, blank=True, null=True) slug = models.SlugField(max_length=255, unique=True, blank=True, null=True) arabic_name = models.CharField(max_length=255, blank=True, null=True) - logo = models.ImageField(_("logo"), upload_to="car_make", blank=True, null=True) + logo = models.ImageField(_("logo"), upload_to="car_make", blank=True, null=True,default="user-logo.png") is_sa_import = models.BooleanField(default=False) car_type = models.SmallIntegerField(choices=CarType.choices, blank=True, null=True) @@ -719,6 +719,12 @@ class Car(Base): return active_reservations.exists() @property + def logo(self): + return getattr(self.id_car_make, "logo", "") + @property + def additional_services(self): + return self.finances.additional_services.all() + @property def ready(self): try: return all( @@ -1174,7 +1180,13 @@ class Dealer(models.Model, LocalizedNameMixin): blank=True, null=True, verbose_name=_("Logo"), - default="logo.png", + default="user-logo.png", + ) + thumbnail = ImageSpecField( + source="logo", + processors=[ResizeToFill(40, 40)], + format="WEBP", + options={"quality": 80}, ) entity = models.ForeignKey( EntityModel, on_delete=models.SET_NULL, null=True, blank=True @@ -1272,7 +1284,7 @@ class Staff(models.Model, LocalizedNameMixin): max_length=200, blank=True, null=True, verbose_name=_("Address") ) logo = models.ImageField( - upload_to="logos/staff", blank=True, null=True, verbose_name=_("Image") + upload_to="logos/staff", blank=True, null=True, verbose_name=_("Image"),default="user-logo.png" ) thumbnail = ImageSpecField( source="logo", @@ -1497,6 +1509,12 @@ class Customer(models.Model): image = models.ImageField( upload_to="customers/", blank=True, null=True, verbose_name=_("Image") ) + thumbnail = ImageSpecField( + source="image", + processors=[ResizeToFill(40, 40)], + format="WEBP", + options={"quality": 80}, + ) created = models.DateTimeField(auto_now_add=True, verbose_name=_("Created")) updated = models.DateTimeField(auto_now=True, verbose_name=_("Updated")) slug = models.SlugField( @@ -1639,7 +1657,13 @@ class Organization(models.Model, LocalizedNameMixin): max_length=200, blank=True, null=True, verbose_name=_("Address") ) logo = models.ImageField( - upload_to="logos", blank=True, null=True, verbose_name=_("Logo") + upload_to="logos", blank=True, null=True, verbose_name=_("Logo"),default="user-logo.png" + ) + thumbnail = ImageSpecField( + source="logo", + processors=[ResizeToFill(40, 40)], + format="WEBP", + options={"quality": 80}, ) active = models.BooleanField(default=True, verbose_name=_("Active")) created = models.DateTimeField(auto_now_add=True, verbose_name=_("Created")) @@ -2453,7 +2477,13 @@ class Vendor(models.Model, LocalizedNameMixin): email = models.EmailField(max_length=255, verbose_name=_("Email Address")) address = models.CharField(max_length=200, verbose_name=_("Address")) logo = models.ImageField( - upload_to="logos/vendors", blank=True, null=True, verbose_name=_("Logo") + upload_to="logos/vendors", blank=True, null=True, verbose_name=_("Logo"),default="user-logo.png" + ) + thumbnail = ImageSpecField( + source="logo", + processors=[ResizeToFill(40, 40)], + format="WEBP", + options={"quality": 80}, ) active = models.BooleanField(default=True, verbose_name=_("Active")) created_at = models.DateTimeField(auto_now_add=True, verbose_name=_("Created At")) @@ -2776,7 +2806,7 @@ class SaleOrder(models.Model): if self.invoice: # Check if get_itemtxs_data returns data before proceeding # You might want to handle what get_itemtxs_data returns if it can be empty - item_data = self.invoice.get_itemtxs_data() + item_data = self.estimate.get_itemtxs_data()[0] if item_data: return item_data return [] # Return an empty list if no invoice or no item data @@ -3301,7 +3331,7 @@ class ExtraInfo(models.Model): return f"ExtraInfo for {self.content_object} ({self.content_type})" @classmethod - def get_sale_orders(cls, staff=None, is_dealer=False): + def get_sale_orders(cls, staff=None, is_dealer=False,dealer=None): if not staff and not is_dealer: return [] @@ -3310,12 +3340,18 @@ class ExtraInfo(models.Model): if is_dealer: qs = cls.objects.filter( + dealer=dealer, content_type=content_type, related_content_type=related_content_type, related_object_id__isnull=False, - ) + ).union(cls.objects.filter( + dealer=dealer, + content_type=ContentType.objects.get_for_model(EstimateModel), + related_content_type=ContentType.objects.get_for_model(User), + )) else: qs = cls.objects.filter( + dealer=dealer, content_type=content_type, related_content_type=related_content_type, related_object_id=staff.pk, @@ -3330,7 +3366,7 @@ class ExtraInfo(models.Model): ] @classmethod - def get_invoices(cls, staff=None, is_dealer=False): + def get_invoices(cls, staff=None, is_dealer=False,dealer=None): if not staff and not is_dealer: return [] @@ -3339,17 +3375,22 @@ class ExtraInfo(models.Model): if is_dealer: qs = cls.objects.filter( + dealer=dealer, content_type=content_type, related_content_type=related_content_type, related_object_id__isnull=False, - ) + ).union(cls.objects.filter( + dealer=dealer, + content_type=content_type, + related_content_type=ContentType.objects.get_for_model(User), + )) else: qs = cls.objects.filter( + dealer=dealer, content_type=content_type, related_content_type=related_content_type, related_object_id=staff.pk, ) - print(qs[0].content_object.invoicemodel_set.first()) return [ x.content_object.invoicemodel_set.first() for x in qs diff --git a/inventory/notifications/__init__.py b/inventory/notifications/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/inventory/notifications/sse.py b/inventory/notifications/sse.py new file mode 100644 index 00000000..83c32205 --- /dev/null +++ b/inventory/notifications/sse.py @@ -0,0 +1,205 @@ +# import json +# from django.contrib.auth.models import AnonymousUser +# from django.contrib.auth import get_user_model +# from django.db import close_old_connections +# from urllib.parse import parse_qs +# from channels.db import database_sync_to_async +# from inventory.models import Notification +# import asyncio + +# @database_sync_to_async +# def get_notifications(user, last_id): +# return Notification.objects.filter( +# user=user, id__gt=last_id, is_read=False +# ).order_by("created") + +# class NotificationSSEApp: +# async def __call__(self, scope, receive, send): +# if scope["type"] != "http": +# return + +# query_string = parse_qs(scope["query_string"].decode()) +# last_id = int(query_string.get("last_id", [0])[0]) + +# # Get user from scope if using AuthMiddlewareStack +# user = scope.get("user", AnonymousUser()) +# if not user.is_authenticated: +# await send({ +# "type": "http.response.start", +# "status": 403, +# "headers": [(b"content-type", b"text/plain")], +# }) +# await send({ +# "type": "http.response.body", +# "body": b"Unauthorized", +# }) +# return + +# await send({ +# "type": "http.response.start", +# "status": 200, +# "headers": [ +# (b"content-type", b"text/event-stream"), +# (b"cache-control", b"no-cache"), +# (b"x-accel-buffering", b"no"), +# ] +# }) + +# try: +# while True: +# close_old_connections() + +# notifications = await get_notifications(user, last_id) +# for notification in notifications: +# data = { +# "id": notification.id, +# "message": notification.message, +# "created": notification.created.isoformat(), +# "is_read": notification.is_read, +# } + +# event_str = ( +# f"id: {notification.id}\n" +# f"event: notification\n" +# f"data: {json.dumps(data)}\n\n" +# ) + +# await send({ +# "type": "http.response.body", +# "body": event_str.encode("utf-8"), +# "more_body": True +# }) + +# last_id = notification.id + +# await asyncio.sleep(2) + +# except asyncio.CancelledError: +# pass + +import json +import time +from django.contrib.auth.models import AnonymousUser +from urllib.parse import parse_qs +from channels.db import database_sync_to_async +from django.contrib.auth import get_user_model +from inventory.models import Notification +import asyncio +from datetime import datetime + +@database_sync_to_async +def get_user(user_id): + User = get_user_model() + try: + return User.objects.get(id=user_id) + except User.DoesNotExist: + return AnonymousUser() + +@database_sync_to_async +def get_notifications(user, last_id): + notifications = Notification.objects.filter( + user=user, + id__gt=last_id, + is_read=False + ).order_by("created") + + return [ + { + 'id': n.id, + 'message': n.message, + 'created': n.created.isoformat(), # Convert datetime to string + 'is_read': n.is_read + } + for n in notifications + ] + +class NotificationSSEApp: + async def __call__(self, scope, receive, send): + if scope["type"] != "http": + return + + # Parse query parameters + query_string = parse_qs(scope["query_string"].decode()) + last_id = int(query_string.get("last_id", [0])[0]) + + # Get user from scope + user = scope.get("user") + if not user or user.is_anonymous: + await self._send_response(send, 403, b"Unauthorized") + return + + # Send SSE headers + await self._send_headers(send) + + try: + while True: + try: + message = await asyncio.wait_for(receive(), timeout=3) + if message["type"] == "http.disconnect": + print("🔌 Client disconnected") + break + except asyncio.TimeoutError: + notifications = await get_notifications(user, last_id) + + for notification in notifications: + await self._send_notification(send, notification) + if notification['id'] > last_id: + last_id = notification['id'] + + # Send keep-alive comment every 15 seconds + await send({ + "type": "http.response.body", + "body": b":keep-alive\n\n", + "more_body": True + }) + + # await asyncio.sleep(3) + + except (asyncio.CancelledError, ConnectionResetError): + pass + finally: + await self._close_connection(send) + + async def _send_headers(self, send): + await send({ + "type": "http.response.start", + "status": 200, + "headers": [ + (b"content-type", b"text/event-stream"), + (b"cache-control", b"no-cache"), + (b"connection", b"keep-alive"), + (b"x-accel-buffering", b"no"), + ] + }) + + async def _send_notification(self, send, notification): + try: + event_str = ( + f"id: {notification['id']}\n" + f"event: notification\n" + f"data: {json.dumps(notification)}\n\n" + ) + await send({ + "type": "http.response.body", + "body": event_str.encode("utf-8"), + "more_body": True + }) + except Exception as e: + print(f"Error sending notification: {e}") + + async def _send_response(self, send, status, body): + await send({ + "type": "http.response.start", + "status": status, + "headers": [(b"content-type", b"text/plain")] + }) + await send({ + "type": "http.response.body", + "body": body + }) + + async def _close_connection(self, send): + await send({ + "type": "http.response.body", + "body": b"" + }) \ No newline at end of file diff --git a/inventory/override.py b/inventory/override.py index 600090d3..a5fb6036 100644 --- a/inventory/override.py +++ b/inventory/override.py @@ -1,3 +1,4 @@ +from datetime import timezone import logging from .models import Dealer from django.core.exceptions import ImproperlyConfigured, ValidationError @@ -19,7 +20,7 @@ from django.contrib import messages from django.http import HttpResponseRedirect from django.shortcuts import get_object_or_404 from django.urls import reverse -from django_ledger.models import ItemTransactionModel +from django_ledger.models import ItemTransactionModel,InvoiceModel,LedgerModel,EntityModel from django.views.generic.detail import DetailView from django_ledger.forms.purchase_order import ( ApprovedPurchaseOrderModelUpdateForm, @@ -35,6 +36,11 @@ from django.views.generic.edit import UpdateView from django.views.generic.base import RedirectView from django.views.generic.list import ListView from django.utils.translation import gettext_lazy as _ +from django_ledger.forms.invoice import (BaseInvoiceModelUpdateForm, InvoiceModelCreateForEstimateForm, + get_invoice_itemtxs_formset_class, + DraftInvoiceModelUpdateForm, InReviewInvoiceModelUpdateForm, + ApprovedInvoiceModelUpdateForm, PaidInvoiceModelUpdateForm, + AccruedAndApprovedInvoiceModelUpdateForm, InvoiceModelCreateForm) logger = logging.getLogger(__name__) @@ -315,41 +321,68 @@ class BasePurchaseOrderActionActionView( f"User {user_username} attempting to call action '{self.action_name}' " f"on Purchase Order ID: {po_model.pk} (Entity: {entity_slug})." ) - try: - getattr(po_model, self.action_name)(commit=self.commit, **kwargs) - # --- Single-line log for successful action --- - logger.info( - f"User {user_username} successfully executed action '{self.action_name}' " - f"on Purchase Order ID: {po_model.pk}." - ) - messages.add_message( - request, - message="PO updated successfully.", - level=messages.SUCCESS, - ) - except ValidationError as e: - # --- Single-line log for ValidationError --- - print( - f"User {user_username} encountered a validation error " - f"while performing action '{self.action_name}' on Purchase Order ID: {po_model.pk}. " - f"Error: {e}" - ) - logger.warning( - f"User {user_username} encountered a validation error " - f"while performing action '{self.action_name}' on Purchase Order ID: {po_model.pk}. " - f"Error: {e}" - ) - except AttributeError as e: - print( - f"User {user_username} encountered an AttributeError " - f"while performing action '{self.action_name}' on Purchase Order ID: {po_model.pk}. " - f"Error: {e}" - ) - logger.warning( - f"User {user_username} encountered an AttributeError " - f"while performing action '{self.action_name}' on Purchase Order ID: {po_model.pk}. " - f"Error: {e}" - ) + print(self.action_name) + if self.action_name == "mark_as_fulfilled": + try: + if po_model.can_fulfill(): + po_model.mark_as_fulfilled() + # po_model.date_fulfilled = timezone.now() + po_model.save() + messages.add_message( + request, + message="PO marked as fulfilled successfully.", + level=messages.SUCCESS, + ) + logger.info( + f"User {user_username} successfully executed action '{self.action_name}' " + f"on Purchase Order ID: {po_model.pk}." + ) + except Exception as e: + messages.add_message( + request, + message=f"Failed to mark PO {po_model.po_number} as fulfilled. {e}", + level=messages.ERROR, + ) + logger.warning( + f"User {user_username} encountered an exception " + f"while performing action '{self.action_name}' on Purchase Order ID: {po_model.pk}. " + f"Error: {e}" + ) + else: + try: + getattr(po_model, self.action_name)(commit=self.commit, **kwargs) + logger.info( + f"User {user_username} successfully executed action '{self.action_name}' " + f"on Purchase Order ID: {po_model.pk}." + ) + messages.add_message( + request, + message="PO updated successfully.", + level=messages.SUCCESS, + ) + except ValidationError as e: + # --- Single-line log for ValidationError --- + print( + f"User {user_username} encountered a validation error " + f"while performing action '{self.action_name}' on Purchase Order ID: {po_model.pk}. " + f"Error: {e}" + ) + logger.warning( + f"User {user_username} encountered a validation error " + f"while performing action '{self.action_name}' on Purchase Order ID: {po_model.pk}. " + f"Error: {e}" + ) + except AttributeError as e: + print( + f"User {user_username} encountered an AttributeError " + f"while performing action '{self.action_name}' on Purchase Order ID: {po_model.pk}. " + f"Error: {e}" + ) + logger.warning( + f"User {user_username} encountered an AttributeError " + f"while performing action '{self.action_name}' on Purchase Order ID: {po_model.pk}. " + f"Error: {e}" + ) return response @@ -731,3 +764,212 @@ class InventoryListView(LoginRequiredMixin, PermissionRequiredMixin, ListView): entity_slug=self.kwargs["entity_slug"], ) return super().get_queryset() + + +class InvoiceModelUpdateView(LoginRequiredMixin, PermissionRequiredMixin, UpdateView): + slug_url_kwarg = 'invoice_pk' + slug_field = 'uuid' + context_object_name = 'invoice' + # template_name = 'inventory/sales/invoices/invoice_update.html' + form_class = BaseInvoiceModelUpdateForm + http_method_names = ['get', 'post'] + + action_update_items = False + + def get_form_class(self): + invoice_model: InvoiceModel = self.object + + if invoice_model.is_draft(): + return DraftInvoiceModelUpdateForm + elif invoice_model.is_review(): + return InReviewInvoiceModelUpdateForm + elif invoice_model.is_approved(): + if invoice_model.accrue: + return AccruedAndApprovedInvoiceModelUpdateForm + return ApprovedInvoiceModelUpdateForm + elif invoice_model.is_paid(): + return PaidInvoiceModelUpdateForm + return BaseInvoiceModelUpdateForm + + def get_form(self, form_class=None): + form_class = self.get_form_class() + if self.request.method == 'POST' and self.action_update_items: + return form_class( + entity_slug=self.kwargs['entity_slug'], + user_model=self.request.dealer.user, + instance=self.object + ) + return form_class( + entity_slug=self.kwargs['entity_slug'], + user_model=self.request.dealer.user, + **self.get_form_kwargs() + ) + + def get_context_data(self, itemtxs_formset=None, **kwargs): + context = super().get_context_data(**kwargs) + invoice_model: InvoiceModel = self.object + title = f'Invoice {invoice_model.invoice_number}' + context['page_title'] = title + context['header_title'] = title + + ledger_model: LedgerModel = self.object.ledger + + if not invoice_model.is_configured(): + messages.add_message( + request=self.request, + message=f'Invoice {invoice_model.invoice_number} must have all accounts configured.', + level=messages.ERROR, + extra_tags='is-danger' + ) + + if not invoice_model.is_paid(): + if ledger_model.locked: + messages.add_message(self.request, + messages.ERROR, + f'Warning! This invoice is locked. Must unlock before making any changes.', + extra_tags='is-danger') + + if ledger_model.locked: + messages.add_message(self.request, + messages.ERROR, + f'Warning! This Invoice is Locked. Must unlock before making any changes.', + extra_tags='is-danger') + + if not ledger_model.is_posted(): + messages.add_message(self.request, + messages.INFO, + f'This Invoice has not been posted. Must post to see ledger changes.', + extra_tags='is-info') + + if not itemtxs_formset: + itemtxs_qs = invoice_model.itemtransactionmodel_set.all().select_related('item_model') + itemtxs_qs, itemtxs_agg = invoice_model.get_itemtxs_data(queryset=itemtxs_qs) + invoice_itemtxs_formset_class = get_invoice_itemtxs_formset_class(invoice_model) + itemtxs_formset = invoice_itemtxs_formset_class( + entity_slug=self.kwargs['entity_slug'], + user_model=self.request.dealer.user, + invoice_model=invoice_model, + queryset=itemtxs_qs + ) + else: + itemtxs_qs, itemtxs_agg = invoice_model.get_itemtxs_data(queryset=itemtxs_formset.queryset) + + context['itemtxs_formset'] = itemtxs_formset + context['total_amount__sum'] = itemtxs_agg['total_amount__sum'] + return context + + def get_success_url(self): + entity_slug = self.kwargs['entity_slug'] + invoice_pk = self.kwargs['invoice_pk'] + return reverse('invoice_detail', + kwargs={ + 'dealer_slug': self.request.dealer.slug, + 'entity_slug': entity_slug, + 'pk': invoice_pk + }) + + # def get_queryset(self): + # qs = super().get_queryset() + # return qs.prefetch_related('itemtransactionmodel_set') + def get_queryset(self): + if self.queryset is None: + self.queryset = InvoiceModel.objects.for_entity( + entity_slug=self.kwargs['entity_slug'], + user_model=self.request.user + ).select_related('customer', 'ledger').order_by('-created') + return super().get_queryset().prefetch_related('itemtransactionmodel_set') + + + def form_valid(self, form): + invoice_model: InvoiceModel = form.save(commit=False) + if invoice_model.can_migrate(): + invoice_model.migrate_state( + user_model=self.request.dealer.user, + entity_slug=self.kwargs['entity_slug'] + ) + messages.add_message(self.request, + messages.SUCCESS, + f'Invoice {self.object.invoice_number} successfully updated.', + extra_tags='is-success') + return super().form_valid(form) + + def get(self, request, entity_slug, invoice_pk, *args, **kwargs): + if self.action_update_items: + return HttpResponseRedirect( + redirect_to=reverse('invoice_update', + kwargs={ + 'dealer_slug': request.dealer.slug, + 'entity_slug': entity_slug, + 'pk': invoice_pk + }) + ) + return super(InvoiceModelUpdateView, self).get(request, *args, **kwargs) + + def post(self, request, entity_slug, invoice_pk, *args, **kwargs): + if self.action_update_items: + if not request.user.is_authenticated: + return HttpResponseForbidden() + + queryset = self.get_queryset() + invoice_model = self.get_object(queryset=queryset) + self.object = invoice_model + invoice_itemtxs_formset_class = get_invoice_itemtxs_formset_class(invoice_model) + itemtxs_formset = invoice_itemtxs_formset_class(request.POST, + user_model=self.request.dealer.user, + invoice_model=invoice_model, + entity_slug=entity_slug) + + if not invoice_model.can_edit_items(): + messages.add_message( + request, + message=f'Cannot update items once Invoice is {invoice_model.get_invoice_status_display()}', + level=messages.ERROR, + extra_tags='is-danger' + ) + context = self.get_context_data(itemtxs_formset=itemtxs_formset) + return self.render_to_response(context=context) + + if itemtxs_formset.has_changed(): + if itemtxs_formset.is_valid(): + itemtxs_list = itemtxs_formset.save(commit=False) + entity_qs = EntityModel.objects.for_user(user_model=self.request.dealer.user) + entity_model: EntityModel = get_object_or_404(entity_qs, slug__exact=entity_slug) + + for itemtxs in itemtxs_list: + itemtxs.invoice_model_id = invoice_model.uuid + itemtxs.clean() + + itemtxs_list = itemtxs_formset.save() + itemtxs_qs = invoice_model.update_amount_due() + invoice_model.get_state(commit=True) + invoice_model.clean() + invoice_model.save( + update_fields=['amount_due', + 'amount_receivable', + 'amount_unearned', + 'amount_earned', + 'updated'] + ) + + invoice_model.migrate_state( + entity_slug=entity_slug, + user_model=self.request.user, + raise_exception=False, + itemtxs_qs=itemtxs_qs + ) + + messages.add_message(request, + message=f'Items for Invoice {invoice_model.invoice_number} saved.', + level=messages.SUCCESS, + extra_tags='is-success') + return HttpResponseRedirect( + redirect_to=reverse('django_ledger:invoice-update', + kwargs={ + 'entity_slug': entity_slug, + 'invoice_pk': invoice_pk + }) + ) + + # if not valid, return formset with errors... + return self.render_to_response(context=self.get_context_data(itemtxs_formset=itemtxs_formset)) + return super(InvoiceModelUpdateView, self).post(request, **kwargs) diff --git a/inventory/signals.py b/inventory/signals.py index 8242fb5f..54b4946e 100644 --- a/inventory/signals.py +++ b/inventory/signals.py @@ -264,18 +264,19 @@ def create_item_model(sender, instance, created, **kwargs): uom_model=uom, coa_model=coa, ) + instance.item_model = inventory + inventory.save() # inventory = entity.create_item_inventory( # name=instance.vin, # uom_model=uom, # item_type=ItemModel.ITEM_TYPE_LUMP_SUM # ) - instance.item_model = inventory - inventory.additional_info = {} - inventory.additional_info.update({"car_info": instance.to_dict()}) - inventory.save() - else: - instance.item_model.additional_info.update({"car_info": instance.to_dict()}) - instance.item_model.save() + # inventory.additional_info = {} + # inventory.additional_info.update({"car_info": instance.to_dict()}) + # inventory.save() + # else: + # instance.item_model.additional_info.update({"car_info": instance.to_dict()}) + # instance.item_model.save() # # update price - CarFinance @@ -370,14 +371,14 @@ def update_item_model_cost(sender, instance, created, **kwargs): instance.car.item_model.default_amount = instance.marked_price if not isinstance(instance.car.item_model.additional_info, dict): instance.car.item_model.additional_info = {} - instance.car.item_model.additional_info.update({"car_finance": instance.to_dict()}) - instance.car.item_model.additional_info.update( - { - "additional_services": [ - service.to_dict() for service in instance.additional_services.all() - ] - } - ) + # instance.car.item_model.additional_info.update({"car_finance": instance.to_dict()}) + # instance.car.item_model.additional_info.update( + # { + # "additional_services": [ + # service.to_dict() for service in instance.additional_services.all() + # ] + # } + # ) instance.car.item_model.save() print(f"Inventory item updated with CarFinance data for Car: {instance.car}") @@ -959,24 +960,24 @@ def add_service_to_staff(sender, instance, created, **kwargs): ########################################################## -@receiver(post_save, sender=PurchaseOrderModel) -def create_po_fulfilled_notification(sender, instance, created, **kwargs): - if instance.po_status == "fulfilled": - dealer = models.Dealer.objects.get(entity=instance.entity) - accountants = ( - models.CustomGroup.objects.filter(dealer=dealer, name="Inventory") - .first() - .group.user_set.exclude(email=dealer.user.email) - .distinct() - ) - for accountant in accountants: - models.Notification.objects.create( - user=accountant, - message=f""" - New Purchase Order {instance.po_number} has been added to dealer {dealer.name}. - View - """, - ) +# @receiver(post_save, sender=PurchaseOrderModel) +# def create_po_fulfilled_notification(sender, instance, created, **kwargs): +# if instance.po_status == "fulfilled": +# dealer = models.Dealer.objects.get(entity=instance.entity) +# accountants = ( +# models.CustomGroup.objects.filter(dealer=dealer, name="Inventory") +# .first() +# .group.user_set.exclude(email=dealer.user.email) +# .distinct() +# ) +# for accountant in accountants: +# models.Notification.objects.create( +# user=accountant, +# message=f""" +# New Purchase Order {instance.po_number} has been added to dealer {dealer.name}. +# View +# """, +# ) @receiver(post_save, sender=models.Car) @@ -1005,7 +1006,7 @@ def po_fullfilled_notification(sender, instance, created, **kwargs): if instance.is_fulfilled(): dealer = models.Dealer.objects.get(entity=instance.entity) recipients = User.objects.filter( - groups__customgroup__dealer=instance.dealer, + groups__customgroup__dealer=dealer, groups__customgroup__name__in=["Manager", "Inventory"], ).distinct() for recipient in recipients: @@ -1099,7 +1100,8 @@ def estimate_in_approve_notification(sender, instance, created, **kwargs): related_content_type=ContentType.objects.get_for_model(models.Staff), object_id=instance.pk, ).first() - + if not recipient: + return models.Notification.objects.create( user=recipient.related_object.user, message=f""" diff --git a/inventory/tasks.py b/inventory/tasks.py index 17a1bdbd..36bcee2d 100644 --- a/inventory/tasks.py +++ b/inventory/tasks.py @@ -4,9 +4,10 @@ from django_ledger.io import roles from django_q.tasks import async_task from django.core.mail import send_mail from appointment.models import StaffMember -from django.contrib.auth.models import User, Group, Permission +from allauth.account.models import EmailAddress from inventory.models import DealerSettings, Dealer from django.utils.translation import gettext_lazy as _ +from django.contrib.auth.models import User, Group, Permission logger = logging.getLogger(__name__) logging.basicConfig(level=logging.INFO) @@ -220,7 +221,7 @@ def create_coa_accounts(instance): "role": roles.LIABILITY_CL_TAXES_PAYABLE, "balance_type": roles.CREDIT, "locked": False, - "default": True, # Default for LIABILITY_CL_TAXES_PAYABLE + "default": False, # Default for LIABILITY_CL_TAXES_PAYABLE }, { "code": "2070", @@ -239,6 +240,14 @@ def create_coa_accounts(instance): "locked": False, "default": True, # Default for LIABILITY_CL_DEFERRED_REVENUE }, + { + "code": "2200", + "name": "Tax Payable", + "role": roles.LIABILITY_CL_TAXES_PAYABLE, + "balance_type": roles.CREDIT, + "locked": False, + "default": True, + }, { "code": "2210", "name": "Long-term Bank Loans", @@ -1141,6 +1150,15 @@ def create_user_dealer(email, password, name, arabic_name, phone, crn, vrn, addr user = User.objects.create(username=email, email=email) user.set_password(password) user.save() + + #TODO remove this later + EmailAddress.objects.create( + user=user, + email=user.email, + verified=True, + primary=True + ) + group = Group.objects.create(name=f"{user.pk}-Admin") user.groups.add(group) for perm in Permission.objects.filter( @@ -1149,7 +1167,7 @@ def create_user_dealer(email, password, name, arabic_name, phone, crn, vrn, addr group.permissions.add(perm) StaffMember.objects.create(user=user) - Dealer.objects.create( + dealer = Dealer.objects.create( user=user, name=name, arabic_name=arabic_name, @@ -1158,6 +1176,7 @@ def create_user_dealer(email, password, name, arabic_name, phone, crn, vrn, addr phone_number=phone, address=address, ) + return dealer # def create_groups(dealer_slug): diff --git a/inventory/templatetags/custom_filters.py b/inventory/templatetags/custom_filters.py index f9a085e1..267041cb 100644 --- a/inventory/templatetags/custom_filters.py +++ b/inventory/templatetags/custom_filters.py @@ -472,6 +472,7 @@ def po_item_formset_table(context, po_model, itemtxs_formset, user): @register.inclusion_tag("bill/tags/bill_item_formset.html", takes_context=True) def bill_item_formset_table(context, item_formset): + bill = BillModel.objects.get(uuid=context["view"].kwargs["bill_pk"]) for item in item_formset: if item: item.initial["quantity"] = item.instance.po_quantity @@ -484,6 +485,7 @@ def bill_item_formset_table(context, item_formset): return { "dealer_slug": context["view"].kwargs["dealer_slug"], "entity_slug": context["view"].kwargs["entity_slug"], + "bill": bill, "bill_pk": context["view"].kwargs["bill_pk"], "total_amount__sum": context["total_amount__sum"], "item_formset": item_formset, @@ -672,3 +674,12 @@ def count_checked(permissions, group_permission_ids): # def count_checked(permissions, group_permission_ids): # """Count how many permissions are checked from the allowed list""" # return sum(1 for perm in permissions if perm.id in group_permission_ids) + +@register.inclusion_tag('sales/tags/invoice_item_formset.html', takes_context=True) +def invoice_item_formset_table(context, itemtxs_formset): + return { + 'entity_slug': context['view'].kwargs['entity_slug'], + 'invoice_model': context['invoice'], + 'total_amount__sum': context['total_amount__sum'], + 'itemtxs_formset': itemtxs_formset, + } diff --git a/inventory/urls.py b/inventory/urls.py index 582b1e63..1f39b39f 100644 --- a/inventory/urls.py +++ b/inventory/urls.py @@ -80,11 +80,11 @@ urlpatterns = [ views.CustomerDetailView.as_view(), name="customer_detail", ), - path( - "/customers//add-note/", - views.add_note_to_customer, - name="add_note_to_customer", - ), + # path( + # "/customers//add-note/", + # views.add_note_to_customer, + # name="add_note_to_customer", + # ), path( "/customers//update/", views.CustomerUpdateView.as_view(), @@ -198,11 +198,11 @@ urlpatterns = [ views.lead_transfer, name="lead_transfer", ), - path( - "/crm/opportunities//add_note/", - views.add_note_to_opportunity, - name="add_note_to_opportunity", - ), + # path( + # "/crm/opportunities//add_note/", + # views.add_note_to_opportunity, + # name="add_note_to_opportunity", + # ), path( "/crm/opportunities/create/", views.OpportunityCreateView.as_view(), @@ -836,10 +836,15 @@ urlpatterns = [ name="invoice_create", ), path( - "/sales/invoices//", + "/sales//invoices//", views.InvoiceDetailView.as_view(), name="invoice_detail", ), + # path( + # "/sales//invoices//update", + # views.InvoiceDetailView.as_view(), + # name="invoice_update", + # ), path( "/sales/invoices//preview/", views.InvoicePreviewView.as_view(), @@ -876,7 +881,17 @@ urlpatterns = [ views.PaymentCreateView, name="payment_create", ), - # path("sales/payments/create/", views.PaymentCreateView, name="payment_create"), + # path( + # "/sales/payments///create/", + # views.InvoiceModelUpdateView.as_view(), + # name="invoice_update", + # ), + # path( + # "/sales/payments///create/", + # views.InvoiceModelUpdateView.as_view(), + # name="payment_create", + # ), + # path("/sales/payments/create/", views.PaymentCreateView, name="payment_create"), path( "/sales/payments//payment_details/", views.PaymentDetailView, diff --git a/inventory/utils.py b/inventory/utils.py index 5d4b8c00..368cca6e 100644 --- a/inventory/utils.py +++ b/inventory/utils.py @@ -14,6 +14,7 @@ from django_q.tasks import async_task from django.core.mail import send_mail from plans.models import AbstractOrder from django_ledger.models import ( + EstimateModel, InvoiceModel, BillModel, VendorModel, @@ -25,8 +26,9 @@ from django.utils.translation import gettext_lazy as _ from django.contrib.contenttypes.models import ContentType from django_ledger.models.transactions import TransactionModel from django_ledger.models.journal_entry import JournalEntryModel - +from django.db import transaction import logging +from django_ledger.io import roles logger = logging.getLogger(__name__) @@ -452,30 +454,35 @@ def get_financial_values(model): } -def set_invoice_payment(dealer, entity, invoice, amount, payment_method): - """ - Processes and applies a payment for a specified invoice. This function calculates - finance details, handles associated account transactions, and updates the invoice - status accordingly. +# def set_invoice_payment(dealer, entity, invoice, amount, payment_method): +# """ +# Processes and applies a payment for a specified invoice. This function calculates +# finance details, handles associated account transactions, and updates the invoice +# status accordingly. - :param dealer: Dealer object responsible for processing the payment - :type dealer: Dealer - :param entity: Entity object associated with the invoice and payment - :type entity: Entity - :param invoice: The invoice object for which the payment is being made - :type invoice: Invoice - :param amount: The amount being paid towards the invoice - :type amount: Decimal - :param payment_method: The payment method used for the transaction - :type payment_method: str - :return: None - """ - calculator = CarFinanceCalculator(invoice) - finance_data = calculator.get_finance_data() +# :param dealer: Dealer object responsible for processing the payment +# :type dealer: Dealer +# :param entity: Entity object associated with the invoice and payment +# :type entity: Entity +# :param invoice: The invoice object for which the payment is being made +# :type invoice: Invoice +# :param amount: The amount being paid towards the invoice +# :type amount: Decimal +# :param payment_method: The payment method used for the transaction +# :type payment_method: str +# :return: None +# """ +# calculator = CarFinanceCalculator(invoice) +# finance_data = calculator.get_finance_data() - handle_account_process(invoice, amount, finance_data) - invoice.make_payment(amount) - invoice.save() +# handle_account_process(invoice, amount, finance_data) +# if invoice.can_migrate(): +# invoice.migrate_state( +# user_model=dealer.user, +# entity_slug=entity.slug +# ) +# invoice.make_payment(amount) +# invoice.save() def set_bill_payment(dealer, entity, bill, amount, payment_method): @@ -996,16 +1003,25 @@ class CarFinanceCalculator: ADDITIONAL_SERVICES_KEY = "additional_services" def __init__(self, model): - self.dealer = models.Dealer.objects.get(entity=model.entity) + if isinstance(model, InvoiceModel): + self.dealer = models.Dealer.objects.get(entity=model.ce_model.entity) + self.extra_info = models.ExtraInfo.objects.get( + dealer=self.dealer, + content_type=ContentType.objects.get_for_model(model.ce_model), + object_id=model.ce_model.pk, + ) + elif isinstance(model, EstimateModel): + self.dealer = models.Dealer.objects.get(entity=model.entity) + self.extra_info = models.ExtraInfo.objects.get( + dealer=self.dealer, + content_type=ContentType.objects.get_for_model(model), + object_id=model.pk, + ) self.model = model self.vat_rate = self._get_vat_rate() self.item_transactions = self._get_item_transactions() - self.additional_services = self._get_additional_services() - self.extra_info = models.ExtraInfo.objects.get( - dealer=self.dealer, - content_type=ContentType.objects.get_for_model(model), - object_id=model.pk, - ) + # self.additional_services = self._get_additional_services() + def _get_vat_rate(self): vat = models.VatRate.objects.filter(dealer=self.dealer, is_active=True).first() @@ -1013,77 +1029,62 @@ class CarFinanceCalculator: raise ObjectDoesNotExist("No active VAT rate found") return vat.rate + def _get_additional_services(self): + return [x for item in self.item_transactions + for x in item.item_model.car.additional_services + ] def _get_item_transactions(self): return self.model.get_itemtxs_data()[0].all() + def get_items(self): + return self._get_item_transactions() @staticmethod def _get_quantity(item): return item.ce_quantity or item.quantity - def _get_nested_value(self, item, *keys): - current = item.item_model.additional_info - for key in keys: - current = current.get(key, {}) - return current + # def _get_nested_value(self, item, *keys): + # current = item.item_model.additional_info + # for key in keys: + # current = current.get(key, {}) + # return current def _get_car_data(self, item): quantity = self._get_quantity(item) - car_finance = self._get_nested_value(item, self.CAR_FINANCE_KEY) - car_info = self._get_nested_value(item, self.CAR_INFO_KEY) - unit_price = Decimal(car_finance.get("marked_price", 0)) + car = item.item_model.car + unit_price = Decimal(car.finances.marked_price) + return { "item_number": item.item_model.item_number, - "vin": car_info.get("vin"), - "make": car_info.get("make"), - "model": car_info.get("model"), - "year": car_info.get("year"), - "logo": getattr(item.item_model.car.id_car_make, "logo", ""), - "trim": car_info.get("trim"), - "mileage": car_info.get("mileage"), - "cost_price": car_finance.get("cost_price"), - "selling_price": car_finance.get("selling_price"), - "marked_price": car_finance.get("marked_price"), - "discount": car_finance.get("discount_amount"), + "vin": car.vin, #car_info.get("vin"), + "make": car.id_car_make ,#car_info.get("make"), + "model": car.id_car_model ,#car_info.get("model"), + "year": car.year ,# car_info.get("year"), + "logo": car.logo, # getattr(car.id_car_make, "logo", ""), + "trim": car.id_car_trim ,# car_info.get("trim"), + "mileage": car.mileage ,# car_info.get("mileage"), + "cost_price": car.finances.cost_price, + "selling_price": car.finances.selling_price, + "marked_price": car.finances.marked_price, + "discount": car.finances.discount_amount, "quantity": quantity, "unit_price": unit_price, "total": unit_price * Decimal(quantity), - "total_vat": car_finance.get("total_vat"), - "additional_services": self._get_nested_value( - item, self.ADDITIONAL_SERVICES_KEY - ), + "total_vat": car.finances.total_vat, + "additional_services": car.additional_services,# self._get_nested_value( + #item, self.ADDITIONAL_SERVICES_KEY + #), } - def _get_additional_services(self): - return [ - { - "name": service.get("name"), - "price": service.get("price"), - "taxable": service.get("taxable"), - "price_": service.get("price_"), - } - for item in self.item_transactions - for service in self._get_nested_value(item, self.ADDITIONAL_SERVICES_KEY) - or [] - ] - def calculate_totals(self): total_price = sum( - Decimal(self._get_nested_value(item, self.CAR_FINANCE_KEY, "marked_price")) - * int(self._get_quantity(item)) + Decimal(item.item_model.car.finances.marked_price) for item in self.item_transactions ) total_additionals = sum( - Decimal(x.get("price_")) for x in self._get_additional_services() - ) + Decimal(item.price_) for item in self._get_additional_services()) total_discount = self.extra_info.data.get("discount", 0) - # total_discount = sum( - # Decimal( - # self._get_nested_value(item, self.CAR_FINANCE_KEY, "discount_amount") - # ) - # for item in self.item_transactions - # ) total_price_discounted = total_price if total_discount: total_price_discounted = total_price - Decimal(total_discount) @@ -1092,11 +1093,11 @@ class CarFinanceCalculator: return { "total_price_before_discount": round( total_price, 2 - ), # total_price_before_discount, - "total_price": round(total_price_discounted, 2), # total_price_discounted, - "total_vat_amount": round(total_vat_amount, 2), # total_vat_amount, + ), + "total_price": round(total_price_discounted, 2), + "total_vat_amount": round(total_vat_amount, 2), "total_discount": round(Decimal(total_discount)), - "total_additionals": round(total_additionals, 2), # total_additionals, + "total_additionals": round(total_additionals, 2), "grand_total": round( total_price_discounted + total_vat_amount + total_additionals, 2 ), @@ -1116,9 +1117,167 @@ class CarFinanceCalculator: "total_discount": totals["total_discount"], "total_additionals": totals["total_additionals"], "grand_total": totals["grand_total"], - "additionals": self.additional_services, + "additionals": self._get_additional_services(), "vat": self.vat_rate, } +# class CarFinanceCalculator: +# """ +# Class responsible for calculating car financing details. + +# This class provides methods and attributes required for calculating various +# aspects related to car financing, such as VAT calculation, pricing, discounts, +# and additional services. It processes data about cars, computes totals (e.g., +# price, VAT, discounts), and aggregates the financial data for reporting or +# further processing. + +# :ivar model: The data model passed to the calculator for retrieving transaction data. +# :type model: Any +# :ivar vat_rate: The current active VAT rate retrieved from the database. +# :type vat_rate: Decimal +# :ivar item_transactions: A collection of item transactions retrieved from the model. +# :type item_transactions: list +# :ivar additional_services: A list of additional services with details (e.g., name, price, taxable status). +# :type additional_services: list +# """ + +# VAT_OBJ_NAME = "vat_rate" +# CAR_FINANCE_KEY = "car_finance" +# CAR_INFO_KEY = "car_info" +# ADDITIONAL_SERVICES_KEY = "additional_services" + +# def __init__(self, model): +# if isinstance(model, InvoiceModel): +# self.dealer = models.Dealer.objects.get(entity=model.ce_model.entity) +# self.extra_info = models.ExtraInfo.objects.get( +# dealer=self.dealer, +# content_type=ContentType.objects.get_for_model(model.ce_model), +# object_id=model.ce_model.pk, +# ) +# elif isinstance(model, EstimateModel): +# self.dealer = models.Dealer.objects.get(entity=model.entity) +# self.extra_info = models.ExtraInfo.objects.get( +# dealer=self.dealer, +# content_type=ContentType.objects.get_for_model(model), +# object_id=model.pk, +# ) +# self.model = model +# self.vat_rate = self._get_vat_rate() +# self.item_transactions = self._get_item_transactions() +# self.additional_services = self._get_additional_services() + + +# def _get_vat_rate(self): +# vat = models.VatRate.objects.filter(dealer=self.dealer, is_active=True).first() +# if not vat: +# raise ObjectDoesNotExist("No active VAT rate found") +# return vat.rate + +# def _get_item_transactions(self): +# return self.model.get_itemtxs_data()[0].all() + +# @staticmethod +# def _get_quantity(item): +# return item.ce_quantity or item.quantity + +# def _get_nested_value(self, item, *keys): +# current = item.item_model.additional_info +# for key in keys: +# current = current.get(key, {}) +# return current + +# def _get_car_data(self, item): +# quantity = self._get_quantity(item) +# car_finance = self._get_nested_value(item, self.CAR_FINANCE_KEY) +# car_info = self._get_nested_value(item, self.CAR_INFO_KEY) +# unit_price = Decimal(car_finance.get("marked_price", 0)) +# return { +# "item_number": item.item_model.item_number, +# "vin": car_info.get("vin"), +# "make": car_info.get("make"), +# "model": car_info.get("model"), +# "year": car_info.get("year"), +# "logo": getattr(item.item_model.car.id_car_make, "logo", ""), +# "trim": car_info.get("trim"), +# "mileage": car_info.get("mileage"), +# "cost_price": car_finance.get("cost_price"), +# "selling_price": car_finance.get("selling_price"), +# "marked_price": car_finance.get("marked_price"), +# "discount": car_finance.get("discount_amount"), +# "quantity": quantity, +# "unit_price": unit_price, +# "total": unit_price * Decimal(quantity), +# "total_vat": car_finance.get("total_vat"), +# "additional_services": self._get_nested_value( +# item, self.ADDITIONAL_SERVICES_KEY +# ), +# } + +# def _get_additional_services(self): +# return [ +# { +# "name": service.get("name"), +# "price": service.get("price"), +# "taxable": service.get("taxable"), +# "price_": service.get("price_"), +# } +# for item in self.item_transactions +# for service in self._get_nested_value(item, self.ADDITIONAL_SERVICES_KEY) +# or [] +# ] + +# def calculate_totals(self): +# total_price = sum( +# Decimal(self._get_nested_value(item, self.CAR_FINANCE_KEY, "marked_price")) +# * int(self._get_quantity(item)) +# for item in self.item_transactions +# ) +# total_additionals = sum( +# Decimal(x.get("price_")) for x in self._get_additional_services() +# ) + +# total_discount = self.extra_info.data.get("discount", 0) + +# # total_discount = sum( +# # Decimal( +# # self._get_nested_value(item, self.CAR_FINANCE_KEY, "discount_amount") +# # ) +# # for item in self.item_transactions +# # ) +# total_price_discounted = total_price +# if total_discount: +# total_price_discounted = total_price - Decimal(total_discount) +# total_vat_amount = total_price_discounted * self.vat_rate + +# return { +# "total_price_before_discount": round( +# total_price, 2 +# ), # total_price_before_discount, +# "total_price": round(total_price_discounted, 2), # total_price_discounted, +# "total_vat_amount": round(total_vat_amount, 2), # total_vat_amount, +# "total_discount": round(Decimal(total_discount)), +# "total_additionals": round(total_additionals, 2), # total_additionals, +# "grand_total": round( +# total_price_discounted + total_vat_amount + total_additionals, 2 +# ), +# } + +# def get_finance_data(self): +# totals = self.calculate_totals() +# return { +# "cars": [self._get_car_data(item) for item in self.item_transactions], +# "quantity": sum( +# self._get_quantity(item) for item in self.item_transactions +# ), +# "total_price": totals["total_price"], +# "total_price_before_discount": totals["total_price_before_discount"], +# "total_vat": totals["total_vat_amount"] + totals["total_price"], +# "total_vat_amount": totals["total_vat_amount"], +# "total_discount": totals["total_discount"], +# "total_additionals": totals["total_additionals"], +# "grand_total": totals["grand_total"], +# "additionals": self.additional_services, +# "vat": self.vat_rate, +# } def get_item_transactions(txs): @@ -1175,134 +1334,224 @@ def get_local_name(self): return getattr(self, "name", None) -def handle_account_process(invoice, amount, finance_data): + +@transaction.atomic +def set_invoice_payment(dealer, entity, invoice, amount, payment_method): """ - Processes accounting transactions based on an invoice, financial data, - and related entity accounts configuration. This function handles the - creation of accounts if they do not already exist, and processes journal - entries and transactions. - - :param invoice: The invoice object to process transactions for. - :type invoice: InvoiceModel - :param amount: Total monetary value for the transaction. - :type amount: Decimal - :param finance_data: Dictionary containing financial details such as - 'grand_total', 'total_vat_amount', and other related data. - :type finance_data: dict - :return: None + Records the customer payment (`make_payment`) and posts the full + accounting (sales + VAT + COGS + Inventory). """ - for i in invoice.get_itemtxs_data()[0]: - # car = models.Car.objects.get(vin=invoice.get_itemtxs_data()[0].first().item_model.name) - car = i.item_model.car - entity = invoice.ledger.entity - coa = entity.get_default_coa() + invoice.make_payment(amount) + invoice.save() - cash_account = ( - entity.get_all_accounts() - .filter(role_default=True, role=roles.ASSET_CA_CASH) - .first() - ) - inventory_account = car.get_inventory_account() - revenue_account = car.get_revenue_account() - cogs_account = car.get_cogs_account() + _post_sale_and_cogs(invoice, dealer) - # make_account = entity.get_all_accounts().filter(name=car.id_car_make.name,role=roles.COGS).first() - # if not make_account: - # last_account = entity.get_all_accounts().filter(role=roles.COGS).order_by('-created').first() - # if len(last_account.code) == 4: - # code = f"{int(last_account.code)}{1:03d}" - # elif len(last_account.code) > 4: - # code = f"{int(last_account.code)+1}" +def _post_sale_and_cogs(invoice, dealer): + """ + For every car line on the invoice: + 1) Cash / A-R / VAT / Revenue journal + 2) COGS / Inventory journal + """ + entity = invoice.ledger.entity + calc = CarFinanceCalculator(invoice) + data = calc.get_finance_data() - # make_account = entity.create_account( - # name=car.id_car_make.name, - # code=code, - # role=roles.COGS, - # coa_model=coa, - # balance_type="debit", - # active=True - # ) + cash_acc = entity.get_all_accounts().filter(role_default=True, role=roles.ASSET_CA_CASH).first() + ar_acc = entity.get_all_accounts().filter(role_default=True, role=roles.ASSET_CA_RECEIVABLES).first() + vat_acc = entity.get_all_accounts().filter(role_default=True, role=roles.LIABILITY_CL_TAXES_PAYABLE).first() + car_rev = entity.get_all_accounts().filter(role_default=True, role=roles.INCOME_OPERATIONAL).first() + add_rev = entity.get_all_accounts().filter(role_default=True, role=roles.INCOME_OPERATIONAL).first() + cogs_acc = entity.get_all_accounts().filter(role_default=True, role=roles.COGS).first() + inv_acc = entity.get_all_accounts().filter(role_default=True, role=roles.ASSET_CA_INVENTORY).first() - # # get or create additional services account - # additional_services_account = entity.get_default_coa_accounts().filter(name="Additional Services",role=roles.COGS).first() - # if not additional_services_account: - # last_account = entity.get_all_accounts().filter(role=roles.COGS).order_by('-created').first() - # if len(last_account.code) == 4: - # code = f"{int(last_account.code)}{1:03d}" - # elif len(last_account.code) > 4: - # code = f"{int(last_account.code)+1}" + for car_data in data['cars']: + car = invoice.get_itemtxs_data()[0].filter( + item_model__car__vin=car_data['vin'] + ).first().item_model.car + qty = Decimal(car_data['quantity']) - # additional_services_account = entity.create_account( - # name="Additional Services", - # code=code, - # role=roles.COGS, - # coa_model=coa, - # balance_type="debit", - # active=True - # ) + net_car_price = Decimal(car_data['total']) + net_add_price = Decimal(data['total_additionals']) + vat_amount = Decimal(data['total_vat_amount']) * qty + grand_total = net_car_price + net_add_price + vat_amount + cost_total = Decimal(car_data['cost_price']) * qty - # inventory_account = entity.get_default_coa_accounts().filter(role=roles.ASSET_CA_INVENTORY).first() + # ------------------------------------------------------------------ + # 2A. Journal: Cash / A-R / VAT / Sales + # ------------------------------------------------------------------ - # vat_payable_account = entity.get_default_coa_accounts().get(name="VAT Payable", active=True) - - journal = JournalEntryModel.objects.create( - posted=False, - description=f"Payment for Invoice {invoice.invoice_number}", + je_sale = JournalEntryModel.objects.create( ledger=invoice.ledger, + description=f"Sale {car.vin}", + origin=f"Invoice {invoice.invoice_number}", locked=False, - origin=f"Sale of {car.id_car_make.name}{car.vin}: Invoice {invoice.invoice_number}", + posted=False + ) + # Dr Cash (what the customer paid) + TransactionModel.objects.create( + journal_entry=je_sale, + account=cash_acc, + amount=grand_total, + tx_type='debit' ) + # # Cr A/R (clear the receivable) + # TransactionModel.objects.create( + # journal_entry=je_sale, + # account=ar_acc, + # amount=grand_total, + # tx_type='credit' + # ) + + # Cr VAT Payable TransactionModel.objects.create( - journal_entry=journal, - account=cash_account, - amount=Decimal(finance_data.get("grand_total")), - tx_type="debit", - description="", + journal_entry=je_sale, + account=vat_acc, + amount=vat_amount, + tx_type='credit' ) + # Cr Sales – Car TransactionModel.objects.create( - journal_entry=journal, - account=revenue_account, - amount=Decimal(finance_data.get("grand_total")), - tx_type="credit", - description="", + journal_entry=je_sale, + account=car_rev, + amount=net_car_price, + tx_type='credit' ) - journal_cogs = JournalEntryModel.objects.create( - posted=False, - description=f"COGS of {car.id_car_make.name}{car.vin}: Invoice {invoice.invoice_number}", - ledger=invoice.ledger, - locked=False, - origin="Payment", - ) - TransactionModel.objects.create( - journal_entry=journal_cogs, - account=cogs_account, - amount=Decimal(car.finances.cost_price), - tx_type="debit", - description="", - ) - - TransactionModel.objects.create( - journal_entry=journal_cogs, - account=inventory_account, - amount=Decimal(car.finances.cost_price), - tx_type="credit", - description="", - ) - try: - car.item_model.for_inventory = False - logger.debug(f"Set item_model.for_inventory to False for car {car.vin}.") - except Exception as e: - logger.error( - f"Error updating item_model.for_inventory for car {car.vin} (Invoice {invoice.invoice_number}): {e}", - exc_info=True, + if net_add_price > 0: + # Cr Sales – Additional Services + TransactionModel.objects.create( + journal_entry=je_sale, + account=add_rev, + amount=net_add_price, + tx_type='credit' ) + # ------------------------------------------------------------------ + # 2B. Journal: COGS / Inventory reduction + # ------------------------------------------------------------------ + je_cogs = JournalEntryModel.objects.create( + ledger=invoice.ledger, + description=f"COGS {car.vin}", + origin=f"Invoice {invoice.invoice_number}", + locked=False, + posted=False + ) + + # Dr COGS + TransactionModel.objects.create( + journal_entry=je_cogs, + account=cogs_acc, + amount=cost_total, + tx_type='debit' + ) + + # Cr Inventory + TransactionModel.objects.create( + journal_entry=je_cogs, + account=inv_acc, + amount=cost_total, + tx_type='credit' + ) + # ------------------------------------------------------------------ + # 2C. Update car state flags inside the same transaction + # ------------------------------------------------------------------ + entity.get_items_inventory().filter(name=car.vin).update(for_inventory=False) + # car.item_model.for_inventory = False + # car.item_model.save(update_fields=['for_inventory']) + car.finances.selling_price = grand_total car.finances.is_sold = True car.finances.save() - car.item_model.save() +# def handle_account_process(invoice, amount, finance_data): +# """ +# Processes accounting transactions based on an invoice, financial data, +# and related entity accounts configuration. This function handles the +# creation of accounts if they do not already exist, and processes journal +# entries and transactions. + +# :param invoice: The invoice object to process transactions for. +# :type invoice: InvoiceModel +# :param amount: Total monetary value for the transaction. +# :type amount: Decimal +# :param finance_data: Dictionary containing financial details such as +# 'grand_total', 'total_vat_amount', and other related data. +# :type finance_data: dict +# :return: None +# """ +# for i in invoice.get_itemtxs_data()[0]: +# # car = models.Car.objects.get(vin=invoice.get_itemtxs_data()[0].first().item_model.name) +# car = i.item_model.car +# entity = invoice.ledger.entity +# coa = entity.get_default_coa() + +# cash_account = ( +# entity.get_all_accounts() +# .filter(role_default=True, role=roles.ASSET_CA_CASH) +# .first() +# ) +# inventory_account = car.get_inventory_account() +# revenue_account = car.get_revenue_account() +# cogs_account = car.get_cogs_account() + +# journal = JournalEntryModel.objects.create( +# posted=False, +# description=f"Payment for Invoice {invoice.invoice_number}", +# ledger=invoice.ledger, +# locked=False, +# origin=f"Sale of {car.id_car_make.name}{car.vin}: Invoice {invoice.invoice_number}", +# ) + +# TransactionModel.objects.create( +# journal_entry=journal, +# account=cash_account, +# amount=Decimal(finance_data.get("grand_total")), +# tx_type="debit", +# description="", +# ) + +# TransactionModel.objects.create( +# journal_entry=journal, +# account=revenue_account, +# amount=Decimal(finance_data.get("grand_total")), +# tx_type="credit", +# description="", +# ) + +# journal_cogs = JournalEntryModel.objects.create( +# posted=False, +# description=f"COGS of {car.id_car_make.name}{car.vin}: Invoice {invoice.invoice_number}", +# ledger=invoice.ledger, +# locked=False, +# origin="Payment", +# ) +# TransactionModel.objects.create( +# journal_entry=journal_cogs, +# account=cogs_account, +# amount=Decimal(car.finances.cost_price), +# tx_type="debit", +# description="", +# ) + +# TransactionModel.objects.create( +# journal_entry=journal_cogs, +# account=inventory_account, +# amount=Decimal(car.finances.cost_price), +# tx_type="credit", +# description="", +# ) +# try: +# car.item_model.for_inventory = False +# logger.debug(f"Set item_model.for_inventory to False for car {car.vin}.") +# except Exception as e: +# logger.error( +# f"Error updating item_model.for_inventory for car {car.vin} (Invoice {invoice.invoice_number}): {e}", +# exc_info=True, +# ) + +# car.finances.is_sold = True +# car.finances.save() +# car.item_model.save() # TransactionModel.objects.create( # journal_entry=journal, diff --git a/inventory/views.py b/inventory/views.py index 137c29ff..666c2011 100644 --- a/inventory/views.py +++ b/inventory/views.py @@ -67,7 +67,7 @@ from django.contrib.messages.views import SuccessMessageMixin from django.contrib.auth.mixins import PermissionRequiredMixin from django.contrib.auth.decorators import permission_required from django.shortcuts import render, get_object_or_404, redirect -from plans.models import Order, PlanPricing, AbstractOrder, UserPlan, BillingInfo +from plans.models import Plan, Order, PlanPricing, AbstractOrder, UserPlan, BillingInfo from django.views.generic import ( View, ListView, @@ -145,6 +145,7 @@ from .override import ( BillModelUpdateView as BillModelUpdateViewBase, BaseBillActionView as BaseBillActionViewBase, InventoryListView as InventoryListViewBase, + InvoiceModelUpdateView as InvoiceModelUpdateViewBase, ) from django_ledger.models import ( @@ -180,8 +181,6 @@ from django_ledger.views.mixins import ( ) # Other -from plans.models import Plan - from . import models, forms, tables from django_tables2 import SingleTableView from django_tables2.export.views import ExportMixin @@ -1145,7 +1144,6 @@ class CarListView(LoginRequiredMixin, PermissionRequiredMixin, ListView): context = super().get_context_data(**kwargs) dealer = get_user_type(self.request) cars = models.Car.objects.filter(dealer=dealer).order_by("receiving_date") - context["stats"] = { "all": cars.count(), "available": cars.filter(status="available").count(), @@ -1231,7 +1229,7 @@ def inventory_stats_view(request, dealer_slug): # Base queryset for cars belonging to the dealer cars = models.Car.objects.filter(dealer=request.dealer) - + print(cars) # Count for total, reserved, showroom, and unreserved cars total_cars = cars.count() reserved_cars = models.CarReservation.objects.count() @@ -1517,13 +1515,13 @@ class CarFinanceCreateView(LoginRequiredMixin, PermissionRequiredMixin, CreateVi 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) - form.fields[ - "additional_finances" - ].queryset = models.AdditionalServices.objects.filter(dealer=dealer) - return form + # def get_form(self, form_class=None): + # form = super().get_form(form_class) + # dealer = get_user_type(self.request) + # form.fields[ + # "additional_finances" + # ].queryset = models.AdditionalServices.objects.filter(dealer=dealer) + # return form class CarFinanceUpdateView( @@ -1570,21 +1568,21 @@ class CarFinanceUpdateView( kwargs["instance"] = self.get_object() return kwargs - def get_initial(self): - initial = super().get_initial() - instance = self.get_object() - dealer = get_user_type(self.request) - selected_items = instance.additional_services.filter(dealer=dealer) - initial["additional_finances"] = selected_items - return initial + # def get_initial(self): + # initial = super().get_initial() + # instance = self.get_object() + # dealer = get_user_type(self.request) + # 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) - form.fields[ - "additional_finances" - ].queryset = models.AdditionalServices.objects.filter(dealer=dealer) - return form + # def get_form(self, form_class=None): + # form = super().get_form(form_class) + # dealer = get_user_type(self.request) + # form.fields[ + # "additional_finances" + # ].queryset = models.AdditionalServices.objects.filter(dealer=dealer) + # return form class CarUpdateView( @@ -2405,7 +2403,6 @@ class CustomerCreateView( success_message = "Customer created successfully" def form_valid(self, form): - sleep(5) if customer := models.Customer.objects.filter( email=form.instance.email ).first(): @@ -4328,12 +4325,12 @@ def sales_list_view(request, dealer_slug): qs = [] try: if any([request.is_dealer, request.is_manager, request.is_accountant]): - qs = models.ExtraInfo.get_sale_orders(staff=staff, is_dealer=True) + qs = models.ExtraInfo.get_sale_orders(staff=staff, is_dealer=True,dealer=dealer) elif request.is_staff: - qs = models.ExtraInfo.get_sale_orders(staff=staff) + qs = models.ExtraInfo.get_sale_orders(staff=staff,dealer=dealer) except Exception as e: print(e) - print(qs[0]) + # query = request.GET.get('q') # # if query: # # qs = qs.filter( @@ -4418,7 +4415,6 @@ class EstimateListView(LoginRequiredMixin, PermissionRequiredMixin, ListView): def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) dealer = get_object_or_404(models.Dealer, slug=self.kwargs["dealer_slug"]) - staff = getattr(self.request.user.staffmember, "staff", None) if any( [ @@ -4431,16 +4427,19 @@ class EstimateListView(LoginRequiredMixin, PermissionRequiredMixin, ListView): dealer=dealer, content_type=ContentType.objects.get_for_model(EstimateModel), related_content_type=ContentType.objects.get_for_model(models.Staff), - ) - print(qs) + ).union(models.ExtraInfo.objects.filter( + dealer=dealer, + content_type=ContentType.objects.get_for_model(EstimateModel), + related_content_type=ContentType.objects.get_for_model(User), + )) + elif self.request.is_staff and self.request.is_sales: qs = models.ExtraInfo.objects.filter( dealer=dealer, content_type=ContentType.objects.get_for_model(EstimateModel), related_content_type=ContentType.objects.get_for_model(models.Staff), - related_object_id=staff.pk, + related_object_id=self.request.staff.pk, ) - context["staff_estimates"] = qs return context @@ -4579,6 +4578,7 @@ def create_estimate(request, dealer_slug, slug=None): ).all() for i in car_instance[: int(quantities[0])]: + print(i) items_txs.append( { "item_number": i.item_model.item_number, @@ -4641,11 +4641,11 @@ def create_estimate(request, dealer_slug, slug=None): opportunity.estimate = estimate opportunity.save() - if staff := getattr(request.user.staffmember, "staff", None): + if request.is_staff: models.ExtraInfo.objects.create( dealer=dealer, content_object=estimate, - related_object=staff, + related_object=request.staff, created_by=request.user, ) else: @@ -4839,21 +4839,21 @@ def create_sale_order(request, dealer_slug, pk): return redirect("estimate_detail", dealer_slug=dealer_slug, pk=estimate.pk) form = forms.SaleOrderForm() - customer = estimate.customer.customer_set.first() - form.fields["estimate"].queryset = EstimateModel.objects.filter(pk=pk) - form.initial["estimate"] = estimate - form.fields["customer"].queryset = models.Customer.objects.filter(pk=customer.pk) - form.initial["customer"] = customer - if hasattr(estimate, "opportunity"): - form.initial["opportunity"] = estimate.opportunity - else: - form.fields["opportunity"].widget = HiddenInput() + # customer = estimate.customer.customer_set.first() + # form.fields["estimate"].queryset = EstimateModel.objects.filter(pk=pk) + # form.initial["estimate"] = estimate + # form.fields["customer"].queryset = models.Customer.objects.filter(pk=customer.pk) + # form.initial["customer"] = customer + # if hasattr(estimate, "opportunity"): + # form.initial["opportunity"] = estimate.opportunity + # else: + # form.fields["opportunity"].widget = HiddenInput() calculator = CarFinanceCalculator(estimate) finance_data = calculator.get_finance_data() return render( request, - "sales/estimates/sale_order_form1.html", + "sales/estimates/sale_order_form.html", {"form": form, "estimate": estimate, "items": items, "data": finance_data}, ) @@ -4872,6 +4872,7 @@ def update_estimate_discount(request, dealer_slug, pk): extra_info.data.update({"discount": Decimal(discount_amount)}) extra_info.save() + messages.success(request, "Discount updated successfully") return redirect("estimate_detail", dealer_slug=dealer_slug, pk=pk) @@ -4888,7 +4889,7 @@ def update_estimate_additionals(request, dealer_slug, pk): form.cleaned_data["additional_finances"] ) car.finances.save() - + messages.success(request, "Additional Finances updated successfully") return redirect("estimate_detail", dealer_slug=dealer_slug, pk=pk) @@ -5147,9 +5148,9 @@ class InvoiceListView(LoginRequiredMixin, PermissionRequiredMixin, ListView): self.request.is_accountant, ] ): - qs = models.ExtraInfo.get_invoices(staff=staff, is_dealer=True) + qs = models.ExtraInfo.get_invoices(staff=staff, is_dealer=True,dealer=dealer) elif self.request.is_staff: - qs = models.ExtraInfo.get_invoices(staff=staff) + qs = models.ExtraInfo.get_invoices(staff=staff,dealer=dealer) except Exception as e: print(e) @@ -5285,7 +5286,7 @@ class ApprovedInvoiceModelUpdateFormView( def get_success_url(self): return reverse_lazy( "invoice_detail", - kwargs={"dealer_slug": self.kwargs["dealer_slug"], "pk": self.object.pk}, + kwargs={"dealer_slug": self.kwargs["dealer_slug"],"entity_slug": self.kwargs["entity_slug"], "pk": self.object.pk}, ) @@ -5333,7 +5334,7 @@ class PaidInvoiceModelUpdateFormView( def get_success_url(self): return reverse_lazy( "invoice_detail", - kwargs={"dealer_slug": self.kwargs["dealer_slug"], "pk": self.object.pk}, + kwargs={"dealer_slug": self.kwargs["dealer_slug"],"entity_slug": self.kwargs["entity_slug"], "pk": self.object.pk}, ) def form_valid(self, form): @@ -5341,7 +5342,7 @@ class PaidInvoiceModelUpdateFormView( if invoice.get_amount_open() > 0: messages.error(self.request, "Invoice is not fully paid") - return redirect("invoice_detail", pk=invoice.pk) + return redirect("invoice_detail",dealer_slug=self.kwargs["dealer_slug"],entity_slug=self.kwargs["entity_slug"], pk=invoice.pk) else: invoice.post_ledger() invoice.save() @@ -5373,12 +5374,12 @@ def invoice_mark_as(request, dealer_slug, pk): if mark and mark == "accept": if not invoice.can_approve(): messages.error(request, "invoice is not ready for approval") - return redirect("invoice_detail", dealer_slug=dealer_slug, pk=invoice.pk) + return redirect("invoice_detail", dealer_slug=dealer_slug,entity_slug=request.entity.slug, pk=invoice.pk) invoice.mark_as_approved( entity_slug=dealer.entity.slug, user_model=dealer.entity.admin ) invoice.save() - return redirect("invoice_detail", dealer_slug=dealer_slug, pk=invoice.pk) + return redirect("invoice_detail", dealer_slug=dealer_slug,entity_slug=request.entity.slug, pk=invoice.pk) @login_required @@ -5441,7 +5442,7 @@ def invoice_create(request, dealer_slug, pk): estimate.save() invoice.save() messages.success(request, "Invoice created successfully") - return redirect("invoice_detail", dealer_slug=dealer.slug, pk=invoice.pk) + return redirect("invoice_detail", dealer_slug=dealer.slug,entity_slug=entity.slug, pk=invoice.pk) else: print(form.errors) form = forms.InvoiceModelCreateForm( @@ -5501,6 +5502,32 @@ class InvoicePreviewView(LoginRequiredMixin, PermissionRequiredMixin, DetailView # payments +class InvoiceModelUpdateView(InvoiceModelUpdateViewBase): + template_name = 'sales/invoices/invoice_update.html' + permission_required = ["django_ledger.change_invoicemodel"] + + +# def PaymentCreateView(request,dealer_slug,entity_slug,invoice_pk): +# from django_ledger.forms.invoice import AccruedAndApprovedInvoiceModelUpdateForm +# invoice = get_object_or_404(InvoiceModel,pk=invoice_pk) + +# form = AccruedAndApprovedInvoiceModelUpdateForm(entity_slug=entity_slug,user_model=request.dealer.user) +# if request.method == "POST": +# if form.is_valid(): +# invoice_model: InvoiceModel = form.save(commit=False) +# if invoice_model.can_migrate(): +# invoice_model.migrate_state( +# user_model=request.dealer.user, +# entity_slug=entity_slug +# ) +# invoice_model.save() +# messages.success(request, "Invoice updated successfully") +# return redirect("invoice_detail", dealer_slug=dealer_slug,entity_slug=entity_slug, invoice_pk=invoice_model.pk) +# else: +# print(form.errors) + +# context = { "invoice": invoice, "form": form } +# return render(request, "sales/payments/payment_form1.html", context) @login_required @permission_required("inventory.add_payment", raise_exception=True) @@ -5529,15 +5556,15 @@ def PaymentCreateView(request, dealer_slug, pk): """ dealer = get_object_or_404(models.Dealer, slug=dealer_slug) invoice = InvoiceModel.objects.filter(pk=pk).first() - bill = BillModel.objects.filter(pk=pk).first() - model = invoice if invoice else bill + # bill = BillModel.objects.filter(pk=pk).first() + model = invoice + entity = dealer.entity form = forms.PaymentForm() - breakpoint() + if request.method == "POST": form = forms.PaymentForm(request.POST) - # --- Define user and model context for logging here user_id = request.user.id if request.user.is_authenticated else "Anonymous" user_username = ( request.user.username if request.user.is_authenticated else "anonymous" @@ -5546,19 +5573,19 @@ def PaymentCreateView(request, dealer_slug, pk): if form.is_valid(): amount = form.cleaned_data.get("amount") invoice = form.cleaned_data.get("invoice") - bill = form.cleaned_data.get("bill") + # bill = form.cleaned_data.get("bill") payment_method = form.cleaned_data.get("payment_method") - redirect_url = "invoice_detail" if invoice else "bill_detail" - model = invoice if invoice else bill + response = redirect("invoice_detail", dealer_slug=dealer.slug,entity_slug=entity.slug, pk=model.pk)# if invoice else "bill_detail" + # model = invoice if invoice else bill if not model.is_approved(): model.mark_as_approved(user_model=entity.admin) if model.amount_paid == model.amount_due: messages.error(request, _("fully paid")) - return redirect(redirect_url, dealer_slug=dealer.slug, pk=model.pk) + return response if model.amount_paid + amount > model.amount_due: messages.error(request, _("Amount exceeds due amount")) - return redirect(redirect_url, dealer_slug=dealer.slug, pk=model.pk) + return response try: if invoice: @@ -5566,14 +5593,14 @@ def PaymentCreateView(request, dealer_slug, pk): logger.info( f"User {user_username} (ID: {user_id}) successfully processed payment for Invoice ID: {invoice.pk} (Dealer: {dealer.slug}) Amount: {amount}, Method: {payment_method}." ) - elif bill: - set_bill_payment(dealer, entity, bill, amount, payment_method) - logger.info( - f"User {user_username} (ID: {user_id}) successfully processed payment for Bill ID: {bill.pk} (Dealer: {dealer.slug}) Amount: {amount}, Method: {payment_method}." - ) + # elif bill: + # set_bill_payment(dealer, entity, bill, amount, payment_method) + # logger.info( + # f"User {user_username} (ID: {user_id}) successfully processed payment for Bill ID: {bill.pk} (Dealer: {dealer.slug}) Amount: {amount}, Method: {payment_method}." + # ) messages.success(request, _("Payment created successfully")) - return redirect(redirect_url, dealer_slug=dealer.slug, pk=model.pk) + return response except Exception as e: logger.error( f"User {user_username} (ID: {user_id}) encountered error creating payment " @@ -5594,7 +5621,7 @@ def PaymentCreateView(request, dealer_slug, pk): form.initial["bill"] = model form.fields["invoice"].widget = HiddenInput() return render( - request, "sales/payments/payment_form.html", {"model": model, "form": form} + request, "sales/payments/payment_form1.html", {"model": model, "form": form} ) @@ -5725,7 +5752,7 @@ def payment_mark_as_paid(request, dealer_slug, pk): exc_info=True, ) messages.error(request, f"Error: {str(e)}") - return redirect("invoice_detail", dealer_slug=dealer_slug, pk=invoice.pk) + return redirect("invoice_detail", dealer_slug=dealer_slug,entity_slug=request.entity.slug, pk=invoice.pk) # activity log @@ -6577,41 +6604,43 @@ def send_lead_email(request, dealer_slug, slug, email_pk=None): ) messages.success(request, _("Email Draft successfully")) - try: - if getattr(lead, "opportunity", None): - # Log success when opportunity exists and redirecting - logger.info( - f"User {user_username} successfully drafted email for Lead ID: {lead.pk} ('{lead.slug}'). " - f"Lead has an Opportunity (ID: {lead.opportunity.pk}), redirecting to opportunity detail." - ) - response = HttpResponse( - redirect( - "opportunity_detail", - dealer_slug=dealer_slug, - slug=lead.opportunity.slug, - ) - ) - response["HX-Redirect"] = reverse( - "opportunity_detail", args=[lead.opportunity.slug] - ) - else: - # Log success when no opportunity and redirecting to lead detail - logger.info( - f"User {user_username} successfully drafted email for Lead ID: {lead.pk} ('{lead.slug}'). " - f"Lead has no Opportunity, redirecting to lead detail." - ) - response = HttpResponse() - response["HX-Redirect"] = reverse( - "lead_detail", dealer_slug=dealer_slug, slug=lead.slug - ) - return response - except models.Lead.opportunity.RelatedObjectDoesNotExist: - # --- Log when Lead.opportunity does not exist (Draft status) --- - logger.info( - f"User {user_username} drafted email for Lead ID: {lead.pk} ('{lead.slug}'). " - f"Lead's opportunity does not exist. Redirecting to lead list." - ) - return redirect("lead_list", dealer_slug=dealer.slug) + # try: + # if getattr(lead, "opportunity", None): + # # Log success when opportunity exists and redirecting + # logger.info( + # f"User {user_username} successfully drafted email for Lead ID: {lead.pk} ('{lead.slug}'). " + # f"Lead has an Opportunity (ID: {lead.opportunity.pk}), redirecting to opportunity detail." + # ) + # # response = HttpResponse( + # # redirect( + # # "opportunity_detail", + # # dealer_slug=dealer_slug, + # # slug=lead.opportunity.slug, + # # ) + # # ) + # # response["HX-Redirect"] = reverse( + # # "opportunity_detail", args=[lead.opportunity.slug] + # # ) + + # else: + # # Log success when no opportunity and redirecting to lead detail + # logger.info( + # f"User {user_username} successfully drafted email for Lead ID: {lead.pk} ('{lead.slug}'). " + # f"Lead has no Opportunity, redirecting to lead detail." + # ) + # # response = HttpResponse() + # # response["HX-Redirect"] = reverse( + # # "lead_detail", dealer_slug=dealer_slug, slug=lead.slug + # # ) + # return response + # except models.Lead.opportunity.RelatedObjectDoesNotExist: + # # --- Log when Lead.opportunity does not exist (Draft status) --- + # logger.info( + # f"User {user_username} drafted email for Lead ID: {lead.pk} ('{lead.slug}'). " + # f"Lead's opportunity does not exist. Redirecting to lead list." + # ) + # return response + # return redirect("lead_list", dealer_slug=dealer.slug) if request.method == "POST": email_pk = request.POST.get("email_pk") @@ -6643,25 +6672,30 @@ def send_lead_email(request, dealer_slug, slug, email_pk=None): activity_type=models.ActionChoices.EMAIL, ) messages.success(request, _("Email sent successfully")) - try: - if lead.opportunity: - # Log success when opportunity exists and redirecting after sending email - logger.info( - f"User {user_username} successfully sent email for Lead ID: {lead.pk} ('{lead.slug}'). " - f"Lead has an Opportunity (ID: {lead.opportunity.pk}), redirecting to opportunity detail." - ) - return redirect( - "opportunity_detail", - dealer_slug=dealer_slug, - slug=lead.opportunity.slug, - ) - except models.Lead.opportunity.RelatedObjectDoesNotExist: - # --- Log when Lead.opportunity does not exist (POST request for sending) --- - logger.info( - f"User {user_username} sent email for Lead ID: {lead.pk} ('{lead.slug}'). " - f"Lead's opportunity does not exist. Redirecting to lead list." - ) - return redirect("lead_list", dealer_slug=dealer_slug) + response = HttpResponse() + response["HX-Refresh"] = "true" + return response + # try: + # if lead.opportunity: + # # Log success when opportunity exists and redirecting after sending email + # logger.info( + # f"User {user_username} successfully sent email for Lead ID: {lead.pk} ('{lead.slug}'). " + # f"Lead has an Opportunity (ID: {lead.opportunity.pk}), redirecting to opportunity detail." + # ) + # return response + # # return redirect( + # # "opportunity_detail", + # # dealer_slug=dealer_slug, + # # slug=lead.opportunity.slug, + # # ) + # except models.Lead.opportunity.RelatedObjectDoesNotExist: + # # --- Log when Lead.opportunity does not exist (POST request for sending) --- + # logger.info( + # f"User {user_username} sent email for Lead ID: {lead.pk} ('{lead.slug}'). " + # f"Lead's opportunity does not exist. Redirecting to lead list." + # ) + # return response + # return redirect("lead_list", dealer_slug=dealer_slug) msg = f""" السلام عليكم Dear {lead.full_name}, @@ -6745,14 +6779,18 @@ class OpportunityCreateView( def get_form(self, form_class=None): dealer = get_object_or_404(models.Dealer, slug=self.kwargs.get("dealer_slug")) - staff = getattr(self.request.user.staffmember, "staff", None) form = super().get_form(form_class) form.fields["car"].queryset = models.Car.objects.filter( dealer=dealer, status="available", finances__marked_price__gt=0 ) - form.fields["lead"].queryset = models.Lead.objects.filter( - dealer=dealer, staff=staff - ) + if self.request.is_dealer: + form.fields["lead"].queryset = models.Lead.objects.filter( + dealer=dealer + ) + elif self.request.is_staff: + form.fields["lead"].queryset = models.Lead.objects.filter( + dealer=dealer, staff=self.request.staff + ) return form def get_success_url(self): @@ -9304,7 +9342,7 @@ def sse_stream(request): ) last_id = notification.id - sleep(2) + sleep(3) response = StreamingHttpResponse(event_stream(), content_type="text/event-stream") response["Cache-Control"] = "no-cache" @@ -10258,6 +10296,12 @@ def upload_cars(request, dealer_slug, pk=None): csv_data = io.StringIO(file_content) reader = csv.DictReader(csv_data) data = [x for x in reader] + if len(data) < item.quantity: + messages.error( + request, + f"CSV file has {len(data)} rows, but the quantity of the item is {item.quantity}.", + ) + return response for row in data: # Log VIN decoding and initial validation for each row logger.debug( diff --git a/static/images/CACHE/images/logos/staff/customer2/56d21851a30bf81ad30874af4c9f0290.webp b/static/images/CACHE/images/logos/staff/customer2/56d21851a30bf81ad30874af4c9f0290.webp new file mode 100644 index 00000000..ae8e9ad5 Binary files /dev/null and b/static/images/CACHE/images/logos/staff/customer2/56d21851a30bf81ad30874af4c9f0290.webp differ diff --git a/static/images/CACHE/images/user-logo/138397308bd57adbff0504b57626b17b.webp b/static/images/CACHE/images/user-logo/138397308bd57adbff0504b57626b17b.webp new file mode 100644 index 00000000..4d76ce00 Binary files /dev/null and b/static/images/CACHE/images/user-logo/138397308bd57adbff0504b57626b17b.webp differ diff --git a/static/images/logos/staff/customer2.jpg b/static/images/logos/staff/customer2.jpg new file mode 100644 index 00000000..10c5ecb6 Binary files /dev/null and b/static/images/logos/staff/customer2.jpg differ diff --git a/static/images/logos/users/customer1.jpg b/static/images/logos/users/customer1.jpg new file mode 100644 index 00000000..1dd2c040 Binary files /dev/null and b/static/images/logos/users/customer1.jpg differ diff --git a/static/images/user-logo.png b/static/images/user-logo.png new file mode 100644 index 00000000..f5e79dfd Binary files /dev/null and b/static/images/user-logo.png differ diff --git a/static/user-logo.png b/static/user-logo.png new file mode 100644 index 00000000..f5e79dfd Binary files /dev/null and b/static/user-logo.png differ diff --git a/staticfiles/images/CACHE/images/logos/staff/customer2/56d21851a30bf81ad30874af4c9f0290.webp b/staticfiles/images/CACHE/images/logos/staff/customer2/56d21851a30bf81ad30874af4c9f0290.webp new file mode 100644 index 00000000..ae8e9ad5 Binary files /dev/null and b/staticfiles/images/CACHE/images/logos/staff/customer2/56d21851a30bf81ad30874af4c9f0290.webp differ diff --git a/staticfiles/images/CACHE/images/user-logo/138397308bd57adbff0504b57626b17b.webp b/staticfiles/images/CACHE/images/user-logo/138397308bd57adbff0504b57626b17b.webp new file mode 100644 index 00000000..4d76ce00 Binary files /dev/null and b/staticfiles/images/CACHE/images/user-logo/138397308bd57adbff0504b57626b17b.webp differ diff --git a/staticfiles/images/logos/staff/customer2.jpg b/staticfiles/images/logos/staff/customer2.jpg new file mode 100644 index 00000000..10c5ecb6 Binary files /dev/null and b/staticfiles/images/logos/staff/customer2.jpg differ diff --git a/staticfiles/images/logos/users/customer1.jpg b/staticfiles/images/logos/users/customer1.jpg new file mode 100644 index 00000000..1dd2c040 Binary files /dev/null and b/staticfiles/images/logos/users/customer1.jpg differ diff --git a/staticfiles/images/user-logo.png b/staticfiles/images/user-logo.png new file mode 100644 index 00000000..f5e79dfd Binary files /dev/null and b/staticfiles/images/user-logo.png differ diff --git a/staticfiles/js/formSubmitHandler.js b/staticfiles/js/formSubmitHandler.js new file mode 100644 index 00000000..d2533f99 --- /dev/null +++ b/staticfiles/js/formSubmitHandler.js @@ -0,0 +1,51 @@ +// static/js/formSubmitHandler.js + +document.addEventListener('DOMContentLoaded', function() { + // Initialize all forms with submit buttons + const forms = document.querySelectorAll('form'); + + forms.forEach(form => { + const submitBtn = form.querySelector('button[type="submit"]'); + + if (submitBtn) { + // Store original button HTML + if (!submitBtn.dataset.originalHtml) { + submitBtn.dataset.originalHtml = submitBtn.innerHTML; + } + + form.addEventListener('submit', function(e) { + // Only proceed if form is valid + if (form.checkValidity()) { + disableSubmitButton(submitBtn); + } + }); + } + }); +}); + +/** + * Disable and show loading state on submit button + * @param {HTMLElement} button - The submit button element + */ +function disableSubmitButton(button) { + button.disabled = true; + button.innerHTML = ` + + + + ${button.dataset.savingText || 'Processing...'} + `; + button.classList.add('submitting'); +} + +/** + * Reset submit button to original state + * @param {HTMLElement} button - The submit button element + */ +function resetSubmitButton(button) { + if (button.dataset.originalHtml) { + button.innerHTML = button.dataset.originalHtml; + } + button.disabled = false; + button.classList.remove('submitting'); +} \ No newline at end of file diff --git a/staticfiles/user-logo.png b/staticfiles/user-logo.png new file mode 100644 index 00000000..f5e79dfd Binary files /dev/null and b/staticfiles/user-logo.png differ diff --git a/templates/base.html b/templates/base.html index a487fd54..ebcf636b 100644 --- a/templates/base.html +++ b/templates/base.html @@ -54,14 +54,8 @@ {% comment %} {% endcomment %} {% if LANGUAGE_CODE == 'ar' %} - - + + {% else %}
{% include 'bill/includes/card_bill.html' with dealer_slug=request.dealer.slug bill=bill_model style='bill-detail' entity_slug=view.kwargs.entity_slug %} -
+ {% csrf_token %}
{{ form|crispy }}
+
+ + + + \ No newline at end of file diff --git a/templates/crm/leads/lead_detail.html b/templates/crm/leads/lead_detail.html index 2bcb8cb3..75bec8ac 100644 --- a/templates/crm/leads/lead_detail.html +++ b/templates/crm/leads/lead_detail.html @@ -64,14 +64,8 @@
-

{{ lead.first_name }} {{ lead.last_name }}

- {% if lead.staff %} -

- {{ _("Assigned to") }}: {{ lead.staff }} -

- {% else %} -

{{ _("Not Assigned") }}

- {% endif %} +

{{ lead.first_name|capfirst }} {{ lead.last_name|capfirst }}

+

{{ lead.email|capfirst }}

@@ -107,6 +101,34 @@
+
+
+
+
+
{{ _("Assigned To") }}
+
+
+ {% if lead.staff.logo %} + Logo + {% endif %} +
+ + {% if lead.staff == request.staff %} + {{ _("Me") }} + {% elif LANGUAGE_CODE == "en" %} + {{ lead.staff.name|capfirst }} + {% else %} + {{ lead.staff.arabic_name }} + {% endif %} + +
+
+
+
+
@@ -492,12 +514,23 @@

{{ _("Emails") }}

{% if perms.inventory.change_lead %} - + {% comment %} - + {% endcomment %} + {% endif %}
@@ -791,21 +824,7 @@
{% include 'modal/delete_modal.html' %} - {% comment %} {% endcomment %} + {% include "components/email_modal.html" %} {% include "components/task_modal.html" with content_type="lead" slug=lead.slug %} diff --git a/templates/crm/leads/lead_form.html b/templates/crm/leads/lead_form.html index c9b93a34..4885db4e 100644 --- a/templates/crm/leads/lead_form.html +++ b/templates/crm/leads/lead_form.html @@ -42,7 +42,7 @@
- + {% csrf_token %} {{ form|crispy }}
diff --git a/templates/crm/leads/lead_send.html b/templates/crm/leads/lead_send.html index 39845e5a..1ea38305 100644 --- a/templates/crm/leads/lead_send.html +++ b/templates/crm/leads/lead_send.html @@ -8,7 +8,8 @@ diff --git a/templates/inventory/car_detail.html b/templates/inventory/car_detail.html index 95408ea0..b54d11d2 100644 --- a/templates/inventory/car_detail.html +++ b/templates/inventory/car_detail.html @@ -262,6 +262,10 @@ {{ car.finances.cost_price|floatformat:2 }} {% endif %} + + {% trans "Marked Price"|capfirst %} + {{ car.finances.marked_price|floatformat:2 }} + {% trans "Selling Price"|capfirst %} {{ car.finances.selling_price|floatformat:2 }} diff --git a/templates/ledger/ledger/ledger_list.html b/templates/ledger/ledger/ledger_list.html index e5777e85..0c967059 100644 --- a/templates/ledger/ledger/ledger_list.html +++ b/templates/ledger/ledger/ledger_list.html @@ -30,7 +30,7 @@ {% if ledger.invoicemodel %} {% if perms.django_ledger.view_invoicemodel %} - {{ ledger.get_wrapped_model_instance }} + {{ ledger.get_wrapped_model_instance }} {% endif %} {% elif ledger.billmodel %} {% if perms.django_ledger.view_billmodel %} diff --git a/templates/notifications.html b/templates/notifications.html index 095f9f99..5358e127 100644 --- a/templates/notifications.html +++ b/templates/notifications.html @@ -77,6 +77,16 @@