Compare commits

...

7 Commits

14 changed files with 1426 additions and 1270 deletions

View File

@ -146,6 +146,7 @@ class StaffForm(forms.ModelForm):
)
class Meta:
model = Staff
fields = ["first_name","last_name", "arabic_name", "phone_number", "address", "logo", "group"]

22
inventory/hooks.py Normal file
View File

@ -0,0 +1,22 @@
import logging
from inventory.models import Dealer
from .utils import get_accounts_data,create_account
logger = logging.getLogger(__name__)
def check_create_coa_accounts(task):
logger.info("Checking if all accounts are created")
instance_id = task.args[0]
instance = Dealer.objects.get(pk=instance_id)
entity = instance.entity
coa = entity.get_default_coa()
for account_data in get_accounts_data():
if entity.get_all_accounts().filter(code=account_data["code"]).exists():
logger.info(f"Default account already exists: {account_data['code']}")
continue
logger.info(f"Default account does not exist: {account_data['code']}")
create_account(entity, coa, account_data)
def print_results(task):
print(task.kwargs.get("dealer"))

View File

@ -0,0 +1,9 @@
from django.core.management.base import BaseCommand
from django_q.tasks import async_task, result
class Command(BaseCommand):
def handle(self, *args, **kwargs):
from inventory.models import Dealer
instance = Dealer.objects.first()
async_task(name="test_task_test",func="inventory.tasks.test_task",dealer=instance,hook="inventory.hooks.print_results")

View File

@ -2,6 +2,7 @@ import uuid
from datetime import datetime
from django.conf import settings
from django.contrib.auth.models import Permission
from inventory.validators import SaudiPhoneNumberValidator
from decimal import Decimal
from django.urls import reverse
from django.utils.text import slugify
@ -1305,7 +1306,7 @@ class Dealer(models.Model, LocalizedNameMixin):
)
arabic_name = models.CharField(max_length=255, verbose_name=_("Arabic Name"))
name = models.CharField(max_length=255, verbose_name=_("English Name"))
phone_number = PhoneNumberField(region="SA", verbose_name=_("Phone Number"))
phone_number = models.CharField(max_length=255, verbose_name=_("Phone Number"),validators=[SaudiPhoneNumberValidator])
address = models.CharField(
max_length=200, blank=True, null=True, verbose_name=_("Address")
)
@ -1431,7 +1432,7 @@ class Staff(models.Model):
last_name = models.CharField(max_length=255, verbose_name=_("Last Name"))
arabic_name = models.CharField(max_length=255, verbose_name=_("Arabic Name"))
phone_number = PhoneNumberField(region="SA", verbose_name=_("Phone Number"))
phone_number = models.CharField(max_length=255, verbose_name=_("Phone Number"),validators=[SaudiPhoneNumberValidator])
staff_type = models.CharField(
choices=StaffTypes.choices, max_length=255, verbose_name=_("Staff Type")
)
@ -1825,7 +1826,7 @@ class Organization(models.Model, LocalizedNameMixin):
)
vrn = models.CharField(max_length=15, verbose_name=_("VAT Registration Number"))
email = models.EmailField(verbose_name=_("Email"))
phone_number = PhoneNumberField(region="SA", verbose_name=_("Phone Number"))
phone_number = models.CharField(max_length=255, verbose_name=_("Phone Number"),validators=[SaudiPhoneNumberValidator])
address = models.CharField(
max_length=200, blank=True, null=True, verbose_name=_("Address")
)
@ -1959,7 +1960,7 @@ class Representative(models.Model, LocalizedNameMixin):
id_number = models.CharField(
max_length=10, unique=True, verbose_name=_("ID Number")
)
phone_number = PhoneNumberField(region="SA", verbose_name=_("Phone Number"))
phone_number = models.CharField(max_length=255, verbose_name=_("Phone Number"),validators=[SaudiPhoneNumberValidator])
email = models.EmailField(max_length=255, verbose_name=_("Email Address"))
address = models.CharField(
max_length=200, blank=True, null=True, verbose_name=_("Address")
@ -1979,7 +1980,7 @@ class Lead(models.Model):
first_name = models.CharField(max_length=50, verbose_name=_("First Name"))
last_name = models.CharField(max_length=50, verbose_name=_("Last Name"))
email = models.EmailField(verbose_name=_("Email"))
phone_number = PhoneNumberField(region="SA", verbose_name=_("Phone Number"))
phone_number = models.CharField(max_length=255, verbose_name=_("Phone Number"),validators=[SaudiPhoneNumberValidator])
address = models.CharField(
max_length=200, blank=True, null=True, verbose_name=_("Address")
)
@ -2667,7 +2668,7 @@ class Vendor(models.Model, LocalizedNameMixin):
arabic_name = models.CharField(max_length=255, verbose_name=_("Arabic Name"))
name = models.CharField(max_length=255, verbose_name=_("English Name"))
contact_person = models.CharField(max_length=100, verbose_name=_("Contact Person"))
phone_number = PhoneNumberField(region="SA", verbose_name=_("Phone Number"))
phone_number = models.CharField(max_length=255, verbose_name=_("Phone Number"),validators=[SaudiPhoneNumberValidator])
email = models.EmailField(max_length=255, verbose_name=_("Email Address"))
address = models.CharField(max_length=200, verbose_name=_("Address"))
logo = models.ImageField(
@ -3219,6 +3220,7 @@ class CustomGroup(models.Model):
"activity",
"payment",
"vendor",
],
other_perms=[
"view_car",
@ -3229,7 +3231,8 @@ class CustomGroup(models.Model):
"view_saleorder",
"view_leads",
"view_opportunity",
"view_customer",
'view_customer'
],
)
self.set_permissions(

View File

@ -178,7 +178,8 @@ def create_ledger_entity(sender, instance, created, **kwargs):
entity.create_uom(name=u[1], unit_abbr=u[0])
# Create COA accounts, background task
async_task(create_coa_accounts, instance)
async_task(name="create_coa_accounts",func=create_coa_accounts,dealer_pk=instance.pk,hook="inventory.hooks.check_create_coa_accounts")
# async_task('inventory.tasks.check_create_coa_accounts', instance, schedule_type='O', schedule_time=timedelta(seconds=20))
# create_settings(instance.pk)
# create_accounts_for_make(instance.pk)

File diff suppressed because it is too large Load Diff

View File

@ -1,284 +1,284 @@
import json
from . import models as m
from django.urls import reverse
from django.test import Client, TestCase
from django.contrib.auth import get_user_model
from django_ledger.io.io_core import get_localdate
# import json
# from . import models as m
# from django.urls import reverse
# from django.test import Client, TestCase
# from django.contrib.auth import get_user_model
# from django_ledger.io.io_core import get_localdate
from django.core.exceptions import ObjectDoesNotExist
from decimal import Decimal
from unittest.mock import MagicMock
from inventory.models import VatRate
from inventory.utils import CarFinanceCalculator
# from django.core.exceptions import ObjectDoesNotExist
# from decimal import Decimal
# from unittest.mock import MagicMock
# from inventory.models import VatRate
# from inventory.utils import CarFinanceCalculator
User = get_user_model()
# User = get_user_model()
# Create your tests here.
class ModelTest(TestCase):
"""
Test case class designed to test models in the application. The tests
verify model creation, related object creation, and appropriate
functionality for specific business logic, ensuring correctness and
reliability of the defined models.
# # Create your tests here.
# class ModelTest(TestCase):
# """
# Test case class designed to test models in the application. The tests
# verify model creation, related object creation, and appropriate
# functionality for specific business logic, ensuring correctness and
# reliability of the defined models.
This class tests the following:
- Dealer model, including associated user and entity creation.
- Car model, ensuring product creation and validations.
- Testing of car finances, including finance totals and relationships.
- Creation of additional services, ensuring the generation of corresponding
item services.
# This class tests the following:
# - Dealer model, including associated user and entity creation.
# - Car model, ensuring product creation and validations.
# - Testing of car finances, including finance totals and relationships.
# - Creation of additional services, ensuring the generation of corresponding
# item services.
:ivar vat: Vat rate created for testing purposes.
:type vat: VatRate instance
:ivar dealer: Dealer instance created for testing.
:type dealer: Dealer instance
:ivar car_make: Car make created for testing purposes.
:type car_make: CarMake instance
:ivar car_model: Car model created for testing purposes.
:type car_model: CarModel instance
:ivar car_serie: Car series created for testing purposes.
:type car_serie: CarSerie instance
:ivar trim: Car trim created for testing purposes.
:type trim: CarTrim instance
:ivar car: Car object created for testing.
:type car: Car instance
:ivar car_finances: Car finance object for the car under test.
:type car_finances: CarFinance instance
"""
# :ivar vat: Vat rate created for testing purposes.
# :type vat: VatRate instance
# :ivar dealer: Dealer instance created for testing.
# :type dealer: Dealer instance
# :ivar car_make: Car make created for testing purposes.
# :type car_make: CarMake instance
# :ivar car_model: Car model created for testing purposes.
# :type car_model: CarModel instance
# :ivar car_serie: Car series created for testing purposes.
# :type car_serie: CarSerie instance
# :ivar trim: Car trim created for testing purposes.
# :type trim: CarTrim instance
# :ivar car: Car object created for testing.
# :type car: Car instance
# :ivar car_finances: Car finance object for the car under test.
# :type car_finances: CarFinance instance
# """
def setUp(self):
email = "RkzgO@example.com"
name = "John Doe"
password = "password"
crn = "123456789"
vrn = "123456789"
phone = "123456789"
address = "123 Main St"
arabic_name = "الاسم بالعربية"
# def setUp(self):
# email = "RkzgO@example.com"
# name = "John Doe"
# password = "password"
# crn = "123456789"
# vrn = "123456789"
# phone = "123456789"
# address = "123 Main St"
# arabic_name = "الاسم بالعربية"
self.vat = m.VatRate.objects.create(rate=0.15)
# self.vat = m.VatRate.objects.create(rate=0.15)
user = User.objects.create(username=email, email=email)
user.set_password(password)
user.save()
# user = User.objects.create(username=email, email=email)
# user.set_password(password)
# user.save()
self.dealer = m.Dealer.objects.create(
user=user,
name=name,
arabic_name=arabic_name,
crn=crn,
vrn=vrn,
phone_number=phone,
address=address,
)
# self.dealer = m.Dealer.objects.create(
# user=user,
# name=name,
# arabic_name=arabic_name,
# crn=crn,
# vrn=vrn,
# phone_number=phone,
# address=address,
# )
self.car_make = m.CarMake.objects.create(name="Make")
self.car_model = m.CarModel.objects.create(
name="Model", id_car_make=self.car_make
)
self.car_serie = m.CarSerie.objects.create(
name="Serie", id_car_model=self.car_model
)
self.trim = m.CarTrim.objects.create(name="Trim", id_car_serie=self.car_serie)
self.car = m.Car.objects.create(
vin="123456789",
dealer=self.dealer,
id_car_make=self.car_make,
id_car_model=self.car_model,
id_car_serie=self.car_serie,
year=2020,
id_car_trim=self.trim,
receiving_date=get_localdate(),
)
# self.car_make = m.CarMake.objects.create(name="Make")
# self.car_model = m.CarModel.objects.create(
# name="Model", id_car_make=self.car_make
# )
# self.car_serie = m.CarSerie.objects.create(
# name="Serie", id_car_model=self.car_model
# )
# self.trim = m.CarTrim.objects.create(name="Trim", id_car_serie=self.car_serie)
# self.car = m.Car.objects.create(
# vin="123456789",
# dealer=self.dealer,
# id_car_make=self.car_make,
# id_car_model=self.car_model,
# id_car_serie=self.car_serie,
# year=2020,
# id_car_trim=self.trim,
# receiving_date=get_localdate(),
# )
self.car_finances = m.CarFinance.objects.create(
car=self.car, selling_price=1000, cost_price=500, discount_amount=200
)
# self.car_finances = m.CarFinance.objects.create(
# car=self.car, selling_price=1000, cost_price=500, discount_amount=200
# )
def test_dealer_creation_creates_user_and_entity(self):
dealer = self.dealer
# def test_dealer_creation_creates_user_and_entity(self):
# dealer = self.dealer
self.assertEqual(User.objects.count(), 1)
self.assertEqual(m.Dealer.objects.count(), 1)
self.assertEqual(User.objects.first().username, "RkzgO@example.com")
self.assertEqual(User.objects.first().email, "RkzgO@example.com")
self.assertTrue(User.objects.first().check_password("password"))
self.assertEqual(dealer.user, User.objects.first())
self.assertEqual(dealer.name, "John Doe")
self.assertEqual(dealer.arabic_name, "الاسم بالعربية")
self.assertEqual(dealer.crn, "123456789")
self.assertEqual(dealer.vrn, "123456789")
self.assertEqual(dealer.phone_number, "123456789")
self.assertEqual(dealer.address, "123 Main St")
# self.assertEqual(User.objects.count(), 1)
# self.assertEqual(m.Dealer.objects.count(), 1)
# self.assertEqual(User.objects.first().username, "RkzgO@example.com")
# self.assertEqual(User.objects.first().email, "RkzgO@example.com")
# self.assertTrue(User.objects.first().check_password("password"))
# self.assertEqual(dealer.user, User.objects.first())
# self.assertEqual(dealer.name, "John Doe")
# self.assertEqual(dealer.arabic_name, "الاسم بالعربية")
# self.assertEqual(dealer.crn, "123456789")
# self.assertEqual(dealer.vrn, "123456789")
# self.assertEqual(dealer.phone_number, "123456789")
# self.assertEqual(dealer.address, "123 Main St")
self.assertIsNotNone(dealer.entity)
self.assertEqual(dealer.entity.name, dealer.name)
# self.assertIsNotNone(dealer.entity)
# self.assertEqual(dealer.entity.name, dealer.name)
self.assertEqual(dealer.entity.get_all_accounts().count(), 57)
self.assertEqual(dealer.entity.get_uom_all().count(), 16)
# self.assertEqual(dealer.entity.get_all_accounts().count(), 57)
# self.assertEqual(dealer.entity.get_uom_all().count(), 16)
def test_car_creation_creates_product(self):
dealer = self.dealer
# def test_car_creation_creates_product(self):
# dealer = self.dealer
self.assertEqual(m.Car.objects.count(), 1)
self.assertEqual(self.car.vin, "123456789")
self.assertEqual(self.car.dealer, dealer)
self.assertEqual(self.car.id_car_make, self.car_make)
self.assertEqual(self.car.id_car_model, self.car_model)
self.assertEqual(self.car.id_car_serie, self.car_serie)
self.assertEqual(self.car.year, 2020)
self.assertEqual(self.car.id_car_trim, self.trim)
# self.assertEqual(m.Car.objects.count(), 1)
# self.assertEqual(self.car.vin, "123456789")
# self.assertEqual(self.car.dealer, dealer)
# self.assertEqual(self.car.id_car_make, self.car_make)
# self.assertEqual(self.car.id_car_model, self.car_model)
# self.assertEqual(self.car.id_car_serie, self.car_serie)
# self.assertEqual(self.car.year, 2020)
# self.assertEqual(self.car.id_car_trim, self.trim)
product = dealer.entity.get_items_all().filter(name=self.car.vin).first()
self.assertEqual(product.name, self.car.vin)
# product = dealer.entity.get_items_all().filter(name=self.car.vin).first()
# self.assertEqual(product.name, self.car.vin)
def test_car_finances_creation(self):
self.assertEqual(m.CarFinance.objects.count(), 1)
self.assertEqual(self.car_finances.car, self.car)
self.assertEqual(self.car_finances.selling_price, 1000)
self.assertEqual(self.car_finances.cost_price, 500)
self.assertEqual(self.car_finances.discount_amount, 200)
# def test_car_finances_creation(self):
# self.assertEqual(m.CarFinance.objects.count(), 1)
# self.assertEqual(self.car_finances.car, self.car)
# self.assertEqual(self.car_finances.selling_price, 1000)
# self.assertEqual(self.car_finances.cost_price, 500)
# self.assertEqual(self.car_finances.discount_amount, 200)
def test_car_finance_total(self):
self.assertEqual(m.CarFinance.objects.count(), 1)
self.assertEqual(self.car_finances.total, 1000)
self.assertEqual(self.car_finances.total_discount, 800)
self.assertEqual(self.car_finances.total_vat, 920)
# def test_car_finance_total(self):
# self.assertEqual(m.CarFinance.objects.count(), 1)
# self.assertEqual(self.car_finances.total, 1000)
# self.assertEqual(self.car_finances.total_discount, 800)
# self.assertEqual(self.car_finances.total_vat, 920)
def test_car_additional_services_create_item_service(self):
m.AdditionalServices.objects.create(
name="Service",
price=100,
description="Description",
dealer=self.dealer,
taxable=True,
uom=m.UnitOfMeasure.PIECE,
)
# def test_car_additional_services_create_item_service(self):
# m.AdditionalServices.objects.create(
# name="Service",
# price=100,
# description="Description",
# dealer=self.dealer,
# taxable=True,
# uom=m.UnitOfMeasure.PIECE,
# )
self.assertEqual(
m.ItemModel.objects.filter(
name="Service",
default_amount=100,
is_product_or_service=True,
item_role="service",
).count(),
1,
)
# self.assertEqual(
# m.ItemModel.objects.filter(
# name="Service",
# default_amount=100,
# is_product_or_service=True,
# item_role="service",
# ).count(),
# 1,
# )
class AuthenticationTest(TestCase):
"""
Represents a set of test cases for user authentication and signup validation within
a web application. These tests ensure the correctness of authentication functionalities
such as login and signing up with appropriate JSON data handling.
# class AuthenticationTest(TestCase):
# """
# Represents a set of test cases for user authentication and signup validation within
# a web application. These tests ensure the correctness of authentication functionalities
# such as login and signing up with appropriate JSON data handling.
:ivar client: Django test client used to simulate HTTP requests within the test framework.
:type client: Client
:ivar url: URL for account signup endpoint used in the test cases.
:type url: str
"""
# :ivar client: Django test client used to simulate HTTP requests within the test framework.
# :type client: Client
# :ivar url: URL for account signup endpoint used in the test cases.
# :type url: str
# """
def setUp(self):
self.client = Client()
self.url = reverse("account_signup")
# def setUp(self):
# self.client = Client()
# self.url = reverse("account_signup")
def test_login(self):
url = reverse("account_login")
response = self.client.post(
url, {"email": "RkzgO@example.com", "password": "password"}
)
self.assertEqual(response.status_code, 200)
# def test_login(self):
# url = reverse("account_login")
# response = self.client.post(
# url, {"email": "RkzgO@example.com", "password": "password"}
# )
# self.assertEqual(response.status_code, 200)
def test_valid_data(self):
# Create valid JSON data
data = {
"wizardValidationForm1": {
"email": "test@example.com",
"password": "password123",
"confirm_password": "password123",
},
"wizardValidationForm2": {
"name": "John Doe",
"arabic_name": "جون دو",
"phone_number": "1234567890",
},
"wizardValidationForm3": {
"crn": "123456",
"vrn": "789012",
"address": "123 Main St",
},
}
# def test_valid_data(self):
# # Create valid JSON data
# data = {
# "wizardValidationForm1": {
# "email": "test@example.com",
# "password": "password123",
# "confirm_password": "password123",
# },
# "wizardValidationForm2": {
# "name": "John Doe",
# "arabic_name": "جون دو",
# "phone_number": "1234567890",
# },
# "wizardValidationForm3": {
# "crn": "123456",
# "vrn": "789012",
# "address": "123 Main St",
# },
# }
# Send a POST request with the JSON data
response = self.client.post(
self.url, data=json.dumps(data), content_type="application/json"
)
# # Send a POST request with the JSON data
# response = self.client.post(
# self.url, data=json.dumps(data), content_type="application/json"
# )
# Check the response
self.assertEqual(response.status_code, 200)
self.assertEqual(response.json(), {"message": "User created successfully."})
# # Check the response
# self.assertEqual(response.status_code, 200)
# self.assertEqual(response.json(), {"message": "User created successfully."})
def test_passwords_do_not_match(self):
# Create JSON data with mismatched passwords
data = {
"wizardValidationForm1": {
"email": "test@example.com",
"password": "password123",
"confirm_password": "differentpassword",
},
"wizardValidationForm2": {
"name": "John Doe",
"arabic_name": "جون دو",
"phone_number": "1234567890",
},
"wizardValidationForm3": {
"crn": "123456",
"vrn": "789012",
"address": "123 Main St",
},
}
# def test_passwords_do_not_match(self):
# # Create JSON data with mismatched passwords
# data = {
# "wizardValidationForm1": {
# "email": "test@example.com",
# "password": "password123",
# "confirm_password": "differentpassword",
# },
# "wizardValidationForm2": {
# "name": "John Doe",
# "arabic_name": "جون دو",
# "phone_number": "1234567890",
# },
# "wizardValidationForm3": {
# "crn": "123456",
# "vrn": "789012",
# "address": "123 Main St",
# },
# }
# Send a POST request with the JSON data
response = self.client.post(
self.url, data=json.dumps(data), content_type="application/json"
)
# # Send a POST request with the JSON data
# response = self.client.post(
# self.url, data=json.dumps(data), content_type="application/json"
# )
# Check the response
self.assertEqual(response.status_code, 400)
self.assertEqual(response.json(), {"error": "Passwords do not match."})
# # Check the response
# self.assertEqual(response.status_code, 400)
# self.assertEqual(response.json(), {"error": "Passwords do not match."})
def test_missing_required_fields(self):
# Create JSON data with missing required fields
data = {
"wizardValidationForm1": {
"email": "test@example.com",
"password": "password123",
# Missing "confirm_password"
},
"wizardValidationForm2": {
"name": "John Doe",
"arabic_name": "جون دو",
"phone_number": "1234567890",
},
"wizardValidationForm3": {
"crn": "123456",
"vrn": "789012",
"address": "123 Main St",
},
}
# def test_missing_required_fields(self):
# # Create JSON data with missing required fields
# data = {
# "wizardValidationForm1": {
# "email": "test@example.com",
# "password": "password123",
# # Missing "confirm_password"
# },
# "wizardValidationForm2": {
# "name": "John Doe",
# "arabic_name": "جون دو",
# "phone_number": "1234567890",
# },
# "wizardValidationForm3": {
# "crn": "123456",
# "vrn": "789012",
# "address": "123 Main St",
# },
# }
# Send a POST request with the JSON data
response = self.client.post(
self.url, data=json.dumps(data), content_type="application/json"
)
# # Send a POST request with the JSON data
# response = self.client.post(
# self.url, data=json.dumps(data), content_type="application/json"
# )
# Check the response
self.assertEqual(response.status_code, 400)
self.assertIn(
"error", response.json()
) # Assuming the view returns an error for missing fields
# # Check the response
# self.assertEqual(response.status_code, 400)
# self.assertIn(
# "error", response.json()
# ) # Assuming the view returns an error for missing fields
# class CarFinanceCalculatorTests(TestCase):

View File

@ -1,5 +1,6 @@
import json
import secrets
import logging
import datetime
import requests
from decimal import Decimal
@ -27,7 +28,7 @@ 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_q.models import Schedule as DjangoQSchedule
from django_ledger.io import roles
logger = logging.getLogger(__name__)
@ -223,13 +224,21 @@ def reserve_car(car, request):
:return: Redirection to the car's detail page.
"""
try:
reserved_until = timezone.now() + timezone.timedelta(hours=24)
models.CarReservation.objects.create(
# reserved_until = timezone.now() + timezone.timedelta(hours=24)
reserved_until = timezone.now() + timezone.timedelta(minutes=1)
reservation = models.CarReservation.objects.create(
car=car, reserved_by=request.user, reserved_until=reserved_until
)
car.status = models.CarStatusChoices.RESERVED
car.save()
# --- Logging for Success ---
DjangoQSchedule.objects.create(
name=f"remove_reservation_for_car_with_vin_{car.vin}",
func='inventory.tasks.remove_reservation_by_id',
args=reservation.pk,
schedule_type=DjangoQSchedule.ONCE,
next_run=reserved_until,
)
logger.info(
f"Car {car.pk} ('{car.id_car_make} {car.id_car_model}') reserved successfully "
f"by user {request.user}. "
@ -1288,7 +1297,7 @@ def get_finance_data(estimate,dealer):
)
discount = extra_info.data.get("discount", 0)
discount = Decimal(discount)
additional_services = car.get_additional_services()
discounted_price=(Decimal(car.marked_price) - discount)
vat_amount = discounted_price * vat.rate
@ -1305,7 +1314,7 @@ def get_finance_data(estimate,dealer):
"final_price": discounted_price+ vat_amount,
"total_services_vat":total_services_vat,
"total_vat":total_vat,
"grand_total": discounted_price + vat_amount + additional_services.get("total")
"grand_total": discounted_price + total_vat + additional_services.get("total")
}
@ -1581,6 +1590,7 @@ def _post_sale_and_cogs(invoice, dealer):
vat_amount = Decimal(data['vat_amount'])
grand_total = net_car_price + net_additionals_price + vat_amount
cost_total = Decimal(car.cost_price)
discount_amount =Decimal(data['discount_amount'])
# ------------------------------------------------------------------
# 2A. Journal: Cash / A-R / VAT / Sales
@ -1666,6 +1676,7 @@ def _post_sale_and_cogs(invoice, dealer):
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.discount_amount=discount_amount
car.selling_price = grand_total
# car.is_sold = True
car.save()
@ -1913,3 +1924,416 @@ def handle_payment(request, order):
# def get_user_quota(user):
# return user.dealer.quota
def get_accounts_data():
return [
# Current Assets (must start with 1)
{
"code": "1010",
"name": "Cash on Hand",
"role": roles.ASSET_CA_CASH,
"balance_type": roles.DEBIT,
"locked": False,
"default": True, # Default for ASSET_CA_CASH
},
{
"code": "1020",
"name": "Bank",
"role": roles.ASSET_CA_CASH,
"balance_type": roles.DEBIT,
"locked": False,
"default": False,
},
{
"code": "1030",
"name": "Accounts Receivable",
"role": roles.ASSET_CA_RECEIVABLES,
"balance_type": roles.DEBIT,
"locked": False,
"default": True, # Default for ASSET_CA_RECEIVABLES
},
{
"code": "1040",
"name": "Inventory (Cars)",
"role": roles.ASSET_CA_INVENTORY,
"balance_type": roles.DEBIT,
"locked": False,
"default": True, # Default for ASSET_CA_INVENTORY
},
{
"code": "1045",
"name": "Spare Parts Inventory",
"role": roles.ASSET_CA_INVENTORY,
"balance_type": roles.DEBIT,
"locked": False,
"default": False,
},
{
"code": "1050",
"name": "Employee Advances",
"role": roles.ASSET_CA_RECEIVABLES,
"balance_type": roles.DEBIT,
"locked": False,
"default": False,
},
{
"code": "1060",
"name": "Prepaid Expenses",
"role": roles.ASSET_CA_PREPAID,
"balance_type": roles.DEBIT,
"locked": False,
"default": True, # Default for ASSET_CA_PREPAID
},
{
"code": "1070",
"name": "Notes Receivable",
"role": roles.ASSET_LTI_NOTES_RECEIVABLE,
"balance_type": roles.DEBIT,
"locked": False,
"default": True, # Default for ASSET_LTI_NOTES_RECEIVABLE
},
# Fixed Assets (must also start with 1)
{
"code": "1110",
"name": "Lands",
"role": roles.ASSET_LTI_LAND,
"balance_type": roles.DEBIT,
"locked": False,
"default": True, # Default for ASSET_LTI_LAND
},
{
"code": "1111",
"name": "Buildings",
"role": roles.ASSET_PPE_BUILDINGS,
"balance_type": roles.DEBIT,
"locked": False,
"default": True, # Default for ASSET_PPE_BUILDINGS
},
{
"code": "1112",
"name": "Company Vehicles",
"role": roles.ASSET_PPE_EQUIPMENT,
"balance_type": roles.DEBIT,
"locked": False,
"default": True, # Default for ASSET_PPE_EQUIPMENT
},
{
"code": "1113",
"name": "Equipment & Tools",
"role": roles.ASSET_PPE_EQUIPMENT,
"balance_type": roles.DEBIT,
"locked": False,
"default": False,
},
{
"code": "1114",
"name": "Furniture & Fixtures",
"role": roles.ASSET_PPE_EQUIPMENT,
"balance_type": roles.DEBIT,
"locked": False,
"default": False,
},
{
"code": "1115",
"name": "Other Fixed Assets",
"role": roles.ASSET_PPE_EQUIPMENT,
"balance_type": roles.DEBIT,
"locked": False,
"default": False,
},
{
"code": "1120",
"name": "Long-term Investments",
"role": roles.ASSET_LTI_SECURITIES,
"balance_type": roles.DEBIT,
"locked": False,
"default": True, # Default for ASSET_LTI_SECURITIES
},
{
"code": "1130",
"name": "Intangible Assets",
"role": roles.ASSET_INTANGIBLE_ASSETS,
"balance_type": roles.DEBIT,
"locked": False,
"default": True, # Default for ASSET_INTANGIBLE_ASSETS
},
# Current Liabilities (must start with 2)
{
"code": "2010",
"name": "Accounts Payable",
"role": roles.LIABILITY_CL_ACC_PAYABLE,
"balance_type": roles.CREDIT,
"locked": True,
"default": True, # Default for LIABILITY_CL_ACC_PAYABLE
},
{
"code": "2020",
"name": "Notes Payable",
"role": roles.LIABILITY_CL_ST_NOTES_PAYABLE,
"balance_type": roles.CREDIT,
"locked": False,
"default": True, # Default for LIABILITY_CL_ST_NOTES_PAYABLE
},
{
"code": "2030",
"name": "Short-term Loans",
"role": roles.LIABILITY_CL_ST_NOTES_PAYABLE,
"balance_type": roles.CREDIT,
"locked": False,
"default": False,
},
{
"code": "2040",
"name": "Employee Payables",
"role": roles.LIABILITY_CL_WAGES_PAYABLE,
"balance_type": roles.CREDIT,
"locked": False,
"default": True, # Default for LIABILITY_CL_WAGES_PAYABLE
},
{
"code": "2050",
"name": "Accrued Expenses",
"role": roles.LIABILITY_CL_OTHER,
"balance_type": roles.CREDIT,
"locked": False,
"default": True, # Default for LIABILITY_CL_OTHER
},
{
"code": "2060",
"name": "Accrued Taxes",
"role": roles.LIABILITY_CL_TAXES_PAYABLE,
"balance_type": roles.CREDIT,
"locked": False,
"default": False, # Default for LIABILITY_CL_TAXES_PAYABLE
},
{
"code": "2070",
"name": "Provisions",
"role": roles.LIABILITY_CL_OTHER,
"balance_type": roles.CREDIT,
"locked": False,
"default": False,
},
# Long-term Liabilities (must also start with 2)
{
"code": "2103",
"name": "Deferred Revenue",
"role": roles.LIABILITY_CL_DEFERRED_REVENUE,
"balance_type": roles.CREDIT,
"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",
"role": roles.LIABILITY_LTL_NOTES_PAYABLE,
"balance_type": roles.CREDIT,
"locked": False,
"default": True, # Default for LIABILITY_LTL_NOTES_PAYABLE
},
{
"code": "2220",
"name": "Lease Liabilities",
"role": roles.LIABILITY_LTL_NOTES_PAYABLE,
"balance_type": roles.CREDIT,
"locked": False,
"default": False,
},
{
"code": "2230",
"name": "Other Long-term Liabilities",
"role": roles.LIABILITY_LTL_NOTES_PAYABLE,
"balance_type": roles.CREDIT,
"locked": False,
"default": False,
},
# Equity (must start with 3)
{
"code": "3010",
"name": "Capital",
"role": roles.EQUITY_CAPITAL,
"balance_type": roles.CREDIT,
"locked": True,
"default": True, # Default for EQUITY_CAPITAL
},
{
"code": "3020",
"name": "Statutory Reserve",
"role": roles.EQUITY_ADJUSTMENT,
"balance_type": roles.CREDIT,
"locked": False,
"default": True, # Default for EQUITY_ADJUSTMENT
},
{
"code": "3030",
"name": "Retained Earnings",
"role": roles.EQUITY_ADJUSTMENT,
"balance_type": roles.CREDIT,
"locked": False,
"default": False,
},
{
"code": "3040",
"name": "Profit & Loss for the Period",
"role": roles.EQUITY_ADJUSTMENT,
"balance_type": roles.CREDIT,
"locked": False,
"default": False,
},
# Revenue (must start with 4)
{
"code": "4010",
"name": "Car Sales",
"role": roles.INCOME_OPERATIONAL,
"balance_type": roles.CREDIT,
"locked": True,
"default": True, # Default for INCOME_OPERATIONAL
},
{
"code": "4020",
"name": "After-Sales Services",
"role": roles.INCOME_OPERATIONAL,
"balance_type": roles.CREDIT,
"locked": False,
"default": False,
},
{
"code": "4030",
"name": "Car Rental Income",
"role": roles.INCOME_PASSIVE,
"balance_type": roles.CREDIT,
"locked": False,
"default": True, # Default for INCOME_PASSIVE
},
{
"code": "4040",
"name": "Other Income",
"role": roles.INCOME_OTHER,
"balance_type": roles.CREDIT,
"locked": False,
"default": True, # Default for INCOME_OTHER
},
# Expenses (must start with 5 for COGS, 6 for others)
{
"code": "5010",
"name": "Cost of Goods Sold",
"role": roles.COGS,
"balance_type": roles.DEBIT,
"locked": True,
"default": True, # Default for COGS
},
{
"code": "5015",
"name": "Spare Parts Cost Consumed",
"role": roles.COGS,
"balance_type": roles.DEBIT,
"locked": False,
"default": False,
},
{
"code": "6010",
"name": "Salaries & Wages",
"role": roles.EXPENSE_OPERATIONAL,
"balance_type": roles.DEBIT,
"locked": False,
"default": True, # Default for EXPENSE_OPERATIONAL
},
{
"code": "6020",
"name": "Rent",
"role": roles.EXPENSE_OPERATIONAL,
"balance_type": roles.DEBIT,
"locked": False,
"default": False,
},
{
"code": "6030",
"name": "Utilities",
"role": roles.EXPENSE_OPERATIONAL,
"balance_type": roles.DEBIT,
"locked": False,
"default": False,
},
{
"code": "6040",
"name": "Advertising & Marketing",
"role": roles.EXPENSE_OPERATIONAL,
"balance_type": roles.DEBIT,
"locked": False,
"default": False,
},
{
"code": "6050",
"name": "Maintenance",
"role": roles.EXPENSE_OPERATIONAL,
"balance_type": roles.DEBIT,
"locked": False,
"default": False,
},
{
"code": "6060",
"name": "Operating Expenses",
"role": roles.EXPENSE_OPERATIONAL,
"balance_type": roles.DEBIT,
"locked": False,
"default": False,
},
{
"code": "6070",
"name": "Depreciation",
"role": roles.EXPENSE_DEPRECIATION,
"balance_type": roles.DEBIT,
"locked": False,
"default": True, # Default for EXPENSE_DEPRECIATION
},
{
"code": "6080",
"name": "Fees & Taxes",
"role": roles.EXPENSE_OPERATIONAL,
"balance_type": roles.DEBIT,
"locked": False,
"default": False,
},
{
"code": "6090",
"name": "Bank Charges",
"role": roles.EXPENSE_OPERATIONAL,
"balance_type": roles.DEBIT,
"locked": False,
"default": False,
},
{
"code": "6100",
"name": "Other Expenses",
"role": roles.EXPENSE_OTHER,
"balance_type": roles.DEBIT,
"locked": False,
"default": True, # Default for EXPENSE_OTHER
},
]
def create_account(entity, coa, account_data):
try:
account = entity.create_account(
coa_model=coa,
code=account_data["code"],
name=account_data["name"],
role=account_data["role"],
balance_type=_(account_data["balance_type"]),
active=True,
)
account.role_default = account_data["default"]
account.save()
logger.info(f"Created default account: {account}")
except Exception as e:
logger.error(f"Error creating default account: {account_data['code']}, {e}")

View File

@ -451,39 +451,45 @@ def general_dashboard(request,dealer_slug):
total_cars_sold = cars_sold_filtered.count()
total_cost_of_cars_sold = cars_sold_filtered.aggregate(total=Sum('cost_price'))['total'] or 0
total_revenue_from_cars = cars_sold_filtered.aggregate(
total=Sum(F('selling_price') - F('discount_amount'))
)['total'] or 0
# Correct calculation for VAT based on the selling price
total_vat_collected_from_cars = cars_sold_filtered.aggregate(
total=Sum(F('selling_price') * VAT_RATE)
total=Sum(F('marked_price') - F('discount_amount'))
)['total'] or 0
total_vat_collected_from_cars = cars_sold_filtered.annotate(
final_price=F('marked_price') - F('discount_amount')).aggregate(
total=Sum(F('final_price') * VAT_RATE))['total'] or 0
net_profit_from_cars = total_revenue_from_cars - total_cost_of_cars_sold
total_discount = cars_sold_filtered.aggregate(total=Sum('discount_amount'))['total'] or 0
# Sales breakdown by type
new_cars_sold = cars_sold_filtered.filter(stock_type='new')
total_new_cars_sold = new_cars_sold.count()
total_cost_of_new_cars_sold = new_cars_sold.aggregate(total=Sum('cost_price'))['total'] or 0
# total_revenue_from_new_cars=sum([ car.final_price for car in new_cars_sold])
total_revenue_from_new_cars = new_cars_sold.aggregate(
total=Sum(F('selling_price') - F('discount_amount'))
total=Sum(F('marked_price') - F('discount_amount'))
)['total'] or 0
total_vat_collected_from_new_cars = new_cars_sold.annotate(
final_price=F('marked_price') - F('discount_amount')).aggregate(
total=Sum(F('final_price') * VAT_RATE))['total'] or 0
net_profit_from_new_cars = total_revenue_from_new_cars - total_cost_of_new_cars_sold
total_vat_collected_from_new_cars = new_cars_sold.aggregate(
total=Sum(F('selling_price') * VAT_RATE)
)['total'] or 0
used_cars_sold = cars_sold_filtered.filter(stock_type='used')
total_used_cars_sold = used_cars_sold.count()
total_cost_of_used_cars_sold = used_cars_sold.aggregate(total=Sum('cost_price'))['total'] or 0
total_revenue_from_used_cars = used_cars_sold.aggregate(
total=Sum(F('selling_price') - F('discount_amount'))
total=Sum(F('marked_price') - F('discount_amount'))
)['total'] or 0
total_vat_collected_from_used_cars = used_cars_sold.annotate(
final_price=F('marked_price') - F('discount_amount')).aggregate(
total=Sum(F('final_price') * VAT_RATE))['total'] or 0
net_profit_from_used_cars = total_revenue_from_used_cars - total_cost_of_used_cars_sold
total_vat_collected_from_used_cars = used_cars_sold.aggregate(
total=Sum(F('selling_price') * VAT_RATE)
)['total'] or 0
# Service & Overall KPIs
total_revenue_from_services = sum([car.get_additional_services()['total'] for car in cars_sold_filtered])
@ -502,8 +508,8 @@ def general_dashboard(request,dealer_slug):
month=ExtractMonth('sold_date')
).values('month').annotate(
total_cars=Count('pk'),
total_revenue=Sum(F('selling_price') - F('discount_amount')),
total_profit=Sum(F('selling_price') - F('discount_amount') - F('cost_price'))
total_revenue=Sum(F('marked_price') - F('discount_amount')),
total_profit=Sum(F('marked_price') - F('discount_amount') - F('cost_price'))
).order_by('month')
monthly_cars_sold = [0] * 12
@ -1477,8 +1483,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)
cars = models.Car.objects.filter(dealer=request.dealer)
# Count for total, reserved, showroom, and unreserved cars
total_cars = cars.count()
reserved_cars = models.CarReservation.objects.count()
@ -11067,6 +11072,9 @@ def purchase_report_csv_export(request,dealer_slug):
@login_required
def car_sale_report_view(request, dealer_slug):
dealer = get_object_or_404(models.Dealer, slug=dealer_slug)
vat = models.VatRate.objects.filter(dealer=dealer,is_active=True).first()
VAT_RATE=vat.rate
cars_sold = models.Car.objects.filter(dealer=dealer, status='sold')
@ -11092,8 +11100,13 @@ def car_sale_report_view(request, dealer_slug):
# # Calculate summary data for the filtered results
total_cars_sold=cars_sold.count()
total_revenue_from_cars = sum([ car.final_price for car in cars_sold])
total_vat_on_cars=sum([car.vat_amount for car in cars_sold])
total_revenue_from_cars = cars_sold.aggregate(
total=Sum(F('marked_price') - F('discount_amount'))
)['total'] or 0
total_vat_on_cars=cars_sold.annotate(
final_price=F('marked_price') - F('discount_amount')).aggregate(
total=Sum(F('final_price') * VAT_RATE))['total'] or 0
total_revenue_from_additonals=sum([car.get_additional_services()['total'] for car in cars_sold])
total_vat_from_additonals=sum([car.get_additional_services()['services_vat'] for car in cars_sold])

View File

@ -433,6 +433,15 @@
</div>
</a>
</div>
{% if request.user.is_authenticated%}
<div class="d-flex mx-4 px-4">
<div class="navbar-logo">
<div class="d-flex align-items-center">
<h5 class="text-warning ms-2 d-none d-sm-block">{% trans 'Hello, ' %}{{ request.user.first_name|default:request.dealer.name }} {{ request.user.last_name }}</h5>
</div>
</a>
</div>
{% endif %}
<ul class="navbar-nav navbar-nav-icons flex-row gap-2" hx-boost="false">
<li class="nav-item">
@ -537,7 +546,7 @@
</ul>
</hr>
<div class="px-3">
<a class="btn btn-sm btn-phoenix-danger d-flex flex-center w-100" href="{% url 'account_logout' %}"> <span class="me-2" data-feather="log-out"> </span>{% trans 'Sign Out' %}</a>
<a class="btn btn-sm btn-phoenix-danger d-flex flex-center w-100" href="{% url 'account_logout' %}"> <span class="fas fa-power-off me-2"> </span>{% trans 'Sign Out' %}</a>
</div>
<div class="my-2 text-center fw-bold fs-10 text-body-quaternary">
<a class="text-body-quaternary me-1" href="">{% trans 'Privacy policy' %}</a>&bull;<a class="text-body-quaternary mx-1" href="">{% trans 'Terms' %}</a>&bull;<a class="text-body-quaternary ms-1" href="">Cookies</a>

View File

@ -88,7 +88,7 @@
<td class="align-middle white-space-nowrap text-center fw-bold text-body-tertiary">
{% if car.stock_type == "new" %}
<span class="badge badge-phoenix badge-phoenix-success"><span class="badge-label">{{ _("New") }}</span></span>
{% elif car.status == "used" %}
{% elif car.stock_type == "used" %}
<span class="badge badge-phoenix badge-phoenix-info"><span class="badge-label">{{ _("Used") }}</span></span>
{% endif %}
</td>

View File

@ -5,10 +5,50 @@
{% endblock %}
{% block content %}
{% block customCSS%}
<style>
.road {
width: 70%;
height: 10vh;
position: relative;
overflow: hidden; /* This is essential for the animation to work correctly */
}
.moving-car {
position: absolute;
font-size: 3rem;
font-weight: bold;
color: #2e5466;
bottom: 20%;
white-space: nowrap; /* Prevents the text from wrapping to a new line */
left: 100%; /* Start the text outside the right edge */
animation: moveText 5s linear infinite alternate;
}
@keyframes moveText {
0% {
transform: translateX(0);
}
100% {
transform: translateX(-100%);
}
}
</style>
{% endblock %}
{% if inventory.total_cars > 0 %}
<div class="row justify-content-between">
<div class="col-sm-12 ">
<div class="card border h-100 w-100 p-lg-10">
{% comment %} <div class="road-container">
<img src="https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcSY0ESBb8625a12EguCyY8j4eL93sY3ibEAuQ&s" alt="Car" id="car" class="img-fluid" style="height:30px; width:30px;">
</div> {% endcomment %}
<div class="road">
<p class="moving-car ">Powered By Tenhal&nbsp;&nbsp;&nbsp;&nbsp;</p>
</div>
<div class="bg-holder bg-card"
style="background-image:url({% static 'images/spot-illustrations/32.png' %});
background-position: top right"></div>
@ -100,3 +140,29 @@
{% include "empty-illustration-page.html" with value="car" url=create_car_url %}
{% endif %}
{% endblock %}
{% block customJS%}
<script>
document.addEventListener('DOMContentLoaded', () => {
const car = document.getElementById('car');
let positionX = 10;
const speed = 5;
document.addEventListener('keydown', (event) => {
if (event.key === 'ArrowRight') {
positionX += speed;
car.style.left = positionX + '%';
} else if (event.key === 'ArrowLeft') {
positionX -= speed;
if (positionX < 0) {
positionX = 0;
}
car.style.left = positionX + '%';
}
});
});
</script>
{% endblock %}

View File

@ -172,7 +172,7 @@
{% for item in items %}
<div class="option" data-value="{{ item.hash }}" data-image="{{item.logo}}">
<img src="{{item.logo}}" alt="{{item.model}}">
<span>{{item.make}} {{item.model}} {{item.serie}} {{item.trim}} {{item.color_name}}</span>
<span>{{item.vin}} {{item.make}} {{item.model}} {{item.serie}} {{item.trim}} {{item.color_name}}</span>
<div class="color-box" style="background-color: rgb({{ item.exterior_color }});"></div>
<div class="color-box" style="background-color: rgb({{ item.interior_color }});"></div>
<span style="color:gray;">({{item.hash_count}} in stock)</span>

View File

@ -29,7 +29,6 @@
{% else %}
<span class="rounded-circle d-inline-flex align-items-center justify-content-center bg-primary text-white"
style="width: 160px; height: 160px; font-size: 4rem;">
{{ staff.get_local_name|first|upper }}
</span>
{% endif %}
{% comment %} <input class="d-none" id="avatarFile" type="file"/>
@ -38,7 +37,7 @@
<i class="fas fa-camera"></i>
</label> {% endcomment %}
</div>
<h2 class="h3 mb-1 fw-bold">{{ staff.get_local_name }}</h2>
<h2 class="h3 mb-1 fw-bold">{{ staff.fullname }}</h2>
<p class="text-body-secondary mt-3 mb-0">
<i class="fas fa-clock me-1"></i>{% trans 'Joined' %} {{ staff.created|timesince }} {% trans 'ago' %}
</p>