This commit is contained in:
Faheedkhan 2025-06-22 13:40:18 +03:00
commit 10cb1235d6
124 changed files with 6561 additions and 3902 deletions

View File

@ -2,5 +2,5 @@ from django.apps import AppConfig
class ApiConfig(AppConfig): class ApiConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField' default_auto_field = "django.db.models.BigAutoField"
name = 'api' name = "api"

View File

@ -32,4 +32,4 @@
# await self.send(text_data=json.dumps({ # await self.send(text_data=json.dumps({
# 'message': 'VIN received', # 'message': 'VIN received',
# 'vin': vin # 'vin': vin
# })) # }))

View File

@ -4,19 +4,28 @@ from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
initial = True initial = True
dependencies = [ dependencies = []
]
operations = [ operations = [
migrations.CreateModel( migrations.CreateModel(
name='CarVIN', name="CarVIN",
fields=[ fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), (
('vin', models.CharField(max_length=17, verbose_name='VIN')), "id",
('created', models.DateTimeField(auto_now_add=True, verbose_name='created')), models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("vin", models.CharField(max_length=17, verbose_name="VIN")),
(
"created",
models.DateTimeField(auto_now_add=True, verbose_name="created"),
),
], ],
), ),
] ]

View File

@ -8,4 +8,3 @@ class CarVIN(models.Model):
def __str__(self): def __str__(self):
return self.vin return self.vin

View File

@ -6,78 +6,93 @@ from inventory import models as inventory_models
class CarVINSerializer(serializers.ModelSerializer): class CarVINSerializer(serializers.ModelSerializer):
class Meta: class Meta:
model = models.CarVIN model = models.CarVIN
fields = ['vin'] fields = ["vin"]
def create(self, validated_data): def create(self, validated_data):
vin = validated_data.pop('vin') vin = validated_data.pop("vin")
return models.CarVIN.objects.create(vin=vin, **validated_data) return models.CarVIN.objects.create(vin=vin, **validated_data)
class CarMakeSerializer(serializers.ModelSerializer): class CarMakeSerializer(serializers.ModelSerializer):
car_models = serializers.PrimaryKeyRelatedField(many=True, read_only=True, source='carmodel_set') car_models = serializers.PrimaryKeyRelatedField(
many=True, read_only=True, source="carmodel_set"
)
class Meta: class Meta:
model = inventory_models.CarMake model = inventory_models.CarMake
fields = '__all__' fields = "__all__"
class CarModelSerializer(serializers.ModelSerializer): class CarModelSerializer(serializers.ModelSerializer):
car_series = serializers.PrimaryKeyRelatedField(many=True, read_only=True, source='carserie_set') car_series = serializers.PrimaryKeyRelatedField(
many=True, read_only=True, source="carserie_set"
)
class Meta: class Meta:
model = inventory_models.CarModel model = inventory_models.CarModel
fields = '__all__' fields = "__all__"
class CarSerieSerializer(serializers.ModelSerializer): class CarSerieSerializer(serializers.ModelSerializer):
car_trims = serializers.PrimaryKeyRelatedField(many=True, read_only=True, source='cartrim_set') car_trims = serializers.PrimaryKeyRelatedField(
many=True, read_only=True, source="cartrim_set"
)
class Meta: class Meta:
model = inventory_models.CarSerie model = inventory_models.CarSerie
fields = '__all__' fields = "__all__"
class CarTrimSerializer(serializers.ModelSerializer): class CarTrimSerializer(serializers.ModelSerializer):
car_equipments = serializers.PrimaryKeyRelatedField(many=True, read_only=True, source='carequipment_set') car_equipments = serializers.PrimaryKeyRelatedField(
car_specification_values = serializers.PrimaryKeyRelatedField(many=True, read_only=True, many=True, read_only=True, source="carequipment_set"
source='carspecificationvalue_set') )
car_specification_values = serializers.PrimaryKeyRelatedField(
many=True, read_only=True, source="carspecificationvalue_set"
)
class Meta: class Meta:
model = inventory_models.CarTrim model = inventory_models.CarTrim
fields = '__all__' fields = "__all__"
class CarEquipmentSerializer(serializers.ModelSerializer): class CarEquipmentSerializer(serializers.ModelSerializer):
car_option_values = serializers.PrimaryKeyRelatedField(many=True, read_only=True, source='caroptionvalue_set') car_option_values = serializers.PrimaryKeyRelatedField(
many=True, read_only=True, source="caroptionvalue_set"
)
class Meta: class Meta:
model = inventory_models.CarEquipment model = inventory_models.CarEquipment
fields = '__all__' fields = "__all__"
class CarSpecificationSerializer(serializers.ModelSerializer): class CarSpecificationSerializer(serializers.ModelSerializer):
child_specifications = serializers.PrimaryKeyRelatedField(many=True, read_only=True, source='carspecification_set') child_specifications = serializers.PrimaryKeyRelatedField(
many=True, read_only=True, source="carspecification_set"
)
class Meta: class Meta:
model = inventory_models.CarSpecification model = inventory_models.CarSpecification
fields = '__all__' fields = "__all__"
class CarSpecificationValueSerializer(serializers.ModelSerializer): class CarSpecificationValueSerializer(serializers.ModelSerializer):
class Meta: class Meta:
model = inventory_models.CarSpecificationValue model = inventory_models.CarSpecificationValue
fields = '__all__' fields = "__all__"
class CarOptionSerializer(serializers.ModelSerializer): class CarOptionSerializer(serializers.ModelSerializer):
child_options = serializers.PrimaryKeyRelatedField(many=True, read_only=True, source='caroption_set') child_options = serializers.PrimaryKeyRelatedField(
many=True, read_only=True, source="caroption_set"
)
class Meta: class Meta:
model = inventory_models.CarOption model = inventory_models.CarOption
fields = '__all__' fields = "__all__"
class CarOptionValueSerializer(serializers.ModelSerializer): class CarOptionValueSerializer(serializers.ModelSerializer):
class Meta: class Meta:
model = inventory_models.CarOptionValue model = inventory_models.CarOptionValue
fields = '__all__' fields = "__all__"

View File

@ -2,19 +2,12 @@ import requests
def get_bearer(): def get_bearer():
api_token = "f5204a00-6f31-4de2-96d8-ed998e0d230c" api_token = "f5204a00-6f31-4de2-96d8-ed998e0d230c"
api_secret = "8c11320781a5b8f4f327b6937e6f8241" api_secret = "8c11320781a5b8f4f327b6937e6f8241"
url = "https://carapi.app/api/auth/login" url = "https://carapi.app/api/auth/login"
headers = { headers = {"accept": "text/plain", "Content-Type": "application/json"}
"accept": "text/plain", data = {"api_token": api_token, "api_secret": api_secret}
"Content-Type": "application/json"
}
data = {
"api_token": api_token,
"api_secret": api_secret
}
response = requests.post(url, headers=headers, json=data) response = requests.post(url, headers=headers, json=data)
@ -26,11 +19,8 @@ def get_bearer():
def get_car_data(vin): def get_car_data(vin):
url = f"https://carapi.app/api/vin/{vin}?verbose=no&all_trims=no" url = f"https://carapi.app/api/vin/{vin}?verbose=no&all_trims=no"
headers = { headers = {"Authorization": f"Bearer {get_bearer()}"}
"Authorization": f"Bearer {get_bearer()}"
}
try: try:
response = requests.get(url, headers=headers) response = requests.get(url, headers=headers)
@ -52,10 +42,8 @@ def get_from_cardatabase(vin):
url = "https://api.vehicledatabases.com/premium/vin-decode/{vin}" url = "https://api.vehicledatabases.com/premium/vin-decode/{vin}"
payload = {} payload = {}
headers = { headers = {"x-AuthKey": "3cefdfd4272445f1929b5801c55d8fa5"}
'x-AuthKey': '3cefdfd4272445f1929b5801c55d8fa5'
}
response = requests.request("GET", url, headers=headers, data=payload) response = requests.request("GET", url, headers=headers, data=payload)
print(response.text) print(response.text)

View File

@ -1,2 +1 @@
# Create your tests here.
# Create your tests here.

View File

@ -5,20 +5,20 @@ from api import views
router = routers.DefaultRouter() router = routers.DefaultRouter()
router.register(r'car-makes', views.CarMakeViewSet) router.register(r"car-makes", views.CarMakeViewSet)
router.register(r'car-models', views.CarModelViewSet) router.register(r"car-models", views.CarModelViewSet)
router.register(r'car-series', views.CarSerieViewSet) router.register(r"car-series", views.CarSerieViewSet)
router.register(r'car-trims', views.CarTrimViewSet) router.register(r"car-trims", views.CarTrimViewSet)
router.register(r'car-equipments', views.CarEquipmentViewSet) router.register(r"car-equipments", views.CarEquipmentViewSet)
router.register(r'car-specifications', views.CarSpecificationViewSet) router.register(r"car-specifications", views.CarSpecificationViewSet)
router.register(r'car-specification-values', views.CarSpecificationValueViewSet) router.register(r"car-specification-values", views.CarSpecificationValueViewSet)
router.register(r'car-options', views.CarOptionViewSet) router.register(r"car-options", views.CarOptionViewSet)
router.register(r'car-option-values', views.CarOptionValueViewSet) router.register(r"car-option-values", views.CarOptionValueViewSet)
urlpatterns = [ urlpatterns = [
path('', include(router.urls)), path("", include(router.urls)),
path('cars/vin/', views.CarVINViewSet.as_view(), name='car_vin'), path("cars/vin/", views.CarVINViewSet.as_view(), name="car_vin"),
path("cars/", views.car_list, name="car-list"), path("cars/", views.car_list, name="car-list"),
path('login/', views.LoginView.as_view(), name='login'), path("login/", views.LoginView.as_view(), name="login"),
path("decode-vin/", views.VinDecodeAPIView.as_view(), name="api-decode-vin"), path("decode-vin/", views.VinDecodeAPIView.as_view(), name="api-decode-vin"),
] ]

View File

@ -16,7 +16,9 @@ logger = logging.getLogger(__name__)
class LoginView(APIView): class LoginView(APIView):
permission_classes = [permissions.AllowAny,] permission_classes = [
permissions.AllowAny,
]
# def post(self, request, *args, **kwargs): # def post(self, request, *args, **kwargs):
# username = request.data.get('username') # username = request.data.get('username')
@ -35,7 +37,7 @@ class LoginView(APIView):
class CarVINViewSet(APIView): class CarVINViewSet(APIView):
queryset = models.CarVIN.objects.all().order_by('-created') queryset = models.CarVIN.objects.all().order_by("-created")
serializer_class = serializers.CarVINSerializer serializer_class = serializers.CarVINSerializer
def get(self, request): def get(self, request):
@ -100,30 +102,27 @@ class CarOptionValueViewSet(viewsets.ModelViewSet):
serializer_class = serializers.CarOptionValueSerializer serializer_class = serializers.CarOptionValueSerializer
def car_list(request): def car_list(request):
dealer = get_user_type(request) dealer = get_user_type(request)
page = request.GET.get("page", 1) page = request.GET.get("page", 1)
per_page = 10 per_page = 10
cars = inventory_models.Car.objects.all().values( cars = inventory_models.Car.objects.all().values(
"vin", "vin", "year", "id_car_make__name", "id_car_model__name", "status"
"year",
"id_car_make__name",
"id_car_model__name",
"status"
) )
paginator = Paginator(cars, per_page) paginator = Paginator(cars, per_page)
page_obj = paginator.get_page(page) page_obj = paginator.get_page(page)
return JsonResponse({ return JsonResponse(
"data": list(page_obj), # Convert QuerySet to list {
"page": page_obj.number, # Current page number "data": list(page_obj), # Convert QuerySet to list
"per_page": per_page, "page": page_obj.number, # Current page number
"total_pages": paginator.num_pages, # Total pages "per_page": per_page,
"total_items": paginator.count # Total records "total_pages": paginator.num_pages, # Total pages
}) "total_items": paginator.count, # Total records
}
)
class VinDecodeAPIView(APIView): class VinDecodeAPIView(APIView):
@ -162,4 +161,4 @@ class VinDecodeAPIView(APIView):
"modelYear": result.get("year_model"), "modelYear": result.get("year_model"),
} }
return Response({"success": True, "data": vin_data}, status=status.HTTP_200_OK) return Response({"success": True, "data": vin_data}, status=status.HTTP_200_OK)

View File

@ -15,13 +15,11 @@ from channels.routing import ProtocolTypeRouter, URLRouter
from channels.auth import AuthMiddlewareStack from channels.auth import AuthMiddlewareStack
from api import routing from api import routing
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'car_inventory.settings') os.environ.setdefault("DJANGO_SETTINGS_MODULE", "car_inventory.settings")
application = ProtocolTypeRouter({ application = ProtocolTypeRouter(
"http": get_asgi_application(), {
"websocket": AuthMiddlewareStack( "http": get_asgi_application(),
URLRouter( "websocket": AuthMiddlewareStack(URLRouter(routing.websocket_urlpatterns)),
routing.websocket_urlpatterns }
) )
),
})

View File

@ -4,6 +4,7 @@ from django.conf.urls.static import static
from django.conf import settings from django.conf import settings
from django.conf.urls.i18n import i18n_patterns from django.conf.urls.i18n import i18n_patterns
from inventory import views from inventory import views
# import debug_toolbar # import debug_toolbar
from schema_graph.views import Schema from schema_graph.views import Schema
# from two_factor.urls import urlpatterns as tf_urls # from two_factor.urls import urlpatterns as tf_urls
@ -11,24 +12,22 @@ from schema_graph.views import Schema
urlpatterns = [ urlpatterns = [
# path('__debug__/', include(debug_toolbar.urls)), # path('__debug__/', include(debug_toolbar.urls)),
# path('silk/', include('silk.urls', namespace='silk')), # path('silk/', include('silk.urls', namespace='silk')),
path('api-auth/', include('rest_framework.urls')), path("api-auth/", include("rest_framework.urls")),
path('api/', include('api.urls')), path("api/", include("api.urls")),
# path('dj-rest-auth/', include('dj_rest_auth.urls')), # path('dj-rest-auth/', include('dj_rest_auth.urls')),
] ]
urlpatterns += i18n_patterns( urlpatterns += i18n_patterns(
path('admin/', admin.site.urls), path("admin/", admin.site.urls),
path('switch_language/', views.switch_language, name='switch_language'), path("switch_language/", views.switch_language, name="switch_language"),
path('accounts/', include('allauth.urls')), path("accounts/", include("allauth.urls")),
# path('prometheus/', include('django_prometheus.urls')), # path('prometheus/', include('django_prometheus.urls')),
path('', include('inventory.urls')), path("", include("inventory.urls")),
path('ledger/', include('django_ledger.urls', namespace='django_ledger')), path("ledger/", include("django_ledger.urls", namespace="django_ledger")),
path("haikalbot/", include("haikalbot.urls")), path("haikalbot/", include("haikalbot.urls")),
path('appointment/', include('appointment.urls')), path("appointment/", include("appointment.urls")),
path('plans/', include('plans.urls')), path("plans/", include("plans.urls")),
path("schema/", Schema.as_view()), path("schema/", Schema.as_view()),
path('tours/', include('tours.urls')), path("tours/", include("tours.urls")),
# path('', include(tf_urls)), # path('', include(tf_urls)),
) )

View File

@ -11,6 +11,6 @@ import os
from django.core.wsgi import get_wsgi_application from django.core.wsgi import get_wsgi_application
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'car_inventory.settings') os.environ.setdefault("DJANGO_SETTINGS_MODULE", "car_inventory.settings")
application = get_wsgi_application() application = get_wsgi_application()

View File

@ -6,23 +6,23 @@
# -- Project information ----------------------------------------------------- # -- Project information -----------------------------------------------------
# https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information # https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information
project = 'Haikal' project = "Haikal"
copyright = '2024, Marwan Alwali' copyright = "2024, Marwan Alwali"
author = 'Marwan Alwali' author = "Marwan Alwali"
release = '01/11/2024' release = "01/11/2024"
# -- General configuration --------------------------------------------------- # -- General configuration ---------------------------------------------------
# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration # https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration
extensions = [] extensions = []
templates_path = ['_templates'] templates_path = ["_templates"]
exclude_patterns = [] exclude_patterns = []
language = '[en,ar]' language = "[en,ar]"
# -- Options for HTML output ------------------------------------------------- # -- Options for HTML output -------------------------------------------------
# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output # https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output
html_theme = 'alabaster' html_theme = "alabaster"
html_static_path = ['_static'] html_static_path = ["_static"]

View File

@ -5,6 +5,7 @@ os.environ.setdefault("DJANGO_SETTINGS_MODULE", "car_inventory.settings")
django.setup() django.setup()
from inventory.models import * from inventory.models import *
# from rich import print # from rich import print
import random import random
import datetime import datetime
@ -31,7 +32,6 @@ def run():
"LGJE1EE0XSM333551", "LGJE1EE0XSM333551",
"LGJE1EE02SM333561", "LGJE1EE02SM333561",
"LGJE1EE0XSM333565", "LGJE1EE0XSM333565",
] ]
for vin in vin_list: for vin in vin_list:
try: try:
@ -69,5 +69,6 @@ def run():
except Exception as e: except Exception as e:
print(e) print(e)
if __name__ == "__main__": if __name__ == "__main__":
run() run()

View File

@ -4,10 +4,16 @@ from .models import AnalysisCache
@admin.register(AnalysisCache) @admin.register(AnalysisCache)
class AnalysisCacheAdmin(admin.ModelAdmin): class AnalysisCacheAdmin(admin.ModelAdmin):
list_display = ('prompt_hash', 'dealer_id', 'created_at', 'expires_at', 'is_expired') list_display = (
list_filter = ('dealer_id', 'created_at') "prompt_hash",
search_fields = ('prompt_hash',) "dealer_id",
readonly_fields = ('prompt_hash', 'created_at', 'updated_at') "created_at",
"expires_at",
"is_expired",
)
list_filter = ("dealer_id", "created_at")
search_fields = ("prompt_hash",)
readonly_fields = ("prompt_hash", "created_at", "updated_at")
def is_expired(self, obj): def is_expired(self, obj):
return obj.is_expired() return obj.is_expired()

View File

@ -20,10 +20,10 @@ from sqlalchemy.orm import relationship
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
# Configuration settings # Configuration settings
LLM_MODEL = getattr(settings, 'MODEL_ANALYZER_LLM_MODEL', 'qwen3:8b') LLM_MODEL = getattr(settings, "MODEL_ANALYZER_LLM_MODEL", "qwen3:8b")
LLM_TEMPERATURE = getattr(settings, 'MODEL_ANALYZER_LLM_TEMPERATURE', 0.3) LLM_TEMPERATURE = getattr(settings, "MODEL_ANALYZER_LLM_TEMPERATURE", 0.3)
LLM_MAX_TOKENS = getattr(settings, 'MODEL_ANALYZER_LLM_MAX_TOKENS', 2048) LLM_MAX_TOKENS = getattr(settings, "MODEL_ANALYZER_LLM_MAX_TOKENS", 2048)
CACHE_TIMEOUT = getattr(settings, 'MODEL_ANALYZER_CACHE_TIMEOUT', 3600) CACHE_TIMEOUT = getattr(settings, "MODEL_ANALYZER_CACHE_TIMEOUT", 3600)
system_instruction = """ system_instruction = """
You are a specialized AI agent designed to analyze Django models and extract relevant information based on user input in Arabic or English. You must: You are a specialized AI agent designed to analyze Django models and extract relevant information based on user input in Arabic or English. You must:
@ -91,52 +91,55 @@ class ModelAnalysis:
class DjangoModelAnalyzer: class DjangoModelAnalyzer:
def __init__(self): def __init__(self):
self.analysis_patterns = { self.analysis_patterns = {
'count': { "count": {
'patterns': [r'\b(count|number|how many)\b'], "patterns": [r"\b(count|number|how many)\b"],
'fields': ['id'], "fields": ["id"],
'weight': 1.0 "weight": 1.0,
}, },
'aggregate': { "aggregate": {
'patterns': [r'\b(average|avg|mean|sum|total)\b'], "patterns": [r"\b(average|avg|mean|sum|total)\b"],
'fields': ['price', 'amount', 'value', 'cost', 'quantity'], "fields": ["price", "amount", "value", "cost", "quantity"],
'weight': 0.8 "weight": 0.8,
},
"temporal": {
"patterns": [r"\b(date|time|when|period)\b"],
"fields": ["created_at", "updated_at", "date", "timestamp"],
"weight": 0.7,
}, },
'temporal': {
'patterns': [r'\b(date|time|when|period)\b'],
'fields': ['created_at', 'updated_at', 'date', 'timestamp'],
'weight': 0.7
}
} }
def analyze_prompt(self, prompt: str, model_structure: List) -> ModelAnalysis: def analyze_prompt(self, prompt: str, model_structure: List) -> ModelAnalysis:
# Initialize LLM # Initialize LLM
llm = ChatOllama( llm = ChatOllama(model=LLM_MODEL, temperature=LLM_TEMPERATURE)
model=LLM_MODEL,
temperature=LLM_TEMPERATURE
)
# Get model analysis from LLM # Get model analysis from LLM
messages = [ messages = [
SystemMessage(content=system_instruction), SystemMessage(content=system_instruction),
HumanMessage(content=prompt) HumanMessage(content=prompt),
] ]
try: try:
response = llm.invoke(messages) response = llm.invoke(messages)
if not response or not hasattr(response, 'content') or response.content is None: if (
not response
or not hasattr(response, "content")
or response.content is None
):
raise ValueError("Empty response from LLM") raise ValueError("Empty response from LLM")
analysis_requirements = self._parse_llm_response(response.content) analysis_requirements = self._parse_llm_response(response.content)
except Exception as e: except Exception as e:
logger.error(f"Error in LLM analysis: {e}") logger.error(f"Error in LLM analysis: {e}")
analysis_requirements = self._pattern_based_analysis(prompt, model_structure) analysis_requirements = self._pattern_based_analysis(
prompt, model_structure
)
return self._enhance_analysis(analysis_requirements, model_structure) return self._enhance_analysis(analysis_requirements, model_structure)
def _parse_llm_response(self, response: str) -> Dict: def _parse_llm_response(self, response: str) -> Dict:
try: try:
json_match = re.search(r'({.*})', response.replace('\n', ' '), re.DOTALL) json_match = re.search(r"({.*})", response.replace("\n", " "), re.DOTALL)
if json_match: if json_match:
return json.loads(json_match.group(1)) return json.loads(json_match.group(1))
return {} return {}
@ -149,20 +152,22 @@ class DjangoModelAnalyzer:
relevant_fields = [] relevant_fields = []
for analysis_name, config in self.analysis_patterns.items(): for analysis_name, config in self.analysis_patterns.items():
for pattern in config['patterns']: for pattern in config["patterns"]:
if re.search(pattern, prompt.lower()): if re.search(pattern, prompt.lower()):
relevant_fields.extend(config['fields']) relevant_fields.extend(config["fields"])
analysis_type = analysis_name analysis_type = analysis_name
break break
if analysis_type: if analysis_type:
break break
return { return {
'analysis_type': analysis_type or 'basic', "analysis_type": analysis_type or "basic",
'fields': list(set(relevant_fields)) or ['id', 'name'] "fields": list(set(relevant_fields)) or ["id", "name"],
} }
def _enhance_analysis(self, requirements: Dict, model_structure: List) -> ModelAnalysis: def _enhance_analysis(
self, requirements: Dict, model_structure: List
) -> ModelAnalysis:
app_label = requirements.get("analysis_requirements", {}).get("app_label") app_label = requirements.get("analysis_requirements", {}).get("app_label")
model_name = requirements.get("analysis_requirements", {}).get("model_name") model_name = requirements.get("analysis_requirements", {}).get("model_name")
fields = requirements.get("analysis_requirements", {}).get("fields") or [] fields = requirements.get("analysis_requirements", {}).get("fields") or []
@ -185,26 +190,31 @@ class DjangoModelAnalyzer:
field_analysis = FieldAnalysis( field_analysis = FieldAnalysis(
name=field_name, name=field_name,
field_type=field.get_internal_type(), field_type=field.get_internal_type(),
is_required=not field.null if hasattr(field, 'null') else True, is_required=not field.null if hasattr(field, "null") else True,
is_relation=field.is_relation, is_relation=field.is_relation,
related_model=field.related_model.__name__ if field.is_relation and hasattr(field, related_model=field.related_model.__name__
'related_model') and field.related_model else None if field.is_relation
and hasattr(field, "related_model")
and field.related_model
else None,
) )
field_analysis.analysis_relevance = self._calculate_field_relevance( field_analysis.analysis_relevance = self._calculate_field_relevance(
field_analysis, field_analysis, requirements.get("analysis_type", "basic")
requirements.get('analysis_type', 'basic')
) )
relevant_fields.append(field_analysis) relevant_fields.append(field_analysis)
if field.is_relation: if field.is_relation:
relationships.append({ relationships.append(
'field': field_name, {
'type': field.get_internal_type(), "field": field_name,
'to': field.related_model.__name__ if hasattr(field, "type": field.get_internal_type(),
'related_model') and field.related_model else '' "to": field.related_model.__name__
}) if hasattr(field, "related_model") and field.related_model
else "",
}
)
except FieldDoesNotExist: except FieldDoesNotExist:
logger.warning(f"Field {field_name} not found in {model_name}") logger.warning(f"Field {field_name} not found in {model_name}")
@ -212,16 +222,20 @@ class DjangoModelAnalyzer:
return ModelAnalysis( return ModelAnalysis(
app_label=app_label, app_label=app_label,
model_name=model_name, model_name=model_name,
relevant_fields=sorted(relevant_fields, key=lambda x: x.analysis_relevance, reverse=True), relevant_fields=sorted(
relevant_fields, key=lambda x: x.analysis_relevance, reverse=True
),
relationships=relationships, relationships=relationships,
confidence_score=self._calculate_confidence_score(relevant_fields) confidence_score=self._calculate_confidence_score(relevant_fields),
) )
def _calculate_field_relevance(self, field: FieldAnalysis, analysis_type: str) -> float: def _calculate_field_relevance(
self, field: FieldAnalysis, analysis_type: str
) -> float:
base_score = 0.5 base_score = 0.5
if analysis_type in self.analysis_patterns: if analysis_type in self.analysis_patterns:
if field.name in self.analysis_patterns[analysis_type]['fields']: if field.name in self.analysis_patterns[analysis_type]["fields"]:
base_score += self.analysis_patterns[analysis_type]['weight'] base_score += self.analysis_patterns[analysis_type]["weight"]
if field.is_required: if field.is_required:
base_score += 0.2 base_score += 0.2
if field.is_relation: if field.is_relation:
@ -257,26 +271,32 @@ def get_all_model_structures(filtered_apps: Optional[List[str]] = None) -> List[
if field.is_relation: if field.is_relation:
# Get related model name safely # Get related model name safely
related_model_name = None related_model_name = None
if hasattr(field, 'related_model') and field.related_model: if hasattr(field, "related_model") and field.related_model:
related_model_name = field.related_model.__name__ related_model_name = field.related_model.__name__
elif hasattr(field, 'model') and field.model: elif hasattr(field, "model") and field.model:
related_model_name = field.model.__name__ related_model_name = field.model.__name__
if related_model_name: # Only add relationship if we have a valid related model if (
relationships.append({ related_model_name
"field": field.name, ): # Only add relationship if we have a valid related model
"type": field.get_internal_type(), relationships.append(
"to": related_model_name {
}) "field": field.name,
"type": field.get_internal_type(),
"to": related_model_name,
}
)
else: else:
fields[field.name] = field.get_internal_type() fields[field.name] = field.get_internal_type()
structures.append({ structures.append(
"app_label": app_label, {
"model_name": model.__name__, "app_label": app_label,
"fields": fields, "model_name": model.__name__,
"relationships": relationships "fields": fields,
}) "relationships": relationships,
}
)
return structures return structures
@ -332,25 +352,42 @@ def apply_filters(queryset: QuerySet, filters: Dict[str, Any]) -> QuerySet:
for key, value in filters.items(): for key, value in filters.items():
if isinstance(value, dict): if isinstance(value, dict):
# Handle complex filters # Handle complex filters
operation = value.get('operation', 'exact') operation = value.get("operation", "exact")
filter_value = value.get('value') filter_value = value.get("value")
if not filter_value and operation != 'isnull': if not filter_value and operation != "isnull":
continue continue
if operation == 'contains': if operation == "contains":
q_objects.append(Q(**{f"{key}__icontains": filter_value})) q_objects.append(Q(**{f"{key}__icontains": filter_value}))
elif operation == 'in': elif operation == "in":
if isinstance(filter_value, list) and filter_value: if isinstance(filter_value, list) and filter_value:
q_objects.append(Q(**{f"{key}__in": filter_value})) q_objects.append(Q(**{f"{key}__in": filter_value}))
elif operation in ['gt', 'gte', 'lt', 'lte', 'exact', 'iexact', 'startswith', 'endswith']: elif operation in [
"gt",
"gte",
"lt",
"lte",
"exact",
"iexact",
"startswith",
"endswith",
]:
q_objects.append(Q(**{f"{key}__{operation}": filter_value})) q_objects.append(Q(**{f"{key}__{operation}": filter_value}))
elif operation == 'between' and isinstance(filter_value, list) and len(filter_value) >= 2: elif (
q_objects.append(Q(**{ operation == "between"
f"{key}__gte": filter_value[0], and isinstance(filter_value, list)
f"{key}__lte": filter_value[1] and len(filter_value) >= 2
})) ):
elif operation == 'isnull': q_objects.append(
Q(
**{
f"{key}__gte": filter_value[0],
f"{key}__lte": filter_value[1],
}
)
)
elif operation == "isnull":
q_objects.append(Q(**{f"{key}__isnull": bool(filter_value)})) q_objects.append(Q(**{f"{key}__isnull": bool(filter_value)}))
else: else:
# Simple exact match # Simple exact match
@ -363,10 +400,10 @@ def apply_filters(queryset: QuerySet, filters: Dict[str, Any]) -> QuerySet:
def process_aggregation( def process_aggregation(
queryset: QuerySet, queryset: QuerySet,
aggregation: str, aggregation: str,
fields: List[str], fields: List[str],
group_by: Optional[List[str]] = None group_by: Optional[List[str]] = None,
) -> Union[Dict[str, Any], List[Dict[str, Any]]]: ) -> Union[Dict[str, Any], List[Dict[str, Any]]]:
""" """
Process aggregation queries with support for grouping. Process aggregation queries with support for grouping.
@ -383,13 +420,7 @@ def process_aggregation(
if not fields: if not fields:
return {"error": "No fields specified for aggregation"} return {"error": "No fields specified for aggregation"}
agg_func_map = { agg_func_map = {"sum": Sum, "avg": Avg, "count": Count, "max": Max, "min": Min}
"sum": Sum,
"avg": Avg,
"count": Count,
"max": Max,
"min": Min
}
agg_func = agg_func_map.get(aggregation.lower()) agg_func = agg_func_map.get(aggregation.lower())
if not agg_func: if not agg_func:
@ -404,22 +435,25 @@ def process_aggregation(
agg_dict[f"{aggregation}_{field}"] = agg_func(field) agg_dict[f"{aggregation}_{field}"] = agg_func(field)
if not agg_dict: if not agg_dict:
return {"error": "No valid fields for aggregation after excluding group_by fields"} return {
"error": "No valid fields for aggregation after excluding group_by fields"
}
# Apply group_by and aggregation # Apply group_by and aggregation
return list(queryset.values(*group_by).annotate(**agg_dict)) return list(queryset.values(*group_by).annotate(**agg_dict))
else: else:
# Simple aggregation without grouping # Simple aggregation without grouping
return queryset.aggregate(**{ return queryset.aggregate(
f"{aggregation}_{field}": agg_func(field) **{f"{aggregation}_{field}": agg_func(field) for field in fields}
for field in fields )
})
except Exception as e: except Exception as e:
logger.error(f"Aggregation error: {e}") logger.error(f"Aggregation error: {e}")
return {"error": f"Aggregation failed: {str(e)}"} return {"error": f"Aggregation failed: {str(e)}"}
def prepare_chart_data(data: List[Dict], fields: List[str], chart_type: str) -> Optional[Dict[str, Any]]: def prepare_chart_data(
data: List[Dict], fields: List[str], chart_type: str
) -> Optional[Dict[str, Any]]:
""" """
Prepare data for chart visualization. Prepare data for chart visualization.
@ -449,15 +483,18 @@ def prepare_chart_data(data: List[Dict], fields: List[str], chart_type: str) ->
return { return {
"type": chart_type, "type": chart_type,
"labels": [str(label).replace(f"{fields[0]}_", "") for label in labels], "labels": [str(label).replace(f"{fields[0]}_", "") for label in labels],
"data": [float(value) if isinstance(value, (int, float)) else 0 for value in values], "data": [
float(value) if isinstance(value, (int, float)) else 0
for value in values
],
"backgroundColor": [ "backgroundColor": [
"rgba(54, 162, 235, 0.6)", "rgba(54, 162, 235, 0.6)",
"rgba(255, 99, 132, 0.6)", "rgba(255, 99, 132, 0.6)",
"rgba(255, 206, 86, 0.6)", "rgba(255, 206, 86, 0.6)",
"rgba(75, 192, 192, 0.6)", "rgba(75, 192, 192, 0.6)",
"rgba(153, 102, 255, 0.6)", "rgba(153, 102, 255, 0.6)",
"rgba(255, 159, 64, 0.6)" "rgba(255, 159, 64, 0.6)",
] ],
} }
# For regular query results as list of dictionaries # For regular query results as list of dictionaries
@ -483,13 +520,14 @@ def prepare_chart_data(data: List[Dict], fields: List[str], chart_type: str) ->
"labels": labels, "labels": labels,
"data": data_values, "data": data_values,
"backgroundColor": [ "backgroundColor": [
"rgba(54, 162, 235, 0.6)", "rgba(54, 162, 235, 0.6)",
"rgba(255, 99, 132, 0.6)", "rgba(255, 99, 132, 0.6)",
"rgba(255, 206, 86, 0.6)", "rgba(255, 206, 86, 0.6)",
"rgba(75, 192, 192, 0.6)", "rgba(75, 192, 192, 0.6)",
"rgba(153, 102, 255, 0.6)", "rgba(153, 102, 255, 0.6)",
"rgba(255, 159, 64, 0.6)" "rgba(255, 159, 64, 0.6)",
] * (len(data_values) // 6 + 1) # Repeat colors as needed ]
* (len(data_values) // 6 + 1), # Repeat colors as needed
} }
else: else:
# For other charts, create dataset for each field after the first # For other charts, create dataset for each field after the first
@ -501,17 +539,13 @@ def prepare_chart_data(data: List[Dict], fields: List[str], chart_type: str) ->
"data": [float(item.get(field, 0) or 0) for item in data], "data": [float(item.get(field, 0) or 0) for item in data],
"backgroundColor": f"rgba({50 + i * 50}, {100 + i * 40}, 235, 0.6)", "backgroundColor": f"rgba({50 + i * 50}, {100 + i * 40}, 235, 0.6)",
"borderColor": f"rgba({50 + i * 50}, {100 + i * 40}, 235, 1.0)", "borderColor": f"rgba({50 + i * 50}, {100 + i * 40}, 235, 1.0)",
"borderWidth": 1 "borderWidth": 1,
} }
datasets.append(dataset) datasets.append(dataset)
except (ValueError, TypeError) as e: except (ValueError, TypeError) as e:
logger.warning(f"Error processing field {field} for chart: {e}") logger.warning(f"Error processing field {field} for chart: {e}")
return { return {"type": chart_type, "labels": labels, "datasets": datasets}
"type": chart_type,
"labels": labels,
"datasets": datasets
}
except Exception as e: except Exception as e:
logger.error(f"Error preparing chart data: {e}") logger.error(f"Error preparing chart data: {e}")
return None return None
@ -556,7 +590,7 @@ def query_django_model(parsed: Dict[str, Any]) -> Dict[str, Any]:
return { return {
"status": "error", "status": "error",
"error": "app_label and model are required", "error": "app_label and model are required",
"language": language "language": language,
} }
# Get model class # Get model class
@ -566,7 +600,7 @@ def query_django_model(parsed: Dict[str, Any]) -> Dict[str, Any]:
return { return {
"status": "error", "status": "error",
"error": f"Model '{model_name}' not found in app '{app_label}'", "error": f"Model '{model_name}' not found in app '{app_label}'",
"language": language "language": language,
} }
# Validate fields against model # Validate fields against model
@ -592,7 +626,7 @@ def query_django_model(parsed: Dict[str, Any]) -> Dict[str, Any]:
return { return {
"status": "error", "status": "error",
"error": f"Invalid filters: {str(e)}", "error": f"Invalid filters: {str(e)}",
"language": language "language": language,
} }
# Handle aggregations # Handle aggregations
@ -603,7 +637,7 @@ def query_django_model(parsed: Dict[str, Any]) -> Dict[str, Any]:
return { return {
"status": "error", "status": "error",
"error": result["error"], "error": result["error"],
"language": language "language": language,
} }
chart_data = None chart_data = None
@ -614,7 +648,7 @@ def query_django_model(parsed: Dict[str, Any]) -> Dict[str, Any]:
"status": "success", "status": "success",
"data": result, "data": result,
"chart": chart_data, "chart": chart_data,
"language": language "language": language,
} }
# Handle regular queries # Handle regular queries
@ -649,9 +683,9 @@ def query_django_model(parsed: Dict[str, Any]) -> Dict[str, Any]:
"total_count": len(data), "total_count": len(data),
"fields": fields, "fields": fields,
"model": model_name, "model": model_name,
"app": app_label "app": app_label,
}, },
"language": language "language": language,
} }
except Exception as e: except Exception as e:
@ -659,7 +693,7 @@ def query_django_model(parsed: Dict[str, Any]) -> Dict[str, Any]:
return { return {
"status": "error", "status": "error",
"error": f"Query execution failed: {str(e)}", "error": f"Query execution failed: {str(e)}",
"language": language "language": language,
} }
except Exception as e: except Exception as e:
@ -667,11 +701,13 @@ def query_django_model(parsed: Dict[str, Any]) -> Dict[str, Any]:
return { return {
"status": "error", "status": "error",
"error": f"Unexpected error: {str(e)}", "error": f"Unexpected error: {str(e)}",
"language": parsed.get("language", "en") "language": parsed.get("language", "en"),
} }
def determine_aggregation_type(prompt: str, fields: List[FieldAnalysis]) -> Optional[str]: def determine_aggregation_type(
prompt: str, fields: List[FieldAnalysis]
) -> Optional[str]:
""" """
Determine the appropriate aggregation type based on the prompt and fields. Determine the appropriate aggregation type based on the prompt and fields.
@ -682,21 +718,39 @@ def determine_aggregation_type(prompt: str, fields: List[FieldAnalysis]) -> Opti
Returns: Returns:
Aggregation type or None Aggregation type or None
""" """
if any(pattern in prompt.lower() for pattern in ['average', 'avg', 'mean', 'معدل', 'متوسط']): if any(
return 'avg' pattern in prompt.lower()
elif any(pattern in prompt.lower() for pattern in ['sum', 'total', 'مجموع', 'إجمالي']): for pattern in ["average", "avg", "mean", "معدل", "متوسط"]
return 'sum' ):
elif any(pattern in prompt.lower() for pattern in ['count', 'number', 'how many', 'عدد', 'كم']): return "avg"
return 'count' elif any(
elif any(pattern in prompt.lower() for pattern in ['maximum', 'max', 'highest', 'أقصى', 'أعلى']): pattern in prompt.lower() for pattern in ["sum", "total", "مجموع", "إجمالي"]
return 'max' ):
elif any(pattern in prompt.lower() for pattern in ['minimum', 'min', 'lowest', 'أدنى', 'أقل']): return "sum"
return 'min' elif any(
pattern in prompt.lower()
for pattern in ["count", "number", "how many", "عدد", "كم"]
):
return "count"
elif any(
pattern in prompt.lower()
for pattern in ["maximum", "max", "highest", "أقصى", "أعلى"]
):
return "max"
elif any(
pattern in prompt.lower()
for pattern in ["minimum", "min", "lowest", "أدنى", "أقل"]
):
return "min"
# Check field types for numeric fields to determine default aggregation # Check field types for numeric fields to determine default aggregation
numeric_fields = [field for field in fields if field.field_type in ['DecimalField', 'FloatField', 'IntegerField']] numeric_fields = [
field
for field in fields
if field.field_type in ["DecimalField", "FloatField", "IntegerField"]
]
if numeric_fields: if numeric_fields:
return 'sum' # Default to sum for numeric fields return "sum" # Default to sum for numeric fields
return None return None
@ -713,32 +767,47 @@ def determine_chart_type(prompt: str, fields: List[FieldAnalysis]) -> Optional[s
Chart type or None Chart type or None
""" """
# Check for explicit chart type mentions in prompt # Check for explicit chart type mentions in prompt
if any(term in prompt.lower() for term in ['line chart', 'time series', 'trend', 'رسم خطي', 'اتجاه']): if any(
return 'line' term in prompt.lower()
elif any(term in prompt.lower() for term in ['bar chart', 'histogram', 'column', 'رسم شريطي', 'أعمدة']): for term in ["line chart", "time series", "trend", "رسم خطي", "اتجاه"]
return 'bar' ):
elif any(term in prompt.lower() for term in ['pie chart', 'circle chart', 'رسم دائري', 'فطيرة']): return "line"
return 'pie' elif any(
elif any(term in prompt.lower() for term in ['doughnut', 'دونات']): term in prompt.lower()
return 'doughnut' for term in ["bar chart", "histogram", "column", "رسم شريطي", "أعمدة"]
elif any(term in prompt.lower() for term in ['radar', 'spider', 'رادار']): ):
return 'radar' return "bar"
elif any(
term in prompt.lower()
for term in ["pie chart", "circle chart", "رسم دائري", "فطيرة"]
):
return "pie"
elif any(term in prompt.lower() for term in ["doughnut", "دونات"]):
return "doughnut"
elif any(term in prompt.lower() for term in ["radar", "spider", "رادار"]):
return "radar"
# Determine chart type based on field types and count # Determine chart type based on field types and count
date_fields = [field for field in fields if field.field_type in ['DateField', 'DateTimeField']] date_fields = [
numeric_fields = [field for field in fields if field.field_type in ['DecimalField', 'FloatField', 'IntegerField']] field for field in fields if field.field_type in ["DateField", "DateTimeField"]
]
numeric_fields = [
field
for field in fields
if field.field_type in ["DecimalField", "FloatField", "IntegerField"]
]
if date_fields and numeric_fields: if date_fields and numeric_fields:
return 'line' # Time series data return "line" # Time series data
elif len(fields) == 2 and len(numeric_fields) >= 1: elif len(fields) == 2 and len(numeric_fields) >= 1:
return 'bar' # Category and value return "bar" # Category and value
elif len(fields) == 1 or (len(fields) == 2 and len(numeric_fields) == 1): elif len(fields) == 1 or (len(fields) == 2 and len(numeric_fields) == 1):
return 'pie' # Single dimension data return "pie" # Single dimension data
elif len(fields) > 2: elif len(fields) > 2:
return 'bar' # Multi-dimensional data return "bar" # Multi-dimensional data
# Default # Default
return 'bar' return "bar"
def analyze_prompt(prompt: str) -> Dict[str, Any]: def analyze_prompt(prompt: str) -> Dict[str, Any]:
@ -752,8 +821,8 @@ def analyze_prompt(prompt: str) -> Dict[str, Any]:
Dictionary with query results Dictionary with query results
""" """
# Detect language # Detect language
language = "ar" if bool(re.search(r'[\u0600-\u06FF]', prompt)) else "en" language = "ar" if bool(re.search(r"[\u0600-\u06FF]", prompt)) else "en"
filtered_apps = ['inventory'] filtered_apps = ["inventory"]
try: try:
analyzer = DjangoModelAnalyzer() analyzer = DjangoModelAnalyzer()
model_structure = get_all_model_structures(filtered_apps=filtered_apps) model_structure = get_all_model_structures(filtered_apps=filtered_apps)
@ -761,23 +830,27 @@ def analyze_prompt(prompt: str) -> Dict[str, Any]:
analysis = analyzer.analyze_prompt(prompt, model_structure) analysis = analyzer.analyze_prompt(prompt, model_structure)
print(analysis) print(analysis)
if not analysis or not analysis.app_label or not analysis.model_name: if not analysis or not analysis.app_label or not analysis.model_name:
return { return {
"status": "error", "status": "error",
"message": "تعذر العثور على النموذج المطلوب" if language == "ar" else "Missing model information", "message": "تعذر العثور على النموذج المطلوب"
"language": language if language == "ar"
else "Missing model information",
"language": language,
} }
query_params = { query_params = {
"app_label": analysis.app_label, "app_label": analysis.app_label,
"model_name": analysis.model_name, "model_name": analysis.model_name,
"fields": [field.name for field in analysis.relevant_fields], "fields": [field.name for field in analysis.relevant_fields],
"joins": [{"path": rel["field"], "type": rel["type"]} for rel in analysis.relationships], "joins": [
{"path": rel["field"], "type": rel["type"]}
for rel in analysis.relationships
],
"filters": {}, "filters": {},
"aggregation": determine_aggregation_type(prompt, analysis.relevant_fields), "aggregation": determine_aggregation_type(prompt, analysis.relevant_fields),
"chart": determine_chart_type(prompt, analysis.relevant_fields), "chart": determine_chart_type(prompt, analysis.relevant_fields),
"language": language "language": language,
} }
return query_django_model(query_params) return query_django_model(query_params)
@ -785,6 +858,8 @@ def analyze_prompt(prompt: str) -> Dict[str, Any]:
logger.error(f"Error analyzing prompt: {e}") logger.error(f"Error analyzing prompt: {e}")
return { return {
"status": "error", "status": "error",
"error": "حدث خطأ أثناء تحليل الاستعلام" if language == "ar" else f"Error analyzing prompt: {str(e)}", "error": "حدث خطأ أثناء تحليل الاستعلام"
"language": language if language == "ar"
} else f"Error analyzing prompt: {str(e)}",
"language": language,
}

View File

@ -2,5 +2,5 @@ from django.apps import AppConfig
class HaikalbotConfig(AppConfig): class HaikalbotConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField' default_auto_field = "django.db.models.BigAutoField"
name = 'haikalbot' name = "haikalbot"

View File

@ -63,7 +63,6 @@ class DatabaseType(Enum):
MYSQL = "mysql" MYSQL = "mysql"
@dataclass @dataclass
class DatabaseConnection: class DatabaseConnection:
db_type: DatabaseType db_type: DatabaseType
@ -95,18 +94,23 @@ class DatabaseSchema(BaseModel):
description="Database schema with table names as keys and column info as values" description="Database schema with table names as keys and column info as values"
) )
relationships: Optional[List[Dict[str, Any]]] = Field( relationships: Optional[List[Dict[str, Any]]] = Field(
default=None, default=None, description="Foreign key relationships between tables"
description="Foreign key relationships between tables"
) )
class InsightRequest(BaseModel): class InsightRequest(BaseModel):
prompt: str = Field(description="Natural language query from user") prompt: str = Field(description="Natural language query from user")
database_path: Optional[str] = Field(default=None, description="Path to database file (for SQLite)") database_path: Optional[str] = Field(
default=None, description="Path to database file (for SQLite)"
)
chart_type: Optional[str] = Field(default=None, description="Preferred chart type") chart_type: Optional[str] = Field(default=None, description="Preferred chart type")
limit: Optional[int] = Field(default=1000, description="Maximum number of results") limit: Optional[int] = Field(default=1000, description="Maximum number of results")
language: Optional[str] = Field(default="auto", description="Response language preference") language: Optional[str] = Field(
use_django: Optional[bool] = Field(default=True, description="Use Django database if available") default="auto", description="Response language preference"
)
use_django: Optional[bool] = Field(
default=True, description="Use Django database if available"
)
class DatabaseInsightSystem: class DatabaseInsightSystem:
@ -114,7 +118,7 @@ class DatabaseInsightSystem:
self.config = config or DatabaseConfig() self.config = config or DatabaseConfig()
self.model = OpenAIModel( self.model = OpenAIModel(
model_name=self.config.LLM_MODEL, model_name=self.config.LLM_MODEL,
provider=OpenAIProvider(base_url=self.config.LLM_BASE_URL) provider=OpenAIProvider(base_url=self.config.LLM_BASE_URL),
) )
self.db_connection = None self.db_connection = None
self._setup_agents() self._setup_agents()
@ -148,7 +152,7 @@ class DatabaseInsightSystem:
} }
Handle both English and Arabic prompts. For Arabic text, respond in Arabic. Handle both English and Arabic prompts. For Arabic text, respond in Arabic.
Focus on providing actionable insights, not just raw data.""" Focus on providing actionable insights, not just raw data.""",
) )
def _get_django_database_config(self) -> Optional[DatabaseConnection]: def _get_django_database_config(self) -> Optional[DatabaseConnection]:
@ -158,27 +162,31 @@ class DatabaseInsightSystem:
try: try:
# Get default database configuration # Get default database configuration
db_config = settings.DATABASES.get('default', {}) db_config = settings.DATABASES.get("default", {})
if not db_config: if not db_config:
logger.warning("No default database configuration found in Django settings") logger.warning(
"No default database configuration found in Django settings"
)
return None return None
engine = db_config.get('ENGINE', '') engine = db_config.get("ENGINE", "")
db_name = db_config.get('NAME', '') db_name = db_config.get("NAME", "")
host = db_config.get('HOST', 'localhost') host = db_config.get("HOST", "localhost")
port = db_config.get('PORT', None) port = db_config.get("PORT", None)
user = db_config.get('USER', '') user = db_config.get("USER", "")
password = db_config.get('PASSWORD', '') password = db_config.get("PASSWORD", "")
# Determine database type from engine # Determine database type from engine
if 'sqlite' in engine.lower(): if "sqlite" in engine.lower():
db_type = DatabaseType.SQLITE db_type = DatabaseType.SQLITE
connection_string = db_name # For SQLite, NAME is the file path connection_string = db_name # For SQLite, NAME is the file path
elif 'postgresql' in engine.lower(): elif "postgresql" in engine.lower():
db_type = DatabaseType.POSTGRESQL db_type = DatabaseType.POSTGRESQL
port = port or 5432 port = port or 5432
connection_string = f"postgresql://{user}:{password}@{host}:{port}/{db_name}" connection_string = (
elif 'mysql' in engine.lower(): f"postgresql://{user}:{password}@{host}:{port}/{db_name}"
)
elif "mysql" in engine.lower():
db_type = DatabaseType.MYSQL db_type = DatabaseType.MYSQL
port = port or 3306 port = port or 3306
connection_string = f"mysql://{user}:{password}@{host}:{port}/{db_name}" connection_string = f"mysql://{user}:{password}@{host}:{port}/{db_name}"
@ -193,7 +201,7 @@ class DatabaseInsightSystem:
host=host, host=host,
port=port, port=port,
user=user, user=user,
password=password password=password,
) )
except Exception as e: except Exception as e:
@ -218,8 +226,7 @@ class DatabaseInsightSystem:
if request.database_path: if request.database_path:
# Assume SQLite for direct file path # Assume SQLite for direct file path
self.db_connection = DatabaseConnection( self.db_connection = DatabaseConnection(
db_type=DatabaseType.SQLITE, db_type=DatabaseType.SQLITE, connection_string=request.database_path
connection_string=request.database_path
) )
return await self._analyze_sqlite_schema(request.database_path) return await self._analyze_sqlite_schema(request.database_path)
@ -247,24 +254,28 @@ class DatabaseInsightSystem:
cursor.execute(f"PRAGMA table_info({table})") cursor.execute(f"PRAGMA table_info({table})")
columns = [] columns = []
for col in cursor.fetchall(): for col in cursor.fetchall():
columns.append({ columns.append(
"name": col[1], {
"type": col[2], "name": col[1],
"notnull": bool(col[3]), "type": col[2],
"default_value": col[4], "notnull": bool(col[3]),
"primary_key": bool(col[5]) "default_value": col[4],
}) "primary_key": bool(col[5]),
}
)
schema_data[table] = columns schema_data[table] = columns
# Get foreign key relationships # Get foreign key relationships
cursor.execute(f"PRAGMA foreign_key_list({table})") cursor.execute(f"PRAGMA foreign_key_list({table})")
for fk in cursor.fetchall(): for fk in cursor.fetchall():
relationships.append({ relationships.append(
"from_table": table, {
"from_column": fk[3], "from_table": table,
"to_table": fk[2], "from_column": fk[3],
"to_column": fk[4] "to_table": fk[2],
}) "to_column": fk[4],
}
)
conn.close() conn.close()
return DatabaseSchema(tables=schema_data, relationships=relationships) return DatabaseSchema(tables=schema_data, relationships=relationships)
@ -287,27 +298,33 @@ class DatabaseInsightSystem:
for field in model._meta.get_fields(): for field in model._meta.get_fields():
if not field.is_relation: if not field.is_relation:
columns.append({ columns.append(
"name": field.name, {
"type": field.get_internal_type(), "name": field.name,
"notnull": not getattr(field, 'null', True), "type": field.get_internal_type(),
"primary_key": getattr(field, 'primary_key', False) "notnull": not getattr(field, "null", True),
}) "primary_key": getattr(field, "primary_key", False),
}
)
else: else:
# Handle relationships # Handle relationships
if hasattr(field, 'related_model') and field.related_model: if hasattr(field, "related_model") and field.related_model:
relationships.append({ relationships.append(
"from_table": table_name, {
"from_column": field.name, "from_table": table_name,
"to_table": field.related_model._meta.db_table, "from_column": field.name,
"relationship_type": field.get_internal_type() "to_table": field.related_model._meta.db_table,
}) "relationship_type": field.get_internal_type(),
}
)
schema_data[table_name] = columns schema_data[table_name] = columns
return DatabaseSchema(tables=schema_data, relationships=relationships) return DatabaseSchema(tables=schema_data, relationships=relationships)
async def _analyze_postgresql_schema(self, connection_string: str) -> DatabaseSchema: async def _analyze_postgresql_schema(
self, connection_string: str
) -> DatabaseSchema:
"""Analyze PostgreSQL database schema.""" """Analyze PostgreSQL database schema."""
if not POSTGRESQL_AVAILABLE: if not POSTGRESQL_AVAILABLE:
raise ImportError("psycopg2 is not available") raise ImportError("psycopg2 is not available")
@ -325,47 +342,56 @@ class DatabaseInsightSystem:
FROM information_schema.tables FROM information_schema.tables
WHERE table_schema = 'public' WHERE table_schema = 'public'
""") """)
tables = [row['table_name'] for row in cursor.fetchall()] tables = [row["table_name"] for row in cursor.fetchall()]
schema_data = {} schema_data = {}
relationships = [] relationships = []
for table in tables: for table in tables:
# Get column information # Get column information
cursor.execute(""" cursor.execute(
"""
SELECT column_name, data_type, is_nullable, column_default SELECT column_name, data_type, is_nullable, column_default
FROM information_schema.columns FROM information_schema.columns
WHERE table_name = %s WHERE table_name = %s
ORDER BY ordinal_position ORDER BY ordinal_position
""", (table,)) """,
(table,),
)
columns = [] columns = []
for col in cursor.fetchall(): for col in cursor.fetchall():
columns.append({ columns.append(
"name": col['column_name'], {
"type": col['data_type'], "name": col["column_name"],
"notnull": col['is_nullable'] == 'NO', "type": col["data_type"],
"default_value": col['column_default'], "notnull": col["is_nullable"] == "NO",
"primary_key": False # Will be updated below "default_value": col["column_default"],
}) "primary_key": False, # Will be updated below
}
)
# Get primary key information # Get primary key information
cursor.execute(""" cursor.execute(
"""
SELECT column_name SELECT column_name
FROM information_schema.key_column_usage FROM information_schema.key_column_usage
WHERE table_name = %s WHERE table_name = %s
AND constraint_name LIKE '%_pkey' AND constraint_name LIKE '%_pkey'
""", (table,)) """,
(table,),
)
pk_columns = [row['column_name'] for row in cursor.fetchall()] pk_columns = [row["column_name"] for row in cursor.fetchall()]
for col in columns: for col in columns:
if col['name'] in pk_columns: if col["name"] in pk_columns:
col['primary_key'] = True col["primary_key"] = True
schema_data[table] = columns schema_data[table] = columns
# Get foreign key relationships # Get foreign key relationships
cursor.execute(""" cursor.execute(
"""
SELECT kcu.column_name, SELECT kcu.column_name,
ccu.table_name AS foreign_table_name, ccu.table_name AS foreign_table_name,
ccu.column_name AS foreign_column_name ccu.column_name AS foreign_column_name
@ -376,15 +402,19 @@ class DatabaseInsightSystem:
ON ccu.constraint_name = tc.constraint_name ON ccu.constraint_name = tc.constraint_name
WHERE tc.constraint_type = 'FOREIGN KEY' WHERE tc.constraint_type = 'FOREIGN KEY'
AND tc.table_name = %s AND tc.table_name = %s
""", (table,)) """,
(table,),
)
for fk in cursor.fetchall(): for fk in cursor.fetchall():
relationships.append({ relationships.append(
"from_table": table, {
"from_column": fk['column_name'], "from_table": table,
"to_table": fk['foreign_table_name'], "from_column": fk["column_name"],
"to_column": fk['foreign_column_name'] "to_table": fk["foreign_table_name"],
}) "to_column": fk["foreign_column_name"],
}
)
conn.close() conn.close()
return DatabaseSchema(tables=schema_data, relationships=relationships) return DatabaseSchema(tables=schema_data, relationships=relationships)
@ -404,6 +434,7 @@ class DatabaseInsightSystem:
# Parse connection string to get connection parameters # Parse connection string to get connection parameters
# Format: mysql://user:password@host:port/database # Format: mysql://user:password@host:port/database
import urllib.parse import urllib.parse
parsed = urllib.parse.urlparse(connection_string) parsed = urllib.parse.urlparse(connection_string)
conn = pymysql.connect( conn = pymysql.connect(
@ -412,7 +443,7 @@ class DatabaseInsightSystem:
user=parsed.username, user=parsed.username,
password=parsed.password, password=parsed.password,
database=parsed.path[1:], # Remove leading slash database=parsed.path[1:], # Remove leading slash
cursorclass=pymysql.cursors.DictCursor cursorclass=pymysql.cursors.DictCursor,
) )
cursor = conn.cursor() cursor = conn.cursor()
@ -429,13 +460,15 @@ class DatabaseInsightSystem:
cursor.execute(f"DESCRIBE {table}") cursor.execute(f"DESCRIBE {table}")
columns = [] columns = []
for col in cursor.fetchall(): for col in cursor.fetchall():
columns.append({ columns.append(
"name": col['Field'], {
"type": col['Type'], "name": col["Field"],
"notnull": col['Null'] == 'NO', "type": col["Type"],
"default_value": col['Default'], "notnull": col["Null"] == "NO",
"primary_key": col['Key'] == 'PRI' "default_value": col["Default"],
}) "primary_key": col["Key"] == "PRI",
}
)
schema_data[table] = columns schema_data[table] = columns
@ -451,12 +484,14 @@ class DatabaseInsightSystem:
""") """)
for fk in cursor.fetchall(): for fk in cursor.fetchall():
relationships.append({ relationships.append(
"from_table": table, {
"from_column": fk['COLUMN_NAME'], "from_table": table,
"to_table": fk['REFERENCED_TABLE_NAME'], "from_column": fk["COLUMN_NAME"],
"to_column": fk['REFERENCED_COLUMN_NAME'] "to_table": fk["REFERENCED_TABLE_NAME"],
}) "to_column": fk["REFERENCED_COLUMN_NAME"],
}
)
conn.close() conn.close()
return DatabaseSchema(tables=schema_data, relationships=relationships) return DatabaseSchema(tables=schema_data, relationships=relationships)
@ -467,7 +502,7 @@ class DatabaseInsightSystem:
def _detect_language(self, text: str) -> str: def _detect_language(self, text: str) -> str:
"""Detect if text is Arabic or English.""" """Detect if text is Arabic or English."""
arabic_chars = re.findall(r'[\u0600-\u06FF]', text) arabic_chars = re.findall(r"[\u0600-\u06FF]", text)
return "ar" if len(arabic_chars) > len(text) * 0.3 else "en" return "ar" if len(arabic_chars) > len(text) * 0.3 else "en"
def _execute_query_sync(self, query: str) -> List[Dict]: def _execute_query_sync(self, query: str) -> List[Dict]:
@ -480,13 +515,19 @@ class DatabaseInsightSystem:
raise ValueError("No database connection established") raise ValueError("No database connection established")
if self.db_connection.db_type == DatabaseType.SQLITE: if self.db_connection.db_type == DatabaseType.SQLITE:
return await self._execute_sqlite_query(self.db_connection.connection_string, query) return await self._execute_sqlite_query(
self.db_connection.connection_string, query
)
# elif self.db_connection.db_type == DatabaseType.DJANGO and DJANGO_AVAILABLE: # elif self.db_connection.db_type == DatabaseType.DJANGO and DJANGO_AVAILABLE:
# return await self._execute_django_query(query) # return await self._execute_django_query(query)
elif self.db_connection.db_type == DatabaseType.POSTGRESQL: elif self.db_connection.db_type == DatabaseType.POSTGRESQL:
return await self._execute_postgresql_query(self.db_connection.connection_string, query) return await self._execute_postgresql_query(
self.db_connection.connection_string, query
)
elif self.db_connection.db_type == DatabaseType.MYSQL: elif self.db_connection.db_type == DatabaseType.MYSQL:
return await self._execute_mysql_query(self.db_connection.connection_string, query) return await self._execute_mysql_query(
self.db_connection.connection_string, query
)
else: else:
raise ValueError(f"Unsupported database type: {self.db_connection.db_type}") raise ValueError(f"Unsupported database type: {self.db_connection.db_type}")
@ -528,7 +569,9 @@ class DatabaseInsightSystem:
logger.error(f"Django query execution failed: {e}") logger.error(f"Django query execution failed: {e}")
raise raise
async def _execute_postgresql_query(self, connection_string: str, query: str) -> List[Dict]: async def _execute_postgresql_query(
self, connection_string: str, query: str
) -> List[Dict]:
"""Execute SQL query on PostgreSQL database.""" """Execute SQL query on PostgreSQL database."""
try: try:
import psycopg2 import psycopg2
@ -548,7 +591,9 @@ class DatabaseInsightSystem:
logger.error(f"PostgreSQL query execution failed: {e}") logger.error(f"PostgreSQL query execution failed: {e}")
raise raise
async def _execute_mysql_query(self, connection_string: str, query: str) -> List[Dict]: async def _execute_mysql_query(
self, connection_string: str, query: str
) -> List[Dict]:
"""Execute SQL query on MySQL database.""" """Execute SQL query on MySQL database."""
try: try:
import pymysql import pymysql
@ -562,7 +607,7 @@ class DatabaseInsightSystem:
user=parsed.username, user=parsed.username,
password=parsed.password, password=parsed.password,
database=parsed.path[1:], database=parsed.path[1:],
cursorclass=pymysql.cursors.DictCursor cursorclass=pymysql.cursors.DictCursor,
) )
cursor = conn.cursor() cursor = conn.cursor()
@ -576,7 +621,9 @@ class DatabaseInsightSystem:
logger.error(f"MySQL query execution failed: {e}") logger.error(f"MySQL query execution failed: {e}")
raise raise
def _prepare_chart_data(self, data: List[Dict], chart_type: str, fields: List[str]) -> Optional[Dict]: def _prepare_chart_data(
self, data: List[Dict], chart_type: str, fields: List[str]
) -> Optional[Dict]:
"""Prepare data for chart visualization.""" """Prepare data for chart visualization."""
if not data or not fields: if not data or not fields:
return None return None
@ -613,7 +660,7 @@ class DatabaseInsightSystem:
"backgroundColor": [ "backgroundColor": [
f"rgba({50 + i * 30}, {100 + i * 25}, {200 + i * 20}, 0.7)" f"rgba({50 + i * 30}, {100 + i * 25}, {200 + i * 20}, 0.7)"
for i in range(len(values)) for i in range(len(values))
] ],
} }
else: else:
# Multiple datasets for other chart types # Multiple datasets for other chart types
@ -627,21 +674,19 @@ class DatabaseInsightSystem:
value = 0 value = 0
dataset_values.append(value) dataset_values.append(value)
datasets.append({ datasets.append(
"label": field, {
"data": dataset_values, "label": field,
"backgroundColor": f"rgba({50 + i * 40}, {100 + i * 30}, 235, 0.6)", "data": dataset_values,
"borderColor": f"rgba({50 + i * 40}, {100 + i * 30}, 235, 1.0)", "backgroundColor": f"rgba({50 + i * 40}, {100 + i * 30}, 235, 0.6)",
"borderWidth": 2 "borderColor": f"rgba({50 + i * 40}, {100 + i * 30}, 235, 1.0)",
}) "borderWidth": 2,
}
)
except Exception as e: except Exception as e:
logger.warning(f"Error processing field {field}: {e}") logger.warning(f"Error processing field {field}: {e}")
return { return {"type": chart_type, "labels": labels, "datasets": datasets}
"type": chart_type,
"labels": labels,
"datasets": datasets
}
except Exception as e: except Exception as e:
logger.error(f"Chart preparation failed: {e}") logger.error(f"Chart preparation failed: {e}")
@ -659,14 +704,18 @@ class DatabaseInsightSystem:
"data": [], "data": [],
"metadata": {}, "metadata": {},
"error": str(e), "error": str(e),
"language": "en" "language": "en",
} }
async def get_insights(self, request: InsightRequest) -> QueryResult: async def get_insights(self, request: InsightRequest) -> QueryResult:
"""Main method to get database insights from natural language prompt.""" """Main method to get database insights from natural language prompt."""
try: try:
# Detect language # Detect language
language = self._detect_language(request.prompt) if request.language == "auto" else request.language language = (
self._detect_language(request.prompt)
if request.language == "auto"
else request.language
)
# Analyze database schema # Analyze database schema
schema = await self.analyze_database_schema(request) schema = await self.analyze_database_schema(request)
@ -674,7 +723,7 @@ class DatabaseInsightSystem:
# Generate query plan using AI # Generate query plan using AI
query_response = await self.query_agent.run( query_response = await self.query_agent.run(
f"User prompt: {request.prompt}\nLanguage: {language}", f"User prompt: {request.prompt}\nLanguage: {language}",
database_schema=schema database_schema=schema,
) )
# Parse AI response # Parse AI response
@ -682,13 +731,15 @@ class DatabaseInsightSystem:
query_plan = json.loads(query_response.output) query_plan = json.loads(query_response.output)
except json.JSONDecodeError: except json.JSONDecodeError:
# Fallback: extract SQL from response # Fallback: extract SQL from response
sql_match = re.search(r'SELECT.*?;', query_response.output, re.IGNORECASE | re.DOTALL) sql_match = re.search(
r"SELECT.*?;", query_response.output, re.IGNORECASE | re.DOTALL
)
if sql_match: if sql_match:
query_plan = { query_plan = {
"sql_query": sql_match.group(0), "sql_query": sql_match.group(0),
"chart_suggestion": "bar", "chart_suggestion": "bar",
"expected_fields": [], "expected_fields": [],
"language": language "language": language,
} }
else: else:
raise ValueError("Could not parse AI response") raise ValueError("Could not parse AI response")
@ -710,21 +761,25 @@ class DatabaseInsightSystem:
elif data: elif data:
# Use first few fields if no specific fields suggested # Use first few fields if no specific fields suggested
available_fields = list(data[0].keys()) if data else [] available_fields = list(data[0].keys()) if data else []
chart_data = self._prepare_chart_data(data, chart_type, available_fields[:3]) chart_data = self._prepare_chart_data(
data, chart_type, available_fields[:3]
)
# Prepare result # Prepare result
return QueryResult( return QueryResult(
status="success", status="success",
data=data[:request.limit] if data else [], data=data[: request.limit] if data else [],
metadata={ metadata={
"total_count": len(data) if data else 0, "total_count": len(data) if data else 0,
"query": sql_query, "query": sql_query,
"analysis": query_plan.get("analysis", ""), "analysis": query_plan.get("analysis", ""),
"fields": expected_fields or (list(data[0].keys()) if data else []), "fields": expected_fields or (list(data[0].keys()) if data else []),
"database_type": self.db_connection.db_type.value if self.db_connection else "unknown" "database_type": self.db_connection.db_type.value
if self.db_connection
else "unknown",
}, },
chart_data=chart_data, chart_data=chart_data,
language=language language=language,
) )
except Exception as e: except Exception as e:
@ -734,7 +789,7 @@ class DatabaseInsightSystem:
data=[], data=[],
metadata={}, metadata={},
error=str(e), error=str(e),
language=language if 'language' in locals() else "en" language=language if "language" in locals() else "en",
) )
# # Static method for Django view compatibility # # Static method for Django view compatibility
@ -798,4 +853,4 @@ def analyze_prompt_sync(prompt: str, **kwargs) -> Dict[str, Any]:
""" """
system = DatabaseInsightSystem() system = DatabaseInsightSystem()
request = InsightRequest(prompt=prompt, **kwargs) request = InsightRequest(prompt=prompt, **kwargs)
return system.get_insights_sync(request) return system.get_insights_sync(request)

View File

@ -9,7 +9,9 @@ from django.template.loaders.app_directories import get_app_template_dirs
class Command(BaseCommand): class Command(BaseCommand):
help = "Generate YAML support knowledge base from Django views, models, and templates" help = (
"Generate YAML support knowledge base from Django views, models, and templates"
)
def handle(self, *args, **kwargs): def handle(self, *args, **kwargs):
output_file = "haikal_kb.yaml" output_file = "haikal_kb.yaml"
@ -22,7 +24,7 @@ class Command(BaseCommand):
"features": {}, "features": {},
"user_workflows": {}, # New section for step-by-step instructions "user_workflows": {}, # New section for step-by-step instructions
"templates": {}, "templates": {},
"glossary": {} "glossary": {},
} }
def extract_doc(item): def extract_doc(item):
@ -42,31 +44,48 @@ class Command(BaseCommand):
def get_all_model_classes(): def get_all_model_classes():
all_models = [] all_models = []
for model in apps.get_models(): for model in apps.get_models():
all_models.append((model._meta.app_label, model.__name__, extract_doc(model))) all_models.append(
(model._meta.app_label, model.__name__, extract_doc(model))
)
return all_models return all_models
def get_all_templates(): def get_all_templates():
template_dirs = get_app_template_dirs('templates') template_dirs = get_app_template_dirs("templates")
templates = [] templates = []
for template_dir in template_dirs: for template_dir in template_dirs:
app_name = os.path.basename(os.path.dirname(os.path.dirname(template_dir))) app_name = os.path.basename(
os.path.dirname(os.path.dirname(template_dir))
)
for root, dirs, files in os.walk(template_dir): for root, dirs, files in os.walk(template_dir):
for file in files: for file in files:
if file.endswith(('.html', '.htm', '.txt')): if file.endswith((".html", ".htm", ".txt")):
rel_path = os.path.relpath(os.path.join(root, file), template_dir) rel_path = os.path.relpath(
with open(os.path.join(root, file), 'r', encoding='utf-8', errors='ignore') as f: os.path.join(root, file), template_dir
)
with open(
os.path.join(root, file),
"r",
encoding="utf-8",
errors="ignore",
) as f:
try: try:
content = f.read() content = f.read()
# Extract template comment documentation if it exists # Extract template comment documentation if it exists
doc = "" doc = ""
if '{# DOC:' in content and '#}' in content: if "{# DOC:" in content and "#}" in content:
doc_parts = content.split('{# DOC:') doc_parts = content.split("{# DOC:")
for part in doc_parts[1:]: for part in doc_parts[1:]:
if '#}' in part: if "#}" in part:
doc += part.split('#}')[0].strip() + "\n" doc += (
part.split("#}")[0].strip() + "\n"
)
except Exception as e: except Exception as e:
self.stdout.write(self.style.WARNING(f"Error reading {rel_path}: {e}")) self.stdout.write(
self.style.WARNING(
f"Error reading {rel_path}: {e}"
)
)
continue continue
templates.append((app_name, rel_path, doc.strip())) templates.append((app_name, rel_path, doc.strip()))
@ -75,17 +94,24 @@ class Command(BaseCommand):
# Look for workflow documentation files # Look for workflow documentation files
def get_workflow_docs(): def get_workflow_docs():
workflows = {} workflows = {}
workflow_dir = os.path.join(settings.BASE_DIR, 'docs', 'workflows') workflow_dir = os.path.join(settings.BASE_DIR, "docs", "workflows")
if os.path.exists(workflow_dir): if os.path.exists(workflow_dir):
for file in os.listdir(workflow_dir): for file in os.listdir(workflow_dir):
if file.endswith('.yaml') or file.endswith('.yml'): if file.endswith(".yaml") or file.endswith(".yml"):
try: try:
with open(os.path.join(workflow_dir, file), 'r') as f: with open(os.path.join(workflow_dir, file), "r") as f:
workflow_data = yaml.safe_load(f) workflow_data = yaml.safe_load(f)
for workflow_name, workflow_info in workflow_data.items(): for (
workflow_name,
workflow_info,
) in workflow_data.items():
workflows[workflow_name] = workflow_info workflows[workflow_name] = workflow_info
except Exception as e: except Exception as e:
self.stdout.write(self.style.WARNING(f"Error reading workflow file {file}: {e}")) self.stdout.write(
self.style.WARNING(
f"Error reading workflow file {file}: {e}"
)
)
return workflows return workflows
# Extract views # Extract views
@ -96,26 +122,28 @@ class Command(BaseCommand):
kb["features"][name] = { kb["features"][name] = {
"description": doc, "description": doc,
"source": f"{app}.views.{name}", "source": f"{app}.views.{name}",
"type": "view_function" "type": "view_function",
} }
# Look for @workflow decorator or WORKFLOW tag in docstring # Look for @workflow decorator or WORKFLOW tag in docstring
if hasattr(obj, 'workflow_steps') or 'WORKFLOW:' in doc: if hasattr(obj, "workflow_steps") or "WORKFLOW:" in doc:
workflow_name = name.replace('_', ' ').title() workflow_name = name.replace("_", " ").title()
steps = [] steps = []
if hasattr(obj, 'workflow_steps'): if hasattr(obj, "workflow_steps"):
steps = obj.workflow_steps steps = obj.workflow_steps
elif 'WORKFLOW:' in doc: elif "WORKFLOW:" in doc:
workflow_section = doc.split('WORKFLOW:')[1].strip() workflow_section = doc.split("WORKFLOW:")[1].strip()
steps_text = workflow_section.split('\n') steps_text = workflow_section.split("\n")
steps = [step.strip() for step in steps_text if step.strip()] steps = [
step.strip() for step in steps_text if step.strip()
]
if steps: if steps:
kb["user_workflows"][workflow_name] = { kb["user_workflows"][workflow_name] = {
"description": f"How to {name.replace('_', ' ')}", "description": f"How to {name.replace('_', ' ')}",
"steps": steps, "steps": steps,
"source": f"{app}.views.{name}" "source": f"{app}.views.{name}",
} }
# Extract models # Extract models
@ -124,7 +152,7 @@ class Command(BaseCommand):
kb["features"][name] = { kb["features"][name] = {
"description": doc, "description": doc,
"source": f"{app}.models.{name}", "source": f"{app}.models.{name}",
"type": "model_class" "type": "model_class",
} }
# Extract templates # Extract templates
@ -134,7 +162,7 @@ class Command(BaseCommand):
kb["templates"][template_id] = { kb["templates"][template_id] = {
"description": doc, "description": doc,
"path": template_path, "path": template_path,
"app": app "app": app,
} }
# Add workflow documentation # Add workflow documentation
@ -153,9 +181,9 @@ class Command(BaseCommand):
"Select the car series from the available options", "Select the car series from the available options",
"Select the trim level for the car", "Select the trim level for the car",
"Fill in additional details like color, mileage, and price", "Fill in additional details like color, mileage, and price",
"Click 'Save' to add the car to inventory, or 'Save & Add Another' to continue adding cars" "Click 'Save' to add the car to inventory, or 'Save & Add Another' to continue adding cars",
], ],
"source": "manual_documentation" "source": "manual_documentation",
}, },
"Create New Invoice": { "Create New Invoice": {
"description": "How to create a new invoice", "description": "How to create a new invoice",
@ -167,13 +195,15 @@ class Command(BaseCommand):
"Select the car(s) to include in the invoice", "Select the car(s) to include in the invoice",
"Add any additional services or parts by clicking 'Add Item'", "Add any additional services or parts by clicking 'Add Item'",
"Set the payment terms and due date", "Set the payment terms and due date",
"Click 'Save Draft' to save without finalizing, or 'Finalize Invoice' to complete" "Click 'Save Draft' to save without finalizing, or 'Finalize Invoice' to complete",
], ],
"source": "manual_documentation" "source": "manual_documentation",
} },
} }
with open(output_file, "w", encoding="utf-8") as f: with open(output_file, "w", encoding="utf-8") as f:
yaml.dump(kb, f, allow_unicode=True, sort_keys=False) yaml.dump(kb, f, allow_unicode=True, sort_keys=False)
self.stdout.write(self.style.SUCCESS(f"✅ YAML knowledge base saved to {output_file}")) self.stdout.write(
self.style.SUCCESS(f"✅ YAML knowledge base saved to {output_file}")
)

View File

@ -7,44 +7,90 @@ from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
initial = True initial = True
dependencies = [ dependencies = [
('inventory', '__first__'), ("inventory", "__first__"),
migrations.swappable_dependency(settings.AUTH_USER_MODEL), migrations.swappable_dependency(settings.AUTH_USER_MODEL),
] ]
operations = [ operations = [
migrations.CreateModel( migrations.CreateModel(
name='AnalysisCache', name="AnalysisCache",
fields=[ fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), (
('prompt_hash', models.CharField(db_index=True, max_length=64)), "id",
('dealer_id', models.IntegerField(blank=True, db_index=True, null=True)), models.BigAutoField(
('created_at', models.DateTimeField(default=django.utils.timezone.now)), auto_created=True,
('updated_at', models.DateTimeField(auto_now=True)), primary_key=True,
('expires_at', models.DateTimeField(db_index=True)), serialize=False,
('result', models.JSONField()), verbose_name="ID",
('user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), ),
),
("prompt_hash", models.CharField(db_index=True, max_length=64)),
(
"dealer_id",
models.IntegerField(blank=True, db_index=True, null=True),
),
("created_at", models.DateTimeField(default=django.utils.timezone.now)),
("updated_at", models.DateTimeField(auto_now=True)),
("expires_at", models.DateTimeField(db_index=True)),
("result", models.JSONField()),
(
"user",
models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.CASCADE,
to=settings.AUTH_USER_MODEL,
),
),
], ],
options={ options={
'verbose_name_plural': 'Analysis caches', "verbose_name_plural": "Analysis caches",
'indexes': [models.Index(fields=['prompt_hash', 'dealer_id'], name='haikalbot_a_prompt__b98e1e_idx'), models.Index(fields=['expires_at'], name='haikalbot_a_expires_e790cd_idx')], "indexes": [
models.Index(
fields=["prompt_hash", "dealer_id"],
name="haikalbot_a_prompt__b98e1e_idx",
),
models.Index(
fields=["expires_at"], name="haikalbot_a_expires_e790cd_idx"
),
],
}, },
), ),
migrations.CreateModel( migrations.CreateModel(
name='ChatLog', name="ChatLog",
fields=[ fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), (
('user_message', models.TextField()), "id",
('chatbot_response', models.TextField()), models.BigAutoField(
('timestamp', models.DateTimeField(auto_now_add=True, db_index=True)), auto_created=True,
('dealer', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='chatlogs', to='inventory.dealer')), primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("user_message", models.TextField()),
("chatbot_response", models.TextField()),
("timestamp", models.DateTimeField(auto_now_add=True, db_index=True)),
(
"dealer",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="chatlogs",
to="inventory.dealer",
),
),
], ],
options={ options={
'ordering': ['-timestamp'], "ordering": ["-timestamp"],
'indexes': [models.Index(fields=['dealer', 'timestamp'], name='haikalbot_c_dealer__6f8d63_idx')], "indexes": [
models.Index(
fields=["dealer", "timestamp"],
name="haikalbot_c_dealer__6f8d63_idx",
)
],
}, },
), ),
] ]

View File

@ -24,15 +24,18 @@ class ChatLog(models.Model):
:ivar timestamp: The date and time when the chat log entry was created. :ivar timestamp: The date and time when the chat log entry was created.
:type timestamp: datetime :type timestamp: datetime
""" """
dealer = models.ForeignKey(Dealer, on_delete=models.CASCADE, related_name='chatlogs', db_index=True)
dealer = models.ForeignKey(
Dealer, on_delete=models.CASCADE, related_name="chatlogs", db_index=True
)
user_message = models.TextField() user_message = models.TextField()
chatbot_response = models.TextField() chatbot_response = models.TextField()
timestamp = models.DateTimeField(auto_now_add=True, db_index=True) timestamp = models.DateTimeField(auto_now_add=True, db_index=True)
class Meta: class Meta:
ordering = ['-timestamp'] ordering = ["-timestamp"]
indexes = [ indexes = [
models.Index(fields=['dealer', 'timestamp']), models.Index(fields=["dealer", "timestamp"]),
] ]
def __str__(self): def __str__(self):
@ -62,8 +65,11 @@ class AnalysisCache(models.Model):
:ivar result: The cached analysis result :ivar result: The cached analysis result
:type result: dict :type result: dict
""" """
prompt_hash = models.CharField(max_length=64, db_index=True) prompt_hash = models.CharField(max_length=64, db_index=True)
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, null=True, blank=True) user = models.ForeignKey(
settings.AUTH_USER_MODEL, on_delete=models.CASCADE, null=True, blank=True
)
dealer_id = models.IntegerField(null=True, blank=True, db_index=True) dealer_id = models.IntegerField(null=True, blank=True, db_index=True)
created_at = models.DateTimeField(default=timezone.now) created_at = models.DateTimeField(default=timezone.now)
updated_at = models.DateTimeField(auto_now=True) updated_at = models.DateTimeField(auto_now=True)
@ -72,8 +78,8 @@ class AnalysisCache(models.Model):
class Meta: class Meta:
indexes = [ indexes = [
models.Index(fields=['prompt_hash', 'dealer_id']), models.Index(fields=["prompt_hash", "dealer_id"]),
models.Index(fields=['expires_at']), models.Index(fields=["expires_at"]),
] ]
verbose_name_plural = "Analysis caches" verbose_name_plural = "Analysis caches"

View File

@ -1,2 +1 @@
# Create your tests here. # Create your tests here.

View File

@ -43,10 +43,7 @@ Provide a clear step-by-step guide with numbered instructions. Include:
Helpful Step-by-Step Instructions:""" Helpful Step-by-Step Instructions:"""
PROMPT = PromptTemplate( PROMPT = PromptTemplate(template=template, input_variables=["context", "question"])
template=template,
input_variables=["context", "question"]
)
# Setup QA chain # Setup QA chain
qa = RetrievalQA.from_chain_type( qa = RetrievalQA.from_chain_type(
@ -54,23 +51,25 @@ qa = RetrievalQA.from_chain_type(
chain_type="stuff", chain_type="stuff",
retriever=index.vectorstore.as_retriever(), retriever=index.vectorstore.as_retriever(),
return_source_documents=True, return_source_documents=True,
chain_type_kwargs={"prompt": PROMPT} chain_type_kwargs={"prompt": PROMPT},
) )
# Function to run a query # Function to run a query
def ask_haikal(query): def ask_haikal(query):
response = qa.invoke({"query": query}) response = qa.invoke({"query": query})
print("\n" + "="*50) print("\n" + "=" * 50)
print(f"Question: {query}") print(f"Question: {query}")
print("="*50) print("=" * 50)
print("\nAnswer:") print("\nAnswer:")
print(response["result"]) print(response["result"])
print("\nSources:") print("\nSources:")
for doc in response["source_documents"]: for doc in response["source_documents"]:
print(f"- {doc.metadata.get('source', 'Unknown source')}") print(f"- {doc.metadata.get('source', 'Unknown source')}")
print("="*50) print("=" * 50)
return response["result"] return response["result"]
# # Example query # # Example query
# if __name__ == "__main__": # if __name__ == "__main__":
# query = "How do I add a new car to the inventory? answer in Arabic" # query = "How do I add a new car to the inventory? answer in Arabic"

View File

@ -15,17 +15,16 @@ def export_to_excel(self, data, filename):
HttpResponse: Response with Excel file HttpResponse: Response with Excel file
""" """
# Convert data to DataFrame # Convert data to DataFrame
df = pd.DataFrame(data) df = pd.DataFrame(data)
# Create Excel file in memory # Create Excel file in memory
excel_file = BytesIO() excel_file = BytesIO()
with pd.ExcelWriter(excel_file, engine='xlsxwriter') as writer: with pd.ExcelWriter(excel_file, engine="xlsxwriter") as writer:
df.to_excel(writer, sheet_name='Model Analysis', index=False) df.to_excel(writer, sheet_name="Model Analysis", index=False)
# Auto-adjust columns width # Auto-adjust columns width
worksheet = writer.sheets['Model Analysis'] worksheet = writer.sheets["Model Analysis"]
for i, col in enumerate(df.columns): for i, col in enumerate(df.columns):
max_width = max(df[col].astype(str).map(len).max(), len(col)) + 2 max_width = max(df[col].astype(str).map(len).max(), len(col)) + 2
worksheet.set_column(i, i, max_width) worksheet.set_column(i, i, max_width)
@ -34,9 +33,9 @@ def export_to_excel(self, data, filename):
excel_file.seek(0) excel_file.seek(0)
response = HttpResponse( response = HttpResponse(
excel_file.read(), excel_file.read(),
content_type='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' content_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
) )
response['Content-Disposition'] = f'attachment; filename="{filename}.xlsx"' response["Content-Disposition"] = f'attachment; filename="{filename}.xlsx"'
return response return response
@ -52,7 +51,6 @@ def export_to_csv(self, data, filename):
HttpResponse: Response with CSV file HttpResponse: Response with CSV file
""" """
# Convert data to DataFrame # Convert data to DataFrame
df = pd.DataFrame(data) df = pd.DataFrame(data)
@ -61,6 +59,6 @@ def export_to_csv(self, data, filename):
df.to_csv(csv_file, index=False) df.to_csv(csv_file, index=False)
# Set up response # Set up response
response = HttpResponse(csv_file.getvalue(), content_type='text/csv') response = HttpResponse(csv_file.getvalue(), content_type="text/csv")
response['Content-Disposition'] = f'attachment; filename="{filename}.csv"' response["Content-Disposition"] = f'attachment; filename="{filename}.csv"'
return response return response

View File

@ -1,10 +1,10 @@
def format_response(prompt, language, request_id, timestamp): def format_response(prompt, language, request_id, timestamp):
""" """
Format a standardized response structure based on language. Format a standardized response structure based on language.
This utility creates a consistent response structure with the appropriate This utility creates a consistent response structure with the appropriate
keys based on the specified language. keys based on the specified language.
:param prompt: The original user prompt :param prompt: The original user prompt
:type prompt: str :type prompt: str
:param language: Language code ('en' or 'ar') :param language: Language code ('en' or 'ar')
@ -16,28 +16,28 @@ def format_response(prompt, language, request_id, timestamp):
:return: Formatted response structure :return: Formatted response structure
:rtype: dict :rtype: dict
""" """
if language == 'ar': if language == "ar":
return { return {
'حالة': "نجاح", "حالة": "نجاح",
'معرف_الطلب': request_id, "معرف_الطلب": request_id,
'الطابع_الزمني': timestamp, "الطابع_الزمني": timestamp,
'الاستعلام': prompt, "الاستعلام": prompt,
'التحليلات': [] "التحليلات": [],
} }
else: else:
return { return {
'status': "success", "status": "success",
'request_id': request_id, "request_id": request_id,
'timestamp': timestamp, "timestamp": timestamp,
'prompt': prompt, "prompt": prompt,
'insights': [] "insights": [],
} }
def format_error_response(message, status_code, language='en'): def format_error_response(message, status_code, language="en"):
""" """
Format a standardized error response. Format a standardized error response.
:param message: Error message :param message: Error message
:type message: str :type message: str
:param status_code: HTTP status code :param status_code: HTTP status code
@ -47,24 +47,16 @@ def format_error_response(message, status_code, language='en'):
:return: Formatted error response :return: Formatted error response
:rtype: dict :rtype: dict
""" """
if language == 'ar': if language == "ar":
return { return {"حالة": "خطأ", "رسالة": message, "رمز_الحالة": status_code}
'حالة': "خطأ",
'رسالة': message,
'رمز_الحالة': status_code
}
else: else:
return { return {"status": "error", "message": message, "status_code": status_code}
'status': "error",
'message': message,
'status_code': status_code
}
def format_insights_for_display(insights, language='en'): def format_insights_for_display(insights, language="en"):
""" """
Format insights for human-readable display. Format insights for human-readable display.
:param insights: Raw insights data :param insights: Raw insights data
:type insights: dict :type insights: dict
:param language: Language code ('en' or 'ar') :param language: Language code ('en' or 'ar')
@ -73,47 +65,55 @@ def format_insights_for_display(insights, language='en'):
:rtype: str :rtype: str
""" """
formatted_text = "" formatted_text = ""
# Determine keys based on language # Determine keys based on language
insights_key = 'التحليلات' if language == 'ar' else 'insights' insights_key = "التحليلات" if language == "ar" else "insights"
recs_key = 'التوصيات' if language == 'ar' else 'recommendations' recs_key = "التوصيات" if language == "ar" else "recommendations"
# Format insights # Format insights
if insights_key in insights and insights[insights_key]: if insights_key in insights and insights[insights_key]:
header = "## نتائج التحليل\n\n" if language == 'ar' else "## Analysis Results\n\n" header = (
"## نتائج التحليل\n\n" if language == "ar" else "## Analysis Results\n\n"
)
formatted_text += header formatted_text += header
for insight in insights[insights_key]: for insight in insights[insights_key]:
if isinstance(insight, dict): if isinstance(insight, dict):
# Add insight type as header # Add insight type as header
if 'type' in insight or 'نوع' in insight: if "type" in insight or "نوع" in insight:
type_key = 'نوع' if language == 'ar' else 'type' type_key = "نوع" if language == "ar" else "type"
insight_type = insight.get(type_key, insight.get('type', insight.get('نوع', ''))) insight_type = insight.get(
type_key, insight.get("type", insight.get("نوع", ""))
)
formatted_text += f"### {insight_type}\n\n" formatted_text += f"### {insight_type}\n\n"
# Format results if present # Format results if present
results_key = 'النتائج' if language == 'ar' else 'results' results_key = "النتائج" if language == "ar" else "results"
if results_key in insight: if results_key in insight:
for result in insight[results_key]: for result in insight[results_key]:
model_key = 'النموذج' if language == 'ar' else 'model' model_key = "النموذج" if language == "ar" else "model"
error_key = 'خطأ' if language == 'ar' else 'error' error_key = "خطأ" if language == "ar" else "error"
count_key = 'العدد' if language == 'ar' else 'count' count_key = "العدد" if language == "ar" else "count"
model_name = result.get(model_key, result.get('model', '')) model_name = result.get(model_key, result.get("model", ""))
if error_key in result: if error_key in result:
formatted_text += f"- **{model_name}**: {result[error_key]}\n" formatted_text += (
f"- **{model_name}**: {result[error_key]}\n"
)
elif count_key in result: elif count_key in result:
formatted_text += f"- **{model_name}**: {result[count_key]}\n" formatted_text += (
f"- **{model_name}**: {result[count_key]}\n"
)
formatted_text += "\n" formatted_text += "\n"
# Format recommendations # Format recommendations
if recs_key in insights and insights[recs_key]: if recs_key in insights and insights[recs_key]:
header = "## التوصيات\n\n" if language == 'ar' else "## Recommendations\n\n" header = "## التوصيات\n\n" if language == "ar" else "## Recommendations\n\n"
formatted_text += header formatted_text += header
for rec in insights[recs_key]: for rec in insights[recs_key]:
formatted_text += f"- {rec}\n" formatted_text += f"- {rec}\n"
return formatted_text return formatted_text

View File

@ -5,20 +5,22 @@ from django.utils.translation import gettext as _
from django.views import View from django.views import View
import logging import logging
from .ai_agent import analyze_prompt from .ai_agent import analyze_prompt
# from .haikal_agent import DatabaseInsightSystem, analyze_prompt_sync # from .haikal_agent import DatabaseInsightSystem, analyze_prompt_sync
from .utils.export import export_to_excel, export_to_csv from .utils.export import export_to_excel, export_to_csv
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
# analyze_prompt_ai = DatabaseInsightSystem # analyze_prompt_ai = DatabaseInsightSystem
class HaikalBot(LoginRequiredMixin, View): class HaikalBot(LoginRequiredMixin, View):
def get(self, request, *args, **kwargs): def get(self, request, *args, **kwargs):
""" """
Render the chat interface. Render the chat interface.
""" """
context = { context = {
'dark_mode': request.session.get('dark_mode', False), "dark_mode": request.session.get("dark_mode", False),
'page_title': _('AI Assistant') "page_title": _("AI Assistant"),
} }
return render(request, "haikalbot/chat.html", context) return render(request, "haikalbot/chat.html", context)
@ -31,7 +33,9 @@ class HaikalBot(LoginRequiredMixin, View):
language = request.POST.get("language", request.LANGUAGE_CODE) language = request.POST.get("language", request.LANGUAGE_CODE)
if not prompt: if not prompt:
error_msg = _("Prompt is required.") if language != "ar" else "الاستعلام مطلوب." error_msg = (
_("Prompt is required.") if language != "ar" else "الاستعلام مطلوب."
)
return JsonResponse({"status": "error", "error": error_msg}, status=400) return JsonResponse({"status": "error", "error": error_msg}, status=400)
try: try:
result = analyze_prompt(prompt) result = analyze_prompt(prompt)
@ -54,8 +58,11 @@ class HaikalBot(LoginRequiredMixin, View):
if language == "ar": if language == "ar":
error_msg = "حدث خطأ أثناء معالجة طلبك." error_msg = "حدث خطأ أثناء معالجة طلبك."
return JsonResponse({ return JsonResponse(
"status": "error", {
"error": error_msg, "status": "error",
"details": str(e) if request.user.is_staff else None "error": error_msg,
}, status=500) "details": str(e) if request.user.is_staff else None,
},
status=500,
)

View File

@ -2,6 +2,7 @@
from django.contrib import admin from django.contrib import admin
from . import models from . import models
from django_ledger import models as ledger_models from django_ledger import models as ledger_models
# from django_pdf_actions.actions import export_to_pdf_landscape, export_to_pdf_portrait # from django_pdf_actions.actions import export_to_pdf_landscape, export_to_pdf_portrait
# from appointment import models as appointment_models # from appointment import models as appointment_models
from import_export.admin import ExportMixin from import_export.admin import ExportMixin
@ -18,10 +19,12 @@ from import_export.resources import ModelResource
# class CarSeriesAdmin(ExportMixin, admin.ModelAdmin): # class CarSeriesAdmin(ExportMixin, admin.ModelAdmin):
# resource_class = CarSerieResource # resource_class = CarSerieResource
class CarTrimResource(ModelResource): class CarTrimResource(ModelResource):
class Meta: class Meta:
model = models.CarTrim model = models.CarTrim
@admin.register(models.CarTrim) @admin.register(models.CarTrim)
class CarTrimAdmin(ExportMixin, admin.ModelAdmin): class CarTrimAdmin(ExportMixin, admin.ModelAdmin):
resource_class = CarTrimResource resource_class = CarTrimResource
@ -71,44 +74,55 @@ admin.site.register(models.DealersMake)
@admin.register(models.Car) @admin.register(models.Car)
class CarAdmin(admin.ModelAdmin): class CarAdmin(admin.ModelAdmin):
search_fields = ('vin',) search_fields = ("vin",)
# actions = [export_to_pdf_landscape, export_to_pdf_portrait] # actions = [export_to_pdf_landscape, export_to_pdf_portrait]
@admin.register(models.CarMake) @admin.register(models.CarMake)
class CarMakeAdmin(admin.ModelAdmin): class CarMakeAdmin(admin.ModelAdmin):
list_display = ('name', 'arabic_name', 'is_sa_import') list_display = ("name", "arabic_name", "is_sa_import")
search_fields = ('name', 'arabic_name') search_fields = ("name", "arabic_name")
list_filter = ('is_sa_import', 'name',) list_filter = (
"is_sa_import",
"name",
)
# actions = [export_to_pdf_landscape, export_to_pdf_portrait] # actions = [export_to_pdf_landscape, export_to_pdf_portrait]
class Meta: class Meta:
verbose_name = "Car Make" verbose_name = "Car Make"
ordering = ('name',) ordering = ("name",)
@admin.register(models.CarModel) @admin.register(models.CarModel)
class CarModelAdmin(admin.ModelAdmin): class CarModelAdmin(admin.ModelAdmin):
list_display = ('name', 'arabic_name', 'id_car_make', 'get_is_sa_import') list_display = ("name", "arabic_name", "id_car_make", "get_is_sa_import")
search_fields = ('id_car_model', 'name', 'arabic_name') search_fields = ("id_car_model", "name", "arabic_name")
list_filter = ('id_car_make__is_sa_import', 'id_car_make') list_filter = ("id_car_make__is_sa_import", "id_car_make")
sortable_by = ['name', 'arabic_name', 'id_car_make'] sortable_by = ["name", "arabic_name", "id_car_make"]
def get_is_sa_import(self, obj): def get_is_sa_import(self, obj):
return obj.id_car_make.is_sa_import return obj.id_car_make.is_sa_import
get_is_sa_import.boolean = True get_is_sa_import.boolean = True
get_is_sa_import.short_description = 'Is SA Import' get_is_sa_import.short_description = "Is SA Import"
class Meta: class Meta:
verbose_name = "Car Model" verbose_name = "Car Model"
ordering = ('name',) ordering = ("name",)
@admin.register(models.CarSerie) @admin.register(models.CarSerie)
class CarSeriesAdmin(admin.ModelAdmin): class CarSeriesAdmin(admin.ModelAdmin):
list_display = ('name', 'arabic_name', 'id_car_model', ) list_display = (
search_fields = ('name',) "name",
list_filter = ('id_car_model__id_car_make__is_sa_import', "arabic_name",
'id_car_model__id_car_make__name',) "id_car_model",
)
search_fields = ("name",)
list_filter = (
"id_car_model__id_car_make__is_sa_import",
"id_car_model__id_car_make__name",
)
class Meta: class Meta:
verbose_name = "Car Series" verbose_name = "Car Series"
@ -130,9 +144,9 @@ class CarSeriesAdmin(admin.ModelAdmin):
@admin.register(models.CarSpecification) @admin.register(models.CarSpecification)
class CarSpecificationAdmin(admin.ModelAdmin): class CarSpecificationAdmin(admin.ModelAdmin):
list_display = ('name', 'arabic_name', 'id_parent') list_display = ("name", "arabic_name", "id_parent")
search_fields = ('name', 'id_parent') search_fields = ("name", "id_parent")
list_filter = ('id_parent',) list_filter = ("id_parent",)
class Meta: class Meta:
verbose_name = "Car Specification" verbose_name = "Car Specification"
@ -140,12 +154,14 @@ class CarSpecificationAdmin(admin.ModelAdmin):
@admin.register(models.CarOption) @admin.register(models.CarOption)
class CarOptionAdmin(admin.ModelAdmin): class CarOptionAdmin(admin.ModelAdmin):
list_display = ('name', 'arabic_name', 'id_parent') list_display = ("name", "arabic_name", "id_parent")
search_fields = ('name', 'id_parent') search_fields = ("name", "id_parent")
# list_filter = ('id_parent',) # list_filter = ('id_parent',)
class Meta: class Meta:
verbose_name = "Car Option" verbose_name = "Car Option"
# @admin.register(models.UserActivityLog) # @admin.register(models.UserActivityLog)
# class UserActivityLogAdmin(admin.ModelAdmin): # class UserActivityLogAdmin(admin.ModelAdmin):
# list_display = ('user', 'action', 'timestamp') # list_display = ('user', 'action', 'timestamp')
@ -156,5 +172,3 @@ class CarOptionAdmin(admin.ModelAdmin):
# class ItemTransactionModelAdmin(admin.ModelAdmin): # class ItemTransactionModelAdmin(admin.ModelAdmin):
# actions = [export_to_pdf_landscape, export_to_pdf_portrait] # actions = [export_to_pdf_landscape, export_to_pdf_portrait]

View File

@ -1,8 +1,9 @@
from django.apps import AppConfig from django.apps import AppConfig
class InventoryConfig(AppConfig): class InventoryConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField' default_auto_field = "django.db.models.BigAutoField"
name = 'inventory' name = "inventory"
def ready(self): def ready(self):
import inventory.signals import inventory.signals

View File

@ -1,5 +1,6 @@
from django.conf import settings from django.conf import settings
def currency_context(request): def currency_context(request):
""" """
Provides a context dictionary containing the currency setting. This is typically Provides a context dictionary containing the currency setting. This is typically
@ -14,9 +15,7 @@ def currency_context(request):
project's CURRENCY setting. project's CURRENCY setting.
:rtype: dict :rtype: dict
""" """
return { return {"CURRENCY": settings.CURRENCY}
'CURRENCY': settings.CURRENCY
}
def breadcrumbs(request): def breadcrumbs(request):
@ -37,8 +36,8 @@ def breadcrumbs(request):
:rtype: dict :rtype: dict
""" """
breadcrumbs = [] breadcrumbs = []
path = request.path.strip('/').split('/') path = request.path.strip("/").split("/")
for i in range(len(path)): for i in range(len(path)):
url = '/' + '/'.join(path[:i+1]) + '/' url = "/" + "/".join(path[: i + 1]) + "/"
breadcrumbs.append({'name': path[i].capitalize(), 'url': url}) breadcrumbs.append({"name": path[i].capitalize(), "url": url})
return {'breadcrumbs': breadcrumbs} return {"breadcrumbs": breadcrumbs}

View File

@ -1,6 +1,7 @@
from django_ledger.models import AccountModel from django_ledger.models import AccountModel
import django_filters import django_filters
class AccountModelFilter(django_filters.FilterSet): class AccountModelFilter(django_filters.FilterSet):
""" """
Handles filtering functionality for the AccountModel. Handles filtering functionality for the AccountModel.
@ -17,6 +18,7 @@ class AccountModelFilter(django_filters.FilterSet):
:ivar fields: List of fields defined in the model that can be filtered. :ivar fields: List of fields defined in the model that can be filtered.
:type fields: list(str) :type fields: list(str)
""" """
class Meta: class Meta:
model = AccountModel model = AccountModel
fields = ['code', 'name','role'] fields = ["code", "name", "role"]

View File

@ -1270,6 +1270,7 @@ class OpportunityForm(forms.ModelForm):
widgets = { widgets = {
"expected_revenue": forms.NumberInput(attrs={"readonly": "readonly"}), "expected_revenue": forms.NumberInput(attrs={"readonly": "readonly"}),
} }
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
# Add a visible number input to display the current value # Add a visible number input to display the current value
@ -1354,19 +1355,11 @@ class SaleOrderForm(forms.ModelForm):
class Meta: class Meta:
model = SaleOrder model = SaleOrder
fields = [ fields = [
"estimate", "customer","expected_delivery_date","estimate","opportunity","comments","order_date","status"]
"payment_method",
"opportunity",
"agreed_price",
"down_payment_amount",
"loan_amount",
"expected_delivery_date",
"comments",
"status"
]
widgets = { widgets = {
"comments": forms.Textarea(attrs={"rows": 3}), "expected_delivery_date": forms.DateInput(attrs={"type": "date", "label": _("Expected Delivery Date")}),
"expected_delivery_date": forms.DateInput(attrs={"type": "date"}), "order_date": forms.DateInput(attrs={"type": "date", "label": _("Order Date")}),
"customer": forms.Select(attrs={"class": "form-control","label": _("Customer"),}),
} }
@ -1909,6 +1902,7 @@ class StaffTaskForm(forms.ModelForm):
############################################################# #############################################################
class ItemInventoryForm(forms.Form): class ItemInventoryForm(forms.Form):
make = forms.ModelChoiceField( make = forms.ModelChoiceField(
queryset=CarMake.objects.all(), queryset=CarMake.objects.all(),
@ -1932,65 +1926,64 @@ class ItemInventoryForm(forms.Form):
) )
##################################################################### #####################################################################
class CSVUploadForm(forms.Form): class CSVUploadForm(forms.Form):
dealer = forms.ModelChoiceField( dealer = forms.ModelChoiceField(
queryset=Dealer.objects.all(), queryset=Dealer.objects.all(), label=_("Dealer"), widget=forms.HiddenInput()
label=_('Dealer'),
widget=forms.HiddenInput()
) )
vendor = forms.ModelChoiceField( vendor = forms.ModelChoiceField(
queryset=Vendor.objects.all(), queryset=Vendor.objects.all(),
label=_('Vendor'), label=_("Vendor"),
widget=forms.Select(attrs={'class': 'form-select'}), widget=forms.Select(attrs={"class": "form-select"}),
required=True required=True,
) )
year = forms.IntegerField( year = forms.IntegerField(
label=_('Year'), label=_("Year"),
widget=forms.NumberInput(attrs={'class': 'form-control'}), widget=forms.NumberInput(attrs={"class": "form-control"}),
required=True required=True,
) )
exterior = forms.ModelChoiceField( exterior = forms.ModelChoiceField(
queryset=ExteriorColors.objects.all(), queryset=ExteriorColors.objects.all(),
label=_('Exterior Color'), label=_("Exterior Color"),
widget=forms.RadioSelect(attrs={'class': 'form-select'}), widget=forms.RadioSelect(attrs={"class": "form-select"}),
required=True required=True,
) )
interior = forms.ModelChoiceField( interior = forms.ModelChoiceField(
queryset=InteriorColors.objects.all(), queryset=InteriorColors.objects.all(),
label=_('Interior Color'), label=_("Interior Color"),
widget=forms.RadioSelect(attrs={'class': 'form-select'}), widget=forms.RadioSelect(attrs={"class": "form-select"}),
required=True required=True,
) )
receiving_date = forms.DateField( receiving_date = forms.DateField(
label=_('Receiving Date'), label=_("Receiving Date"),
widget=forms.DateInput(attrs={'type': 'date', 'class': 'form-control'}), widget=forms.DateInput(attrs={"type": "date", "class": "form-control"}),
required=True required=True,
) )
def clean_csv_file(self): def clean_csv_file(self):
csv_file = self.cleaned_data['csv_file'] csv_file = self.cleaned_data["csv_file"]
if not csv_file.name.endswith('.csv'): if not csv_file.name.endswith(".csv"):
raise forms.ValidationError(_('File is not a CSV file')) raise forms.ValidationError(_("File is not a CSV file"))
# Read and validate CSV structure # Read and validate CSV structure
try: try:
csv_data = TextIOWrapper(csv_file.file, encoding='utf-8') csv_data = TextIOWrapper(csv_file.file, encoding="utf-8")
reader = csv.DictReader(csv_data) reader = csv.DictReader(csv_data)
required_fields = ['vin', 'make', 'model', 'year'] required_fields = ["vin", "make", "model", "year"]
if not all(field in reader.fieldnames for field in required_fields): if not all(field in reader.fieldnames for field in required_fields):
missing = set(required_fields) - set(reader.fieldnames) missing = set(required_fields) - set(reader.fieldnames)
raise forms.ValidationError( raise forms.ValidationError(
_('CSV is missing required columns: %(missing)s'), _("CSV is missing required columns: %(missing)s"),
params={'missing': ', '.join(missing)} params={"missing": ", ".join(missing)},
) )
except Exception as e: except Exception as e:
raise forms.ValidationError(_('Error reading CSV file: %(error)s') % {'error': str(e)}) raise forms.ValidationError(
_("Error reading CSV file: %(error)s") % {"error": str(e)}
)
# Reset file pointer for later processing # Reset file pointer for later processing
csv_file.file.seek(0) csv_file.file.seek(0)
return csv_file return csv_file

View File

@ -3,15 +3,57 @@ import re
def vin_year(vin_char: str) -> int: def vin_year(vin_char: str) -> int:
YEAR_MAPPING = { YEAR_MAPPING = {
'A': 1980, 'B': 1981, 'C': 1982, 'D': 1983, 'E': 1984, 'F': 1985, "A": 1980,
'G': 1986, 'H': 1987, 'J': 1988, 'K': 1989, 'L': 1990, 'M': 1991, "B": 1981,
'N': 1992, 'P': 1993, 'R': 1994, 'S': 1995, 'T': 1996, 'V': 1997, "C": 1982,
'W': 1998, 'X': 1999, 'Y': 2000, '1': 2001, '2': 2002, '3': 2003, "D": 1983,
'4': 2004, '5': 2005, '6': 2006, '7': 2007, '8': 2008, '9': 2009, "E": 1984,
'A': 2010, 'B': 2011, 'C': 2012, 'D': 2013, 'E': 2014, 'F': 2015, "F": 1985,
'G': 2016, 'H': 2017, 'J': 2018, 'K': 2019, 'L': 2020, 'M': 2021, "G": 1986,
'N': 2022, 'P': 2023, 'R': 2024, 'S': 2025, 'T': 2026, 'V': 2027, "H": 1987,
'W': 2028, 'X': 2029, 'Y': 2030, "J": 1988,
"K": 1989,
"L": 1990,
"M": 1991,
"N": 1992,
"P": 1993,
"R": 1994,
"S": 1995,
"T": 1996,
"V": 1997,
"W": 1998,
"X": 1999,
"Y": 2000,
"1": 2001,
"2": 2002,
"3": 2003,
"4": 2004,
"5": 2005,
"6": 2006,
"7": 2007,
"8": 2008,
"9": 2009,
"A": 2010,
"B": 2011,
"C": 2012,
"D": 2013,
"E": 2014,
"F": 2015,
"G": 2016,
"H": 2017,
"J": 2018,
"K": 2019,
"L": 2020,
"M": 2021,
"N": 2022,
"P": 2023,
"R": 2024,
"S": 2025,
"T": 2026,
"V": 2027,
"W": 2028,
"X": 2029,
"Y": 2030,
} }
# Normalize the input character to uppercase # Normalize the input character to uppercase
@ -19,7 +61,9 @@ def vin_year(vin_char: str) -> int:
# Check if the character is valid # Check if the character is valid
if vin_char not in YEAR_MAPPING: if vin_char not in YEAR_MAPPING:
raise ValueError(f"Invalid year character: '{vin_char}'. Expected one of {list(YEAR_MAPPING.keys())}.") raise ValueError(
f"Invalid year character: '{vin_char}'. Expected one of {list(YEAR_MAPPING.keys())}."
)
# Return the corresponding year # Return the corresponding year
return YEAR_MAPPING[vin_char] return YEAR_MAPPING[vin_char]
@ -1355,452 +1399,425 @@ wmi_manufacturer_mapping = {
"MM0": "Mazda", "MM0": "Mazda",
"MM6": "Mazda", "MM6": "Mazda",
"MM7": "Mazda", "MM7": "Mazda",
"MM8": "Mazda" "MM8": "Mazda",
} }
def decode_vds(manufacturer, vds): def decode_vds(manufacturer, vds):
# Mapping of manufacturers to their VDS positions and corresponding models # Mapping of manufacturers to their VDS positions and corresponding models
vds_model_mapping = { vds_model_mapping = {
'Honda': { "Honda": {
1: { 1: {
'F': 'Agila', "F": "Agila",
'G': 'Insignia', "G": "Insignia",
'J': 'Mokka', "J": "Mokka",
'L': 'Antara', "L": "Antara",
'M': 'Movano', "M": "Movano",
'P': ['Astra J', 'Zafira C'], "P": ["Astra J", "Zafira C"],
'R': 'Astra GTC J', "R": "Astra GTC J",
'S': 'Meriva', "S": "Meriva",
'V': 'Combo II', "V": "Combo II",
'W': 'Cascada', "W": "Cascada",
} }
}, },
"Kia": {
'Kia': {
1: { 1: {
'A': ['Rio', 'EV9'], "A": ["Rio", "EV9"],
'C': ['Niro', 'EV6'], "C": ["Niro", "EV6"],
'D': 'Rio', "D": "Rio",
'E': ['Stinger', 'Seltos'], "E": ["Stinger", "Seltos"],
'F': ['Forte', 'K4'], "F": ["Forte", "K4"],
'G': ['Optima', 'Magentis', 'K5'], "G": ["Optima", "Magentis", "K5"],
'H': 'Rondo', "H": "Rondo",
'J': 'Soul', "J": "Soul",
'K': ['Mohave', 'Sorento', 'Sportage'], "K": ["Mohave", "Sorento", "Sportage"],
'L': ['Cadenza', 'K9'], "L": ["Cadenza", "K9"],
'M': 'Sedona', "M": "Sedona",
'N': 'Carnival', "N": "Carnival",
'P': ['Sportage', 'Sorento', 'Telluride'], "P": ["Sportage", "Sorento", "Telluride"],
'R': 'Sorento', "R": "Sorento",
'S': 'K9' "S": "K9",
} }
}, },
"Peugeot": {
'Peugeot': {
1: { 1: {
'A': '604', "A": "604",
'2': '206', "2": "206",
'8': ['406', '508'], "8": ["406", "508"],
'6': '407', "6": "407",
'9': '308', "9": "308",
"4": '308', "4": "308",
'B': 'Expert', "B": "Expert",
'C': ['208', '504'], "C": ["208", "504"],
'D': '301', "D": "301",
'3': '307', "3": "307",
'7': ['306', 'Partner'], "7": ["306", "Partner"],
'U': '2008', "U": "2008",
'M': '3008', "M": "3008",
} }
}, },
"Toyota": {
'Toyota': {
1: { # 5th character in VDS 1: { # 5th character in VDS
'0': 'MR2 Spyder', "0": "MR2 Spyder",
'1': 'Tundra', "1": "Tundra",
'3': ['Echo', 'Yaris'], "3": ["Echo", "Yaris"],
'A': ['Highlander', 'Sequoia', 'Celica', 'Supra'], "A": ["Highlander", "Sequoia", "Celica", "Supra"],
'B': 'Avalon', "B": "Avalon",
'C': ['Sienna', 'Previa'], "C": ["Sienna", "Previa"],
'D': 'T100', "D": "T100",
'E': ['Corolla', 'Matrix'], "E": ["Corolla", "Matrix"],
'F': 'FJ Cruiser', "F": "FJ Cruiser",
'G': 'Hilux', "G": "Hilux",
'H': 'Highlander', "H": "Highlander",
'J': 'Land Cruiser', "J": "Land Cruiser",
'K': 'Camry', "K": "Camry",
'L': ['Tercel', 'Paseo'], "L": ["Tercel", "Paseo"],
'M': 'Previa', "M": "Previa",
'N': 'Tacoma', "N": "Tacoma",
'P': 'Camry', "P": "Camry",
'R': ['4Runner', 'Corolla'], "R": ["4Runner", "Corolla"],
'T': 'Celica FWD', "T": "Celica FWD",
'U': 'Prius', "U": "Prius",
'V': 'RAV4', "V": "RAV4",
'W': 'MR2 non Spyder', "W": "MR2 non Spyder",
'X': 'Cressida', "X": "Cressida",
} }
}, },
"Nissan": {
'Nissan': {
2: { 2: {
'A': ['Armada', 'Titan', 'Maxima'], "A": ["Armada", "Titan", "Maxima"],
'B': 'Sentra', "B": "Sentra",
'C': 'Versa (07-11)', "C": "Versa (07-11)",
'D': ['Truck', 'Xterra (00-04)', 'Frontier'], "D": ["Truck", "Xterra (00-04)", "Frontier"],
'J': 'Maxima', "J": "Maxima",
'L': 'Altima', "L": "Altima",
'N': 'Xterra (05-11)', "N": "Xterra (05-11)",
'P': 'Kicks', "P": "Kicks",
'R': 'Pathfinder', "R": "Pathfinder",
'S': ['240SX', 'Rogue (08-11)'], "S": ["240SX", "Rogue (08-11)"],
'T': 'X Trail', "T": "X Trail",
'U': 'Altima', "U": "Altima",
'Y': 'Patrol', "Y": "Patrol",
'Z': ['300Z', '350Z', 'Murano'], "Z": ["300Z", "350Z", "Murano"],
} }
}, },
"Renault": {
'Renault': {
2: { # 5th character in VDS 2: { # 5th character in VDS
'0': 'Twingo', "0": "Twingo",
'1': 'R4', "1": "R4",
'2': 'R25', "2": "R25",
'3': 'R4', "3": "R4",
'4': ['R21', 'Express'], "4": ["R21", "Express"],
'5': ['Clio I', 'Laguna', 'R19', 'Safrane'], "5": ["Clio I", "Laguna", "R19", "Safrane"],
'A': ['Megane I', 'Master'], "A": ["Megane I", "Master"],
'B': 'Clio II', "B": "Clio II",
'C': 'Kangoo', "C": "Kangoo",
'D': 'Master', "D": "Master",
'E': ['Espace III', 'Avantime'], "E": ["Espace III", "Avantime"],
'G': 'Laguna II', "G": "Laguna II",
'H': 'Master Propulsion', "H": "Master Propulsion",
'J': ['Vel Satis', 'New Trafic'], "J": ["Vel Satis", "New Trafic"],
'K': 'Espace IV', "K": "Espace IV",
'L': 'Trafic', "L": "Trafic",
'M': 'Megan II', "M": "Megan II",
'P': 'Modus', "P": "Modus",
'S': ['Logan', 'Sandero', 'Duster', 'Dokker', 'Lodgy'], "S": ["Logan", "Sandero", "Duster", "Dokker", "Lodgy"],
'Y': 'Koleos', "Y": "Koleos",
} }
}, },
# New Manufacturers Added Below # New Manufacturers Added Below
'Ford': { "Ford": {
3: { 3: {
'A': ['Fiesta', 'Focus'], "A": ["Fiesta", "Focus"],
'B': ['Mustang', 'Explorer'], "B": ["Mustang", "Explorer"],
'C': 'Ranger', "C": "Ranger",
'D': ['Escape', 'Edge'], "D": ["Escape", "Edge"],
'E': 'F-150', "E": "F-150",
'F': 'Transit', "F": "Transit",
'G': 'Bronco', "G": "Bronco",
'H': 'Expedition', "H": "Expedition",
} }
}, },
"BMW": {
'BMW': {
4: { 4: {
'1': '1 Series', "1": "1 Series",
'2': '2 Series', "2": "2 Series",
'3': '3 Series', "3": "3 Series",
'4': '4 Series', "4": "4 Series",
'5': '5 Series', "5": "5 Series",
'6': '6 Series', "6": "6 Series",
'7': '7 Series', "7": "7 Series",
'8': '8 Series', "8": "8 Series",
'X': 'X Series (SUV)', "X": "X Series (SUV)",
'Z': 'Z Series (Roadster)', "Z": "Z Series (Roadster)",
} }
}, },
"Mercedes-Benz": {
'Mercedes-Benz': {
3: { 3: {
'A': 'A-Class', "A": "A-Class",
'B': 'B-Class', "B": "B-Class",
'C': 'C-Class', "C": "C-Class",
'E': 'E-Class', "E": "E-Class",
'G': 'G-Class', "G": "G-Class",
'S': 'S-Class', "S": "S-Class",
'V': 'V-Class', "V": "V-Class",
'X': 'GL-Class', "X": "GL-Class",
} }
}, },
"Volkswagen": {
'Volkswagen': {
2: { 2: {
'1': 'Golf', "1": "Golf",
'2': 'Jetta', "2": "Jetta",
'3': 'Passat', "3": "Passat",
'4': 'Tiguan', "4": "Tiguan",
'5': 'Polo', "5": "Polo",
'6': 'Arteon', "6": "Arteon",
'7': 'Atlas', "7": "Atlas",
'8': 'Touareg', "8": "Touareg",
'9': 'Beetle', "9": "Beetle",
} }
}, },
"Hyundai": {
'Hyundai': {
1: { 1: {
'A': 'Accent', "A": "Accent",
'B': 'Elantra', "B": "Elantra",
'C': 'Sonata', "C": "Sonata",
'D': 'Tucson', "D": "Tucson",
'E': 'Santa Fe', "E": "Santa Fe",
'F': 'Kona', "F": "Kona",
'G': 'Palisade', "G": "Palisade",
'H': 'Veloster', "H": "Veloster",
'J': 'Genesis', "J": "Genesis",
} }
}, },
"Chevrolet": {
'Chevrolet': {
2: { 2: {
'A': 'Camaro', "A": "Camaro",
'B': 'Corvette', "B": "Corvette",
'C': 'Cruze', "C": "Cruze",
'D': 'Malibu', "D": "Malibu",
'E': 'Equinox', "E": "Equinox",
'F': 'Traverse', "F": "Traverse",
'G': 'Silverado', "G": "Silverado",
'H': 'Tahoe', "H": "Tahoe",
'J': 'Suburban', "J": "Suburban",
} }
}, },
"Audi": {
'Audi': {
3: { 3: {
'1': 'A1', "1": "A1",
'2': 'A2', "2": "A2",
'3': 'A3', "3": "A3",
'4': 'A4', "4": "A4",
'5': 'A5', "5": "A5",
'6': 'A6', "6": "A6",
'7': 'A7', "7": "A7",
'8': 'A8', "8": "A8",
'Q': 'Q Series (SUV)', "Q": "Q Series (SUV)",
'T': 'TT', "T": "TT",
} }
}, },
"Subaru": {
'Subaru': {
2: { 2: {
'A': 'Impreza', "A": "Impreza",
'B': 'Legacy', "B": "Legacy",
'C': 'Outback', "C": "Outback",
'D': 'Forester', "D": "Forester",
'E': 'Crosstrek', "E": "Crosstrek",
'F': 'BRZ', "F": "BRZ",
'G': 'Ascent', "G": "Ascent",
} }
}, },
"Mazda": {
'Mazda': {
1: { 1: {
'A': 'Mazda3', "A": "Mazda3",
'B': 'Mazda6', "B": "Mazda6",
'C': 'CX-5', "C": "CX-5",
'D': 'CX-9', "D": "CX-9",
'E': 'MX-5 Miata', "E": "MX-5 Miata",
'F': 'CX-30', "F": "CX-30",
'G': 'RX-8', "G": "RX-8",
} }
}, },
"Dongfeng": {
'Dongfeng': {
1: { 1: {
'A': 'A-Series', "A": "A-Series",
'B': 'SHINE', "B": "SHINE",
'C': 'C-Series', "C": "C-Series",
'D': 'MAGE', "D": "MAGE",
'E': ['CAPTAIN E', 'E32'], "E": ["CAPTAIN E", "E32"],
'F': 'CAPTAIN C', "F": "CAPTAIN C",
'G': 'S50', "G": "S50",
'H': 'Dongfeng Fengshen AX3', "H": "Dongfeng Fengshen AX3",
'J': 'Dongfeng Joyear SUV', "J": "Dongfeng Joyear SUV",
'K': 'Dongfeng Rich 6', "K": "Dongfeng Rich 6",
'L': 'Dongfeng Sokon', "L": "Dongfeng Sokon",
'M': 'Dongfeng Glory 580', "M": "Dongfeng Glory 580",
}, },
2: { 2: {
'3': 'C35', # Specific models in C-Series "3": "C35", # Specific models in C-Series
'1': 'C31', "1": "C31",
'2': 'C32', "2": "C32",
'7': 'C72', "7": "C72",
'6': 'A60', # Specific models in A-Series "6": "A60", # Specific models in A-Series
'3': 'A30', "3": "A30",
'X': ['AX7', 'AX4'], "X": ["AX7", "AX4"],
}, },
3: { # Third character (for AX models) 3: { # Third character (for AX models)
'7': 'AX7', # Resolves AX7 "7": "AX7", # Resolves AX7
'4': 'AX4', # Resolves AX4 "4": "AX4", # Resolves AX4
} },
}, },
"Changan": {
'Changan': {
1: { 1: {
'A': 'Changan CS35', "A": "Changan CS35",
'B': 'Changan CS55', "B": "Changan CS55",
'C': 'Changan CS75', "C": "Changan CS75",
'D': 'Changan CS85', "D": "Changan CS85",
'E': 'Changan CS95', "E": "Changan CS95",
'F': 'Changan Eado', "F": "Changan Eado",
'G': 'Changan Raeton', "G": "Changan Raeton",
'H': 'Changan Alsvin', "H": "Changan Alsvin",
'J': 'Changan UNI-T', "J": "Changan UNI-T",
'K': 'Changan UNI-K', "K": "Changan UNI-K",
'L': 'Changan Oushang', "L": "Changan Oushang",
'M': 'Changan Benni', "M": "Changan Benni",
} }
}, },
"Chery": {
'Chery': {
1: { 1: {
'A': 'Chery Arrizo 5', "A": "Chery Arrizo 5",
'B': 'Chery Arrizo 7', "B": "Chery Arrizo 7",
'C': 'Chery Tiggo 3', "C": "Chery Tiggo 3",
'D': 'Chery Tiggo 5', "D": "Chery Tiggo 5",
'E': 'Chery Tiggo 7', "E": "Chery Tiggo 7",
'F': 'Chery Tiggo 8', "F": "Chery Tiggo 8",
'G': 'Chery QQ', "G": "Chery QQ",
'H': 'Chery Fulwin', "H": "Chery Fulwin",
'J': 'Chery Cowin', "J": "Chery Cowin",
'K': 'Chery eQ1', "K": "Chery eQ1",
'L': 'Chery Exeed TX', "L": "Chery Exeed TX",
'M': 'Chery Exeed LX', "M": "Chery Exeed LX",
} }
}, },
"MG": {
'MG': {
1: { 1: {
'A': 'MG 3', "A": "MG 3",
'B': 'MG 5', "B": "MG 5",
'C': 'MG 6', "C": "MG 6",
'D': 'MG ZS', "D": "MG ZS",
'E': 'MG HS', "E": "MG HS",
'F': 'MG RX5', "F": "MG RX5",
'G': 'MG Marvel R', "G": "MG Marvel R",
'H': 'MG EZS', "H": "MG EZS",
'J': 'MG GT', "J": "MG GT",
'K': 'MG TF', "K": "MG TF",
'L': 'MG Cyberster', "L": "MG Cyberster",
} }
}, },
"JMC": {
'JMC': {
1: { 1: {
'A': 'JMC Yusheng', "A": "JMC Yusheng",
'B': 'JMC Vigus', "B": "JMC Vigus",
'C': 'JMC Baodian', "C": "JMC Baodian",
'D': 'JMC Ford Transit', "D": "JMC Ford Transit",
'E': 'JMC S350', "E": "JMC S350",
'F': 'JMC Teshun', "F": "JMC Teshun",
'G': 'JMC Realm', "G": "JMC Realm",
'H': 'JMC Yuhu', "H": "JMC Yuhu",
'J': 'JMC E200', "J": "JMC E200",
'K': 'JMC E400', "K": "JMC E400",
} }
}, },
"JAC": {
'JAC': {
1: { 1: {
'A': 'JAC J3', "A": "JAC J3",
'B': 'JAC J4', "B": "JAC J4",
'C': 'JAC J5', "C": "JAC J5",
'D': 'JAC J6', "D": "JAC J6",
'E': 'JAC J7', "E": "JAC J7",
'F': 'JAC S2', "F": "JAC S2",
'G': 'JAC S3', "G": "JAC S3",
'H': 'JAC S4', "H": "JAC S4",
'J': 'JAC S5', "J": "JAC S5",
'K': 'JAC S7', "K": "JAC S7",
'L': 'JAC iEV7S', "L": "JAC iEV7S",
'M': 'JAC iEVS4', "M": "JAC iEVS4",
} }
}, },
"BYD": {
'BYD': {
1: { 1: {
'A': 'BYD F3', "A": "BYD F3",
'B': 'BYD F6', "B": "BYD F6",
'C': 'BYD S6', "C": "BYD S6",
'D': 'BYD Tang', "D": "BYD Tang",
'E': 'BYD Song', "E": "BYD Song",
'F': 'BYD Yuan', "F": "BYD Yuan",
'G': 'BYD Qin', "G": "BYD Qin",
'H': 'BYD Han', "H": "BYD Han",
'J': 'BYD e5', "J": "BYD e5",
'K': 'BYD e6', "K": "BYD e6",
'L': 'BYD Dolphin', "L": "BYD Dolphin",
'M': 'BYD Seal', "M": "BYD Seal",
} }
}, },
"Geely": {
'Geely': {
1: { 1: {
'A': 'Geely Emgrand EC7', "A": "Geely Emgrand EC7",
'B': 'Geely Emgrand GS', "B": "Geely Emgrand GS",
'C': 'Geely Emgrand GL', "C": "Geely Emgrand GL",
'D': 'Geely Boyue', "D": "Geely Boyue",
'E': 'Geely Xingyue', "E": "Geely Xingyue",
'F': 'Geely Binrui', "F": "Geely Binrui",
'G': 'Geely Borui', "G": "Geely Borui",
'H': 'Geely Vision', "H": "Geely Vision",
'J': 'Geely Coolray', "J": "Geely Coolray",
'K': 'Geely Monjaro', "K": "Geely Monjaro",
'L': 'Geely Geometry A', "L": "Geely Geometry A",
'M': 'Geely Geometry C', "M": "Geely Geometry C",
} }
}, },
"Great Wall Motors (GWM)": {
'Great Wall Motors (GWM)': {
1: { 1: {
'A': 'GWM Haval H6', "A": "GWM Haval H6",
'B': 'GWM Haval H9', "B": "GWM Haval H9",
'C': 'GWM Haval Jolion', "C": "GWM Haval Jolion",
'D': 'GWM WEY VV5', "D": "GWM WEY VV5",
'E': 'GWM WEY VV6', "E": "GWM WEY VV6",
'F': 'GWM WEY VV7', "F": "GWM WEY VV7",
'G': 'GWM Ora R1', "G": "GWM Ora R1",
'H': 'GWM Ora Good Cat', "H": "GWM Ora Good Cat",
'J': 'GWM Poer', "J": "GWM Poer",
'K': 'GWM Tank 300', "K": "GWM Tank 300",
'L': 'GWM Tank 500', "L": "GWM Tank 500",
} }
}, },
"FAW": {
'FAW': {
1: { 1: {
'A': 'FAW Besturn B50', "A": "FAW Besturn B50",
'B': 'FAW Besturn X40', "B": "FAW Besturn X40",
'C': 'FAW Besturn X80', "C": "FAW Besturn X80",
'D': 'FAW Hongqi H5', "D": "FAW Hongqi H5",
'E': 'FAW Hongqi H7', "E": "FAW Hongqi H7",
'F': 'FAW Hongqi HS5', "F": "FAW Hongqi HS5",
'G': 'FAW Hongqi HS7', "G": "FAW Hongqi HS7",
'H': 'FAW Jiefang Truck', "H": "FAW Jiefang Truck",
'J': 'FAW Oley', "J": "FAW Oley",
'K': 'FAW Vita', "K": "FAW Vita",
} }
}, },
"SAIC Motor": {
'SAIC Motor': {
1: { 1: {
'A': 'SAIC Maxus G10', "A": "SAIC Maxus G10",
'B': 'SAIC Maxus G50', "B": "SAIC Maxus G50",
'C': 'SAIC Maxus T60', "C": "SAIC Maxus T60",
'D': 'SAIC Roewe RX5', "D": "SAIC Roewe RX5",
'E': 'SAIC Roewe i5', "E": "SAIC Roewe i5",
'F': 'SAIC Roewe i6', "F": "SAIC Roewe i6",
'G': 'SAIC MG ZS', "G": "SAIC MG ZS",
'H': 'SAIC MG HS', "H": "SAIC MG HS",
'J': 'SAIC IM LS7', "J": "SAIC IM LS7",
'K': 'SAIC IM Marvel R', "K": "SAIC IM Marvel R",
} }
}, },
} }
@ -1827,7 +1844,9 @@ def decode_vin_haikalna(vin):
pattern = r"^[A-HJ-NPR-Z0-9]{17}$" pattern = r"^[A-HJ-NPR-Z0-9]{17}$"
if not re.match(pattern, vin): if not re.match(pattern, vin):
raise Exception("VIN number must only contain alphanumeric symbols except 'I', 'O', and 'Q' ") raise Exception(
"VIN number must only contain alphanumeric symbols except 'I', 'O', and 'Q' "
)
vin = vin.upper() vin = vin.upper()
@ -1842,10 +1861,9 @@ def decode_vin_haikalna(vin):
model = decode_vds(manufacturer, vds) model = decode_vds(manufacturer, vds)
data = { data = {
'maker': manufacturer, "maker": manufacturer,
'model': model, "model": model,
'modelYear': year, "modelYear": year,
} }
print(data) print(data)
return data return data

View File

@ -6,51 +6,71 @@ from inventory.models import CarTrim
class Command(BaseCommand): class Command(BaseCommand):
help = 'Add id_car_model column to CarTrim model and populate it using a CSV file.' help = "Add id_car_model column to CarTrim model and populate it using a CSV file."
def handle(self, *args, **options): def handle(self, *args, **options):
# Define the file path relative to the project directory # Define the file path relative to the project directory
base_dir = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) base_dir = os.path.dirname(
file_path = os.path.join(base_dir, 'data/mappings.csv') os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
)
file_path = os.path.join(base_dir, "data/mappings.csv")
# Step 1: Add the new column if it does not exist # Step 1: Add the new column if it does not exist
with connection.cursor() as cursor: with connection.cursor() as cursor:
try: try:
cursor.execute("ALTER TABLE inventory_cartrim ADD COLUMN id_car_model INTEGER") cursor.execute(
self.stdout.write(self.style.SUCCESS("Column 'id_car_model' added successfully.")) "ALTER TABLE inventory_cartrim ADD COLUMN id_car_model INTEGER"
)
self.stdout.write(
self.style.SUCCESS("Column 'id_car_model' added successfully.")
)
except Exception as e: except Exception as e:
self.stdout.write(self.style.WARNING(f"Column 'id_car_model' might already exist: {e}")) self.stdout.write(
self.style.WARNING(
f"Column 'id_car_model' might already exist: {e}"
)
)
# Step 2: Read and process the CSV file # Step 2: Read and process the CSV file
try: try:
with open(file_path, mode='r', encoding='utf-8-sig') as csvfile: with open(file_path, mode="r", encoding="utf-8-sig") as csvfile:
reader = csv.DictReader(csvfile) reader = csv.DictReader(csvfile)
for row in reader: for row in reader:
# Extract id_car_serie and id_car_model from the current row # Extract id_car_serie and id_car_model from the current row
id_car_serie = row.get('id_car_serie') id_car_serie = row.get("id_car_serie")
id_car_model = row.get('id_car_model') id_car_model = row.get("id_car_model")
if not id_car_serie or not id_car_model: if not id_car_serie or not id_car_model:
self.stdout.write(self.style.WARNING(f"Skipping row with missing data: {row}")) self.stdout.write(
self.style.WARNING(f"Skipping row with missing data: {row}")
)
continue continue
# Step 3: Update CarTrim rows based on the id_car_serie # Step 3: Update CarTrim rows based on the id_car_serie
updated_count = CarTrim.objects.filter(id_car_serie=id_car_serie).update(id_car_model=id_car_model) updated_count = CarTrim.objects.filter(
id_car_serie=id_car_serie
).update(id_car_model=id_car_model)
# Output progress # Output progress
if updated_count > 0: if updated_count > 0:
self.stdout.write(self.style.SUCCESS( self.stdout.write(
f"Updated {updated_count} rows for id_car_serie={id_car_serie} with id_car_model={id_car_model}." self.style.SUCCESS(
)) f"Updated {updated_count} rows for id_car_serie={id_car_serie} with id_car_model={id_car_model}."
)
)
else: else:
self.stdout.write(self.style.WARNING( self.stdout.write(
f"No rows found for id_car_serie={id_car_serie}." self.style.WARNING(
)) f"No rows found for id_car_serie={id_car_serie}."
)
)
self.stdout.write(self.style.SUCCESS("All rows have been processed successfully!")) self.stdout.write(
self.style.SUCCESS("All rows have been processed successfully!")
)
except FileNotFoundError: except FileNotFoundError:
self.stdout.write(self.style.ERROR(f"File not found: {file_path}")) self.stdout.write(self.style.ERROR(f"File not found: {file_path}"))
except Exception as e: except Exception as e:
self.stdout.write(self.style.ERROR(f"An error occurred: {e}")) self.stdout.write(self.style.ERROR(f"An error occurred: {e}"))

View File

@ -7,24 +7,24 @@ from django.conf import settings
class Command(BaseCommand): class Command(BaseCommand):
help = 'Analyzes the car hierarchy to identify makes without models, models without series, and series without trims' help = "Analyzes the car hierarchy to identify makes without models, models without series, and series without trims"
def add_arguments(self, parser): def add_arguments(self, parser):
parser.add_argument( parser.add_argument(
'--export', "--export",
action='store_true', action="store_true",
help='Export results to CSV files', help="Export results to CSV files",
) )
parser.add_argument( parser.add_argument(
'--export-path', "--export-path",
type=str, type=str,
default='exports', default="exports",
help='Directory to export CSV files (default: "exports")', help='Directory to export CSV files (default: "exports")',
) )
def handle(self, *args, **options): def handle(self, *args, **options):
export = options['export'] export = options["export"]
export_path = options['export_path'] export_path = options["export_path"]
# Create export directory if needed # Create export directory if needed
if export: if export:
@ -35,14 +35,18 @@ class Command(BaseCommand):
# Analyze makes without models # Analyze makes without models
all_makes = CarMake.objects.all() all_makes = CarMake.objects.all()
total_makes = all_makes.count() total_makes = all_makes.count()
makes_without_models = CarMake.objects.annotate(model_count=Count('carmodel')).filter(model_count=0) makes_without_models = CarMake.objects.annotate(
model_count=Count("carmodel")
).filter(model_count=0)
makes_without_models_count = makes_without_models.count() makes_without_models_count = makes_without_models.count()
self.stdout.write(self.style.SUCCESS(f"Total car makes: {total_makes}")) self.stdout.write(self.style.SUCCESS(f"Total car makes: {total_makes}"))
self.stdout.write(self.style.SUCCESS( self.stdout.write(
f"Car makes without models: {makes_without_models_count} " self.style.SUCCESS(
f"({makes_without_models_count/total_makes*100:.2f}% of all makes)" f"Car makes without models: {makes_without_models_count} "
)) f"({makes_without_models_count / total_makes * 100:.2f}% of all makes)"
)
)
if makes_without_models_count > 0: if makes_without_models_count > 0:
self.stdout.write("\nSample of car makes without models:") self.stdout.write("\nSample of car makes without models:")
@ -52,14 +56,18 @@ class Command(BaseCommand):
# Analyze models without series # Analyze models without series
all_models = CarModel.objects.all() all_models = CarModel.objects.all()
total_models = all_models.count() total_models = all_models.count()
models_without_series = CarModel.objects.annotate(serie_count=Count('carserie')).filter(serie_count=0) models_without_series = CarModel.objects.annotate(
serie_count=Count("carserie")
).filter(serie_count=0)
models_without_series_count = models_without_series.count() models_without_series_count = models_without_series.count()
self.stdout.write(self.style.SUCCESS(f"\nTotal car models: {total_models}")) self.stdout.write(self.style.SUCCESS(f"\nTotal car models: {total_models}"))
self.stdout.write(self.style.SUCCESS( self.stdout.write(
f"Car models without series: {models_without_series_count} " self.style.SUCCESS(
f"({models_without_series_count/total_models*100:.2f}% of all models)" f"Car models without series: {models_without_series_count} "
)) f"({models_without_series_count / total_models * 100:.2f}% of all models)"
)
)
if models_without_series_count > 0: if models_without_series_count > 0:
self.stdout.write("\nSample of car models without series:") self.stdout.write("\nSample of car models without series:")
@ -69,14 +77,18 @@ class Command(BaseCommand):
# Analyze series without trims # Analyze series without trims
all_series = CarSerie.objects.all() all_series = CarSerie.objects.all()
total_series = all_series.count() total_series = all_series.count()
series_without_trims = CarSerie.objects.annotate(trim_count=Count('cartrim')).filter(trim_count=0) series_without_trims = CarSerie.objects.annotate(
trim_count=Count("cartrim")
).filter(trim_count=0)
series_without_trims_count = series_without_trims.count() series_without_trims_count = series_without_trims.count()
self.stdout.write(self.style.SUCCESS(f"\nTotal car series: {total_series}")) self.stdout.write(self.style.SUCCESS(f"\nTotal car series: {total_series}"))
self.stdout.write(self.style.SUCCESS( self.stdout.write(
f"Car series without trims: {series_without_trims_count} " self.style.SUCCESS(
f"({series_without_trims_count/total_series*100:.2f}% of all series)" f"Car series without trims: {series_without_trims_count} "
)) f"({series_without_trims_count / total_series * 100:.2f}% of all series)"
)
)
if series_without_trims_count > 0: if series_without_trims_count > 0:
self.stdout.write("\nSample of car series without trims:") self.stdout.write("\nSample of car series without trims:")
@ -90,50 +102,77 @@ class Command(BaseCommand):
if export: if export:
# Export makes without models # Export makes without models
if makes_without_models_count > 0: if makes_without_models_count > 0:
filepath = os.path.join(export_dir, 'makes_without_models.csv') filepath = os.path.join(export_dir, "makes_without_models.csv")
with open(filepath, 'w', newline='') as csvfile: with open(filepath, "w", newline="") as csvfile:
writer = csv.writer(csvfile) writer = csv.writer(csvfile)
writer.writerow(['make_id', 'make_name', 'is_sa_import']) writer.writerow(["make_id", "make_name", "is_sa_import"])
for make in makes_without_models: for make in makes_without_models:
writer.writerow([make.id_car_make, make.name, make.is_sa_import]) writer.writerow(
self.stdout.write(self.style.SUCCESS(f"Exported makes without models to {filepath}")) [make.id_car_make, make.name, make.is_sa_import]
)
self.stdout.write(
self.style.SUCCESS(f"Exported makes without models to {filepath}")
)
# Export models without series # Export models without series
if models_without_series_count > 0: if models_without_series_count > 0:
filepath = os.path.join(export_dir, 'models_without_series.csv') filepath = os.path.join(export_dir, "models_without_series.csv")
with open(filepath, 'w', newline='') as csvfile: with open(filepath, "w", newline="") as csvfile:
writer = csv.writer(csvfile) writer = csv.writer(csvfile)
writer.writerow(['model_id', 'model_name', 'make_id', 'make_name']) writer.writerow(["model_id", "model_name", "make_id", "make_name"])
for model in models_without_series: for model in models_without_series:
writer.writerow([ writer.writerow(
model.id_car_model, [
model.name, model.id_car_model,
model.id_car_make.id_car_make, model.name,
model.id_car_make.name model.id_car_make.id_car_make,
]) model.id_car_make.name,
self.stdout.write(self.style.SUCCESS(f"Exported models without series to {filepath}")) ]
)
self.stdout.write(
self.style.SUCCESS(f"Exported models without series to {filepath}")
)
# Export series without trims # Export series without trims
if series_without_trims_count > 0: if series_without_trims_count > 0:
filepath = os.path.join(export_dir, 'series_without_trims.csv') filepath = os.path.join(export_dir, "series_without_trims.csv")
with open(filepath, 'w', newline='') as csvfile: with open(filepath, "w", newline="") as csvfile:
writer = csv.writer(csvfile) writer = csv.writer(csvfile)
writer.writerow(['serie_id', 'serie_name', 'model_id', 'model_name', 'make_id', 'make_name']) writer.writerow(
[
"serie_id",
"serie_name",
"model_id",
"model_name",
"make_id",
"make_name",
]
)
for serie in series_without_trims: for serie in series_without_trims:
writer.writerow([ writer.writerow(
serie.id_car_serie, [
serie.name, serie.id_car_serie,
serie.id_car_model.id_car_model, serie.name,
serie.id_car_model.name, serie.id_car_model.id_car_model,
serie.id_car_model.id_car_make.id_car_make, serie.id_car_model.name,
serie.id_car_model.id_car_make.name serie.id_car_model.id_car_make.id_car_make,
]) serie.id_car_model.id_car_make.name,
self.stdout.write(self.style.SUCCESS(f"Exported series without trims to {filepath}")) ]
)
self.stdout.write(
self.style.SUCCESS(f"Exported series without trims to {filepath}")
)
# Summary # Summary
self.stdout.write("\n" + "="*50) self.stdout.write("\n" + "=" * 50)
self.stdout.write(self.style.SUCCESS("SUMMARY")) self.stdout.write(self.style.SUCCESS("SUMMARY"))
self.stdout.write("="*50) self.stdout.write("=" * 50)
self.stdout.write(f"Total makes: {total_makes} | Without models: {makes_without_models_count} ({makes_without_models_count/total_makes*100:.2f}%)") self.stdout.write(
self.stdout.write(f"Total models: {total_models} | Without series: {models_without_series_count} ({models_without_series_count/total_models*100:.2f}%)") f"Total makes: {total_makes} | Without models: {makes_without_models_count} ({makes_without_models_count / total_makes * 100:.2f}%)"
self.stdout.write(f"Total series: {total_series} | Without trims: {series_without_trims_count} ({series_without_trims_count/total_series*100:.2f}%)") )
self.stdout.write(
f"Total models: {total_models} | Without series: {models_without_series_count} ({models_without_series_count / total_models * 100:.2f}%)"
)
self.stdout.write(
f"Total series: {total_series} | Without trims: {series_without_trims_count} ({series_without_trims_count / total_series * 100:.2f}%)"
)

View File

@ -1,26 +1,56 @@
from django.core.management.base import BaseCommand from django.core.management.base import BaseCommand
from inventory.services import get_model,get_make,decodevin from inventory.services import get_model, get_make, decodevin
class Command(BaseCommand): class Command(BaseCommand):
help = 'Seed the Customer model with 20 records' help = "Seed the Customer model with 20 records"
def handle(self, *args, **kwargs): def handle(self, *args, **kwargs):
# vin,description = self.generate_vin() # vin,description = self.generate_vin()
result = decodevin("1HGCM82633A123456") result = decodevin("1HGCM82633A123456")
self.stdout.write(self.style.SUCCESS('####################################################################################################')) self.stdout.write(
self.stdout.write(self.style.SUCCESS('####################################################################################################')) self.style.SUCCESS(
"####################################################################################################"
)
)
self.stdout.write(
self.style.SUCCESS(
"####################################################################################################"
)
)
# self.stdout.write(self.style.SUCCESS(f'Generated VIN: {vin}')) # self.stdout.write(self.style.SUCCESS(f'Generated VIN: {vin}'))
# self.stdout.write(self.style.SUCCESS(f'Description: {description}')) # self.stdout.write(self.style.SUCCESS(f'Description: {description}'))
self.stdout.write(self.style.SUCCESS('####################################################################################################')) self.stdout.write(
self.stdout.write(self.style.SUCCESS('####################################################################################################')) self.style.SUCCESS(
self.stdout.write(self.style.SUCCESS(f'Decoded VIN: {result}')) "####################################################################################################"
make,model,year_model = result.values() )
self.stdout.write(self.style.SUCCESS(f'VIN:"1HGCM82633A123456" - Make {make} - Model {model} - Model Year {year_model}')) )
self.stdout.write(
self.style.SUCCESS(
"####################################################################################################"
)
)
self.stdout.write(self.style.SUCCESS(f"Decoded VIN: {result}"))
make, model, year_model = result.values()
self.stdout.write(
self.style.SUCCESS(
f'VIN:"1HGCM82633A123456" - Make {make} - Model {model} - Model Year {year_model}'
)
)
make = get_make(make) make = get_make(make)
model = get_model(model,make) model = get_model(model, make)
self.stdout.write(self.style.SUCCESS(f'Make: {make} - Model: {model} - Year: {year_model}'))
self.stdout.write(self.style.SUCCESS('####################################################################################################'))
self.stdout.write(self.style.SUCCESS('####################################################################################################'))
self.stdout.write(
self.style.SUCCESS(f"Make: {make} - Model: {model} - Year: {year_model}")
)
self.stdout.write(
self.style.SUCCESS(
"####################################################################################################"
)
)
self.stdout.write(
self.style.SUCCESS(
"####################################################################################################"
)
)

View File

@ -1,11 +1,11 @@
from django_ledger.io import roles from django_ledger.io import roles
from django.core.management.base import BaseCommand from django.core.management.base import BaseCommand
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from django_ledger.models import ChartOfAccountModel, AccountModel,EntityModel from django_ledger.models import ChartOfAccountModel, AccountModel, EntityModel
class Command(BaseCommand): class Command(BaseCommand):
help = 'Creates Chart of Accounts for Deepseek entity' help = "Creates Chart of Accounts for Deepseek entity"
def handle(self, *args, **options): def handle(self, *args, **options):
""" """
@ -13,9 +13,7 @@ class Command(BaseCommand):
""" """
# Create Chart of Accounts # Create Chart of Accounts
entity_model = EntityModel.objects.get( entity_model = EntityModel.objects.get(name="Claude")
name="Claude"
)
coa_model = entity_model.get_default_coa() coa_model = entity_model.get_default_coa()
# entity_model.get_all_accounts().delete() # entity_model.get_all_accounts().delete()
# coa_model, created = ChartOfAccountModel.objects.get_or_create( # coa_model, created = ChartOfAccountModel.objects.get_or_create(
@ -1130,389 +1128,382 @@ class Command(BaseCommand):
# } # }
# ] # ]
accounts_data = [ accounts_data = [
# Current Assets (must start with 1) # Current Assets (must start with 1)
{ {
'code': '1010', "code": "1010",
'name': 'Cash on Hand', "name": "Cash on Hand",
'role': roles.ASSET_CA_CASH, "role": roles.ASSET_CA_CASH,
'balance_type': roles.DEBIT, "balance_type": roles.DEBIT,
'locked': True, "locked": True,
'default': True # Default for ASSET_CA_CASH "default": True, # Default for ASSET_CA_CASH
}, },
{ {
'code': '1020', "code": "1020",
'name': 'Bank', "name": "Bank",
'role': roles.ASSET_CA_CASH, "role": roles.ASSET_CA_CASH,
'balance_type': roles.DEBIT, "balance_type": roles.DEBIT,
'locked': True, "locked": True,
'default': False "default": False,
}, },
{ {
'code': '1030', "code": "1030",
'name': 'Accounts Receivable', "name": "Accounts Receivable",
'role': roles.ASSET_CA_RECEIVABLES, "role": roles.ASSET_CA_RECEIVABLES,
'balance_type': roles.DEBIT, "balance_type": roles.DEBIT,
'locked': True, "locked": True,
'default': True # Default for ASSET_CA_RECEIVABLES "default": True, # Default for ASSET_CA_RECEIVABLES
}, },
{ {
'code': '1040', "code": "1040",
'name': 'Inventory (Cars)', "name": "Inventory (Cars)",
'role': roles.ASSET_CA_INVENTORY, "role": roles.ASSET_CA_INVENTORY,
'balance_type': roles.DEBIT, "balance_type": roles.DEBIT,
'locked': True, "locked": True,
'default': True # Default for ASSET_CA_INVENTORY "default": True, # Default for ASSET_CA_INVENTORY
}, },
{ {
'code': '1045', "code": "1045",
'name': 'Spare Parts Inventory', "name": "Spare Parts Inventory",
'role': roles.ASSET_CA_INVENTORY, "role": roles.ASSET_CA_INVENTORY,
'balance_type': roles.DEBIT, "balance_type": roles.DEBIT,
'locked': False, "locked": False,
'default': False "default": False,
}, },
{ {
'code': '1050', "code": "1050",
'name': 'Employee Advances', "name": "Employee Advances",
'role': roles.ASSET_CA_RECEIVABLES, "role": roles.ASSET_CA_RECEIVABLES,
'balance_type': roles.DEBIT, "balance_type": roles.DEBIT,
'locked': False, "locked": False,
'default': False "default": False,
}, },
{ {
'code': '1060', "code": "1060",
'name': 'Prepaid Expenses', "name": "Prepaid Expenses",
'role': roles.ASSET_CA_PREPAID, "role": roles.ASSET_CA_PREPAID,
'balance_type': roles.DEBIT, "balance_type": roles.DEBIT,
'locked': False, "locked": False,
'default': True # Default for ASSET_CA_PREPAID "default": True, # Default for ASSET_CA_PREPAID
}, },
{ {
'code': '1070', "code": "1070",
'name': 'Notes Receivable', "name": "Notes Receivable",
'role': roles.ASSET_LTI_NOTES_RECEIVABLE, "role": roles.ASSET_LTI_NOTES_RECEIVABLE,
'balance_type': roles.DEBIT, "balance_type": roles.DEBIT,
'locked': False, "locked": False,
'default': True # Default for ASSET_LTI_NOTES_RECEIVABLE "default": True, # Default for ASSET_LTI_NOTES_RECEIVABLE
}, },
# Fixed Assets (must also start with 1) # Fixed Assets (must also start with 1)
{ {
'code': '1110', "code": "1110",
'name': 'Lands', "name": "Lands",
'role': roles.ASSET_LTI_LAND, "role": roles.ASSET_LTI_LAND,
'balance_type': roles.DEBIT, "balance_type": roles.DEBIT,
'locked': False, "locked": False,
'default': True # Default for ASSET_LTI_LAND "default": True, # Default for ASSET_LTI_LAND
}, },
{ {
'code': '1111', "code": "1111",
'name': 'Buildings', "name": "Buildings",
'role': roles.ASSET_PPE_BUILDINGS, "role": roles.ASSET_PPE_BUILDINGS,
'balance_type': roles.DEBIT, "balance_type": roles.DEBIT,
'locked': False, "locked": False,
'default': True # Default for ASSET_PPE_BUILDINGS "default": True, # Default for ASSET_PPE_BUILDINGS
}, },
{ {
'code': '1112', "code": "1112",
'name': 'Company Vehicles', "name": "Company Vehicles",
'role': roles.ASSET_PPE_EQUIPMENT, "role": roles.ASSET_PPE_EQUIPMENT,
'balance_type': roles.DEBIT, "balance_type": roles.DEBIT,
'locked': False, "locked": False,
'default': True # Default for ASSET_PPE_EQUIPMENT "default": True, # Default for ASSET_PPE_EQUIPMENT
}, },
{ {
'code': '1113', "code": "1113",
'name': 'Equipment & Tools', "name": "Equipment & Tools",
'role': roles.ASSET_PPE_EQUIPMENT, "role": roles.ASSET_PPE_EQUIPMENT,
'balance_type': roles.DEBIT, "balance_type": roles.DEBIT,
'locked': False, "locked": False,
'default': False "default": False,
}, },
{ {
'code': '1114', "code": "1114",
'name': 'Furniture & Fixtures', "name": "Furniture & Fixtures",
'role': roles.ASSET_PPE_EQUIPMENT, "role": roles.ASSET_PPE_EQUIPMENT,
'balance_type': roles.DEBIT, "balance_type": roles.DEBIT,
'locked': False, "locked": False,
'default': False "default": False,
}, },
{ {
'code': '1115', "code": "1115",
'name': 'Other Fixed Assets', "name": "Other Fixed Assets",
'role': roles.ASSET_PPE_EQUIPMENT, "role": roles.ASSET_PPE_EQUIPMENT,
'balance_type': roles.DEBIT, "balance_type": roles.DEBIT,
'locked': False, "locked": False,
'default': False "default": False,
}, },
{ {
'code': '1120', "code": "1120",
'name': 'Long-term Investments', "name": "Long-term Investments",
'role': roles.ASSET_LTI_SECURITIES, "role": roles.ASSET_LTI_SECURITIES,
'balance_type': roles.DEBIT, "balance_type": roles.DEBIT,
'locked': False, "locked": False,
'default': True # Default for ASSET_LTI_SECURITIES "default": True, # Default for ASSET_LTI_SECURITIES
}, },
{ {
'code': '1130', "code": "1130",
'name': 'Intangible Assets', "name": "Intangible Assets",
'role': roles.ASSET_INTANGIBLE_ASSETS, "role": roles.ASSET_INTANGIBLE_ASSETS,
'balance_type': roles.DEBIT, "balance_type": roles.DEBIT,
'locked': False, "locked": False,
'default': True # Default for ASSET_INTANGIBLE_ASSETS "default": True, # Default for ASSET_INTANGIBLE_ASSETS
}, },
# Current Liabilities (must start with 2) # Current Liabilities (must start with 2)
{ {
'code': '2010', "code": "2010",
'name': 'Accounts Payable', "name": "Accounts Payable",
'role': roles.LIABILITY_CL_ACC_PAYABLE, "role": roles.LIABILITY_CL_ACC_PAYABLE,
'balance_type': roles.CREDIT, "balance_type": roles.CREDIT,
'locked': True, "locked": True,
'default': True # Default for LIABILITY_CL_ACC_PAYABLE "default": True, # Default for LIABILITY_CL_ACC_PAYABLE
}, },
{ {
'code': '2020', "code": "2020",
'name': 'Notes Payable', "name": "Notes Payable",
'role': roles.LIABILITY_CL_ST_NOTES_PAYABLE, "role": roles.LIABILITY_CL_ST_NOTES_PAYABLE,
'balance_type': roles.CREDIT, "balance_type": roles.CREDIT,
'locked': False, "locked": False,
'default': True # Default for LIABILITY_CL_ST_NOTES_PAYABLE "default": True, # Default for LIABILITY_CL_ST_NOTES_PAYABLE
}, },
{ {
'code': '2030', "code": "2030",
'name': 'Short-term Loans', "name": "Short-term Loans",
'role': roles.LIABILITY_CL_ST_NOTES_PAYABLE, "role": roles.LIABILITY_CL_ST_NOTES_PAYABLE,
'balance_type': roles.CREDIT, "balance_type": roles.CREDIT,
'locked': False, "locked": False,
'default': False "default": False,
}, },
{ {
'code': '2040', "code": "2040",
'name': 'Employee Payables', "name": "Employee Payables",
'role': roles.LIABILITY_CL_WAGES_PAYABLE, "role": roles.LIABILITY_CL_WAGES_PAYABLE,
'balance_type': roles.CREDIT, "balance_type": roles.CREDIT,
'locked': False, "locked": False,
'default': True # Default for LIABILITY_CL_WAGES_PAYABLE "default": True, # Default for LIABILITY_CL_WAGES_PAYABLE
}, },
{ {
'code': '2050', "code": "2050",
'name': 'Accrued Expenses', "name": "Accrued Expenses",
'role': roles.LIABILITY_CL_OTHER, "role": roles.LIABILITY_CL_OTHER,
'balance_type': roles.CREDIT, "balance_type": roles.CREDIT,
'locked': False, "locked": False,
'default': True # Default for LIABILITY_CL_OTHER "default": True, # Default for LIABILITY_CL_OTHER
}, },
{ {
'code': '2060', "code": "2060",
'name': 'Accrued Taxes', "name": "Accrued Taxes",
'role': roles.LIABILITY_CL_TAXES_PAYABLE, "role": roles.LIABILITY_CL_TAXES_PAYABLE,
'balance_type': roles.CREDIT, "balance_type": roles.CREDIT,
'locked': False, "locked": False,
'default': True # Default for LIABILITY_CL_TAXES_PAYABLE "default": True, # Default for LIABILITY_CL_TAXES_PAYABLE
}, },
{ {
'code': '2070', "code": "2070",
'name': 'Provisions', "name": "Provisions",
'role': roles.LIABILITY_CL_OTHER, "role": roles.LIABILITY_CL_OTHER,
'balance_type': roles.CREDIT, "balance_type": roles.CREDIT,
'locked': False, "locked": False,
'default': False "default": False,
}, },
# Long-term Liabilities (must also start with 2) # Long-term Liabilities (must also start with 2)
{ {
'code': '2210', "code": "2210",
'name': 'Long-term Bank Loans', "name": "Long-term Bank Loans",
'role': roles.LIABILITY_LTL_NOTES_PAYABLE, "role": roles.LIABILITY_LTL_NOTES_PAYABLE,
'balance_type': roles.CREDIT, "balance_type": roles.CREDIT,
'locked': False, "locked": False,
'default': True # Default for LIABILITY_LTL_NOTES_PAYABLE "default": True, # Default for LIABILITY_LTL_NOTES_PAYABLE
}, },
{ {
'code': '2220', "code": "2220",
'name': 'Lease Liabilities', "name": "Lease Liabilities",
'role': roles.LIABILITY_LTL_NOTES_PAYABLE, "role": roles.LIABILITY_LTL_NOTES_PAYABLE,
'balance_type': roles.CREDIT, "balance_type": roles.CREDIT,
'locked': False, "locked": False,
'default': False "default": False,
}, },
{ {
'code': '2230', "code": "2230",
'name': 'Other Long-term Liabilities', "name": "Other Long-term Liabilities",
'role': roles.LIABILITY_LTL_NOTES_PAYABLE, "role": roles.LIABILITY_LTL_NOTES_PAYABLE,
'balance_type': roles.CREDIT, "balance_type": roles.CREDIT,
'locked': False, "locked": False,
'default': False "default": False,
}, },
# Equity (must start with 3) # Equity (must start with 3)
{ {
'code': '3010', "code": "3010",
'name': 'Capital', "name": "Capital",
'role': roles.EQUITY_CAPITAL, "role": roles.EQUITY_CAPITAL,
'balance_type': roles.CREDIT, "balance_type": roles.CREDIT,
'locked': True, "locked": True,
'default': True # Default for EQUITY_CAPITAL "default": True, # Default for EQUITY_CAPITAL
}, },
{ {
'code': '3020', "code": "3020",
'name': 'Statutory Reserve', "name": "Statutory Reserve",
'role': roles.EQUITY_ADJUSTMENT, "role": roles.EQUITY_ADJUSTMENT,
'balance_type': roles.CREDIT, "balance_type": roles.CREDIT,
'locked': False, "locked": False,
'default': True # Default for EQUITY_ADJUSTMENT "default": True, # Default for EQUITY_ADJUSTMENT
}, },
{ {
'code': '3030', "code": "3030",
'name': 'Retained Earnings', "name": "Retained Earnings",
'role': roles.EQUITY_ADJUSTMENT, "role": roles.EQUITY_ADJUSTMENT,
'balance_type': roles.CREDIT, "balance_type": roles.CREDIT,
'locked': False, "locked": False,
'default': False "default": False,
}, },
{ {
'code': '3040', "code": "3040",
'name': 'Profit & Loss for the Period', "name": "Profit & Loss for the Period",
'role': roles.EQUITY_ADJUSTMENT, "role": roles.EQUITY_ADJUSTMENT,
'balance_type': roles.CREDIT, "balance_type": roles.CREDIT,
'locked': False, "locked": False,
'default': False "default": False,
}, },
# Revenue (must start with 4) # Revenue (must start with 4)
{ {
'code': '4010', "code": "4010",
'name': 'Car Sales', "name": "Car Sales",
'role': roles.INCOME_OPERATIONAL, "role": roles.INCOME_OPERATIONAL,
'balance_type': roles.CREDIT, "balance_type": roles.CREDIT,
'locked': True, "locked": True,
'default': True # Default for INCOME_OPERATIONAL "default": True, # Default for INCOME_OPERATIONAL
}, },
{ {
'code': '4020', "code": "4020",
'name': 'After-Sales Services', "name": "After-Sales Services",
'role': roles.INCOME_OPERATIONAL, "role": roles.INCOME_OPERATIONAL,
'balance_type': roles.CREDIT, "balance_type": roles.CREDIT,
'locked': False, "locked": False,
'default': False "default": False,
}, },
{ {
'code': '4030', "code": "4030",
'name': 'Car Rental Income', "name": "Car Rental Income",
'role': roles.INCOME_PASSIVE, "role": roles.INCOME_PASSIVE,
'balance_type': roles.CREDIT, "balance_type": roles.CREDIT,
'locked': False, "locked": False,
'default': True # Default for INCOME_PASSIVE "default": True, # Default for INCOME_PASSIVE
}, },
{ {
'code': '4040', "code": "4040",
'name': 'Other Income', "name": "Other Income",
'role': roles.INCOME_OTHER, "role": roles.INCOME_OTHER,
'balance_type': roles.CREDIT, "balance_type": roles.CREDIT,
'locked': False, "locked": False,
'default': True # Default for INCOME_OTHER "default": True, # Default for INCOME_OTHER
}, },
# Expenses (must start with 5 for COGS, 6 for others) # Expenses (must start with 5 for COGS, 6 for others)
{ {
'code': '5010', "code": "5010",
'name': 'Cost of Goods Sold', "name": "Cost of Goods Sold",
'role': roles.COGS, "role": roles.COGS,
'balance_type': roles.DEBIT, "balance_type": roles.DEBIT,
'locked': True, "locked": True,
'default': True # Default for COGS "default": True, # Default for COGS
}, },
{ {
'code': '5015', "code": "5015",
'name': 'Spare Parts Cost Consumed', "name": "Spare Parts Cost Consumed",
'role': roles.COGS, "role": roles.COGS,
'balance_type': roles.DEBIT, "balance_type": roles.DEBIT,
'locked': False, "locked": False,
'default': False "default": False,
}, },
{ {
'code': '6010', "code": "6010",
'name': 'Salaries & Wages', "name": "Salaries & Wages",
'role': roles.EXPENSE_OPERATIONAL, "role": roles.EXPENSE_OPERATIONAL,
'balance_type': roles.DEBIT, "balance_type": roles.DEBIT,
'locked': False, "locked": False,
'default': True # Default for EXPENSE_OPERATIONAL "default": True, # Default for EXPENSE_OPERATIONAL
}, },
{ {
'code': '6020', "code": "6020",
'name': 'Rent', "name": "Rent",
'role': roles.EXPENSE_OPERATIONAL, "role": roles.EXPENSE_OPERATIONAL,
'balance_type': roles.DEBIT, "balance_type": roles.DEBIT,
'locked': False, "locked": False,
'default': False "default": False,
}, },
{ {
'code': '6030', "code": "6030",
'name': 'Utilities', "name": "Utilities",
'role': roles.EXPENSE_OPERATIONAL, "role": roles.EXPENSE_OPERATIONAL,
'balance_type': roles.DEBIT, "balance_type": roles.DEBIT,
'locked': False, "locked": False,
'default': False "default": False,
}, },
{ {
'code': '6040', "code": "6040",
'name': 'Advertising & Marketing', "name": "Advertising & Marketing",
'role': roles.EXPENSE_OPERATIONAL, "role": roles.EXPENSE_OPERATIONAL,
'balance_type': roles.DEBIT, "balance_type": roles.DEBIT,
'locked': False, "locked": False,
'default': False "default": False,
}, },
{ {
'code': '6050', "code": "6050",
'name': 'Maintenance', "name": "Maintenance",
'role': roles.EXPENSE_OPERATIONAL, "role": roles.EXPENSE_OPERATIONAL,
'balance_type': roles.DEBIT, "balance_type": roles.DEBIT,
'locked': False, "locked": False,
'default': False "default": False,
}, },
{ {
'code': '6060', "code": "6060",
'name': 'Operating Expenses', "name": "Operating Expenses",
'role': roles.EXPENSE_OPERATIONAL, "role": roles.EXPENSE_OPERATIONAL,
'balance_type': roles.DEBIT, "balance_type": roles.DEBIT,
'locked': False, "locked": False,
'default': False "default": False,
}, },
{ {
'code': '6070', "code": "6070",
'name': 'Depreciation', "name": "Depreciation",
'role': roles.EXPENSE_DEPRECIATION, "role": roles.EXPENSE_DEPRECIATION,
'balance_type': roles.DEBIT, "balance_type": roles.DEBIT,
'locked': False, "locked": False,
'default': True # Default for EXPENSE_DEPRECIATION "default": True, # Default for EXPENSE_DEPRECIATION
}, },
{ {
'code': '6080', "code": "6080",
'name': 'Fees & Taxes', "name": "Fees & Taxes",
'role': roles.EXPENSE_OPERATIONAL, "role": roles.EXPENSE_OPERATIONAL,
'balance_type': roles.DEBIT, "balance_type": roles.DEBIT,
'locked': False, "locked": False,
'default': False "default": False,
}, },
{ {
'code': '6090', "code": "6090",
'name': 'Bank Charges', "name": "Bank Charges",
'role': roles.EXPENSE_OPERATIONAL, "role": roles.EXPENSE_OPERATIONAL,
'balance_type': roles.DEBIT, "balance_type": roles.DEBIT,
'locked': False, "locked": False,
'default': False "default": False,
}, },
{ {
'code': '6100', "code": "6100",
'name': 'Other Expenses', "name": "Other Expenses",
'role': roles.EXPENSE_OTHER, "role": roles.EXPENSE_OTHER,
'balance_type': roles.DEBIT, "balance_type": roles.DEBIT,
'locked': False, "locked": False,
'default': True # Default for EXPENSE_OTHER "default": True, # Default for EXPENSE_OTHER
} },
] ]
created_accounts = [] created_accounts = []
@ -1521,23 +1512,29 @@ class Command(BaseCommand):
try: try:
account = entity_model.create_account( account = entity_model.create_account(
coa_model=coa_model, coa_model=coa_model,
code=account_data['code'], code=account_data["code"],
name=_(account_data['name']), name=_(account_data["name"]),
role=_(account_data['role']), role=_(account_data["role"]),
balance_type=_(account_data['balance_type']), balance_type=_(account_data["balance_type"]),
active=True active=True,
) )
account.role_default = account_data['default'] account.role_default = account_data["default"]
account.save() account.save()
created_accounts.append(account) created_accounts.append(account)
self.stdout.write(self.style.SUCCESS( self.stdout.write(
f"Created account: {account.code} - {account.name}" self.style.SUCCESS(
)) f"Created account: {account.code} - {account.name}"
)
)
except Exception as e: except Exception as e:
self.stdout.write(self.style.ERROR( self.stdout.write(
f"Error creating account {account_data['code']}: {str(e)}" self.style.ERROR(
)) f"Error creating account {account_data['code']}: {str(e)}"
)
)
self.stdout.write(self.style.SUCCESS( self.stdout.write(
f"\nSuccessfully created {len(created_accounts)} accounts in Chart of Accounts" self.style.SUCCESS(
)) f"\nSuccessfully created {len(created_accounts)} accounts in Chart of Accounts"
)
)

View File

@ -7,14 +7,15 @@ from tqdm import tqdm # Progress bar support
# Database connection details # Database connection details
db_config = { db_config = {
'host': 'localhost', "host": "localhost",
'user': 'root', "user": "root",
'password': "Kfsh&rc9788", "password": "Kfsh&rc9788",
'database': 'car2db_june' "database": "car2db_june",
} }
EXCLUDED_TABLES = {"car_serie", "car_generation"} # Tables to exclude from direct dump EXCLUDED_TABLES = {"car_serie", "car_generation"} # Tables to exclude from direct dump
class Command(BaseCommand): class Command(BaseCommand):
help = "Merge car_serie with car_generation, include in final JSON dump with a progress bar." help = "Merge car_serie with car_generation, include in final JSON dump with a progress bar."
@ -22,7 +23,9 @@ class Command(BaseCommand):
try: try:
self.stdout.write(self.style.SUCCESS("Connecting to database...")) self.stdout.write(self.style.SUCCESS("Connecting to database..."))
# Create SQLAlchemy engine # Create SQLAlchemy engine
engine = create_engine(f"mysql+pymysql://{db_config['user']}:{db_config['password']}@{db_config['host']}/{db_config['database']}") engine = create_engine(
f"mysql+pymysql://{db_config['user']}:{db_config['password']}@{db_config['host']}/{db_config['database']}"
)
# Load car_generation table # Load car_generation table
self.stdout.write(self.style.SUCCESS("Loading car_generation data...")) self.stdout.write(self.style.SUCCESS("Loading car_generation data..."))
@ -35,22 +38,39 @@ class Command(BaseCommand):
car_serie_df = pd.read_sql(car_serie_query, engine) car_serie_df = pd.read_sql(car_serie_query, engine)
# Perform a LEFT JOIN to keep all car series and merge with car generations # Perform a LEFT JOIN to keep all car series and merge with car generations
self.stdout.write(self.style.SUCCESS("Merging car_serie with car_generation...")) self.stdout.write(
merged_df = pd.merge(car_serie_df, car_generation_df, on="id_car_generation", how="left") self.style.SUCCESS("Merging car_serie with car_generation...")
)
merged_df = pd.merge(
car_serie_df, car_generation_df, on="id_car_generation", how="left"
)
# Select and rename the relevant columns # Select and rename the relevant columns
final_df = merged_df.rename(columns={ final_df = merged_df.rename(
"id_car_serie": "id_car_serie", columns={
"id_car_model_x": "id_car_model", "id_car_serie": "id_car_serie",
"name_y": "generation_name", "id_car_model_x": "id_car_model",
"name_x": "serie_name", "name_y": "generation_name",
"year_begin": "year_begin", "name_x": "serie_name",
"year_end": "year_end" "year_begin": "year_begin",
})[["id_car_serie", "id_car_model", "generation_name", "serie_name", "year_begin", "year_end"]] "year_end": "year_end",
}
)[
[
"id_car_serie",
"id_car_model",
"generation_name",
"serie_name",
"year_begin",
"year_end",
]
]
# Convert merged data to a JSON-ready format # Convert merged data to a JSON-ready format
self.stdout.write(self.style.SUCCESS("Processing merged data...")) self.stdout.write(self.style.SUCCESS("Processing merged data..."))
car_serie_json = list(tqdm(final_df.to_dict(orient="records"), desc="Processing car_serie")) car_serie_json = list(
tqdm(final_df.to_dict(orient="records"), desc="Processing car_serie")
)
# Export the full database including merged car_serie # Export the full database including merged car_serie
self.export_database_to_json(car_serie_json) self.export_database_to_json(car_serie_json)
@ -59,7 +79,7 @@ class Command(BaseCommand):
self.stdout.write(self.style.ERROR(f"Error: {e}")) self.stdout.write(self.style.ERROR(f"Error: {e}"))
def export_database_to_json(self, car_serie_data): def export_database_to_json(self, car_serie_data):
""" Export the entire MariaDB database to JSON, replacing car_serie with merged data """ """Export the entire MariaDB database to JSON, replacing car_serie with merged data"""
try: try:
self.stdout.write(self.style.SUCCESS("Exporting database to JSON...")) self.stdout.write(self.style.SUCCESS("Exporting database to JSON..."))
# Connect to the database using pymysql # Connect to the database using pymysql
@ -90,13 +110,17 @@ class Command(BaseCommand):
# Save the JSON to a file # Save the JSON to a file
self.stdout.write(self.style.SUCCESS("Saving database_export.json...")) self.stdout.write(self.style.SUCCESS("Saving database_export.json..."))
with open('database_export.json', 'w', encoding='utf-8') as json_file: with open("database_export.json", "w", encoding="utf-8") as json_file:
json.dump(database_json, json_file, indent=4, ensure_ascii=False) json.dump(database_json, json_file, indent=4, ensure_ascii=False)
self.stdout.write(self.style.SUCCESS("✅ Database exported to JSON successfully! (Including merged car_serie)")) self.stdout.write(
self.style.SUCCESS(
"✅ Database exported to JSON successfully! (Including merged car_serie)"
)
)
except Exception as e: except Exception as e:
self.stdout.write(self.style.ERROR(f"Error exporting database: {e}")) self.stdout.write(self.style.ERROR(f"Error exporting database: {e}"))
finally: finally:
if connection: if connection:
connection.close() connection.close()

View File

@ -2,17 +2,17 @@ from django.core.management.base import BaseCommand
from django.utils import timezone from django.utils import timezone
from plans.models import UserPlan from plans.models import UserPlan
class Command(BaseCommand): class Command(BaseCommand):
help = 'Deactivates expired user plans' help = "Deactivates expired user plans"
def handle(self, *args, **options): def handle(self, *args, **options):
expired_plans = UserPlan.objects.filter( expired_plans = UserPlan.objects.filter(active=True, expire__lt=timezone.now())
active=True,
expire__lt=timezone.now()
)
count = expired_plans.count() count = expired_plans.count()
for plan in expired_plans: for plan in expired_plans:
plan.expire_account() plan.expire_account()
self.stdout.write(self.style.SUCCESS(f'Successfully deactivated {count} expired plans')) self.stdout.write(
self.style.SUCCESS(f"Successfully deactivated {count} expired plans")
)

View File

@ -4,69 +4,72 @@ from django.db import transaction, models
from django.utils.text import slugify from django.utils.text import slugify
from django.db.models import Case, When, Value from django.db.models import Case, When, Value
class Command(BaseCommand): class Command(BaseCommand):
help = 'Generate slugs for model instances with proper empty value handling' help = "Generate slugs for model instances with proper empty value handling"
def add_arguments(self, parser): def add_arguments(self, parser):
parser.add_argument( parser.add_argument(
'--model', "--model",
type=str, type=str,
required=True, required=True,
help='Model name (format: "app_label.ModelName")' help='Model name (format: "app_label.ModelName")',
) )
parser.add_argument( parser.add_argument(
'--field', "--field",
type=str, type=str,
default='name', default="name",
help='Field to use as slug source (default: "name")' help='Field to use as slug source (default: "name")',
) )
parser.add_argument( parser.add_argument(
'--batch-size', "--batch-size",
type=int, type=int,
default=1000, default=1000,
help='Number of records to process at once (default: 1000)' help="Number of records to process at once (default: 1000)",
) )
parser.add_argument( parser.add_argument(
'--dry-run', "--dry-run",
action='store_true', action="store_true",
help='Test without actually saving changes' help="Test without actually saving changes",
) )
parser.add_argument( parser.add_argument(
'--fill-empty', "--fill-empty",
action='store_true', action="store_true",
help='Fill empty slugs with model-ID when source field is empty' help="Fill empty slugs with model-ID when source field is empty",
) )
def handle(self, *args, **options): def handle(self, *args, **options):
model = self.get_model(options['model']) model = self.get_model(options["model"])
source_field = options['field'] source_field = options["field"]
batch_size = options['batch_size'] batch_size = options["batch_size"]
dry_run = options['dry_run'] dry_run = options["dry_run"]
fill_empty = options['fill_empty'] fill_empty = options["fill_empty"]
queryset = model.objects.filter(models.Q(slug__isnull=True) | models.Q(slug='')) queryset = model.objects.filter(models.Q(slug__isnull=True) | models.Q(slug=""))
total_count = queryset.count() total_count = queryset.count()
processed = 0 processed = 0
empty_source = 0 empty_source = 0
self.stdout.write( self.stdout.write(
self.style.SUCCESS( self.style.SUCCESS(
f'Generating slugs for {total_count} {model._meta.model_name} records ' f"Generating slugs for {total_count} {model._meta.model_name} records "
f'using field "{source_field}" (batch size: {batch_size})' f'using field "{source_field}" (batch size: {batch_size})'
) )
) )
with transaction.atomic(): with transaction.atomic():
if dry_run: if dry_run:
self.stdout.write(self.style.WARNING('DRY RUN - No changes will be saved')) self.stdout.write(
self.style.WARNING("DRY RUN - No changes will be saved")
)
transaction.set_rollback(True) transaction.set_rollback(True)
for offset in range(0, total_count, batch_size): for offset in range(0, total_count, batch_size):
batch = queryset[offset:offset + batch_size] batch = queryset[offset : offset + batch_size]
updates = [] updates = []
for obj in batch: for obj in batch:
source_value = getattr(obj, source_field, '') source_value = getattr(obj, source_field, "")
if not source_value: if not source_value:
if fill_empty: if fill_empty:
@ -76,12 +79,14 @@ class Command(BaseCommand):
else: else:
self.stdout.write( self.stdout.write(
self.style.WARNING( self.style.WARNING(
f'Skipping {obj} (empty {source_field})' f"Skipping {obj} (empty {source_field})"
) )
) )
continue continue
else: else:
slug_base = slugify(str(source_value))[:50] # Ensure string and truncate slug_base = slugify(str(source_value))[
:50
] # Ensure string and truncate
new_slug = f"{slug_base}-{obj.pk}" # Guaranteed unique new_slug = f"{slug_base}-{obj.pk}" # Guaranteed unique
updates.append((obj.pk, new_slug)) updates.append((obj.pk, new_slug))
@ -94,27 +99,26 @@ class Command(BaseCommand):
) )
self.stdout.write( self.stdout.write(
f'Processed batch {offset//batch_size + 1}: ' f"Processed batch {offset // batch_size + 1}: "
f'{min(offset + batch_size, total_count)}/{total_count}' f"{min(offset + batch_size, total_count)}/{total_count}"
) )
stats = [ stats = [
f"Total processed: {processed}", f"Total processed: {processed}",
f"Records with empty source field: {empty_source}", f"Records with empty source field: {empty_source}",
f"Skipped records: {total_count - processed - empty_source}" f"Skipped records: {total_count - processed - empty_source}",
] ]
self.stdout.write( self.stdout.write(self.style.SUCCESS("\n".join(stats)))
self.style.SUCCESS('\n'.join(stats))
)
def get_model(self, model_path): def get_model(self, model_path):
"""Get model class from 'app_label.ModelName' string""" """Get model class from 'app_label.ModelName' string"""
from django.apps import apps from django.apps import apps
try: try:
app_label, model_name = model_path.split('.') app_label, model_name = model_path.split(".")
return apps.get_model(app_label, model_name) return apps.get_model(app_label, model_name)
except ValueError: except ValueError:
raise self.style.ERROR('Model must be specified as "app_label.ModelName"') raise self.style.ERROR('Model must be specified as "app_label.ModelName"')
except LookupError as e: except LookupError as e:
raise self.style.ERROR(f'Model not found: {e}') raise self.style.ERROR(f"Model not found: {e}")

View File

@ -1,32 +1,59 @@
from django.core.management.base import BaseCommand from django.core.management.base import BaseCommand
from inventory.services import get_model,decodevin from inventory.services import get_model, decodevin
from bs4 import BeautifulSoup from bs4 import BeautifulSoup
import requests import requests
class Command(BaseCommand): class Command(BaseCommand):
help = 'Seed the Customer model with 20 records' help = "Seed the Customer model with 20 records"
def handle(self, *args, **kwargs): def handle(self, *args, **kwargs):
vin,description = self.generate_vin() vin, description = self.generate_vin()
result = decodevin(vin) result = decodevin(vin)
self.stdout.write(self.style.SUCCESS('####################################################################################################')) self.stdout.write(
self.stdout.write(self.style.SUCCESS('####################################################################################################')) self.style.SUCCESS(
self.stdout.write(self.style.SUCCESS(f'Generated VIN: {vin}')) "####################################################################################################"
self.stdout.write(self.style.SUCCESS(f'Description: {description}')) )
self.stdout.write(self.style.SUCCESS('####################################################################################################')) )
self.stdout.write(self.style.SUCCESS('####################################################################################################')) self.stdout.write(
self.stdout.write(self.style.SUCCESS(f'Decoded VIN: {result}')) self.style.SUCCESS(
make,model,year_model = result.values() "####################################################################################################"
self.stdout.write(self.style.SUCCESS(f'VIN: {vin} - Make {make} - Model {model} - Model Year {year_model}')) )
)
self.stdout.write(self.style.SUCCESS(f"Generated VIN: {vin}"))
self.stdout.write(self.style.SUCCESS(f"Description: {description}"))
self.stdout.write(
self.style.SUCCESS(
"####################################################################################################"
)
)
self.stdout.write(
self.style.SUCCESS(
"####################################################################################################"
)
)
self.stdout.write(self.style.SUCCESS(f"Decoded VIN: {result}"))
make, model, year_model = result.values()
self.stdout.write(
self.style.SUCCESS(
f"VIN: {vin} - Make {make} - Model {model} - Model Year {year_model}"
)
)
m = get_model(model) m = get_model(model)
self.stdout.write(self.style.SUCCESS(f'Make: {m.id_car_make} - Model: {m}')) self.stdout.write(self.style.SUCCESS(f"Make: {m.id_car_make} - Model: {m}"))
self.stdout.write(self.style.SUCCESS('####################################################################################################')) self.stdout.write(
self.stdout.write(self.style.SUCCESS('####################################################################################################')) self.style.SUCCESS(
"####################################################################################################"
)
)
self.stdout.write(
self.style.SUCCESS(
"####################################################################################################"
)
)
def generate_vin(self): def generate_vin(self):
# url = "https://www.vindecoder.org/vin-decoder" # url = "https://www.vindecoder.org/vin-decoder"
url = "https://vingenerator.org/" url = "https://vingenerator.org/"
@ -34,5 +61,5 @@ class Command(BaseCommand):
soup = BeautifulSoup(response.content, "html.parser") soup = BeautifulSoup(response.content, "html.parser")
vin = soup.find("input", {"name": "vin"})["value"] vin = soup.find("input", {"name": "vin"})["value"]
description = soup.find("div", {"class": "description"}).text description = soup.find("div", {"class": "description"}).text
return vin,description return vin, description

View File

@ -8,8 +8,15 @@ django.setup()
import json import json
from tqdm import tqdm from tqdm import tqdm
from inventory.models import ( from inventory.models import (
CarMake, CarModel, CarSerie, CarTrim, CarEquipment, CarMake,
CarSpecification, CarSpecificationValue, CarOption, CarOptionValue CarModel,
CarSerie,
CarTrim,
CarEquipment,
CarSpecification,
CarSpecificationValue,
CarOption,
CarOptionValue,
) )
# Load the cleaned JSON data # Load the cleaned JSON data
@ -26,7 +33,7 @@ for item in tqdm(data["car_make"], desc="Inserting CarMake"):
# "arabic_name": item.get("arabic_name", ""), # "arabic_name": item.get("arabic_name", ""),
# "logo": item.get("Logo", ""), # "logo": item.get("Logo", ""),
# "is_sa_import": item.get("is_sa_import", False), # "is_sa_import": item.get("is_sa_import", False),
} },
) )
# Step 2: Insert CarModel # Step 2: Insert CarModel
@ -38,7 +45,7 @@ for item in tqdm(data["car_model"], desc="Inserting CarModel"):
"id_car_make_id": item["id_car_make"], "id_car_make_id": item["id_car_make"],
"name": item["name"], "name": item["name"],
# "arabic_name": item.get("arabic_name", ""), # "arabic_name": item.get("arabic_name", ""),
} },
) )
# Step 3: Insert CarSerie # Step 3: Insert CarSerie
@ -53,7 +60,7 @@ for item in tqdm(data["car_serie"], desc="Inserting CarSerie"):
"year_begin": item.get("year_begin"), "year_begin": item.get("year_begin"),
"year_end": item.get("year_end"), "year_end": item.get("year_end"),
"generation_name": item.get("generation_name", ""), "generation_name": item.get("generation_name", ""),
} },
) )
# Step 4: Insert CarTrim # Step 4: Insert CarTrim
@ -67,7 +74,7 @@ for item in tqdm(data["car_trim"], desc="Inserting CarTrim"):
# "arabic_name": item.get("arabic_name", ""), # "arabic_name": item.get("arabic_name", ""),
"start_production_year": item["start_production_year"], "start_production_year": item["start_production_year"],
"end_production_year": item["end_production_year"], "end_production_year": item["end_production_year"],
} },
) )
# Step 5: Insert CarEquipment # Step 5: Insert CarEquipment
@ -79,12 +86,14 @@ for item in tqdm(data["car_equipment"], desc="Inserting CarEquipment"):
"id_car_trim_id": item["id_car_trim"], "id_car_trim_id": item["id_car_trim"],
"name": item["name"], "name": item["name"],
"year_begin": item.get("year"), "year_begin": item.get("year"),
} },
) )
# Step 6: Insert CarSpecification (Parent specifications first) # Step 6: Insert CarSpecification (Parent specifications first)
parent_specs = [item for item in data["car_specification"] if item["id_parent"] is None] parent_specs = [item for item in data["car_specification"] if item["id_parent"] is None]
child_specs = [item for item in data["car_specification"] if item["id_parent"] is not None] child_specs = [
item for item in data["car_specification"] if item["id_parent"] is not None
]
for item in tqdm(parent_specs, desc="Inserting Parent CarSpecifications"): for item in tqdm(parent_specs, desc="Inserting Parent CarSpecifications"):
CarSpecification.objects.update_or_create( CarSpecification.objects.update_or_create(
@ -92,8 +101,8 @@ for item in tqdm(parent_specs, desc="Inserting Parent CarSpecifications"):
defaults={ defaults={
"name": item["name"], "name": item["name"],
# "arabic_name": item.get("arabic_name", ""), # "arabic_name": item.get("arabic_name", ""),
"id_parent_id": None "id_parent_id": None,
} },
) )
for item in tqdm(child_specs, desc="Inserting Child CarSpecifications"): for item in tqdm(child_specs, desc="Inserting Child CarSpecifications"):
@ -103,12 +112,14 @@ for item in tqdm(child_specs, desc="Inserting Child CarSpecifications"):
defaults={ defaults={
"name": item["name"], "name": item["name"],
# "arabic_name": item.get("arabic_name", ""), # "arabic_name": item.get("arabic_name", ""),
"id_parent_id": item["id_parent"] "id_parent_id": item["id_parent"],
} },
) )
# Step 7: Insert CarSpecificationValue # Step 7: Insert CarSpecificationValue
for item in tqdm(data["car_specification_value"], desc="Inserting CarSpecificationValue"): for item in tqdm(
data["car_specification_value"], desc="Inserting CarSpecificationValue"
):
CarTrim.objects.get(id_car_trim=item["id_car_trim"]) CarTrim.objects.get(id_car_trim=item["id_car_trim"])
CarSpecification.objects.get(id_car_specification=item["id_car_specification"]) CarSpecification.objects.get(id_car_specification=item["id_car_specification"])
CarSpecificationValue.objects.update_or_create( CarSpecificationValue.objects.update_or_create(
@ -118,7 +129,7 @@ for item in tqdm(data["car_specification_value"], desc="Inserting CarSpecificati
"id_car_specification_id": item["id_car_specification"], "id_car_specification_id": item["id_car_specification"],
"value": item["value"], "value": item["value"],
"unit": item.get("unit", ""), "unit": item.get("unit", ""),
} },
) )
# Step 8: Insert CarOption (Parent options first) # Step 8: Insert CarOption (Parent options first)
@ -131,8 +142,8 @@ for item in tqdm(parent_options, desc="Inserting Parent CarOptions"):
defaults={ defaults={
"name": item["name"], "name": item["name"],
# "arabic_name": item.get("arabic_name", ""), # "arabic_name": item.get("arabic_name", ""),
"id_parent_id": None "id_parent_id": None,
} },
) )
for item in tqdm(child_options, desc="Inserting Child CarOptions"): for item in tqdm(child_options, desc="Inserting Child CarOptions"):
@ -142,8 +153,8 @@ for item in tqdm(child_options, desc="Inserting Child CarOptions"):
defaults={ defaults={
"name": item["name"], "name": item["name"],
# "arabic_name": item.get("arabic_name", ""), # "arabic_name": item.get("arabic_name", ""),
"id_parent_id": item["id_parent"] "id_parent_id": item["id_parent"],
} },
) )
# Step 9: Insert CarOptionValue # Step 9: Insert CarOptionValue
@ -156,7 +167,7 @@ for item in tqdm(data["car_option_value"], desc="Inserting CarOptionValue"):
"id_car_option_id": item["id_car_option"], "id_car_option_id": item["id_car_option"],
"id_car_equipment_id": item["id_car_equipment"], "id_car_equipment_id": item["id_car_equipment"],
"is_base": item["is_base"], "is_base": item["is_base"],
} },
) )
print("Data population completed successfully.") print("Data population completed successfully.")

View File

@ -2,11 +2,31 @@
from django.core.management.base import BaseCommand from django.core.management.base import BaseCommand
from appointment.models import Service from appointment.models import Service
import datetime import datetime
class Command(BaseCommand): class Command(BaseCommand):
help = 'create initial services offered' help = "create initial services offered"
def handle(self, *args, **options): def handle(self, *args, **options):
Service.objects.all().delete() Service.objects.all().delete()
Service.objects.create(name='call', price=0,duration=datetime.timedelta(minutes=10),currency='SAR',description='15 min call') Service.objects.create(
Service.objects.create(name='meeting', price=0,duration=datetime.timedelta(minutes=30),currency='SAR',description='30 min meeting') name="call",
Service.objects.create(name='email', price=0,duration=datetime.timedelta(minutes=30),currency='SAR',description='30 min visit') price=0,
duration=datetime.timedelta(minutes=10),
currency="SAR",
description="15 min call",
)
Service.objects.create(
name="meeting",
price=0,
duration=datetime.timedelta(minutes=30),
currency="SAR",
description="30 min meeting",
)
Service.objects.create(
name="email",
price=0,
duration=datetime.timedelta(minutes=30),
currency="SAR",
description="30 min visit",
)

View File

@ -48,7 +48,9 @@ class Command(BaseCommand):
if created: if created:
self.stdout.write(f"Added Exterior Color: {obj.name} ({obj.rgb})") self.stdout.write(f"Added Exterior Color: {obj.name} ({obj.rgb})")
else: else:
self.stdout.write(f"Exterior Color already exists: {obj.name} ({obj.rgb})") self.stdout.write(
f"Exterior Color already exists: {obj.name} ({obj.rgb})"
)
self.stdout.write("Populating Interior Colors...") self.stdout.write("Populating Interior Colors...")
for color in self.interior_colors: for color in self.interior_colors:
@ -58,6 +60,8 @@ class Command(BaseCommand):
if created: if created:
self.stdout.write(f"Added Interior Color: {obj.name} ({obj.rgb})") self.stdout.write(f"Added Interior Color: {obj.name} ({obj.rgb})")
else: else:
self.stdout.write(f"Interior Color already exists: {obj.name} ({obj.rgb})") self.stdout.write(
f"Interior Color already exists: {obj.name} ({obj.rgb})"
)
self.stdout.write("Finished populating colors.") self.stdout.write("Finished populating colors.")

View File

@ -2,24 +2,27 @@ from django.core.management.base import BaseCommand
from faker import Faker from faker import Faker
from inventory.models import Customer, Dealer from inventory.models import Customer, Dealer
class Command(BaseCommand): class Command(BaseCommand):
help = 'Seed the Customer model with 20 records' help = "Seed the Customer model with 20 records"
def handle(self, *args, **kwargs): def handle(self, *args, **kwargs):
fake = Faker() fake = Faker()
dealers = Dealer.objects.all() dealers = Dealer.objects.all()
if not dealers.exists(): if not dealers.exists():
self.stdout.write(self.style.ERROR('No dealers found. Please create dealers first.')) self.stdout.write(
self.style.ERROR("No dealers found. Please create dealers first.")
)
return return
for _ in range(20): for _ in range(20):
dealer = fake.random_element(elements=dealers) dealer = fake.random_element(elements=dealers)
first_name = fake.first_name() first_name = fake.first_name()
middle_name = fake.first_name() if fake.boolean() else '' middle_name = fake.first_name() if fake.boolean() else ""
last_name = fake.last_name() last_name = fake.last_name()
email = fake.unique.email() email = fake.unique.email()
national_id = fake.unique.bothify(text='##########') national_id = fake.unique.bothify(text="##########")
phone_number = fake.unique.phone_number() phone_number = fake.unique.phone_number()
address = fake.address() address = fake.address()
@ -31,7 +34,7 @@ class Command(BaseCommand):
email=email, email=email,
national_id=national_id, national_id=national_id,
phone_number=phone_number, phone_number=phone_number,
address=address address=address,
) )
self.stdout.write(self.style.SUCCESS('Successfully seeded 20 customers.')) self.stdout.write(self.style.SUCCESS("Successfully seeded 20 customers."))

View File

@ -3,7 +3,6 @@ from inventory.models import CarSerie
TRANSLATIONS = { TRANSLATIONS = {
"Liftback 5-doors": "ليفت باك - خمسة أبواب", "Liftback 5-doors": "ليفت باك - خمسة أبواب",
} }
@ -18,9 +17,15 @@ class Command(BaseCommand):
car_serie.arabic_name = arabic_translation car_serie.arabic_name = arabic_translation
car_serie.save() car_serie.save()
updated_count += 1 updated_count += 1
self.stdout.write(self.style.SUCCESS(f"Updated: {car_serie.name} -> {arabic_translation}")) self.stdout.write(
self.style.SUCCESS(
f"Updated: {car_serie.name} -> {arabic_translation}"
)
)
if updated_count: if updated_count:
self.stdout.write(self.style.SUCCESS(f"Successfully updated {updated_count} entries.")) self.stdout.write(
self.style.SUCCESS(f"Successfully updated {updated_count} entries.")
)
else: else:
self.stdout.write(self.style.WARNING("No updates were made.")) self.stdout.write(self.style.WARNING("No updates were made."))

View File

@ -3,38 +3,53 @@ import csv
from django.core.management.base import BaseCommand from django.core.management.base import BaseCommand
from inventory.models import CarSerie, CarModel from inventory.models import CarSerie, CarModel
class Command(BaseCommand): class Command(BaseCommand):
help = "Update or add CarSerie entries from a merged CSV file" help = "Update or add CarSerie entries from a merged CSV file"
def handle(self, *args, **kwargs): def handle(self, *args, **kwargs):
# Path to the merged CSV file # Path to the merged CSV file
base_dir = os.path.dirname(os.path.abspath(__file__)) base_dir = os.path.dirname(os.path.abspath(__file__))
file_path = os.path.join(base_dir, "../../data/Updated_Merged_Car_Generation_and_Serie_Data.csv") # Adjust the path if needed file_path = os.path.join(
base_dir, "../../data/Updated_Merged_Car_Generation_and_Serie_Data.csv"
) # Adjust the path if needed
if not os.path.exists(file_path): if not os.path.exists(file_path):
self.stdout.write(self.style.ERROR(f"File not found: {file_path}")) self.stdout.write(self.style.ERROR(f"File not found: {file_path}"))
return return
with open(file_path, newline='', encoding='utf-8') as csvfile: with open(file_path, newline="", encoding="utf-8") as csvfile:
reader = csv.DictReader(csvfile) reader = csv.DictReader(csvfile)
for row in reader: for row in reader:
try: try:
car_model = CarModel.objects.get(pk=row['id_car_model']) car_model = CarModel.objects.get(pk=row["id_car_model"])
except CarModel.DoesNotExist: except CarModel.DoesNotExist:
self.stdout.write(self.style.WARNING(f"CarModel with ID {row['id_car_model']} not found")) self.stdout.write(
self.style.WARNING(
f"CarModel with ID {row['id_car_model']} not found"
)
)
continue continue
car_serie, created = CarSerie.objects.update_or_create( car_serie, created = CarSerie.objects.update_or_create(
id_car_serie=row['id_car_serie'], id_car_serie=row["id_car_serie"],
defaults={ defaults={
'id_car_model': car_model, "id_car_model": car_model,
'name': row['name'], "name": row["name"],
'arabic_name': "-", "arabic_name": "-",
'year_begin': int(float(row['year_begin'])) if row['year_begin'] else None, "year_begin": int(float(row["year_begin"]))
'year_end': int(float(row['year_end'])) if row['year_end'] else None, if row["year_begin"]
'generation_name': row['generation_name'], else None,
"year_end": int(float(row["year_end"]))
if row["year_end"]
else None,
"generation_name": row["generation_name"],
}, },
) )
action = "Created" if created else "Updated" action = "Created" if created else "Updated"
self.stdout.write(self.style.SUCCESS(f"{action} CarSerie with ID {car_serie.id_car_serie}")) self.stdout.write(
self.style.SUCCESS(
f"{action} CarSerie with ID {car_serie.id_car_serie}"
)
)

View File

@ -1,6 +1,8 @@
from django.core.management.base import BaseCommand from django.core.management.base import BaseCommand
from inventory.models import VatRate from inventory.models import VatRate
from decimal import Decimal from decimal import Decimal
class Command(BaseCommand): class Command(BaseCommand):
def handle(self, *args, **kwargs): def handle(self, *args, **kwargs):
VatRate.objects.get_or_create(rate=Decimal('0.15'), is_active=True) VatRate.objects.get_or_create(rate=Decimal("0.15"), is_active=True)

View File

@ -4,24 +4,23 @@ from plans.models import Plan, Quota, Pricing, PlanPricing
from decimal import Decimal from decimal import Decimal
from django.db.models import Q from django.db.models import Q
class Command(BaseCommand): class Command(BaseCommand):
help = 'Create basic subscription plans structure' help = "Create basic subscription plans structure"
def add_arguments(self, parser): def add_arguments(self, parser):
parser.add_argument( parser.add_argument(
'--reset', "--reset",
action='store_true', action="store_true",
help='Delete existing plans and quotas before creating new ones' help="Delete existing plans and quotas before creating new ones",
) )
def handle(self, *args, **options): def handle(self, *args, **options):
if options['reset']: if options["reset"]:
self.stdout.write(self.style.WARNING('Resetting existing plans data...')) self.stdout.write(self.style.WARNING("Resetting existing plans data..."))
Plan.objects.all().delete() Plan.objects.all().delete()
Quota.objects.filter( Quota.objects.filter(
Q(codename='basic') | Q(codename="basic") | Q(codename="pro") | Q(codename="premium")
Q(codename='pro') |
Q(codename='premium')
).delete() ).delete()
# Ensure no existing plans are marked as default # Ensure no existing plans are marked as default
@ -32,93 +31,89 @@ class Command(BaseCommand):
# Create core quotas # Create core quotas
basic_quota, _ = Quota.objects.update_or_create( basic_quota, _ = Quota.objects.update_or_create(
codename='basic', codename="basic",
defaults={ defaults={
'name': 'Basic Features', "name": "Basic Features",
'description': 'Essential platform access', "description": "Essential platform access",
'is_boolean': True "is_boolean": True,
} },
) )
pro_quota, _ = Quota.objects.update_or_create( pro_quota, _ = Quota.objects.update_or_create(
codename='pro', codename="pro",
defaults={ defaults={
'name': 'Pro Features', "name": "Pro Features",
'description': 'Advanced functionality', "description": "Advanced functionality",
'is_boolean': True "is_boolean": True,
} },
) )
premium_quota, _ = Quota.objects.update_or_create( premium_quota, _ = Quota.objects.update_or_create(
codename='premium', codename="premium",
defaults={ defaults={
'name': 'Premium Features', "name": "Premium Features",
'description': 'Full platform access', "description": "Full platform access",
'is_boolean': True "is_boolean": True,
} },
) )
# Create pricing period # Create pricing period
monthly_pricing, _ = Pricing.objects.update_or_create( monthly_pricing, _ = Pricing.objects.update_or_create(
name='Monthly', name="Monthly", defaults={"period": 30}
defaults={'period': 30}
) )
# Define plan structure # Define plan structure
plans = [ plans = [
{ {
'name': 'Basic', "name": "Basic",
'description': 'Entry-level plan', "description": "Entry-level plan",
'price': Decimal('0.00'), "price": Decimal("0.00"),
'period': None, "period": None,
'quotas': [basic_quota], "quotas": [basic_quota],
'default': True "default": True,
}, },
{ {
'name': 'Pro', "name": "Pro",
'description': 'Professional plan', "description": "Professional plan",
'price': Decimal('29.00'), "price": Decimal("29.00"),
'period': 30, "period": 30,
'quotas': [basic_quota, pro_quota], "quotas": [basic_quota, pro_quota],
'default': False "default": False,
}, },
{ {
'name': 'Premium', "name": "Premium",
'description': 'Full access plan', "description": "Full access plan",
'price': Decimal('99.00'), "price": Decimal("99.00"),
'period': 30, "period": 30,
'quotas': [basic_quota, pro_quota, premium_quota], "quotas": [basic_quota, pro_quota, premium_quota],
'default': None "default": None,
} },
] ]
# Create plans and associations # Create plans and associations
for plan_data in plans: for plan_data in plans:
plan, created = Plan.objects.update_or_create( plan, created = Plan.objects.update_or_create(
name=plan_data['name'], name=plan_data["name"],
defaults={ defaults={
'description': plan_data['description'], "description": plan_data["description"],
'default': plan_data.get('default', False), "default": plan_data.get("default", False),
'available': True, "available": True,
'visible': True "visible": True,
} },
) )
# Set quotas # Set quotas
plan.quotas.set(plan_data['quotas']) plan.quotas.set(plan_data["quotas"])
# Create pricing if applicable # Create pricing if applicable
if plan_data['price'] > 0: if plan_data["price"] > 0:
PlanPricing.objects.update_or_create( PlanPricing.objects.update_or_create(
plan=plan, plan=plan,
pricing=monthly_pricing, pricing=monthly_pricing,
defaults={ defaults={"price": plan_data["price"], "visible": True},
'price': plan_data['price'],
'visible': True
}
) )
status = 'Created' if created else 'Updated' status = "Created" if created else "Updated"
self.stdout.write(self.style.SUCCESS(f'{status} {plan.name} plan')) self.stdout.write(self.style.SUCCESS(f"{status} {plan.name} plan"))
self.stdout.write(self.style.SUCCESS('Successfully created plans structure')) self.stdout.write(self.style.SUCCESS("Successfully created plans structure"))

View File

@ -2,14 +2,16 @@
from decimal import Decimal from decimal import Decimal
from django.core.management.base import BaseCommand from django.core.management.base import BaseCommand
from plans.models import Plan, Quota, PlanQuota, Pricing, PlanPricing from plans.models import Plan, Quota, PlanQuota, Pricing, PlanPricing
class Command(BaseCommand): class Command(BaseCommand):
help = 'Create basic subscription plans structure' help = "Create basic subscription plans structure"
def add_arguments(self, parser): def add_arguments(self, parser):
parser.add_argument( parser.add_argument(
'--reset', "--reset",
action='store_true', action="store_true",
help='Delete existing plans and quotas before creating new ones' help="Delete existing plans and quotas before creating new ones",
) )
def handle(self, *args, **options): def handle(self, *args, **options):
@ -22,13 +24,23 @@ class Command(BaseCommand):
# Order.objects.all().delete() # Order.objects.all().delete()
# BillingInfo.objects.all().delete() # BillingInfo.objects.all().delete()
users_quota = Quota.objects.create(
users_quota = Quota.objects.create(name='Users', codename='Users', unit='number') name="Users", codename="Users", unit="number"
cars_quota = Quota.objects.create(name='Cars', codename='Cars', unit='number') )
cars_quota = Quota.objects.create(name="Cars", codename="Cars", unit="number")
# Create plans # Create plans
basic_plan = Plan.objects.create(name='Basic', description='basic plan', available=True, visible=True) basic_plan = Plan.objects.create(
pro_plan = Plan.objects.create(name='Pro', description='Pro plan', available=True, visible=True) name="Basic", description="basic plan", available=True, visible=True
enterprise_plan = Plan.objects.create(name='Enterprise', description='Enterprise plan', available=True, visible=True) )
pro_plan = Plan.objects.create(
name="Pro", description="Pro plan", available=True, visible=True
)
enterprise_plan = Plan.objects.create(
name="Enterprise",
description="Enterprise plan",
available=True,
visible=True,
)
# Assign quotas to plans # Assign quotas to plans
PlanQuota.objects.create(plan=basic_plan, quota=users_quota, value=3) PlanQuota.objects.create(plan=basic_plan, quota=users_quota, value=3)
@ -44,13 +56,19 @@ class Command(BaseCommand):
# PlanQuota.objects.create(plan=pro_plan, quota=storage_quota, value=100) # PlanQuota.objects.create(plan=pro_plan, quota=storage_quota, value=100)
# Define pricing # Define pricing
basic_pricing = Pricing.objects.create(name='Monthly', period=30) basic_pricing = Pricing.objects.create(name="Monthly", period=30)
pro_pricing = Pricing.objects.create(name='Monthly', period=30) pro_pricing = Pricing.objects.create(name="Monthly", period=30)
enterprise_pricing = Pricing.objects.create(name='Monthly', period=30) enterprise_pricing = Pricing.objects.create(name="Monthly", period=30)
PlanPricing.objects.create(plan=basic_plan, pricing=basic_pricing, price=Decimal('9.99')) PlanPricing.objects.create(
PlanPricing.objects.create(plan=pro_plan, pricing=pro_pricing, price=Decimal('19.99')) plan=basic_plan, pricing=basic_pricing, price=Decimal("9.99")
PlanPricing.objects.create(plan=enterprise_plan, pricing=enterprise_pricing, price=Decimal('29.99')) )
PlanPricing.objects.create(
plan=pro_plan, pricing=pro_pricing, price=Decimal("19.99")
)
PlanPricing.objects.create(
plan=enterprise_plan, pricing=enterprise_pricing, price=Decimal("29.99")
)
# # Create quotas # # Create quotas
# project_quota = Quota.objects.create(name='projects', codename='projects', unit='projects') # project_quota = Quota.objects.create(name='projects', codename='projects', unit='projects')
@ -72,35 +90,34 @@ class Command(BaseCommand):
# basic = Pricing.objects.create(name='Monthly', period=30) # basic = Pricing.objects.create(name='Monthly', period=30)
# pro = Pricing.objects.create(name='Monthly', period=30) # pro = Pricing.objects.create(name='Monthly', period=30)
# basic_pricing = PlanPricing.objects.create(plan=basic_plan, pricing=basic, price=Decimal('19.99')) # basic_pricing = PlanPricing.objects.create(plan=basic_plan, pricing=basic, price=Decimal('19.99'))
# pro_pricing = PlanPricing.objects.create(plan=pro_plan, pricing=pro, price=Decimal('29.99')) # pro_pricing = PlanPricing.objects.create(plan=pro_plan, pricing=pro, price=Decimal('29.99'))
# Create users # Create users
# user = User.objects.first() # user = User.objects.first()
# # Create user plans # # Create user plans
# billing_info = BillingInfo.objects.create( # billing_info = BillingInfo.objects.create(
# user=user, # user=user,
# tax_number='123456789', # tax_number='123456789',
# name='John Doe', # name='John Doe',
# street='123 Main St', # street='123 Main St',
# zipcode='12345', # zipcode='12345',
# city='Anytown', # city='Anytown',
# country='US', # country='US',
# ) # )
# order = Order.objects.create( # order = Order.objects.create(
# user=user, # user=user,
# plan=pro_plan, # plan=pro_plan,
# pricing=pro_pricing, # pricing=pro_pricing,
# amount=pro_pricing.price, # amount=pro_pricing.price,
# currency="SAR", # currency="SAR",
# ) # )
# UserPlan.objects.create( # UserPlan.objects.create(
# user=user, # user=user,
# plan=pro_plan, # plan=pro_plan,
# expire=timezone.now() + timedelta(days=2), # expire=timezone.now() + timedelta(days=2),
# active=True, # active=True,
# ) # )

View File

@ -4,8 +4,9 @@ from inventory.tasks import create_coa_accounts
from inventory.models import Dealer from inventory.models import Dealer
User = get_user_model() User = get_user_model()
class Command(BaseCommand):
class Command(BaseCommand):
def handle(self, *args, **kwargs): def handle(self, *args, **kwargs):
# user = User.objects.last() # user = User.objects.last()
# print(user.email) # print(user.email)
@ -17,4 +18,4 @@ class Command(BaseCommand):
# result = re.match(r'^05\d{8}$', '0625252522') # result = re.match(r'^05\d{8}$', '0625252522')
# print(result) # print(result)
dealer = Dealer.objects.last() dealer = Dealer.objects.last()
create_coa_accounts(dealer.pk) create_coa_accounts(dealer.pk)

View File

@ -1,4 +1,5 @@
from django.core.management.base import BaseCommand from django.core.management.base import BaseCommand
# from background_task.models import Task # from background_task.models import Task
# from background_task import background # from background_task import background
# from django_q.tasks import async_task # from django_q.tasks import async_task
@ -6,7 +7,5 @@ from inventory.tasks import send_email
class Command(BaseCommand): class Command(BaseCommand):
def handle(self, *args, **kwargs): def handle(self, *args, **kwargs):
send_email('ismail.mosa@gmail.com', 'teset1@gmail.com', 'test', 'test') send_email("ismail.mosa@gmail.com", "teset1@gmail.com", "test", "test")

View File

@ -5,17 +5,19 @@ from django.conf import settings
class Command(BaseCommand): class Command(BaseCommand):
help = 'Translates car model names to Arabic and saves them in the arabic_name field.' help = (
"Translates car model names to Arabic and saves them in the arabic_name field."
)
def handle(self, *args, **kwargs): def handle(self, *args, **kwargs):
client = OpenAI(api_key=settings.OPENAI_API_KEY) client = OpenAI(api_key=settings.OPENAI_API_KEY)
car_models = CarModel.objects.all() car_models = CarModel.objects.all()
total = car_models.count() total = car_models.count()
print(f'Translating {total} names...') print(f"Translating {total} names...")
for index, car_model in enumerate(car_models, start=1): for index, car_model in enumerate(car_models, start=1):
if not car_model.arabic_name or car_model.arabic_name == '-': if not car_model.arabic_name or car_model.arabic_name == "-":
if isinstance(car_model.name, int): if isinstance(car_model.name, int):
car_model.arabic_name = car_model.name car_model.arabic_name = car_model.name
car_model.save() car_model.save()
@ -31,12 +33,9 @@ class Command(BaseCommand):
"You are an assistant that translates English car names to Arabic." "You are an assistant that translates English car names to Arabic."
"If the name is purely numeric, keep it as is." "If the name is purely numeric, keep it as is."
"For mixed names like 'D9', translate them as 'دي 9'." "For mixed names like 'D9', translate them as 'دي 9'."
) ),
}, },
{ {"role": "user", "content": car_model.name},
"role": "user",
"content": car_model.name
}
], ],
temperature=0.2, temperature=0.2,
) )
@ -45,4 +44,4 @@ class Command(BaseCommand):
car_model.save() car_model.save()
print(f"[{index}/{total}] .. Done") print(f"[{index}/{total}] .. Done")
except Exception as e: except Exception as e:
print(f"Error translating '{car_model.name}': {e}") print(f"Error translating '{car_model.name}': {e}")

View File

@ -2,32 +2,37 @@ from django.core.management.base import BaseCommand
from inventory.models import CarMake from inventory.models import CarMake
import json import json
class Command(BaseCommand): class Command(BaseCommand):
help = 'Update CarMake model with data from a JSON file' help = "Update CarMake model with data from a JSON file"
def handle(self, *args, **kwargs): def handle(self, *args, **kwargs):
# Load the JSON data from the file # Load the JSON data from the file
with open('carmake_updated_backup.json', 'r', encoding='utf-8') as file: with open("carmake_updated_backup.json", "r", encoding="utf-8") as file:
car_makes_data = json.load(file) car_makes_data = json.load(file)
# Iterate over the data and update the CarMake model # Iterate over the data and update the CarMake model
for car_make_data in car_makes_data: for car_make_data in car_makes_data:
pk = car_make_data['pk'] pk = car_make_data["pk"]
fields = car_make_data['fields'] fields = car_make_data["fields"]
# Get or create the CarMake instance # Get or create the CarMake instance
car_make, created = CarMake.objects.get_or_create(pk=pk) car_make, created = CarMake.objects.get_or_create(pk=pk)
# Update the fields # Update the fields
car_make.name = fields['name'] car_make.name = fields["name"]
car_make.arabic_name = fields['arabic_name'] car_make.arabic_name = fields["arabic_name"]
car_make.logo = fields['logo'] car_make.logo = fields["logo"]
car_make.is_sa_import = fields['is_sa_import'] car_make.is_sa_import = fields["is_sa_import"]
# Save the updated instance # Save the updated instance
car_make.save() car_make.save()
if created: if created:
self.stdout.write(self.style.SUCCESS(f'Created CarMake: {car_make.name}')) self.stdout.write(
self.style.SUCCESS(f"Created CarMake: {car_make.name}")
)
else: else:
self.stdout.write(self.style.SUCCESS(f'Updated CarMake: {car_make.name}')) self.stdout.write(
self.style.SUCCESS(f"Updated CarMake: {car_make.name}")
)

View File

@ -3,35 +3,46 @@ import csv
from django.core.management.base import BaseCommand from django.core.management.base import BaseCommand
from inventory.models import CarModel, CarMake from inventory.models import CarModel, CarMake
class Command(BaseCommand): class Command(BaseCommand):
help = "Update or add CarModel entries from a CSV file" help = "Update or add CarModel entries from a CSV file"
def handle(self, *args, **kwargs): def handle(self, *args, **kwargs):
# Path to the car_model CSV file # Path to the car_model CSV file
base_dir = os.path.dirname(os.path.abspath(__file__)) base_dir = os.path.dirname(os.path.abspath(__file__))
file_path = os.path.join(base_dir, "../../data/car_model.csv") # Adjust path if needed file_path = os.path.join(
base_dir, "../../data/car_model.csv"
) # Adjust path if needed
if not os.path.exists(file_path): if not os.path.exists(file_path):
self.stdout.write(self.style.ERROR(f"File not found: {file_path}")) self.stdout.write(self.style.ERROR(f"File not found: {file_path}"))
return return
with open(file_path, newline='', encoding='utf-8') as csvfile: with open(file_path, newline="", encoding="utf-8") as csvfile:
reader = csv.DictReader(csvfile) reader = csv.DictReader(csvfile)
for row in reader: for row in reader:
try: try:
car_make = CarMake.objects.get(pk=row['id_car_make']) car_make = CarMake.objects.get(pk=row["id_car_make"])
except CarMake.DoesNotExist: except CarMake.DoesNotExist:
self.stdout.write(self.style.WARNING(f"CarMake with ID {row['id_car_make']} not found")) self.stdout.write(
self.style.WARNING(
f"CarMake with ID {row['id_car_make']} not found"
)
)
continue continue
car_model, created = CarModel.objects.update_or_create( car_model, created = CarModel.objects.update_or_create(
id_car_model=row['id_car_model'], id_car_model=row["id_car_model"],
defaults={ defaults={
'id_car_make': car_make, "id_car_make": car_make,
'name': row['name'], "name": row["name"],
'arabic_name': row.get('arabic_name', ''), "arabic_name": row.get("arabic_name", ""),
}, },
) )
action = "Created" if created else "Updated" action = "Created" if created else "Updated"
self.stdout.write(self.style.SUCCESS(f"{action} CarModel with ID {car_model.id_car_model}")) self.stdout.write(
self.style.SUCCESS(
f"{action} CarModel with ID {car_model.id_car_model}"
)
)

View File

@ -3,42 +3,61 @@ import csv
from django.core.management.base import BaseCommand from django.core.management.base import BaseCommand
from inventory.models import CarSpecificationValue, CarSpecification, CarTrim from inventory.models import CarSpecificationValue, CarSpecification, CarTrim
class Command(BaseCommand): class Command(BaseCommand):
help = "Update or add CarSpecificationValue entries from a CSV file" help = "Update or add CarSpecificationValue entries from a CSV file"
def handle(self, *args, **kwargs): def handle(self, *args, **kwargs):
# Path to the car_specification_value CSV file # Path to the car_specification_value CSV file
base_dir = os.path.dirname(os.path.abspath(__file__)) base_dir = os.path.dirname(os.path.abspath(__file__))
file_path = os.path.join(base_dir, "../../data/car_specification_value.csv") # Adjust this path if needed file_path = os.path.join(
base_dir, "../../data/car_specification_value.csv"
) # Adjust this path if needed
if not os.path.exists(file_path): if not os.path.exists(file_path):
self.stdout.write(self.style.ERROR(f"File not found: {file_path}")) self.stdout.write(self.style.ERROR(f"File not found: {file_path}"))
return return
with open(file_path, newline='', encoding='utf-8') as csvfile: with open(file_path, newline="", encoding="utf-8") as csvfile:
reader = csv.DictReader(csvfile) reader = csv.DictReader(csvfile)
for row in reader: for row in reader:
try: try:
car_trim = CarTrim.objects.get(pk=row['id_car_trim']) car_trim = CarTrim.objects.get(pk=row["id_car_trim"])
except CarTrim.DoesNotExist: except CarTrim.DoesNotExist:
self.stdout.write(self.style.WARNING(f"CarTrim with ID {row['id_car_trim']} not found")) self.stdout.write(
self.style.WARNING(
f"CarTrim with ID {row['id_car_trim']} not found"
)
)
continue continue
try: try:
car_specification = CarSpecification.objects.get(pk=row['id_car_specification']) car_specification = CarSpecification.objects.get(
pk=row["id_car_specification"]
)
except CarSpecification.DoesNotExist: except CarSpecification.DoesNotExist:
self.stdout.write(self.style.WARNING(f"CarSpecification with ID {row['id_car_specification']} not found")) self.stdout.write(
self.style.WARNING(
f"CarSpecification with ID {row['id_car_specification']} not found"
)
)
continue continue
car_specification_value, created = CarSpecificationValue.objects.update_or_create( car_specification_value, created = (
id_car_specification_value=row['id_car_specification_value'], CarSpecificationValue.objects.update_or_create(
defaults={ id_car_specification_value=row["id_car_specification_value"],
'id_car_trim': car_trim, defaults={
'id_car_specification': car_specification, "id_car_trim": car_trim,
'value': row['value'], "id_car_specification": car_specification,
'unit': row.get('unit', ''), "value": row["value"],
}, "unit": row.get("unit", ""),
},
)
) )
action = "Created" if created else "Updated" action = "Created" if created else "Updated"
self.stdout.write(self.style.SUCCESS(f"{action} CarSpecificationValue with ID {car_specification_value.id_car_specification_value}")) self.stdout.write(
self.style.SUCCESS(
f"{action} CarSpecificationValue with ID {car_specification_value.id_car_specification_value}"
)
)

View File

@ -3,45 +3,66 @@ import csv
from django.core.management.base import BaseCommand from django.core.management.base import BaseCommand
from inventory.models import CarTrim, CarSerie, CarModel from inventory.models import CarTrim, CarSerie, CarModel
class Command(BaseCommand): class Command(BaseCommand):
help = "Update or add CarTrim entries from a CSV file" help = "Update or add CarTrim entries from a CSV file"
def handle(self, *args, **kwargs): def handle(self, *args, **kwargs):
# Path to the car_trim CSV file # Path to the car_trim CSV file
base_dir = os.path.dirname(os.path.abspath(__file__)) base_dir = os.path.dirname(os.path.abspath(__file__))
file_path = os.path.join(base_dir, "../../data/car_trim.csv") # Adjust path if needed file_path = os.path.join(
base_dir, "../../data/car_trim.csv"
) # Adjust path if needed
if not os.path.exists(file_path): if not os.path.exists(file_path):
self.stdout.write(self.style.ERROR(f"File not found: {file_path}")) self.stdout.write(self.style.ERROR(f"File not found: {file_path}"))
return return
with open(file_path, newline='', encoding='utf-8') as csvfile: with open(file_path, newline="", encoding="utf-8") as csvfile:
reader = csv.DictReader(csvfile) reader = csv.DictReader(csvfile)
for row in reader: for row in reader:
try: try:
car_serie = CarSerie.objects.get(pk=row['id_car_serie']) car_serie = CarSerie.objects.get(pk=row["id_car_serie"])
except CarSerie.DoesNotExist: except CarSerie.DoesNotExist:
self.stdout.write(self.style.WARNING(f"CarSerie with ID {row['id_car_serie']} not found")) self.stdout.write(
self.style.WARNING(
f"CarSerie with ID {row['id_car_serie']} not found"
)
)
continue continue
car_model = None car_model = None
if row['id_car_model']: if row["id_car_model"]:
try: try:
car_model = CarModel.objects.get(pk=row['id_car_model']) car_model = CarModel.objects.get(pk=row["id_car_model"])
except CarModel.DoesNotExist: except CarModel.DoesNotExist:
self.stdout.write(self.style.WARNING(f"CarModel with ID {row['id_car_model']} not found")) self.stdout.write(
self.style.WARNING(
f"CarModel with ID {row['id_car_model']} not found"
)
)
car_trim, created = CarTrim.objects.update_or_create( car_trim, created = CarTrim.objects.update_or_create(
id_car_trim=row['id_car_trim'], id_car_trim=row["id_car_trim"],
defaults={ defaults={
'id_car_serie': car_serie, "id_car_serie": car_serie,
'id_car_model': car_model, "id_car_model": car_model,
'name': row['name'], "name": row["name"],
'arabic_name': row.get('arabic_name', ''), "arabic_name": row.get("arabic_name", ""),
'start_production_year': int(float(row['start_production_year'])) if row['start_production_year'] else None, "start_production_year": int(
'end_production_year': int(float(row['end_production_year'])) if row['end_production_year'] else None, float(row["start_production_year"])
)
if row["start_production_year"]
else None,
"end_production_year": int(float(row["end_production_year"]))
if row["end_production_year"]
else None,
}, },
) )
action = "Created" if created else "Updated" action = "Created" if created else "Updated"
self.stdout.write(self.style.SUCCESS(f"{action} CarTrim with ID {car_trim.id_car_trim}")) self.stdout.write(
self.style.SUCCESS(
f"{action} CarTrim with ID {car_trim.id_car_trim}"
)
)

View File

@ -4,7 +4,7 @@ from django.utils import timezone
from inventory.utils import get_user_type from inventory.utils import get_user_type
logger = logging.getLogger('user_activity') logger = logging.getLogger("user_activity")
class LogUserActivityMiddleware: class LogUserActivityMiddleware:
@ -20,6 +20,7 @@ class LogUserActivityMiddleware:
chain. chain.
:type get_response: Callable :type get_response: Callable
""" """
def __init__(self, get_response): def __init__(self, get_response):
self.get_response = get_response self.get_response = get_response
@ -29,18 +30,16 @@ class LogUserActivityMiddleware:
if request.user.is_authenticated: if request.user.is_authenticated:
action = f"{request.method} {request.path}" action = f"{request.method} {request.path}"
models.UserActivityLog.objects.create( models.UserActivityLog.objects.create(
user=request.user, user=request.user, action=action, timestamp=timezone.now()
action=action,
timestamp=timezone.now()
) )
return response return response
def get_client_ip(self, request): def get_client_ip(self, request):
x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR') x_forwarded_for = request.META.get("HTTP_X_FORWARDED_FOR")
if x_forwarded_for: if x_forwarded_for:
return x_forwarded_for.split(',')[0] return x_forwarded_for.split(",")[0]
return request.META.get('REMOTE_ADDR') return request.META.get("REMOTE_ADDR")
class InjectParamsMiddleware: class InjectParamsMiddleware:
""" """
@ -54,15 +53,16 @@ class InjectParamsMiddleware:
:ivar get_response: The callable to get the next middleware or view response. :ivar get_response: The callable to get the next middleware or view response.
:type get_response: Callable :type get_response: Callable
""" """
def __init__(self, get_response): def __init__(self, get_response):
self.get_response = get_response self.get_response = get_response
def __call__(self, request): def __call__(self, request):
try: try:
# request.entity = request.user.dealer.entity # request.entity = request.user.dealer.entity
request.dealer = get_user_type(request) request.dealer = get_user_type(request)
except Exception: except Exception:
pass pass
response = self.get_response(request) response = self.get_response(request)
return response return response
@ -81,22 +81,24 @@ class InjectDealerMiddleware:
to process the next middleware or the view in the request-response cycle. to process the next middleware or the view in the request-response cycle.
:type get_response: Callable :type get_response: Callable
""" """
def __init__(self, get_response): def __init__(self, get_response):
self.get_response = get_response self.get_response = get_response
def __call__(self, request): def __call__(self, request):
try: try:
request.is_dealer = False request.is_dealer = False
request.is_staff = False request.is_staff = False
if hasattr(request.user, "dealer"): if hasattr(request.user, "dealer"):
request.is_dealer = True request.is_dealer = True
if hasattr(request.user, "staffmember"): if hasattr(request.user, "staffmember"):
request.is_staff = True request.is_staff = True
except Exception: except Exception:
pass pass
response = self.get_response(request) response = self.get_response(request)
return response return response
# class OTPVerificationMiddleware: # class OTPVerificationMiddleware:
# def __init__(self, get_response): # def __init__(self, get_response):
# self.get_response = get_response # self.get_response = get_response
@ -105,4 +107,3 @@ class InjectDealerMiddleware:
# if request.user.is_authenticated and not request.session.get('otp_verified', False): # if request.user.is_authenticated and not request.session.get('otp_verified', False):
# return redirect(reverse('verify_otp')) # return redirect(reverse('verify_otp'))
# return self.get_response(request) # return self.get_response(request)

View File

@ -0,0 +1,880 @@
# Generated by Django 5.2.1 on 2025-06-18 15:44
import datetime
import django.core.validators
import django.db.models.deletion
import django.utils.timezone
import inventory.mixins
import inventory.models
import phonenumber_field.modelfields
import uuid
from decimal import Decimal
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
('appointment', '0001_initial'),
('auth', '0012_alter_user_first_name_max_length'),
('contenttypes', '0002_remove_content_type_name'),
('django_ledger', '0022_alter_billmodel_bill_items_and_more'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='CarEquipment',
fields=[
('id_car_equipment', models.AutoField(primary_key=True, serialize=False)),
('name', models.CharField(blank=True, max_length=255, null=True)),
('arabic_name', models.CharField(blank=True, max_length=255, null=True)),
('year_begin', models.IntegerField(blank=True, null=True)),
('slug', models.SlugField(blank=True, max_length=255, null=True, unique=True)),
],
options={
'verbose_name': 'Equipment',
},
bases=(models.Model, inventory.mixins.LocalizedNameMixin),
),
migrations.CreateModel(
name='CarMake',
fields=[
('id_car_make', models.AutoField(primary_key=True, serialize=False)),
('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')),
('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)),
],
options={
'verbose_name': 'Make',
},
bases=(models.Model, inventory.mixins.LocalizedNameMixin),
),
migrations.CreateModel(
name='ExteriorColors',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=255, verbose_name='Name')),
('arabic_name', models.CharField(max_length=255, verbose_name='Arabic Name')),
('rgb', models.CharField(blank=True, max_length=24, null=True, verbose_name='RGB')),
],
options={
'verbose_name': 'Exterior Colors',
'verbose_name_plural': 'Exterior Colors',
},
bases=(models.Model, inventory.mixins.LocalizedNameMixin),
),
migrations.CreateModel(
name='InteriorColors',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=255, verbose_name='Name')),
('arabic_name', models.CharField(max_length=255, verbose_name='Arabic Name')),
('rgb', models.CharField(blank=True, max_length=24, null=True, verbose_name='RGB')),
],
options={
'verbose_name': 'Interior Colors',
'verbose_name_plural': 'Interior Colors',
},
bases=(models.Model, inventory.mixins.LocalizedNameMixin),
),
migrations.CreateModel(
name='Payment',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('amount', models.DecimalField(decimal_places=2, max_digits=10, verbose_name='amount')),
('payment_method', models.CharField(choices=[('cash', 'cash'), ('credit', 'credit'), ('transfer', 'transfer'), ('debit', 'debit'), ('sadad', 'SADAD')], max_length=50, verbose_name='method')),
('reference_number', models.CharField(blank=True, max_length=100, null=True, verbose_name='reference number')),
('payment_date', models.DateField(auto_now_add=True, verbose_name='date')),
],
options={
'verbose_name': 'payment',
'verbose_name_plural': 'payments',
},
),
migrations.CreateModel(
name='VatRate',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('rate', models.DecimalField(decimal_places=2, default=Decimal('0.15'), max_digits=5)),
('is_active', models.BooleanField(default=True)),
('created_at', models.DateTimeField(auto_now_add=True)),
],
),
migrations.CreateModel(
name='AdditionalServices',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=255, verbose_name='Name')),
('arabic_name', models.CharField(max_length=255, verbose_name='Arabic Name')),
('description', models.TextField(verbose_name='Description')),
('price', models.DecimalField(decimal_places=2, max_digits=14, verbose_name='Price')),
('taxable', models.BooleanField(default=False, verbose_name='taxable')),
('uom', models.CharField(choices=[('EA', 'Each'), ('PR', 'Pair'), ('SET', 'Set'), ('GAL', 'Gallon'), ('L', 'Liter'), ('M', 'Meter'), ('KG', 'Kilogram'), ('HR', 'Hour'), ('BX', 'Box'), ('RL', 'Roll'), ('PKG', 'Package'), ('DZ', 'Dozen'), ('SQ_M', 'Square Meter'), ('PC', 'Piece'), ('BDL', 'Bundle')], max_length=10, verbose_name='Unit of Measurement')),
('item', models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='django_ledger.itemmodel', verbose_name='Item')),
],
options={
'verbose_name': 'Additional Services',
'verbose_name_plural': 'Additional Services',
},
bases=(models.Model, inventory.mixins.LocalizedNameMixin),
),
migrations.CreateModel(
name='Car',
fields=[
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True, verbose_name='Primary Key')),
('slug', models.SlugField(blank=True, help_text='Slug for the object. If not provided, it will be generated automatically.', null=True, unique=True, verbose_name='Slug')),
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')),
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Updated At')),
('vin', models.CharField(max_length=17, unique=True, verbose_name='VIN')),
('year', models.IntegerField(verbose_name='Year')),
('status', models.CharField(choices=[('available', 'Available'), ('sold', 'Sold'), ('hold', 'Hold'), ('damaged', 'Damaged'), ('reserved', 'Reserved'), ('transfer', 'Transfer')], default='available', max_length=10, verbose_name='Status')),
('stock_type', models.CharField(choices=[('new', 'New'), ('used', 'Used')], default='new', max_length=10, verbose_name='Stock Type')),
('remarks', models.TextField(blank=True, null=True, verbose_name='Remarks')),
('mileage', models.IntegerField(blank=True, null=True, verbose_name='Mileage')),
('receiving_date', models.DateTimeField(verbose_name='Receiving Date')),
('hash', models.CharField(blank=True, max_length=64, null=True, verbose_name='Hash')),
('item_model', models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.DO_NOTHING, to='django_ledger.itemmodel', verbose_name='Item Model')),
('id_car_make', models.ForeignKey(blank=True, db_column='id_car_make', null=True, on_delete=django.db.models.deletion.DO_NOTHING, to='inventory.carmake', verbose_name='Make')),
],
options={
'verbose_name': 'Car',
'verbose_name_plural': 'Cars',
},
),
migrations.CreateModel(
name='CarFinance',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('cost_price', models.DecimalField(decimal_places=2, max_digits=14, verbose_name='Cost Price')),
('selling_price', models.DecimalField(decimal_places=2, max_digits=14, verbose_name='Selling Price')),
('discount_amount', models.DecimalField(decimal_places=2, default=Decimal('0.00'), max_digits=14, verbose_name='Discount Amount')),
('is_sold', models.BooleanField(default=False)),
('additional_services', models.ManyToManyField(blank=True, related_name='additional_finances', to='inventory.additionalservices')),
('car', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='finances', to='inventory.car')),
],
options={
'verbose_name': 'Car Financial Details',
'verbose_name_plural': 'Car Financial Details',
},
),
migrations.CreateModel(
name='CarModel',
fields=[
('id_car_model', models.AutoField(primary_key=True, serialize=False)),
('name', models.CharField(blank=True, max_length=255, null=True)),
('arabic_name', models.CharField(blank=True, max_length=255, null=True)),
('slug', models.SlugField(blank=True, max_length=255, null=True, unique=True)),
('id_car_make', models.ForeignKey(db_column='id_car_make', on_delete=django.db.models.deletion.DO_NOTHING, to='inventory.carmake')),
],
options={
'verbose_name': 'Model',
},
bases=(models.Model, inventory.mixins.LocalizedNameMixin),
),
migrations.AddField(
model_name='car',
name='id_car_model',
field=models.ForeignKey(blank=True, db_column='id_car_model', null=True, on_delete=django.db.models.deletion.DO_NOTHING, to='inventory.carmodel', verbose_name='Model'),
),
migrations.CreateModel(
name='CarOption',
fields=[
('id_car_option', models.AutoField(primary_key=True, serialize=False)),
('name', models.CharField(blank=True, max_length=255, null=True)),
('arabic_name', models.CharField(blank=True, max_length=255, null=True)),
('slug', models.SlugField(blank=True, max_length=255, null=True, unique=True)),
('id_parent', models.ForeignKey(blank=True, db_column='id_parent', null=True, on_delete=django.db.models.deletion.DO_NOTHING, to='inventory.caroption')),
],
options={
'verbose_name': 'Option',
},
bases=(models.Model, inventory.mixins.LocalizedNameMixin),
),
migrations.CreateModel(
name='CarOptionValue',
fields=[
('id_car_option_value', models.AutoField(primary_key=True, serialize=False)),
('value', models.CharField(max_length=500)),
('unit', models.CharField(blank=True, max_length=255, null=True)),
('is_base', models.IntegerField()),
('id_car_equipment', models.ForeignKey(db_column='id_car_equipment', on_delete=django.db.models.deletion.DO_NOTHING, to='inventory.carequipment')),
('id_car_option', models.ForeignKey(db_column='id_car_option', on_delete=django.db.models.deletion.DO_NOTHING, to='inventory.caroption')),
],
options={
'verbose_name': 'Option Value',
},
),
migrations.CreateModel(
name='CarRegistration',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('plate_number', models.IntegerField(verbose_name='Plate Number')),
('text1', models.CharField(max_length=1, verbose_name='Text 1')),
('text2', models.CharField(blank=True, max_length=1, null=True, verbose_name='Text 2')),
('text3', models.CharField(blank=True, max_length=1, null=True, verbose_name='Text 3')),
('registration_date', models.DateTimeField(verbose_name='Registration Date')),
('car', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='registrations', to='inventory.car', verbose_name='Car')),
],
options={
'verbose_name': 'Registration',
'verbose_name_plural': 'Registrations',
},
),
migrations.CreateModel(
name='CarSerie',
fields=[
('id_car_serie', models.AutoField(primary_key=True, serialize=False)),
('name', models.CharField(blank=True, max_length=255, null=True)),
('arabic_name', models.CharField(blank=True, max_length=255, null=True)),
('year_begin', models.IntegerField(blank=True, null=True)),
('year_end', models.IntegerField(blank=True, null=True)),
('generation_name', models.CharField(blank=True, max_length=255, null=True)),
('slug', models.SlugField(blank=True, max_length=255, null=True, unique=True)),
('id_car_model', models.ForeignKey(db_column='id_car_model', on_delete=django.db.models.deletion.DO_NOTHING, to='inventory.carmodel')),
],
options={
'verbose_name': 'Series',
},
bases=(models.Model, inventory.mixins.LocalizedNameMixin),
),
migrations.AddField(
model_name='car',
name='id_car_serie',
field=models.ForeignKey(blank=True, db_column='id_car_serie', null=True, on_delete=django.db.models.deletion.DO_NOTHING, to='inventory.carserie', verbose_name='Series'),
),
migrations.CreateModel(
name='CarSpecification',
fields=[
('id_car_specification', models.AutoField(primary_key=True, serialize=False)),
('name', models.CharField(max_length=255)),
('arabic_name', models.CharField(max_length=255)),
('slug', models.SlugField(blank=True, max_length=255, null=True, unique=True)),
('id_parent', models.ForeignKey(blank=True, db_column='id_parent', null=True, on_delete=django.db.models.deletion.DO_NOTHING, to='inventory.carspecification')),
],
options={
'verbose_name': 'Specification',
},
bases=(models.Model, inventory.mixins.LocalizedNameMixin),
),
migrations.CreateModel(
name='CarTrim',
fields=[
('id_car_trim', models.AutoField(primary_key=True, serialize=False)),
('name', models.CharField(blank=True, max_length=255, null=True)),
('arabic_name', models.CharField(blank=True, max_length=255, null=True)),
('start_production_year', models.IntegerField(blank=True, null=True)),
('end_production_year', models.IntegerField(blank=True, null=True)),
('slug', models.SlugField(blank=True, max_length=255, null=True, unique=True)),
('id_car_serie', models.ForeignKey(db_column='id_car_serie', on_delete=django.db.models.deletion.DO_NOTHING, to='inventory.carserie')),
],
options={
'verbose_name': 'Trim',
},
bases=(models.Model, inventory.mixins.LocalizedNameMixin),
),
migrations.CreateModel(
name='CarSpecificationValue',
fields=[
('id_car_specification_value', models.AutoField(primary_key=True, serialize=False)),
('value', models.CharField(max_length=500)),
('unit', models.CharField(blank=True, max_length=255, null=True)),
('id_car_specification', models.ForeignKey(db_column='id_car_specification', on_delete=django.db.models.deletion.DO_NOTHING, to='inventory.carspecification')),
('id_car_trim', models.ForeignKey(db_column='id_car_trim', on_delete=django.db.models.deletion.DO_NOTHING, to='inventory.cartrim')),
],
options={
'verbose_name': 'Specification Value',
},
),
migrations.AddField(
model_name='carequipment',
name='id_car_trim',
field=models.ForeignKey(db_column='id_car_trim', on_delete=django.db.models.deletion.DO_NOTHING, to='inventory.cartrim'),
),
migrations.AddField(
model_name='car',
name='id_car_trim',
field=models.ForeignKey(blank=True, db_column='id_car_trim', null=True, on_delete=django.db.models.deletion.DO_NOTHING, to='inventory.cartrim', verbose_name='Trim'),
),
migrations.CreateModel(
name='CustomCard',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('custom_number', models.CharField(max_length=255, verbose_name='Custom Number')),
('custom_date', models.DateField(verbose_name='Custom Date')),
('car', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='custom_cards', to='inventory.car', verbose_name='Car')),
],
options={
'verbose_name': 'Custom Card',
'verbose_name_plural': 'Custom Cards',
},
),
migrations.CreateModel(
name='Dealer',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('crn', models.CharField(blank=True, max_length=10, null=True, verbose_name='Commercial Registration Number')),
('vrn', models.CharField(blank=True, max_length=15, null=True, verbose_name='VAT Registration Number')),
('arabic_name', models.CharField(max_length=255, verbose_name='Arabic Name')),
('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, 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)),
('entity', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='django_ledger.entitymodel')),
('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='dealer', to=settings.AUTH_USER_MODEL)),
],
options={
'verbose_name': 'Dealer',
'verbose_name_plural': 'Dealers',
},
bases=(models.Model, inventory.mixins.LocalizedNameMixin),
managers=[
('objects', inventory.models.DealerUserManager()),
],
),
migrations.CreateModel(
name='CustomGroup',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=100)),
('group', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to='auth.group', verbose_name='Group')),
('dealer', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='groups', to='inventory.dealer')),
],
),
migrations.CreateModel(
name='Customer',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('title', models.CharField(choices=[('mr', 'Mr'), ('mrs', 'Mrs'), ('ms', 'Ms'), ('miss', 'Miss'), ('dr', 'Dr'), ('prof', 'Prof'), ('prince', 'Prince'), ('princess', 'Princess'), ('company', 'Company'), ('na', 'N/A')], default='na', max_length=10, verbose_name='Title')),
('first_name', models.CharField(max_length=50, verbose_name='First Name')),
('last_name', models.CharField(max_length=50, verbose_name='Last Name')),
('gender', models.CharField(choices=[('m', 'Male'), ('f', 'Female')], max_length=1, verbose_name='Gender')),
('dob', models.DateField(blank=True, null=True, verbose_name='Date of Birth')),
('email', models.EmailField(max_length=254, unique=True, verbose_name='Email')),
('national_id', models.CharField(blank=True, max_length=10, null=True, unique=True, verbose_name='National ID')),
('phone_number', phonenumber_field.modelfields.PhoneNumberField(max_length=128, region='SA', unique=True, verbose_name='Phone Number')),
('address', models.CharField(blank=True, max_length=200, null=True, verbose_name='Address')),
('active', models.BooleanField(default=True, verbose_name='Active')),
('image', models.ImageField(blank=True, null=True, upload_to='customers/', verbose_name='Image')),
('created', models.DateTimeField(auto_now_add=True, verbose_name='Created')),
('updated', models.DateTimeField(auto_now=True, verbose_name='Updated')),
('slug', models.SlugField(blank=True, editable=False, max_length=255, null=True, unique=True)),
('customer_model', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='django_ledger.customermodel')),
('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='customer_profile', to=settings.AUTH_USER_MODEL)),
('dealer', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='customers', to='inventory.dealer')),
],
options={
'verbose_name': 'Customer',
'verbose_name_plural': 'Customers',
},
),
migrations.CreateModel(
name='CarTransfer',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('transfer_date', models.DateTimeField(auto_now_add=True, verbose_name='Transfer Date')),
('quantity', models.IntegerField(default=1, verbose_name='Quantity')),
('remarks', models.TextField(blank=True, null=True, verbose_name='Remarks')),
('status', models.CharField(default='draft', max_length=10, verbose_name=[('draft', 'Draft'), ('approved', 'Approved'), ('pending', 'Pending'), ('accepted', 'Accepted'), ('success', 'Success'), ('reject', 'Reject'), ('cancelled', 'Cancelled')])),
('is_approved', models.BooleanField(default=False)),
('active', models.BooleanField(default=True)),
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')),
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Updated At')),
('car', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='transfer_logs', to='inventory.car', verbose_name='Car')),
('from_dealer', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='transfers_out', to='inventory.dealer', verbose_name='From Dealer')),
('to_dealer', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='transfers_in', to='inventory.dealer', verbose_name='To Dealer')),
],
options={
'verbose_name': 'Car Transfer Log',
'verbose_name_plural': 'Car Transfer Logs',
},
),
migrations.CreateModel(
name='CarLocation',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('description', models.TextField(blank=True, help_text='Optional description about the showroom placement.', null=True, verbose_name='Description')),
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')),
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Last Updated')),
('car', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='location', to='inventory.car', verbose_name='Car')),
('owner', models.ForeignKey(help_text='Dealer who owns the car.', on_delete=django.db.models.deletion.CASCADE, related_name='owned_cars', to='inventory.dealer', verbose_name='Owner')),
('showroom', models.ForeignKey(help_text='Dealer where the car is displayed (can be the owner).', on_delete=django.db.models.deletion.CASCADE, related_name='showroom_cars', to='inventory.dealer', verbose_name='Showroom')),
],
options={
'verbose_name': 'Car Location',
'verbose_name_plural': 'Car Locations',
},
),
migrations.AddField(
model_name='car',
name='dealer',
field=models.ForeignKey(on_delete=django.db.models.deletion.DO_NOTHING, related_name='cars', to='inventory.dealer', verbose_name='Dealer'),
),
migrations.AddField(
model_name='additionalservices',
name='dealer',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='inventory.dealer', verbose_name='Dealer'),
),
migrations.CreateModel(
name='Activity',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('object_id', models.PositiveIntegerField()),
('activity_type', models.CharField(choices=[('call', 'Call'), ('sms', 'SMS'), ('email', 'Email'), ('meeting', 'Meeting'), ('whatsapp', 'WhatsApp'), ('visit', 'Visit'), ('negotiation', 'Negotiation'), ('follow_up', 'Follow Up'), ('won', 'Won'), ('lost', 'Lost'), ('closed', 'Closed'), ('converted', 'Converted'), ('transfer', 'Transfer'), ('add_car', 'Add Car'), ('sale_car', 'Sale Car'), ('reserve_car', 'Reserve Car'), ('transfer_car', 'Transfer Car'), ('remove_car', 'Remove Car'), ('create_quotation', 'Create Quotation'), ('cancel_quotation', 'Cancel Quotation'), ('create_order', 'Create Order'), ('cancel_order', 'Cancel Order'), ('create_invoice', 'Create Invoice'), ('cancel_invoice', 'Cancel Invoice')], max_length=50, verbose_name='Activity Type')),
('notes', models.TextField(blank=True, null=True, verbose_name='Notes')),
('created', models.DateTimeField(auto_now_add=True, verbose_name='Created')),
('updated', models.DateTimeField(auto_now=True, verbose_name='Updated')),
('content_type', models.ForeignKey(on_delete=django.db.models.deletion.DO_NOTHING, to='contenttypes.contenttype')),
('created_by', models.ForeignKey(on_delete=django.db.models.deletion.DO_NOTHING, related_name='activities_created_by', to=settings.AUTH_USER_MODEL)),
('dealer', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='activities', to='inventory.dealer')),
],
options={
'verbose_name': 'Activity',
'verbose_name_plural': 'Activities',
},
),
migrations.CreateModel(
name='DealerSettings',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('additional_info', models.JSONField(blank=True, default=dict, null=True)),
('bill_cash_account', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='bill_cash', to='django_ledger.accountmodel')),
('bill_prepaid_account', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='bill_prepaid', to='django_ledger.accountmodel')),
('bill_unearned_account', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='bill_unearned', to='django_ledger.accountmodel')),
('dealer', models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='settings', to='inventory.dealer')),
('invoice_cash_account', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='invoice_cash', to='django_ledger.accountmodel')),
('invoice_prepaid_account', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='invoice_prepaid', to='django_ledger.accountmodel')),
('invoice_unearned_account', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='invoice_unearned', to='django_ledger.accountmodel')),
],
),
migrations.CreateModel(
name='Email',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('object_id', models.UUIDField()),
('from_email', models.TextField(blank=True, null=True, verbose_name='From Email')),
('to_email', models.TextField(blank=True, null=True, verbose_name='To Email')),
('subject', models.TextField(blank=True, null=True, verbose_name='Subject')),
('message', models.TextField(blank=True, null=True, verbose_name='Message')),
('status', models.CharField(choices=[('SENT', 'Sent'), ('FAILED', 'Failed'), ('DELIVERED', 'Delivered'), ('OPEN', 'Open'), ('DRAFT', 'Draft')], default='OPEN', max_length=20, verbose_name='Status')),
('created', models.DateTimeField(auto_now_add=True, verbose_name='Created')),
('updated', models.DateTimeField(auto_now=True, verbose_name='Updated')),
('content_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='contenttypes.contenttype')),
('created_by', models.ForeignKey(on_delete=django.db.models.deletion.DO_NOTHING, related_name='emails_created', to=settings.AUTH_USER_MODEL)),
],
options={
'verbose_name': 'Email',
'verbose_name_plural': 'Emails',
},
),
migrations.CreateModel(
name='Lead',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('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(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')),
('lead_type', models.CharField(choices=[('customer', 'Customer'), ('organization', 'Organization')], default='customer', max_length=50, verbose_name='Lead Type')),
('source', models.CharField(choices=[('referrals', 'Referrals'), ('whatsapp', 'WhatsApp'), ('showroom', 'Showroom'), ('tiktok', 'TikTok'), ('instagram', 'Instagram'), ('x', 'X'), ('facebook', 'Facebook'), ('motory', 'Motory'), ('influencers', 'Influencers'), ('youtube', 'Youtube'), ('campaign', 'Campaign')], max_length=50, verbose_name='Source')),
('channel', models.CharField(choices=[('walk_in', 'Walk In'), ('toll_free', 'Toll Free'), ('website', 'Website'), ('email', 'Email'), ('form', 'Form')], max_length=50, verbose_name='Channel')),
('status', models.CharField(choices=[('new', 'New'), ('contacted', 'Contacted'), ('qualified', 'Qualified'), ('unqualified', 'Unqualified'), ('converted', 'Converted')], db_index=True, default='new', max_length=50, verbose_name='Status')),
('next_action', models.CharField(blank=True, max_length=255, null=True, verbose_name='Next Action')),
('next_action_date', models.DateTimeField(blank=True, null=True, verbose_name='Next Action Date')),
('is_converted', models.BooleanField(default=False)),
('converted_at', models.DateTimeField(blank=True, null=True)),
('created', models.DateTimeField(auto_now_add=True, db_index=True, verbose_name='Created')),
('updated', models.DateTimeField(auto_now=True, verbose_name='Updated')),
('slug', models.SlugField(blank=True, null=True, unique=True)),
('customer', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='customer_leads', to='inventory.customer')),
('dealer', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='leads', to='inventory.dealer')),
('id_car_make', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.DO_NOTHING, to='inventory.carmake', verbose_name='Make')),
('id_car_model', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.DO_NOTHING, to='inventory.carmodel', verbose_name='Model')),
],
options={
'verbose_name': 'Lead',
'verbose_name_plural': 'Leads',
},
),
migrations.CreateModel(
name='Notes',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('object_id', models.UUIDField()),
('note', models.TextField(verbose_name='Note')),
('created', models.DateTimeField(auto_now_add=True, verbose_name='Created')),
('updated', models.DateTimeField(auto_now=True, verbose_name='Updated')),
('content_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='contenttypes.contenttype')),
('created_by', models.ForeignKey(on_delete=django.db.models.deletion.DO_NOTHING, related_name='notes_created', to=settings.AUTH_USER_MODEL)),
('dealer', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='notes', to='inventory.dealer')),
],
options={
'verbose_name': 'Note',
'verbose_name_plural': 'Notes',
},
),
migrations.CreateModel(
name='Notification',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('message', models.CharField(max_length=255, verbose_name='Message')),
('is_read', models.BooleanField(default=False, verbose_name='Is Read')),
('created', models.DateTimeField(auto_now_add=True, verbose_name='Created')),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='notifications', to=settings.AUTH_USER_MODEL)),
],
options={
'verbose_name': 'Notification',
'verbose_name_plural': 'Notifications',
'ordering': ['-created'],
},
),
migrations.CreateModel(
name='Organization',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=255, verbose_name='Name')),
('arabic_name', models.CharField(max_length=255, verbose_name='Arabic Name')),
('crn', models.CharField(max_length=15, verbose_name='Commercial Registration Number')),
('vrn', models.CharField(max_length=15, verbose_name='VAT Registration Number')),
('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')),
('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')),
('slug', models.SlugField(blank=True, editable=False, max_length=255, null=True, unique=True)),
('customer_model', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='django_ledger.customermodel')),
('dealer', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='organizations', to='inventory.dealer')),
('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='organization_profile', to=settings.AUTH_USER_MODEL)),
],
options={
'verbose_name': 'Organization',
'verbose_name_plural': 'Organizations',
},
bases=(models.Model, inventory.mixins.LocalizedNameMixin),
),
migrations.CreateModel(
name='Opportunity',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('crn', models.CharField(blank=True, max_length=20, null=True, verbose_name='CRN')),
('vrn', models.CharField(blank=True, max_length=20, null=True, verbose_name='VRN')),
('salary', models.DecimalField(blank=True, decimal_places=2, max_digits=10, null=True, verbose_name='Salary')),
('priority', models.CharField(choices=[('high', 'High'), ('medium', 'Medium'), ('low', 'Low')], default='medium', max_length=20, verbose_name='Priority')),
('stage', models.CharField(choices=[('qualification', 'Qualification'), ('test_drive', 'Test Drive'), ('quotation', 'Quotation'), ('negotiation', 'Negotiation'), ('financing', 'Financing'), ('closed_won', 'Closed Won'), ('closed_lost', 'Closed Lost'), ('on_hold', 'On Hold')], max_length=20, verbose_name='Stage')),
('probability', models.PositiveIntegerField(validators=[inventory.models.validate_probability])),
('amount', models.DecimalField(decimal_places=2, max_digits=10, verbose_name='Amount')),
('expected_revenue', models.DecimalField(blank=True, decimal_places=2, max_digits=10, null=True, verbose_name='Expected Revenue')),
('vehicle_of_interest_make', models.CharField(blank=True, max_length=50, null=True)),
('vehicle_of_interest_model', models.CharField(blank=True, max_length=100, null=True)),
('expected_close_date', models.DateField(blank=True, null=True)),
('created', models.DateTimeField(auto_now_add=True, verbose_name='Created')),
('updated', models.DateTimeField(auto_now=True, verbose_name='Updated')),
('slug', models.SlugField(blank=True, help_text='Unique slug for the opportunity.', null=True, unique=True, verbose_name='Slug')),
('loss_reason', models.CharField(blank=True, max_length=255, null=True)),
('car', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='inventory.car', verbose_name='Car')),
('customer', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='opportunities', to='inventory.customer')),
('dealer', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='opportunities', to='inventory.dealer')),
('estimate', models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='opportunity', to='django_ledger.estimatemodel')),
('lead', models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='opportunity', to='inventory.lead')),
('organization', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='inventory.organization', verbose_name='Organization')),
],
options={
'verbose_name': 'Opportunity',
'verbose_name_plural': 'Opportunities',
},
),
migrations.AddField(
model_name='lead',
name='organization',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='organization_leads', to='inventory.organization'),
),
migrations.CreateModel(
name='Refund',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('amount', models.DecimalField(decimal_places=2, max_digits=10, verbose_name='amount')),
('reason', models.TextField(blank=True, verbose_name='reason')),
('refund_date', models.DateField(auto_now_add=True, verbose_name='refund date')),
('payment', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='refund', to='inventory.payment')),
],
options={
'verbose_name': 'refund',
'verbose_name_plural': 'refunds',
},
),
migrations.CreateModel(
name='Representative',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=255, verbose_name='Name')),
('arabic_name', models.CharField(max_length=255, verbose_name='Arabic Name')),
('id_number', models.CharField(max_length=10, unique=True, verbose_name='ID Number')),
('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(blank=True, max_length=200, null=True, verbose_name='Address')),
('dealer', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='representatives', to='inventory.dealer')),
('organization', models.ManyToManyField(related_name='representatives', to='inventory.organization')),
],
options={
'verbose_name': 'Representative',
'verbose_name_plural': 'Representatives',
},
bases=(models.Model, inventory.mixins.LocalizedNameMixin),
),
migrations.CreateModel(
name='SaleOrder',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('payment_method', models.CharField(choices=[('cash', 'Cash'), ('finance', 'Finance'), ('lease', 'Lease'), ('credit_card', 'Credit Card'), ('bank_transfer', 'Bank Transfer'), ('sadad', 'SADAD')], max_length=20)),
('comments', models.TextField(blank=True, null=True)),
('formatted_order_id', models.CharField(editable=False, max_length=10, unique=True)),
('agreed_price', models.DecimalField(decimal_places=2, help_text='The final agreed-upon selling price of the vehicle.', max_digits=12)),
('down_payment_amount', models.DecimalField(decimal_places=2, default=0.0, help_text='The initial payment made by the customer.', max_digits=12)),
('trade_in_value', models.DecimalField(decimal_places=2, default=0.0, help_text='The value of any vehicle traded in by the customer.', max_digits=12)),
('loan_amount', models.DecimalField(decimal_places=2, default=0.0, help_text='The amount financed by a bank or third-party lender.', max_digits=12)),
('total_paid_amount', models.DecimalField(decimal_places=2, default=0.0, help_text='Sum of down payment, trade-in value, and loan amount received so far.', max_digits=12)),
('remaining_balance', models.DecimalField(decimal_places=2, default=0.0, help_text='The remaining amount due from the customer or financing.', max_digits=12)),
('status', models.CharField(choices=[('PENDING_APPROVAL', 'Pending Approval'), ('APPROVED', 'Approved'), ('IN_FINANCING', 'In Financing'), ('PARTIALLY_PAID', 'Partially Paid'), ('FULLY_PAID', 'Fully Paid'), ('PENDING_DELIVERY', 'Pending Delivery'), ('DELIVERED', 'Delivered'), ('CANCELLED', 'Cancelled')], default='PENDING_APPROVAL', help_text='Current status of the sales order.', max_length=20)),
('order_date', models.DateTimeField(default=django.utils.timezone.now, help_text='The date and time the sales order was created.')),
('expected_delivery_date', models.DateField(blank=True, help_text='The planned date for vehicle delivery.', null=True)),
('actual_delivery_date', models.DateTimeField(blank=True, help_text='The actual date and time the vehicle was delivered.', null=True)),
('cancelled_date', models.DateTimeField(blank=True, help_text='The date and time the order was cancelled, if applicable.', null=True)),
('cancellation_reason', models.TextField(blank=True, help_text='Reason for cancellation, if applicable.', null=True)),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('car', models.ForeignKey(blank=True, help_text='The specific vehicle (VIN) being sold.', null=True, on_delete=django.db.models.deletion.PROTECT, related_name='sales_orders', to='inventory.car')),
('created_by', models.ForeignKey(help_text='The user who created this sales order.', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='created_sales_orders', to=settings.AUTH_USER_MODEL)),
('customer', models.ForeignKey(help_text='The customer making the purchase.', on_delete=django.db.models.deletion.PROTECT, related_name='sales_orders', to='inventory.customer')),
('estimate', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='sale_orders', to='django_ledger.estimatemodel', verbose_name='Estimate')),
('invoice', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='sale_orders', to='django_ledger.invoicemodel', verbose_name='Invoice')),
('journal_entry', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='django_ledger.journalentrymodel')),
('last_modified_by', models.ForeignKey(help_text='The user who last modified this sales order.', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='modified_sales_orders', to=settings.AUTH_USER_MODEL)),
('opportunity', models.OneToOneField(help_text='The associated sales opportunity for this order.', on_delete=django.db.models.deletion.CASCADE, related_name='sales_order', to='inventory.opportunity')),
('trade_in_vehicle', models.ForeignKey(blank=True, help_text='The vehicle traded in by the customer, if any.', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='traded_in_on_orders', to='inventory.car')),
],
options={
'verbose_name': 'Sales Order',
'verbose_name_plural': 'Sales Orders',
'ordering': ['-order_date'],
},
),
migrations.CreateModel(
name='Schedule',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('purpose', models.CharField(choices=[('product_demo', 'Product Demo'), ('follow_up_call', 'Follow-Up Call'), ('contract_discussion', 'Contract Discussion'), ('sales_meeting', 'Sales Meeting'), ('support_call', 'Support Call'), ('other', 'Other')], max_length=200)),
('scheduled_at', models.DateTimeField()),
('scheduled_type', models.CharField(choices=[('call', 'Call'), ('meeting', 'Meeting'), ('email', 'Email')], default='Call', max_length=200)),
('duration', models.DurationField(default=datetime.timedelta(seconds=300))),
('notes', models.TextField(blank=True, null=True)),
('status', models.CharField(choices=[('scheduled', 'Scheduled'), ('completed', 'Completed'), ('canceled', 'Canceled')], default='Scheduled', max_length=200)),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('customer', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='schedules', to='django_ledger.customermodel')),
('lead', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='schedules', to='inventory.lead')),
('scheduled_by', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
],
options={
'ordering': ['-scheduled_at'],
},
),
migrations.CreateModel(
name='Staff',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=255, verbose_name='Name')),
('arabic_name', models.CharField(max_length=255, verbose_name='Arabic Name')),
('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')),
('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')),
('slug', models.SlugField(blank=True, editable=False, max_length=255, null=True, unique=True)),
('dealer', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='staff', to='inventory.dealer')),
('staff_member', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='staff', to='appointment.staffmember')),
],
options={
'verbose_name': 'Staff',
'verbose_name_plural': 'Staff',
'permissions': [],
},
bases=(models.Model, inventory.mixins.LocalizedNameMixin),
managers=[
('objects', inventory.models.StaffUserManager()),
],
),
migrations.AddField(
model_name='opportunity',
name='staff',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='owner', to='inventory.staff', verbose_name='Owner'),
),
migrations.CreateModel(
name='LeadStatusHistory',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('old_status', models.CharField(choices=[('new', 'New'), ('contacted', 'Contacted'), ('qualified', 'Qualified'), ('unqualified', 'Unqualified'), ('converted', 'Converted')], max_length=50, verbose_name='Old Status')),
('new_status', models.CharField(choices=[('new', 'New'), ('contacted', 'Contacted'), ('qualified', 'Qualified'), ('unqualified', 'Unqualified'), ('converted', 'Converted')], max_length=50, verbose_name='New Status')),
('changed_at', models.DateTimeField(auto_now_add=True, verbose_name='Changed At')),
('lead', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='status_history', to='inventory.lead')),
('changed_by', models.ForeignKey(on_delete=django.db.models.deletion.DO_NOTHING, related_name='status_changes', to='inventory.staff')),
],
options={
'verbose_name': 'Lead Status History',
'verbose_name_plural': 'Lead Status Histories',
},
),
migrations.AddField(
model_name='lead',
name='staff',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='assigned', to='inventory.staff', verbose_name='Assigned'),
),
migrations.CreateModel(
name='Tasks',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('object_id', models.UUIDField()),
('title', models.CharField(max_length=255, verbose_name='Title')),
('description', models.TextField(blank=True, null=True, verbose_name='Description')),
('due_date', models.DateField(verbose_name='Due Date')),
('completed', models.BooleanField(default=False, verbose_name='Completed')),
('created', models.DateTimeField(auto_now_add=True, verbose_name='Created')),
('updated', models.DateTimeField(auto_now=True, verbose_name='Updated')),
('assigned_to', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='tasks_assigned', to=settings.AUTH_USER_MODEL)),
('content_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='contenttypes.contenttype')),
('created_by', models.ForeignKey(on_delete=django.db.models.deletion.DO_NOTHING, related_name='tasks_created', to=settings.AUTH_USER_MODEL)),
('dealer', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='tasks', to='inventory.dealer')),
],
options={
'verbose_name': 'Task',
'verbose_name_plural': 'Tasks',
},
),
migrations.CreateModel(
name='UserActivityLog',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('action', models.TextField()),
('timestamp', models.DateTimeField(auto_now_add=True)),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
],
options={
'verbose_name': 'User Activity Log',
'verbose_name_plural': 'User Activity Logs',
'ordering': ['-timestamp'],
},
),
migrations.CreateModel(
name='Vendor',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('crn', models.CharField(max_length=10, unique=True, verbose_name='Commercial Registration Number')),
('vrn', models.CharField(max_length=15, unique=True, verbose_name='VAT Registration Number')),
('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', 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')),
('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')),
('dealer', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='vendors', to='inventory.dealer')),
('vendor_model', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.DO_NOTHING, to='django_ledger.vendormodel', verbose_name='Vendor Model')),
],
options={
'verbose_name': 'Vendor',
'verbose_name_plural': 'Vendors',
},
bases=(models.Model, inventory.mixins.LocalizedNameMixin),
),
migrations.AddField(
model_name='car',
name='vendor',
field=models.ForeignKey(on_delete=django.db.models.deletion.DO_NOTHING, related_name='cars', to='inventory.vendor', verbose_name='Vendor'),
),
migrations.CreateModel(
name='CarReservation',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('reserved_at', models.DateTimeField(auto_now_add=True, verbose_name='Reserved At')),
('reserved_until', models.DateTimeField(verbose_name='Reserved Until')),
('car', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='reservations', to='inventory.car', verbose_name='Car')),
('reserved_by', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='reservations', to=settings.AUTH_USER_MODEL, verbose_name='Reserved By')),
],
options={
'verbose_name': 'Car Reservation',
'verbose_name_plural': 'Car Reservations',
'ordering': ['-reserved_at'],
'unique_together': {('car', 'reserved_until')},
},
),
migrations.CreateModel(
name='DealersMake',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('added_at', models.DateTimeField(auto_now_add=True)),
('car_make', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='car_dealers', to='inventory.carmake')),
('dealer', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='dealer_makes', to='inventory.dealer')),
],
options={
'unique_together': {('dealer', 'car_make')},
},
),
migrations.CreateModel(
name='CarColors',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('car', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='colors', to='inventory.car')),
('exterior', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='colors', to='inventory.exteriorcolors')),
('interior', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='colors', to='inventory.interiorcolors')),
],
options={
'verbose_name': 'Color',
'verbose_name_plural': 'Colors',
'unique_together': {('car', 'exterior', 'interior')},
},
),
migrations.CreateModel(
name='PaymentHistory',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('user_data', models.JSONField(blank=True, null=True)),
('amount', models.DecimalField(decimal_places=2, max_digits=10, validators=[django.core.validators.MinValueValidator(0.01)])),
('currency', models.CharField(default='SAR', max_length=3)),
('payment_date', models.DateTimeField(default=django.utils.timezone.now)),
('status', models.CharField(choices=[('initiated', 'initiated'), ('pending', 'Pending'), ('completed', 'Completed'), ('paid', 'Paid'), ('failed', 'Failed'), ('refunded', 'Refunded'), ('cancelled', 'Cancelled')], default='pending', max_length=10)),
('payment_method', models.CharField(choices=[('credit_card', 'Credit Card'), ('debit_card', 'Debit Card'), ('paypal', 'PayPal'), ('bank_transfer', 'Bank Transfer'), ('crypto', 'Cryptocurrency'), ('other', 'Other')], max_length=20)),
('transaction_id', models.CharField(blank=True, max_length=100, null=True, unique=True)),
('invoice_number', models.CharField(blank=True, max_length=50, null=True)),
('order_reference', models.CharField(blank=True, max_length=100, null=True)),
('gateway_response', models.JSONField(blank=True, null=True)),
('gateway_name', models.CharField(blank=True, max_length=50, null=True)),
('description', models.TextField(blank=True, null=True)),
('is_recurring', models.BooleanField(default=False)),
('billing_email', models.EmailField(blank=True, max_length=254, null=True)),
('billing_address', models.TextField(blank=True, null=True)),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='payments', to=settings.AUTH_USER_MODEL)),
],
options={
'verbose_name': 'Payment History',
'verbose_name_plural': 'Payment Histories',
'ordering': ['-payment_date'],
'indexes': [models.Index(fields=['transaction_id'], name='inventory_p_transac_9469f3_idx'), models.Index(fields=['user'], name='inventory_p_user_id_c31626_idx'), models.Index(fields=['status'], name='inventory_p_status_abcb77_idx'), models.Index(fields=['payment_date'], name='inventory_p_payment_b3068c_idx')],
},
),
]

View File

@ -0,0 +1,17 @@
# Generated by Django 5.2.1 on 2025-06-18 15:46
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('inventory', '0001_initial'),
]
operations = [
migrations.RemoveField(
model_name='saleorder',
name='journal_entry',
),
]

View File

@ -0,0 +1,20 @@
# Generated by Django 5.2.1 on 2025-06-18 15:46
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('django_ledger', '0022_alter_billmodel_bill_items_and_more'),
('inventory', '0002_remove_saleorder_journal_entry'),
]
operations = [
migrations.AddField(
model_name='saleorder',
name='journal_entry',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='django_ledger.journalentrymodel'),
),
]

View File

@ -0,0 +1,61 @@
# Generated by Django 5.2.1 on 2025-06-19 13:27
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('inventory', '0003_saleorder_journal_entry'),
]
operations = [
migrations.RemoveField(
model_name='saleorder',
name='agreed_price',
),
migrations.RemoveField(
model_name='saleorder',
name='customer',
),
migrations.RemoveField(
model_name='saleorder',
name='down_payment_amount',
),
migrations.RemoveField(
model_name='saleorder',
name='estimate',
),
migrations.RemoveField(
model_name='saleorder',
name='journal_entry',
),
migrations.RemoveField(
model_name='saleorder',
name='loan_amount',
),
migrations.RemoveField(
model_name='saleorder',
name='opportunity',
),
migrations.RemoveField(
model_name='saleorder',
name='payment_method',
),
migrations.RemoveField(
model_name='saleorder',
name='remaining_balance',
),
migrations.RemoveField(
model_name='saleorder',
name='total_paid_amount',
),
migrations.RemoveField(
model_name='saleorder',
name='trade_in_value',
),
migrations.RemoveField(
model_name='saleorder',
name='trade_in_vehicle',
),
]

View File

@ -0,0 +1,17 @@
# Generated by Django 5.2.1 on 2025-06-19 13:34
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('inventory', '0004_remove_saleorder_agreed_price_and_more'),
]
operations = [
migrations.RemoveField(
model_name='saleorder',
name='car',
),
]

View File

@ -1,4 +1,4 @@
# Generated by Django 5.2.1 on 2025-06-03 11:36 # Generated by Django 5.2.1 on 2025-06-19 13:51
import django.db.models.deletion import django.db.models.deletion
from django.db import migrations, models from django.db import migrations, models
@ -7,14 +7,14 @@ from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
('inventory', '0016_purchaseorderitem_sale'), ('inventory', '0005_remove_saleorder_car'),
] ]
operations = [ operations = [
migrations.AddField( migrations.AddField(
model_name='intendedvehicle', model_name='saleorder',
name='purchase_order', name='dealer',
field=models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, related_name='intended_vehicles', to='inventory.purchaseorder'), field=models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, related_name='sale_orders', to='inventory.dealer'),
preserve_default=False, preserve_default=False,
), ),
] ]

View File

@ -0,0 +1,20 @@
# Generated by Django 5.2.1 on 2025-06-19 13:59
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('django_ledger', '0022_alter_billmodel_bill_items_and_more'),
('inventory', '0006_saleorder_dealer'),
]
operations = [
migrations.AddField(
model_name='saleorder',
name='estimate',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='sale_orders', to='django_ledger.estimatemodel', verbose_name='Estimate'),
),
]

View File

@ -0,0 +1,19 @@
# Generated by Django 5.2.1 on 2025-06-19 14:00
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('inventory', '0007_saleorder_estimate'),
]
operations = [
migrations.AddField(
model_name='saleorder',
name='opportunity',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='sale_orders', to='inventory.opportunity', verbose_name='Opportunity'),
),
]

View File

@ -0,0 +1,19 @@
# Generated by Django 5.2.1 on 2025-06-19 14:19
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('inventory', '0008_saleorder_opportunity'),
]
operations = [
migrations.AddField(
model_name='saleorder',
name='customer',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='sale_orders', to='inventory.customer', verbose_name='Customer'),
),
]

View File

@ -0,0 +1,26 @@
# Generated by Django 5.2.1 on 2025-06-19 14:34
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('inventory', '0009_saleorder_customer'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.AlterField(
model_name='saleorder',
name='created_by',
field=models.ForeignKey(blank=True, help_text='The user who created this sales order.', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='created_sales_orders', to=settings.AUTH_USER_MODEL),
),
migrations.AlterField(
model_name='saleorder',
name='last_modified_by',
field=models.ForeignKey(blank=True, help_text='The user who last modified this sales order.', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='modified_sales_orders', to=settings.AUTH_USER_MODEL),
),
]

View File

@ -1,63 +0,0 @@
# Generated by Django 5.2.1 on 2025-06-03 11:03
import django.db.models.deletion
import django.utils.timezone
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('inventory', '0014_alter_opportunity_amount'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='IntendedVehicle',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('year', models.PositiveIntegerField()),
('color', models.CharField(max_length=30)),
('engine', models.CharField(blank=True, max_length=50, null=True)),
('condition', models.CharField(choices=[('new', 'New'), ('used', 'Used'), ('certified', 'Certified Pre-Owned')], max_length=20)),
('expected_cost', models.DecimalField(decimal_places=2, max_digits=12)),
('make', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='inventory.carmake')),
('model', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='inventory.carmodel')),
('serie', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='inventory.carserie')),
('trim', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='inventory.cartrim')),
],
),
migrations.CreateModel(
name='PurchaseOrder',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('po_number', models.CharField(editable=False, max_length=50, unique=True)),
('status', models.CharField(choices=[('pending', 'Pending'), ('approved', 'Approved'), ('rejected', 'Rejected'), ('completed', 'Completed'), ('canceled', 'Canceled')], default='pending', max_length=20)),
('quantity', models.PositiveIntegerField(default=1)),
('total_cost', models.DecimalField(decimal_places=2, max_digits=12)),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('expected_delivery_date', models.DateField(blank=True, null=True)),
('notes', models.TextField(blank=True, null=True)),
('approved_at', models.DateTimeField(blank=True, null=True)),
('approved_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='approved_orders', to=settings.AUTH_USER_MODEL)),
('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='created_orders', to=settings.AUTH_USER_MODEL)),
('intended_vehicle', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='inventory.intendedvehicle')),
('supplier', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='inventory.vendor')),
],
),
migrations.CreateModel(
name='DeliveryReceipt',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('receipt_number', models.CharField(editable=False, max_length=50, unique=True)),
('received_at', models.DateTimeField(default=django.utils.timezone.now)),
('notes', models.TextField(blank=True, null=True)),
('car', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to='inventory.car')),
('received_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL)),
('purchase_order', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='inventory.purchaseorder')),
],
),
]

View File

@ -1,35 +0,0 @@
# Generated by Django 5.2.1 on 2025-06-03 11:21
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('inventory', '0015_intendedvehicle_purchaseorder_deliveryreceipt'),
]
operations = [
migrations.CreateModel(
name='PurchaseOrderItem',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('quantity', models.PositiveIntegerField(default=1)),
('price', models.DecimalField(decimal_places=2, max_digits=10)),
('purchase_order', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='inventory.purchaseorder')),
('vehicle', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='inventory.intendedvehicle')),
],
),
migrations.CreateModel(
name='Sale',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('sale_date', models.DateTimeField(auto_now_add=True)),
('selling_price', models.DecimalField(decimal_places=2, max_digits=12)),
('car', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='inventory.car')),
('customer', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='inventory.customer')),
('salesperson', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='inventory.staff')),
],
),
]

View File

@ -1,18 +0,0 @@
# Generated by Django 5.2.1 on 2025-06-03 13:56
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('inventory', '0017_intendedvehicle_purchase_order'),
]
operations = [
migrations.AddField(
model_name='intendedvehicle',
name='quantity',
field=models.PositiveIntegerField(default=1),
),
]

View File

@ -1,33 +0,0 @@
# Generated by Django 5.2.1 on 2025-06-03 17:42
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('inventory', '0018_intendedvehicle_quantity'),
]
operations = [
migrations.AlterField(
model_name='intendedvehicle',
name='make',
field=models.CharField(max_length=30),
),
migrations.AlterField(
model_name='intendedvehicle',
name='model',
field=models.CharField(max_length=30),
),
migrations.AlterField(
model_name='intendedvehicle',
name='serie',
field=models.CharField(blank=True, max_length=30, null=True),
),
migrations.AlterField(
model_name='intendedvehicle',
name='trim',
field=models.CharField(blank=True, max_length=30, null=True),
),
]

View File

@ -1,68 +0,0 @@
# Generated by Django 5.2.1 on 2025-06-04 13:33
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('inventory', '0019_alter_intendedvehicle_make_and_more'),
]
operations = [
migrations.RemoveField(
model_name='intendedvehicle',
name='purchase_order',
),
migrations.RemoveField(
model_name='purchaseorder',
name='intended_vehicle',
),
migrations.RemoveField(
model_name='purchaseorderitem',
name='vehicle',
),
migrations.RemoveField(
model_name='purchaseorder',
name='approved_by',
),
migrations.RemoveField(
model_name='purchaseorder',
name='created_by',
),
migrations.RemoveField(
model_name='purchaseorder',
name='supplier',
),
migrations.RemoveField(
model_name='purchaseorderitem',
name='purchase_order',
),
migrations.RemoveField(
model_name='sale',
name='car',
),
migrations.RemoveField(
model_name='sale',
name='customer',
),
migrations.RemoveField(
model_name='sale',
name='salesperson',
),
migrations.DeleteModel(
name='DeliveryReceipt',
),
migrations.DeleteModel(
name='IntendedVehicle',
),
migrations.DeleteModel(
name='PurchaseOrder',
),
migrations.DeleteModel(
name='PurchaseOrderItem',
),
migrations.DeleteModel(
name='Sale',
),
]

View File

View File

@ -1,5 +1,6 @@
from django.utils.translation import get_language from django.utils.translation import get_language
class AddClassMixin: class AddClassMixin:
""" """
Provides functionality for automatically adding CSS classes to form field widgets. Provides functionality for automatically adding CSS classes to form field widgets.
@ -10,6 +11,7 @@ class AddClassMixin:
apply different CSS classes. apply different CSS classes.
""" """
def add_class_to_fields(self): def add_class_to_fields(self):
""" """
Adds the class to the fields of the model. Adds the class to the fields of the model.
@ -20,8 +22,10 @@ class AddClassMixin:
# existing_classes = field.widget.attrs.get('class', '') # existing_classes = field.widget.attrs.get('class', '')
# field.widget.attrs['class'] = f"{existing_classes} form-select form-select-sm".strip() # field.widget.attrs['class'] = f"{existing_classes} form-select form-select-sm".strip()
# else: # else:
existing_classes = field.widget.attrs.get('class', '') existing_classes = field.widget.attrs.get("class", "")
field.widget.attrs['class'] = f"{existing_classes} form-control form-control-sm".strip() field.widget.attrs["class"] = (
f"{existing_classes} form-control form-control-sm".strip()
)
class LocalizedNameMixin: class LocalizedNameMixin:
@ -38,13 +42,14 @@ class LocalizedNameMixin:
:ivar name: Default name used for non-Arabic languages. :ivar name: Default name used for non-Arabic languages.
:type name: Optional[str] :type name: Optional[str]
""" """
def get_local_name(self): def get_local_name(self):
""" """
Returns the localized name based on the current language. Returns the localized name based on the current language.
""" """
if get_language() == 'ar': if get_language() == "ar":
return getattr(self, 'arabic_name', None) return getattr(self, "arabic_name", None)
return getattr(self, 'name', None) return getattr(self, "name", None)
# class AddDealerInstanceMixin: # class AddDealerInstanceMixin:

View File

@ -16,6 +16,7 @@ from django_ledger.models import (
EntityModel, EntityModel,
ItemModel, ItemModel,
CustomerModel, CustomerModel,
JournalEntryModel,
) )
from django_ledger.io.io_core import get_localdate from django_ledger.io.io_core import get_localdate
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
@ -29,7 +30,7 @@ from django_ledger.models import (
EstimateModel, EstimateModel,
InvoiceModel, InvoiceModel,
AccountModel, AccountModel,
EntityManagementModel EntityManagementModel,
) )
from django.contrib.contenttypes.fields import GenericForeignKey from django.contrib.contenttypes.fields import GenericForeignKey
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
@ -630,6 +631,7 @@ class Car(Base):
[ [
self.colors, self.colors,
self.finances, self.finances,
self.finances.selling_price > 0,
] ]
) )
except Exception: except Exception:
@ -712,10 +714,14 @@ class Car(Base):
.filter(name=f"Cogs:{self.id_car_make.name}") .filter(name=f"Cogs:{self.id_car_make.name}")
.first() .first()
) )
def add_colors(self,exterior,interior):
self.colors = CarColors.objects.create(car=self,exterior=exterior,interior=interior) def add_colors(self, exterior, interior):
self.colors = CarColors.objects.create(
car=self, exterior=exterior, interior=interior
)
self.save() self.save()
class CarTransfer(models.Model): class CarTransfer(models.Model):
car = models.ForeignKey( car = models.ForeignKey(
"Car", "Car",
@ -1368,7 +1374,7 @@ class Customer(models.Model):
def full_name(self): def full_name(self):
return f"{self.first_name} {self.last_name}" return f"{self.first_name} {self.last_name}"
def create_customer_model(self,for_lead=False): def create_customer_model(self, for_lead=False):
customer_dict = to_dict(self) customer_dict = to_dict(self)
customer = self.dealer.entity.create_customer( customer = self.dealer.entity.create_customer(
commit=False, commit=False,
@ -1411,7 +1417,7 @@ class Customer(models.Model):
customer.save() customer.save()
return customer return customer
def create_user_model(self,for_lead=False): def create_user_model(self, for_lead=False):
user = User.objects.create_user( user = User.objects.create_user(
username=self.email, username=self.email,
email=self.email, email=self.email,
@ -1501,7 +1507,7 @@ class Organization(models.Model, LocalizedNameMixin):
def __str__(self): def __str__(self):
return self.name return self.name
def create_customer_model(self,for_lead=False): def create_customer_model(self, for_lead=False):
customer_dict = to_dict(self) customer_dict = to_dict(self)
customer = self.dealer.entity.create_customer( customer = self.dealer.entity.create_customer(
commit=False, commit=False,
@ -1543,7 +1549,7 @@ class Organization(models.Model, LocalizedNameMixin):
customer.save() customer.save()
return customer return customer
def create_user_model(self,for_lead=False): def create_user_model(self, for_lead=False):
user = User.objects.create_user( user = User.objects.create_user(
username=self.email, username=self.email,
email=self.email, email=self.email,
@ -1777,6 +1783,7 @@ class Lead(models.Model):
def get_opportunities(self): def get_opportunities(self):
return Opportunity.objects.filter(lead=self) return Opportunity.objects.filter(lead=self)
@property @property
def get_current_action(self): def get_current_action(self):
return ( return (
@ -1912,7 +1919,10 @@ class Opportunity(models.Model):
max_digits=10, decimal_places=2, verbose_name=_("Salary"), blank=True, null=True max_digits=10, decimal_places=2, verbose_name=_("Salary"), blank=True, null=True
) )
priority = models.CharField( priority = models.CharField(
max_length=20, choices=[("high", "High"), ("medium", "Medium"), ("low", "Low")], verbose_name=_("Priority"),default="medium" max_length=20,
choices=[("high", "High"), ("medium", "Medium"), ("low", "Low")],
verbose_name=_("Priority"),
default="medium",
) )
stage = models.CharField( stage = models.CharField(
max_length=20, choices=Stage.choices, verbose_name=_("Stage") max_length=20, choices=Stage.choices, verbose_name=_("Stage")
@ -1933,10 +1943,16 @@ class Opportunity(models.Model):
) )
probability = models.PositiveIntegerField(validators=[validate_probability]) probability = models.PositiveIntegerField(validators=[validate_probability])
amount = models.DecimalField( amount = models.DecimalField(
max_digits=10, decimal_places=2, verbose_name=_("Amount"), max_digits=10,
decimal_places=2,
verbose_name=_("Amount"),
) )
expected_revenue = models.DecimalField( expected_revenue = models.DecimalField(
max_digits=10, decimal_places=2, verbose_name=_("Expected Revenue"), blank=True, null=True max_digits=10,
decimal_places=2,
verbose_name=_("Expected Revenue"),
blank=True,
null=True,
) )
vehicle_of_interest_make = models.CharField(max_length=50, blank=True, null=True) vehicle_of_interest_make = models.CharField(max_length=50, blank=True, null=True)
vehicle_of_interest_model = models.CharField(max_length=100, blank=True, null=True) vehicle_of_interest_model = models.CharField(max_length=100, blank=True, null=True)
@ -1961,6 +1977,7 @@ class Opportunity(models.Model):
def get_notes(self): def get_notes(self):
return self._get_filter(Notes).order_by("-created") return self._get_filter(Notes).order_by("-created")
def get_activities(self): def get_activities(self):
return self._get_filter(Activity) return self._get_filter(Activity)
@ -1969,17 +1986,21 @@ class Opportunity(models.Model):
def get_meetings(self): def get_meetings(self):
return self.lead.get_meetings() return self.lead.get_meetings()
def get_calls(self): def get_calls(self):
return self.lead.get_calls() return self.lead.get_calls()
def get_schedules(self): def get_schedules(self):
return self.lead.get_all_schedules().filter( return (
scheduled_at__gt=timezone.now() self.lead.get_all_schedules()
).order_by("scheduled_at") .filter(scheduled_at__gt=timezone.now())
.order_by("scheduled_at")
)
def get_emails(self): def get_emails(self):
return self._get_filter(Email) return self._get_filter(Email)
def _get_filter(self,Model): def _get_filter(self, Model):
objects = Model.objects.filter( objects = Model.objects.filter(
content_type__model="opportunity", object_id=self.id content_type__model="opportunity", object_id=self.id
) )
@ -2001,9 +2022,7 @@ class Opportunity(models.Model):
opportinity_for = self.organization.name opportinity_for = self.organization.name
if not self.slug: if not self.slug:
self.slug = slugify( self.slug = slugify(f"opportunity {opportinity_for}")
f"opportunity {opportinity_for}"
)
super().save(*args, **kwargs) super().save(*args, **kwargs)
class Meta: class Meta:
@ -2012,7 +2031,9 @@ class Opportunity(models.Model):
def __str__(self): def __str__(self):
if self.customer: if self.customer:
return f"Opportunity for {self.customer.first_name} {self.customer.last_name}" return (
f"Opportunity for {self.customer.first_name} {self.customer.last_name}"
)
return f"Opportunity for {self.organization.name}" return f"Opportunity for {self.organization.name}"
@ -2340,12 +2361,16 @@ class SaleOrder(models.Model):
("DELIVERED", "Delivered"), ("DELIVERED", "Delivered"),
("CANCELLED", "Cancelled"), ("CANCELLED", "Cancelled"),
] ]
dealer = models.ForeignKey(
Dealer, on_delete=models.CASCADE, related_name="sale_orders"
)
estimate = models.ForeignKey( estimate = models.ForeignKey(
EstimateModel, EstimateModel,
on_delete=models.CASCADE, on_delete=models.CASCADE,
related_name="sale_orders", related_name="sale_orders",
verbose_name=_("Estimate"), verbose_name=_("Estimate"),
null=True,
blank=True,
) )
invoice = models.ForeignKey( invoice = models.ForeignKey(
InvoiceModel, InvoiceModel,
@ -2355,93 +2380,25 @@ class SaleOrder(models.Model):
null=True, null=True,
blank=True, blank=True,
) )
payment_method = models.CharField( customer = models.ForeignKey(
max_length=20, Customer,
choices=[ on_delete=models.CASCADE,
("cash", _("Cash")), related_name="sale_orders",
("finance", _("Finance")), verbose_name=_("Customer"),
("lease", _("Lease")), null=True,
("credit_card", _("Credit Card")), blank=True,
("bank_transfer", _("Bank Transfer")), )
("sadad", _("SADAD")), opportunity = models.ForeignKey(
], Opportunity,
on_delete=models.CASCADE,
related_name="sale_orders",
verbose_name=_("Opportunity"),
null=True,
blank=True,
) )
comments = models.TextField(blank=True, null=True) comments = models.TextField(blank=True, null=True)
formatted_order_id = models.CharField(max_length=10, unique=True, editable=False) formatted_order_id = models.CharField(max_length=10, unique=True, editable=False)
# Link to the specific opportunity this sales order is fulfilling
opportunity = models.OneToOneField(
"Opportunity", # Use string reference if Opportunity is defined later or in another app
on_delete=models.CASCADE,
related_name="sales_order",
help_text="The associated sales opportunity for this order.",
)
# Link to the customer who is purchasing the vehicle
customer = models.ForeignKey(
"Customer", # Use string reference
on_delete=models.PROTECT, # Protect customer data if order exists
related_name="sales_orders",
help_text="The customer making the purchase.",
)
# Link to the specific vehicle being sold
# This assumes a Vehicle model exists which represents an actual car in inventory
car = models.ForeignKey(
"Car", # Use string reference to your Vehicle/Inventory model
on_delete=models.PROTECT, # Don't delete vehicle if it's part of an order
related_name="sales_orders",
help_text="The specific vehicle (VIN) being sold.",
null=True,
blank=True,
)
# Financial Details
agreed_price = models.DecimalField(
max_digits=12,
decimal_places=2,
help_text="The final agreed-upon selling price of the vehicle.",
)
down_payment_amount = models.DecimalField(
max_digits=12,
decimal_places=2,
default=0.00,
help_text="The initial payment made by the customer.",
)
trade_in_value = models.DecimalField(
max_digits=12,
decimal_places=2,
default=0.00,
help_text="The value of any vehicle traded in by the customer.",
)
# Reference to the trade-in vehicle if it also exists in inventory
trade_in_vehicle = models.ForeignKey(
"Car", # Assuming Vehicle model can also represent trade-ins
on_delete=models.SET_NULL,
blank=True,
null=True,
related_name="traded_in_on_orders",
help_text="The vehicle traded in by the customer, if any.",
)
loan_amount = models.DecimalField(
max_digits=12,
decimal_places=2,
default=0.00,
help_text="The amount financed by a bank or third-party lender.",
)
total_paid_amount = models.DecimalField(
max_digits=12,
decimal_places=2,
default=0.00,
help_text="Sum of down payment, trade-in value, and loan amount received so far.",
)
remaining_balance = models.DecimalField(
max_digits=12,
decimal_places=2,
default=0.00,
help_text="The remaining amount due from the customer or financing.",
)
# Status and Dates # Status and Dates
status = models.CharField( status = models.CharField(
max_length=20, max_length=20,
@ -2469,13 +2426,13 @@ class SaleOrder(models.Model):
blank=True, null=True, help_text="Reason for cancellation, if applicable." blank=True, null=True, help_text="Reason for cancellation, if applicable."
) )
# Audit fields
created_at = models.DateTimeField(auto_now_add=True) created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True) updated_at = models.DateTimeField(auto_now=True)
created_by = models.ForeignKey( created_by = models.ForeignKey(
settings.AUTH_USER_MODEL, settings.AUTH_USER_MODEL,
on_delete=models.SET_NULL, on_delete=models.SET_NULL,
null=True, null=True,
blank=True,
related_name="created_sales_orders", related_name="created_sales_orders",
help_text="The user who created this sales order.", help_text="The user who created this sales order.",
) )
@ -2483,6 +2440,7 @@ class SaleOrder(models.Model):
settings.AUTH_USER_MODEL, settings.AUTH_USER_MODEL,
on_delete=models.SET_NULL, on_delete=models.SET_NULL,
null=True, null=True,
blank=True,
related_name="modified_sales_orders", related_name="modified_sales_orders",
help_text="The user who last modified this sales order.", help_text="The user who last modified this sales order.",
) )
@ -2501,14 +2459,7 @@ class SaleOrder(models.Model):
next_id = 1 next_id = 1
year = get_localdate().year year = get_localdate().year
self.formatted_order_id = f"O-{year}-{next_id:09d}" self.formatted_order_id = f"O-{year}-{next_id:09d}"
self.total_paid_amount = (
Decimal(self.down_payment_amount)
+ Decimal(self.trade_in_value)
+ Decimal(self.loan_amount)
)
self.remaining_balance = Decimal(self.agreed_price) - Decimal(
self.total_paid_amount
)
super().save(*args, **kwargs) super().save(*args, **kwargs)
def __str__(self): def __str__(self):
@ -2524,13 +2475,16 @@ class SaleOrder(models.Model):
@property @property
def items(self): def items(self):
if self.estimate.get_itemtxs_data(): if self.invoice.get_itemtxs_data():
return self.estimate.get_itemtxs_data()[0] return self.invoice.get_itemtxs_data()[0]
return [] return []
@property @property
def cars(self): def cars(self):
return [x.car for x in self.estimate.get_itemtxs_data()[0]] if self.items:
return [x.item_model.car for x in self.items]
return []
class CustomGroup(models.Model): class CustomGroup(models.Model):
@ -2540,6 +2494,9 @@ class CustomGroup(models.Model):
"auth.Group", verbose_name=_("Group"), on_delete=models.CASCADE "auth.Group", verbose_name=_("Group"), on_delete=models.CASCADE
) )
@property
def entity(self):
return self.invoice.entity
@property @property
def users(self): def users(self):
return self.group.user_set.all() return self.group.user_set.all()
@ -2859,7 +2816,6 @@ class PaymentHistory(models.Model):
return self.status == self.COMPLETED return self.status == self.COMPLETED
###################################################################################################### ######################################################################################################
###################################################################################################### ######################################################################################################
###################################################################################################### ######################################################################################################

View File

@ -27,11 +27,12 @@ def get_make(item):
if not data: if not data:
r = item.split(" ") r = item.split(" ")
for i in r: for i in r:
if data:= CarMake.objects.filter(name__iexact=i).first(): if data := CarMake.objects.filter(name__iexact=i).first():
break break
return data return data
def get_model(item,make):
def get_model(item, make):
""" """
Searches for a car model associated with a specific make by performing an Searches for a car model associated with a specific make by performing an
exact case-insensitive match. If no match is found, it attempts to match exact case-insensitive match. If no match is found, it attempts to match
@ -47,10 +48,11 @@ def get_model(item,make):
if not data: if not data:
r = item.split(" ") r = item.split(" ")
for i in r: for i in r:
if data:=make.carmodel_set.filter(name__iexact=i).first(): if data := make.carmodel_set.filter(name__iexact=i).first():
break break
return data return data
def normalize_name(name): def normalize_name(name):
""" """
Normalizes a given name by removing spaces and hyphens and converting it to lowercase. Normalizes a given name by removing spaces and hyphens and converting it to lowercase.
@ -68,7 +70,6 @@ def normalize_name(name):
def decodevin(vin): def decodevin(vin):
""" """
Decodes a Vehicle Identification Number (VIN) using multiple decoding functions Decodes a Vehicle Identification Number (VIN) using multiple decoding functions
and returns the decoded result. This function attempts to decode the VIN using and returns the decoded result. This function attempts to decode the VIN using
@ -81,9 +82,9 @@ def decodevin(vin):
all decoding attempts fail. all decoding attempts fail.
:rtype: dict | None :rtype: dict | None
""" """
if result:=decode_vin(vin): if result := decode_vin(vin):
return result return result
elif result:=elm(vin): elif result := elm(vin):
return result return result
# elif result:=decode_vin_haikalna(vin): # elif result:=decode_vin_haikalna(vin):
# return result # return result
@ -158,6 +159,3 @@ def elm(vin):
} }
print([x for x in data.values()]) print([x for x in data.values()])
return data if all([x for x in data.values()]) else None return data if all([x for x in data.values()]) else None

View File

@ -12,7 +12,7 @@ from django_ledger.models import (
JournalEntryModel, JournalEntryModel,
TransactionModel, TransactionModel,
LedgerModel, LedgerModel,
AccountModel AccountModel,
) )
from . import models from . import models
from django.utils.timezone import now from django.utils.timezone import now
@ -59,7 +59,10 @@ User = get_user_model()
@receiver(post_save, sender=models.Car) @receiver(post_save, sender=models.Car)
def create_dealers_make(sender, instance, created, **kwargs): def create_dealers_make(sender, instance, created, **kwargs):
if created: if created:
models.DealersMake.objects.get_or_create(dealer=instance.dealer, car_make=instance.id_car_make) models.DealersMake.objects.get_or_create(
dealer=instance.dealer, car_make=instance.id_car_make
)
@receiver(post_save, sender=models.Car) @receiver(post_save, sender=models.Car)
def create_car_location(sender, instance, created, **kwargs): def create_car_location(sender, instance, created, **kwargs):
@ -94,6 +97,7 @@ def create_car_location(sender, instance, created, **kwargs):
except Exception as e: except Exception as e:
print(f"Failed to create CarLocation for car {instance.vin}: {e}") print(f"Failed to create CarLocation for car {instance.vin}: {e}")
# Create Entity # Create Entity
@receiver(post_save, sender=models.Dealer) @receiver(post_save, sender=models.Dealer)
def create_ledger_entity(sender, instance, created, **kwargs): def create_ledger_entity(sender, instance, created, **kwargs):
@ -143,6 +147,7 @@ def create_ledger_entity(sender, instance, created, **kwargs):
# create_settings(instance.pk) # create_settings(instance.pk)
# create_accounts_for_make(instance.pk) # create_accounts_for_make(instance.pk)
@receiver(post_save, sender=models.Dealer) @receiver(post_save, sender=models.Dealer)
def create_dealer_groups(sender, instance, created, **kwargs): def create_dealer_groups(sender, instance, created, **kwargs):
""" """
@ -162,11 +167,18 @@ def create_dealer_groups(sender, instance, created, **kwargs):
def create_groups(): def create_groups():
for group_name in group_names: for group_name in group_names:
group, created = Group.objects.get_or_create(name=f"{instance.pk}_{group_name}") group, created = Group.objects.get_or_create(
group_manager,created = models.CustomGroup.objects.get_or_create(name=group_name, dealer=instance, group=group) name=f"{instance.pk}_{group_name}"
)
group_manager, created = models.CustomGroup.objects.get_or_create(
name=group_name, dealer=instance, group=group
)
group_manager.set_default_permissions() group_manager.set_default_permissions()
instance.user.groups.add(group) instance.user.groups.add(group)
transaction.on_commit(create_groups) transaction.on_commit(create_groups)
# Create Vendor # Create Vendor
@receiver(post_save, sender=models.Vendor) @receiver(post_save, sender=models.Vendor)
def create_ledger_vendor(sender, instance, created, **kwargs): def create_ledger_vendor(sender, instance, created, **kwargs):
@ -189,6 +201,7 @@ def create_ledger_vendor(sender, instance, created, **kwargs):
else: else:
instance.update_vendor_model() instance.update_vendor_model()
# Create Item # Create Item
@receiver(post_save, sender=models.Car) @receiver(post_save, sender=models.Car)
def create_item_model(sender, instance, created, **kwargs): def create_item_model(sender, instance, created, **kwargs):
@ -228,12 +241,13 @@ def create_item_model(sender, instance, created, **kwargs):
# ) # )
instance.item_model = inventory instance.item_model = inventory
inventory.additional_info = {} inventory.additional_info = {}
inventory.additional_info.update({'car_info': instance.to_dict()}) inventory.additional_info.update({"car_info": instance.to_dict()})
inventory.save() inventory.save()
else: else:
instance.item_model.additional_info.update({'car_info': instance.to_dict()}) instance.item_model.additional_info.update({"car_info": instance.to_dict()})
instance.item_model.save() instance.item_model.save()
# # update price - CarFinance # # update price - CarFinance
@receiver(post_save, sender=models.CarFinance) @receiver(post_save, sender=models.CarFinance)
def update_item_model_cost(sender, instance, created, **kwargs): def update_item_model_cost(sender, instance, created, **kwargs):
@ -252,20 +266,55 @@ def update_item_model_cost(sender, instance, created, **kwargs):
if created and not instance.is_sold: if created and not instance.is_sold:
entity = instance.car.dealer.entity entity = instance.car.dealer.entity
coa = entity.get_default_coa() coa = entity.get_default_coa()
inventory_account = entity.get_all_accounts().filter(name=f'Inventory:{instance.car.id_car_make.name}').first() inventory_account = (
entity.get_all_accounts()
.filter(name=f"Inventory:{instance.car.id_car_make.name}")
.first()
)
if not inventory_account: if not inventory_account:
inventory_account = create_make_accounts(entity,coa,[instance.car.id_car_make],"Inventory",roles.ASSET_CA_INVENTORY,"debit") inventory_account = create_make_accounts(
entity,
coa,
[instance.car.id_car_make],
"Inventory",
roles.ASSET_CA_INVENTORY,
"debit",
)
cogs = entity.get_all_accounts().filter(name=f'Cogs:{instance.car.id_car_make.name}').first() cogs = (
entity.get_all_accounts()
.filter(name=f"Cogs:{instance.car.id_car_make.name}")
.first()
)
if not cogs: if not cogs:
cogs = create_make_accounts(entity,coa,[instance.car.id_car_make],"Cogs",roles.COGS,"debit") cogs = create_make_accounts(
revenue = entity.get_all_accounts().filter(name=f'Revenue:{instance.car.id_car_make.name}').first() entity, coa, [instance.car.id_car_make], "Cogs", roles.COGS, "debit"
)
revenue = (
entity.get_all_accounts()
.filter(name=f"Revenue:{instance.car.id_car_make.name}")
.first()
)
if not revenue: if not revenue:
revenue = create_make_accounts(entity,coa,[instance.car.id_car_make],"Revenue",roles.ASSET_CA_RECEIVABLES,"credit") revenue = create_make_accounts(
entity,
coa,
[instance.car.id_car_make],
"Revenue",
roles.ASSET_CA_RECEIVABLES,
"credit",
)
cash_account = entity.get_all_accounts().filter(name="Cash",role=roles.ASSET_CA_CASH).first() cash_account = (
# entity.get_all_accounts()
# .filter(name="Cash", role=roles.ASSET_CA_CASH)
# .first()
entity.get_all_accounts().filter(role=roles.ASSET_CA_CASH,role_default=True).first()
)
ledger = LedgerModel.objects.create(entity=entity, name=f"Inventory Purchase - {instance.car}") ledger = LedgerModel.objects.create(
entity=entity, name=f"Inventory Purchase - {instance.car}"
)
je = JournalEntryModel.objects.create( je = JournalEntryModel.objects.create(
ledger=ledger, ledger=ledger,
description=f"Acquired {instance.car} for inventory", description=f"Acquired {instance.car} for inventory",
@ -289,8 +338,14 @@ def update_item_model_cost(sender, instance, created, **kwargs):
instance.car.item_model.default_amount = instance.selling_price instance.car.item_model.default_amount = instance.selling_price
if not isinstance(instance.car.item_model.additional_info, dict): if not isinstance(instance.car.item_model.additional_info, dict):
instance.car.item_model.additional_info = {} 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({"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(
{
"additional_services": [
service.to_dict() for service in instance.additional_services.all()
]
}
)
instance.car.item_model.save() instance.car.item_model.save()
print(f"Inventory item updated with CarFinance data for Car: {instance.car}") print(f"Inventory item updated with CarFinance data for Car: {instance.car}")
@ -316,6 +371,7 @@ def update_item_model_cost(sender, instance, created, **kwargs):
# quotation.status = 'pending' # quotation.status = 'pending'
# quotation.save() # quotation.save()
@receiver(post_save, sender=models.CarColors) @receiver(post_save, sender=models.CarColors)
def update_car_when_color_changed(sender, instance, **kwargs): def update_car_when_color_changed(sender, instance, **kwargs):
""" """
@ -335,6 +391,7 @@ def update_car_when_color_changed(sender, instance, **kwargs):
car = instance.car car = instance.car
car.save() car.save()
@receiver(post_save, sender=models.Opportunity) @receiver(post_save, sender=models.Opportunity)
def notify_staff_on_deal_stage_change(sender, instance, **kwargs): def notify_staff_on_deal_stage_change(sender, instance, **kwargs):
""" """
@ -409,7 +466,11 @@ def create_item_service(sender, instance, created, **kwargs):
if created: if created:
entity = instance.dealer.entity entity = instance.dealer.entity
uom = entity.get_uom_all().get(unit_abbr=instance.uom) uom = entity.get_uom_all().get(unit_abbr=instance.uom)
cogs = entity.get_all_accounts().filter(role=roles.COGS,active=True,role_default=True).first() cogs = (
entity.get_all_accounts()
.filter(role=roles.COGS, active=True, role_default=True)
.first()
)
service_model = ItemModel.objects.create( service_model = ItemModel.objects.create(
name=instance.name, name=instance.name,
@ -456,7 +517,7 @@ def track_lead_status_change(sender, instance, **kwargs):
lead=instance, lead=instance,
old_status=old_lead.status, old_status=old_lead.status,
new_status=instance.status, new_status=instance.status,
changed_by=instance.staff # Assuming the assigned staff made the change changed_by=instance.staff, # Assuming the assigned staff made the change
) )
except models.Lead.DoesNotExist: except models.Lead.DoesNotExist:
pass # Ignore if the lead doesn't exist (e.g., during initial creation) pass # Ignore if the lead doesn't exist (e.g., during initial creation)
@ -479,7 +540,7 @@ def notify_assigned_staff(sender, instance, created, **kwargs):
if instance.staff: # Check if the lead is assigned if instance.staff: # Check if the lead is assigned
models.Notification.objects.create( models.Notification.objects.create(
user=instance.staff.staff_member.user, user=instance.staff.staff_member.user,
message=f"You have been assigned a new lead: {instance.full_name}." message=f"You have been assigned a new lead: {instance.full_name}.",
) )
@ -501,6 +562,7 @@ def update_car_status_on_reservation_create(sender, instance, created, **kwargs)
car.status = models.CarStatusChoices.RESERVED car.status = models.CarStatusChoices.RESERVED
car.save() car.save()
@receiver(post_delete, sender=models.CarReservation) @receiver(post_delete, sender=models.CarReservation)
def update_car_status_on_reservation_delete(sender, instance, **kwargs): def update_car_status_on_reservation_delete(sender, instance, **kwargs):
""" """
@ -523,6 +585,7 @@ def update_car_status_on_reservation_delete(sender, instance, **kwargs):
car.status = models.CarStatusChoices.AVAILABLE car.status = models.CarStatusChoices.AVAILABLE
car.save() car.save()
@receiver(post_save, sender=models.CarReservation) @receiver(post_save, sender=models.CarReservation)
def update_car_status_on_reservation_update(sender, instance, **kwargs): def update_car_status_on_reservation_update(sender, instance, **kwargs):
""" """
@ -545,6 +608,7 @@ def update_car_status_on_reservation_update(sender, instance, **kwargs):
car.status = models.CarStatusChoices.AVAILABLE car.status = models.CarStatusChoices.AVAILABLE
car.save() car.save()
@receiver(post_save, sender=models.Dealer) @receiver(post_save, sender=models.Dealer)
def create_dealer_settings(sender, instance, created, **kwargs): def create_dealer_settings(sender, instance, created, **kwargs):
""" """
@ -567,13 +631,26 @@ def create_dealer_settings(sender, instance, created, **kwargs):
if created: if created:
models.DealerSettings.objects.create( models.DealerSettings.objects.create(
dealer=instance, dealer=instance,
invoice_cash_account=instance.entity.get_all_accounts().filter(role=roles.ASSET_CA_CASH).first(), invoice_cash_account=instance.entity.get_all_accounts()
invoice_prepaid_account=instance.entity.get_all_accounts().filter(role=roles.ASSET_CA_RECEIVABLES).first(), .filter(role=roles.ASSET_CA_CASH)
invoice_unearned_account=instance.entity.get_all_accounts().filter(role=roles.LIABILITY_CL_DEFERRED_REVENUE).first(), .first(),
bill_cash_account=instance.entity.get_all_accounts().filter(role=roles.ASSET_CA_CASH).first(), invoice_prepaid_account=instance.entity.get_all_accounts()
bill_prepaid_account=instance.entity.get_all_accounts().filter(role=roles.ASSET_CA_PREPAID).first(), .filter(role=roles.ASSET_CA_RECEIVABLES)
bill_unearned_account=instance.entity.get_all_accounts().filter(role=roles.LIABILITY_CL_ACC_PAYABLE).first() .first(),
) invoice_unearned_account=instance.entity.get_all_accounts()
.filter(role=roles.LIABILITY_CL_DEFERRED_REVENUE)
.first(),
bill_cash_account=instance.entity.get_all_accounts()
.filter(role=roles.ASSET_CA_CASH)
.first(),
bill_prepaid_account=instance.entity.get_all_accounts()
.filter(role=roles.ASSET_CA_PREPAID)
.first(),
bill_unearned_account=instance.entity.get_all_accounts()
.filter(role=roles.LIABILITY_CL_ACC_PAYABLE)
.first(),
)
# @receiver(post_save, sender=EstimateModel) # @receiver(post_save, sender=EstimateModel)
# def update_estimate_status(sender, instance,created, **kwargs): # def update_estimate_status(sender, instance,created, **kwargs):
@ -618,6 +695,7 @@ def create_dealer_settings(sender, instance, created, **kwargs):
# """ # """
# VatRate.objects.get_or_create(rate=Decimal('0.15'), is_active=True) # VatRate.objects.get_or_create(rate=Decimal('0.15'), is_active=True)
@receiver(post_save, sender=models.Dealer) @receiver(post_save, sender=models.Dealer)
def create_make_ledger_accounts(sender, instance, created, **kwargs): def create_make_ledger_accounts(sender, instance, created, **kwargs):
""" """
@ -653,7 +731,6 @@ def create_make_ledger_accounts(sender, instance, created, **kwargs):
# ) # )
# @receiver(post_save, sender=VendorModel) # @receiver(post_save, sender=VendorModel)
# def create_vendor_accounts(sender, instance, created, **kwargs):Dealer) # def create_vendor_accounts(sender, instance, created, **kwargs):Dealer)
# if created: # if created:
@ -674,7 +751,8 @@ def create_make_ledger_accounts(sender, instance, created, **kwargs):
# active=True # active=True
# ) # )
def save_journal(car_finance,ledger,vendor):
def save_journal(car_finance, ledger, vendor):
""" """
Saves a journal entry pertaining to a car finance transaction for a specific ledger and vendor. Saves a journal entry pertaining to a car finance transaction for a specific ledger and vendor.
@ -708,32 +786,43 @@ def save_journal(car_finance,ledger,vendor):
ledger.additional_info["je_number"] = journal.je_number ledger.additional_info["je_number"] = journal.je_number
ledger.save() ledger.save()
inventory_account = entity.get_default_coa_accounts().filter(role=roles.ASSET_CA_INVENTORY).first() inventory_account = (
entity.get_default_coa_accounts().filter(role=roles.ASSET_CA_INVENTORY).first()
)
vendor_account = entity.get_default_coa_accounts().filter(name=vendor.name).first() vendor_account = entity.get_default_coa_accounts().filter(name=vendor.name).first()
if not vendor_account: if not vendor_account:
last_account = entity.get_all_accounts().filter(role=roles.LIABILITY_CL_ACC_PAYABLE).order_by('-created').first() last_account = (
if len(last_account.code) == 4: entity.get_all_accounts()
code = f"{int(last_account.code)}{1:03d}" .filter(role=roles.LIABILITY_CL_ACC_PAYABLE)
elif len(last_account.code) > 4: .order_by("-created")
code = f"{int(last_account.code)+1}" .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}"
vendor_account = entity.create_account( vendor_account = entity.create_account(
name=vendor.name, name=vendor.name,
code=code, code=code,
role=roles.LIABILITY_CL_ACC_PAYABLE, role=roles.LIABILITY_CL_ACC_PAYABLE,
coa_model=coa, coa_model=coa,
balance_type="credit", balance_type="credit",
active=True active=True,
) )
additional_services_account = entity.get_default_coa_accounts().filter(name="Additional Services",role=roles.COGS).first() additional_services_account = (
entity.get_default_coa_accounts()
.filter(name="Additional Services", role=roles.COGS)
.first()
)
# Debit Inventory Account # Debit Inventory Account
TransactionModel.objects.create( TransactionModel.objects.create(
journal_entry=journal, journal_entry=journal,
account=inventory_account, account=inventory_account,
amount=car_finance.cost_price, amount=car_finance.cost_price,
tx_type='debit' tx_type="debit",
) )
# Credit Vendor Account # Credit Vendor Account
@ -741,9 +830,10 @@ def save_journal(car_finance,ledger,vendor):
journal_entry=journal, journal_entry=journal,
account=vendor_account, account=vendor_account,
amount=car_finance.cost_price, amount=car_finance.cost_price,
tx_type='credit', tx_type="credit",
) )
@receiver(post_save, sender=models.CarFinance) @receiver(post_save, sender=models.CarFinance)
def update_finance_cost(sender, instance, created, **kwargs): def update_finance_cost(sender, instance, created, **kwargs):
""" """
@ -776,8 +866,8 @@ def update_finance_cost(sender, instance, created, **kwargs):
vendor_name = vendor.name if vendor else "" vendor_name = vendor.name if vendor else ""
name = f"{vin}-{make}-{model}-{year}-{vendor_name}" name = f"{vin}-{make}-{model}-{year}-{vendor_name}"
ledger,_ = LedgerModel.objects.get_or_create(name=name, entity=entity) ledger, _ = LedgerModel.objects.get_or_create(name=name, entity=entity)
save_journal(instance,ledger,vendor) save_journal(instance, ledger, vendor)
# if not created: # if not created:
# if ledger.additional_info.get("je_number"): # if ledger.additional_info.get("je_number"):

View File

@ -47,17 +47,29 @@ class CarTable(tables.Table):
with badge-style formatting based on the status value. with badge-style formatting based on the status value.
:type status: tables.Column :type status: tables.Column
""" """
stock_type = tables.Column(verbose_name=_("Stock Type")) stock_type = tables.Column(verbose_name=_("Stock Type"))
vin = tables.LinkColumn("car_detail", args=[tables.A("pk")], verbose_name=_("VIN"), attrs={"td": {"class": "fw-bold"}}) vin = tables.LinkColumn(
"car_detail",
args=[tables.A("pk")],
verbose_name=_("VIN"),
attrs={"td": {"class": "fw-bold"}},
)
id_car_make = tables.Column(verbose_name=_("Make")) id_car_make = tables.Column(verbose_name=_("Make"))
id_car_model = tables.Column(verbose_name=_("Model")) id_car_model = tables.Column(verbose_name=_("Model"))
year = tables.Column(verbose_name=_("Year")) year = tables.Column(verbose_name=_("Year"))
id_car_serie = tables.Column(verbose_name=_("Series")) id_car_serie = tables.Column(verbose_name=_("Series"))
id_car_trim = tables.Column(verbose_name=_("Trim")) id_car_trim = tables.Column(verbose_name=_("Trim"))
mileage = tables.Column(verbose_name=_("Mileage")) mileage = tables.Column(verbose_name=_("Mileage"))
selling_price = tables.Column(accessor="finances.selling_price", verbose_name=_("Price")) selling_price = tables.Column(
exterior_color = tables.Column(accessor="colors.exterior.name", verbose_name=_("Exterior Color")) accessor="finances.selling_price", verbose_name=_("Price")
interior_color = tables.Column(accessor="colors.interior.name", verbose_name=_("Interior Color")) )
exterior_color = tables.Column(
accessor="colors.exterior.name", verbose_name=_("Exterior Color")
)
interior_color = tables.Column(
accessor="colors.interior.name", verbose_name=_("Interior Color")
)
receiving_date = tables.Column(verbose_name=_("Age")) receiving_date = tables.Column(verbose_name=_("Age"))
status = tables.Column(verbose_name=_("Status")) status = tables.Column(verbose_name=_("Status"))
@ -111,7 +123,9 @@ class CarTable(tables.Table):
"transfer": "badge-phoenix-warning", "transfer": "badge-phoenix-warning",
} }
badge_class = status_badges.get(value.lower(), "badge-secondary") badge_class = status_badges.get(value.lower(), "badge-secondary")
return format_html('<span class="badge badge-phoenix fs-11 {}">{}</span>', badge_class, value) return format_html(
'<span class="badge badge-phoenix fs-11 {}">{}</span>', badge_class, value
)
def render_stock_type(self, value): def render_stock_type(self, value):
type_badges = { type_badges = {
@ -119,4 +133,6 @@ class CarTable(tables.Table):
"used": "badge-phoenix-warning", "used": "badge-phoenix-warning",
} }
badge_class = type_badges.get(value.lower(), "badge-secondary") badge_class = type_badges.get(value.lower(), "badge-secondary")
return format_html('<span class="badge badge-phoenix fs-11 {}">{}</span>', badge_class, value) return format_html(
'<span class="badge badge-phoenix fs-11 {}">{}</span>', badge_class, value
)

File diff suppressed because it is too large Load Diff

View File

@ -4,195 +4,210 @@ from django import template
from django.urls import reverse from django.urls import reverse
from calendar import month_abbr from calendar import month_abbr
from django.conf import settings from django.conf import settings
from django.db.models import Sum
from django.forms import ValidationError from django.forms import ValidationError
from django.utils.formats import number_format from django.utils.formats import number_format
from django_ledger.io.io_core import get_localdate,validate_activity from django_ledger.io.io_core import get_localdate, validate_activity
from django_ledger.models import InvoiceModel, JournalEntryModel, BillModel from django_ledger.models import InvoiceModel, JournalEntryModel, BillModel
from django.db.models import Case, Value, When, IntegerField from django.db.models import Case, Value, When, IntegerField
register = template.Library() register = template.Library()
@register.filter(name='percentage')
@register.filter(name="percentage")
def percentage(value): def percentage(value):
if value is not None: if value is not None:
return '{0:,.2f}%'.format(value * 100) return "{0:,.2f}%".format(value * 100)
return None return None
@register.filter @register.filter
def get_item(dictionary, key): def get_item(dictionary, key):
return dictionary.get(key) return dictionary.get(key)
@register.filter(name='add_class') @register.filter(name="add_class")
def add_class(field, css_class): def add_class(field, css_class):
return field.as_widget(attrs={"class": css_class}) return field.as_widget(attrs={"class": css_class})
@register.filter(name='attr') @register.filter(name="attr")
def attr(field, args): def attr(field, args):
attrs = {} attrs = {}
definitions = args.split(',') definitions = args.split(",")
for definition in definitions: for definition in definitions:
if ':' in definition: if ":" in definition:
key, val = definition.split(':') key, val = definition.split(":")
attrs[key.strip()] = val.strip() attrs[key.strip()] = val.strip()
else: else:
attrs[definition.strip()] = True attrs[definition.strip()] = True
return field.as_widget(attrs=attrs) return field.as_widget(attrs=attrs)
@register.inclusion_tag('ledger/reports/components/period_navigator.html', takes_context=True) @register.inclusion_tag(
"ledger/reports/components/period_navigator.html", takes_context=True
)
def period_navigation(context, base_url: str): def period_navigation(context, base_url: str):
print(context, base_url) print(context, base_url)
kwargs = dict() kwargs = dict()
entity_slug = context['view'].kwargs['entity_slug'] entity_slug = context["view"].kwargs["entity_slug"]
kwargs['entity_slug'] = entity_slug kwargs["entity_slug"] = entity_slug
if context['view'].kwargs.get('ledger_pk'): if context["view"].kwargs.get("ledger_pk"):
kwargs['ledger_pk'] = context['view'].kwargs.get('ledger_pk') kwargs["ledger_pk"] = context["view"].kwargs.get("ledger_pk")
if context['view'].kwargs.get('account_pk'): if context["view"].kwargs.get("account_pk"):
kwargs['account_pk'] = context['view'].kwargs.get('account_pk') kwargs["account_pk"] = context["view"].kwargs.get("account_pk")
if context['view'].kwargs.get('unit_slug'): if context["view"].kwargs.get("unit_slug"):
kwargs['unit_slug'] = context['view'].kwargs.get('unit_slug') kwargs["unit_slug"] = context["view"].kwargs.get("unit_slug")
if context['view'].kwargs.get('coa_slug'): if context["view"].kwargs.get("coa_slug"):
kwargs['coa_slug'] = context['view'].kwargs.get('coa_slug') kwargs["coa_slug"] = context["view"].kwargs.get("coa_slug")
ctx = dict() ctx = dict()
ctx['year'] = context['year'] ctx["year"] = context["year"]
ctx['has_year'] = context.get('has_year') ctx["has_year"] = context.get("has_year")
ctx['has_quarter'] = context.get('has_quarter') ctx["has_quarter"] = context.get("has_quarter")
ctx['has_month'] = context.get('has_month') ctx["has_month"] = context.get("has_month")
ctx['has_date'] = context.get('has_date') ctx["has_date"] = context.get("has_date")
ctx['previous_year'] = context['previous_year'] ctx["previous_year"] = context["previous_year"]
kwargs['year'] = context['previous_year'] kwargs["year"] = context["previous_year"]
ctx['previous_year_url'] = reverse(f'{base_url}-year', kwargs=kwargs) ctx["previous_year_url"] = reverse(f"{base_url}-year", kwargs=kwargs)
ctx['next_year'] = context['next_year'] ctx["next_year"] = context["next_year"]
kwargs['year'] = context['next_year'] kwargs["year"] = context["next_year"]
ctx['next_year_url'] = reverse(f'{base_url}-year', kwargs=kwargs) ctx["next_year_url"] = reverse(f"{base_url}-year", kwargs=kwargs)
kwargs['year'] = context['year'] kwargs["year"] = context["year"]
ctx['current_year_url'] = reverse(f'{base_url}-year', kwargs=kwargs) ctx["current_year_url"] = reverse(f"{base_url}-year", kwargs=kwargs)
dt = get_localdate() dt = get_localdate()
KWARGS_CURRENT_MONTH = { KWARGS_CURRENT_MONTH = {
'entity_slug': context['view'].kwargs['entity_slug'], "entity_slug": context["view"].kwargs["entity_slug"],
'year': dt.year, "year": dt.year,
'month': dt.month "month": dt.month,
} }
if 'unit_slug' in kwargs: if "unit_slug" in kwargs:
KWARGS_CURRENT_MONTH['unit_slug'] = kwargs['unit_slug'] KWARGS_CURRENT_MONTH["unit_slug"] = kwargs["unit_slug"]
if 'account_pk' in kwargs: if "account_pk" in kwargs:
KWARGS_CURRENT_MONTH['account_pk'] = kwargs['account_pk'] KWARGS_CURRENT_MONTH["account_pk"] = kwargs["account_pk"]
if 'ledger_pk' in kwargs: if "ledger_pk" in kwargs:
KWARGS_CURRENT_MONTH['ledger_pk'] = kwargs['ledger_pk'] KWARGS_CURRENT_MONTH["ledger_pk"] = kwargs["ledger_pk"]
if 'coa_slug' in kwargs: if "coa_slug" in kwargs:
KWARGS_CURRENT_MONTH['coa_slug'] = kwargs['coa_slug'] KWARGS_CURRENT_MONTH["coa_slug"] = kwargs["coa_slug"]
ctx['current_month_url'] = reverse(f'{base_url}-month', ctx["current_month_url"] = reverse(f"{base_url}-month", kwargs=KWARGS_CURRENT_MONTH)
kwargs=KWARGS_CURRENT_MONTH)
quarter_urls = list() quarter_urls = list()
ctx['quarter'] = context.get('quarter') ctx["quarter"] = context.get("quarter")
for Q in range(1, 5): for Q in range(1, 5):
kwargs['quarter'] = Q kwargs["quarter"] = Q
quarter_urls.append({ quarter_urls.append(
'url': reverse(f'{base_url}-quarter', kwargs=kwargs), {
'quarter': Q, "url": reverse(f"{base_url}-quarter", kwargs=kwargs),
'quarter_name': f'Q{Q}' "quarter": Q,
}) "quarter_name": f"Q{Q}",
del kwargs['quarter'] }
ctx['quarter_urls'] = quarter_urls )
del kwargs["quarter"]
ctx["quarter_urls"] = quarter_urls
month_urls = list() month_urls = list()
ctx['month'] = context.get('month') ctx["month"] = context.get("month")
for M in range(1, 13): for M in range(1, 13):
kwargs['month'] = M kwargs["month"] = M
month_urls.append({ month_urls.append(
'url': reverse(f'{base_url}-month', kwargs=kwargs), {
'month': M, "url": reverse(f"{base_url}-month", kwargs=kwargs),
'month_abbr': month_abbr[M] "month": M,
}) "month_abbr": month_abbr[M],
ctx['month_urls'] = month_urls }
ctx['from_date'] = context['from_date'] )
ctx['to_date'] = context['to_date'] ctx["month_urls"] = month_urls
ctx["from_date"] = context["from_date"]
ctx["to_date"] = context["to_date"]
ctx.update(kwargs) ctx.update(kwargs)
ctx['date_navigation_url'] = context.get('date_navigation_url') ctx["date_navigation_url"] = context.get("date_navigation_url")
return ctx return ctx
@register.inclusion_tag('ledger/reports/tags/balance_sheet_statement.html', takes_context=True) @register.inclusion_tag(
"ledger/reports/tags/balance_sheet_statement.html", takes_context=True
)
def balance_sheet_statement(context, io_model, to_date=None): def balance_sheet_statement(context, io_model, to_date=None):
user_model = context['user'] user_model = context["user"]
activity = context['request'].GET.get('activity') activity = context["request"].GET.get("activity")
entity_slug = context['view'].kwargs.get('entity_slug') entity_slug = context["view"].kwargs.get("entity_slug")
if not to_date: if not to_date:
to_date = context['to_date'] to_date = context["to_date"]
io_digest = io_model.digest( io_digest = io_model.digest(
activity=activity, activity=activity,
user_model=user_model, user_model=user_model,
equity_only=False, equity_only=False,
entity_slug=entity_slug, entity_slug=entity_slug,
unit_slug=context['unit_slug'], unit_slug=context["unit_slug"],
by_unit=context['by_unit'], by_unit=context["by_unit"],
to_date=to_date, to_date=to_date,
signs=True, signs=True,
process_groups=True, process_groups=True,
balance_sheet_statement=True) balance_sheet_statement=True,
)
return { return {
'entity_slug': entity_slug, "entity_slug": entity_slug,
'user_model': user_model, "user_model": user_model,
'tx_digest': io_digest.get_io_data(), "tx_digest": io_digest.get_io_data(),
} }
@register.inclusion_tag('ledger/reports/tags/income_statement.html', takes_context=True)
@register.inclusion_tag("ledger/reports/tags/income_statement.html", takes_context=True)
def income_statement_table(context, io_model, from_date=None, to_date=None): def income_statement_table(context, io_model, from_date=None, to_date=None):
user_model = context['user'] user_model = context["user"]
activity = context['request'].GET.get('activity') activity = context["request"].GET.get("activity")
activity = validate_activity(activity, raise_404=True) activity = validate_activity(activity, raise_404=True)
entity_slug = context['view'].kwargs.get('entity_slug') entity_slug = context["view"].kwargs.get("entity_slug")
if not from_date: if not from_date:
from_date = context['from_date'] from_date = context["from_date"]
if not to_date: if not to_date:
to_date = context['to_date'] to_date = context["to_date"]
io_digest = io_model.digest( io_digest = io_model.digest(
activity=activity, activity=activity,
user_model=user_model, user_model=user_model,
entity_slug=entity_slug, entity_slug=entity_slug,
unit_slug=context['unit_slug'], unit_slug=context["unit_slug"],
by_unit=context['by_unit'], by_unit=context["by_unit"],
from_date=from_date, from_date=from_date,
to_date=to_date, to_date=to_date,
equity_only=True, equity_only=True,
process_groups=True, process_groups=True,
income_statement=True, income_statement=True,
signs=True signs=True,
) )
return { return {
'entity_slug': entity_slug, "entity_slug": entity_slug,
'user_model': user_model, "user_model": user_model,
'tx_digest': io_digest.get_io_data() "tx_digest": io_digest.get_io_data(),
} }
@register.inclusion_tag('ledger/reports/tags/cash_flow_statement.html', takes_context=True)
@register.inclusion_tag(
"ledger/reports/tags/cash_flow_statement.html", takes_context=True
)
def cash_flow_statement(context, io_model): def cash_flow_statement(context, io_model):
user_model = context['user'] user_model = context["user"]
entity_slug = context['view'].kwargs.get('entity_slug') entity_slug = context["view"].kwargs.get("entity_slug")
from_date = context['from_date'] from_date = context["from_date"]
to_date = context['to_date'] to_date = context["to_date"]
io_digest = io_model.digest( io_digest = io_model.digest(
cash_flow_statement=True, cash_flow_statement=True,
@ -201,57 +216,75 @@ def cash_flow_statement(context, io_model):
equity_only=False, equity_only=False,
signs=True, signs=True,
entity_slug=entity_slug, entity_slug=entity_slug,
unit_slug=context['unit_slug'], unit_slug=context["unit_slug"],
by_unit=context['by_unit'], by_unit=context["by_unit"],
from_date=from_date, from_date=from_date,
to_date=to_date, to_date=to_date,
process_groups=True) process_groups=True,
)
return { return {
'entity_slug': entity_slug, "entity_slug": entity_slug,
'user_model': user_model, "user_model": user_model,
'tx_digest': io_digest.get_io_data() "tx_digest": io_digest.get_io_data(),
} }
@register.inclusion_tag("django_ledger/components/date_picker.html", takes_context=True)
@register.inclusion_tag('django_ledger/components/date_picker.html', takes_context=True)
def date_picker(context, nav_url=None, date_picker_id=None): def date_picker(context, nav_url=None, date_picker_id=None):
try: try:
entity_slug = context['view'].kwargs.get('entity_slug') entity_slug = context["view"].kwargs.get("entity_slug")
except KeyError: except KeyError:
entity_slug = context['entity_slug'] entity_slug = context["entity_slug"]
if not date_picker_id: if not date_picker_id:
date_picker_id = f'djl-datepicker-{randint(10000, 99999)}' date_picker_id = f"djl-datepicker-{randint(10000, 99999)}"
if 'date_picker_ids' not in context: if "date_picker_ids" not in context:
context['date_picker_ids'] = list() context["date_picker_ids"] = list()
context['date_picker_ids'].append(date_picker_id) context["date_picker_ids"].append(date_picker_id)
date_navigation_url = nav_url if nav_url else context.get('date_navigation_url') date_navigation_url = nav_url if nav_url else context.get("date_navigation_url")
return { return {
'entity_slug': entity_slug, "entity_slug": entity_slug,
'date_picker_id': date_picker_id, "date_picker_id": date_picker_id,
'date_navigation_url': date_navigation_url "date_navigation_url": date_navigation_url,
} }
@register.simple_tag(name='get_currency')
@register.simple_tag(name="get_currency")
def get_currency(): def get_currency():
return settings.CURRENCY return settings.CURRENCY
@register.simple_tag(name="num2words", takes_context=True)
@register.simple_tag(name='num2words', takes_context=True)
def number_to_words_english(number): def number_to_words_english(number):
"""Convert a number to words in English.""" """Convert a number to words in English."""
units = ["", "one", "two", "three", "four", "five", "six", "seven", "eight", "nine"] units = ["", "one", "two", "three", "four", "five", "six", "seven", "eight", "nine"]
teens = ["ten", "eleven", "twelve", "thirteen", "fourteen", "fifteen", "sixteen", teens = [
"seventeen", "eighteen", "nineteen"] "ten",
tens = ["", "ten", "twenty", "thirty", "forty", "fifty", "sixty", "seventy", "eleven",
"eighty", "ninety"] "twelve",
"thirteen",
"fourteen",
"fifteen",
"sixteen",
"seventeen",
"eighteen",
"nineteen",
]
tens = [
"",
"ten",
"twenty",
"thirty",
"forty",
"fifty",
"sixty",
"seventy",
"eighty",
"ninety",
]
scales = ["", "thousand", "million", "billion", "trillion"] scales = ["", "thousand", "million", "billion", "trillion"]
if number == 0: if number == 0:
@ -282,15 +315,47 @@ def number_to_words_english(number):
number = number // 1000 number = number // 1000
scale_index += 1 scale_index += 1
return ' '.join(words) return " ".join(words)
def number_to_words_arabic(number): def number_to_words_arabic(number):
"""Convert a number to words in Arabic.""" """Convert a number to words in Arabic."""
units = ["", "واحد", "اثنان", "ثلاثة", "أربعة", "خمسة", "ستة", "سبعة", "ثمانية", "تسعة"] units = [
teens = ["عشرة", "أحد عشر", "اثنا عشر", "ثلاثة عشر", "أربعة عشر", "خمسة عشر", "",
"ستة عشر", "سبعة عشر", "ثمانية عشر", "تسعة عشر"] "واحد",
tens = ["", "عشرة", "عشرون", "ثلاثون", "أربعون", "خمسون", "ستون", "سبعون", "اثنان",
"ثمانون", "تسعون"] "ثلاثة",
"أربعة",
"خمسة",
"ستة",
"سبعة",
"ثمانية",
"تسعة",
]
teens = [
"عشرة",
"أحد عشر",
"اثنا عشر",
"ثلاثة عشر",
"أربعة عشر",
"خمسة عشر",
"ستة عشر",
"سبعة عشر",
"ثمانية عشر",
"تسعة عشر",
]
tens = [
"",
"عشرة",
"عشرون",
"ثلاثون",
"أربعون",
"خمسون",
"ستون",
"سبعون",
"ثمانون",
"تسعون",
]
scales = ["", "ألف", "مليون", "مليار", "تريليون"] scales = ["", "ألف", "مليون", "مليار", "تريليون"]
if number == 0: if number == 0:
@ -321,7 +386,8 @@ def number_to_words_arabic(number):
number = number // 1000 number = number // 1000
scale_index += 1 scale_index += 1
return ' '.join(words) return " ".join(words)
# @register.filter(name='num2words') # @register.filter(name='num2words')
# def num2words(number, language='en'): # def num2words(number, language='en'):
@ -331,25 +397,26 @@ def number_to_words_arabic(number):
# else: # else:
# return number_to_words_english(number) # return number_to_words_english(number)
@register.inclusion_tag('components/date_picker.html', takes_context=True)
@register.inclusion_tag("components/date_picker.html", takes_context=True)
def date_picker(context, nav_url=None, date_picker_id=None): def date_picker(context, nav_url=None, date_picker_id=None):
try: try:
entity_slug = context['view'].kwargs.get('entity_slug') entity_slug = context["view"].kwargs.get("entity_slug")
except KeyError: except KeyError:
entity_slug = context['entity_slug'] entity_slug = context["entity_slug"]
if not date_picker_id: if not date_picker_id:
date_picker_id = f'djl-datepicker-{randint(10000, 99999)}' date_picker_id = f"djl-datepicker-{randint(10000, 99999)}"
if 'date_picker_ids' not in context: if "date_picker_ids" not in context:
context['date_picker_ids'] = list() context["date_picker_ids"] = list()
context['date_picker_ids'].append(date_picker_id) context["date_picker_ids"].append(date_picker_id)
date_navigation_url = nav_url if nav_url else context.get('date_navigation_url') date_navigation_url = nav_url if nav_url else context.get("date_navigation_url")
return { return {
'entity_slug': entity_slug, "entity_slug": entity_slug,
'date_picker_id': date_picker_id, "date_picker_id": date_picker_id,
'date_navigation_url': date_navigation_url "date_navigation_url": date_navigation_url,
} }
@ -358,7 +425,8 @@ def splitlines(value):
"""Splits text into lines""" """Splits text into lines"""
return value.splitlines() return value.splitlines()
@register.filter(name='currency_format')
@register.filter(name="currency_format")
def currency_format(value): def currency_format(value):
if not value: if not value:
value = 0.00 value = 0.00
@ -369,38 +437,47 @@ def currency_format(value):
def filter_by_role(accounts, role_prefix): def filter_by_role(accounts, role_prefix):
return [account for account in accounts if account.role.startswith(role_prefix)] return [account for account in accounts if account.role.startswith(role_prefix)]
@register.inclusion_tag('purchase_orders/tags/po_item_table.html', takes_context=True)
@register.inclusion_tag("purchase_orders/tags/po_item_table.html", takes_context=True)
def po_item_table1(context, queryset): def po_item_table1(context, queryset):
return { return {
'entity_slug': context['entity_slug'], "entity_slug": context["entity_slug"],
'po_model': context['po_model'], "po_model": context["po_model"],
'po_item_list': queryset "po_item_list": queryset,
} }
@register.inclusion_tag('purchase_orders/includes/po_item_formset.html', takes_context=True)
@register.inclusion_tag(
"purchase_orders/includes/po_item_formset.html", takes_context=True
)
def po_item_formset_table(context, po_model, itemtxs_formset): def po_item_formset_table(context, po_model, itemtxs_formset):
return { return {
'entity_slug': context['view'].kwargs['entity_slug'], "entity_slug": context["view"].kwargs["entity_slug"],
'po_model': po_model, "po_model": po_model,
'itemtxs_formset': itemtxs_formset, "itemtxs_formset": itemtxs_formset,
} }
@register.inclusion_tag('bill/tags/bill_item_formset.html', takes_context=True) @register.inclusion_tag("bill/tags/bill_item_formset.html", takes_context=True)
def bill_item_formset_table(context, item_formset): def bill_item_formset_table(context, item_formset):
return { return {
'entity_slug': context['view'].kwargs['entity_slug'], "entity_slug": context["view"].kwargs["entity_slug"],
'bill_pk': context['view'].kwargs['bill_pk'], "bill_pk": context["view"].kwargs["bill_pk"],
'total_amount__sum': context['total_amount__sum'], "total_amount__sum": context["total_amount__sum"],
'item_formset': item_formset, "item_formset": item_formset,
} }
@register.inclusion_tag('bill/transactions/tags/txs_table.html')
def transactions_table(object_type: Union[JournalEntryModel, BillModel, InvoiceModel], style='detail'): @register.inclusion_tag("bill/transactions/tags/txs_table.html")
def transactions_table(
object_type: Union[JournalEntryModel, BillModel, InvoiceModel], style="detail"
):
if isinstance(object_type, JournalEntryModel): if isinstance(object_type, JournalEntryModel):
transaction_model_qs = object_type.transactionmodel_set.all().with_annotated_details().order_by( transaction_model_qs = (
'-timestamp', object_type.transactionmodel_set.all()
) .with_annotated_details()
.order_by("-timestamp")
)
elif isinstance(object_type, BillModel): elif isinstance(object_type, BillModel):
# Specific ordering for BillModel (timestamp ascending, then debit before credit) # Specific ordering for BillModel (timestamp ascending, then debit before credit)
qs = object_type.get_transaction_queryset(annotated=True) qs = object_type.get_transaction_queryset(annotated=True)
@ -417,21 +494,25 @@ def transactions_table(object_type: Union[JournalEntryModel, BillModel, InvoiceM
'pk' # Optional: Tie-breaker for consistent order 'pk' # Optional: Tie-breaker for consistent order
) )
elif isinstance(object_type, InvoiceModel): elif isinstance(object_type, InvoiceModel):
transaction_model_qs = object_type.get_transaction_queryset(annotated=True).order_by('-timestamp') transaction_model_qs = object_type.get_transaction_queryset(
annotated=True
).order_by("-timestamp")
else: else:
raise ValidationError( raise ValidationError(
'Cannot handle object of type {} to get transaction model queryset'.format(type(object_type)) "Cannot handle object of type {} to get transaction model queryset".format(
type(object_type)
)
) )
total_credits = sum(tx.amount for tx in transaction_model_qs if tx.is_credit()) total_credits = sum(tx.amount for tx in transaction_model_qs if tx.is_credit())
total_debits = sum(tx.amount for tx in transaction_model_qs if tx.is_debit()) total_debits = sum(tx.amount for tx in transaction_model_qs if tx.is_debit())
return { return {
'style': style, "style": style,
'transaction_model_qs': transaction_model_qs, "transaction_model_qs": transaction_model_qs,
'total_debits': total_debits, "total_debits": total_debits,
'total_credits': total_credits, "total_credits": total_credits,
'object': object_type "object": object_type,
} }
@ -441,67 +522,67 @@ def get_vehicle_image(car_serie):
Returns the appropriate car image filename based on car series Returns the appropriate car image filename based on car series
""" """
if not car_serie: if not car_serie:
return 'sedan.png' return "sedan.png"
serie_lower = car_serie.name.lower() serie_lower = car_serie.name.lower()
# SUV mapping # SUV mapping
if 'suv' in serie_lower: if "suv" in serie_lower:
if 'sport' in serie_lower or '3 doors' in serie_lower: if "sport" in serie_lower or "3 doors" in serie_lower:
return 'crossover.png' return "crossover.png"
else: else:
return 'suv.png' return "suv.png"
# Pickup mapping # Pickup mapping
elif 'pickup' in serie_lower: elif "pickup" in serie_lower:
if 'cabriolet' in serie_lower: if "cabriolet" in serie_lower:
return 'pickup_cabriolet.png' return "pickup_cabriolet.png"
elif 'double' in serie_lower or 'crew' in serie_lower: elif "double" in serie_lower or "crew" in serie_lower:
return 'double_pickup.png' return "double_pickup.png"
else: else:
return 'single_pickup.png' return "single_pickup.png"
# Van/Minivan mapping # Van/Minivan mapping
elif 'minivan' in serie_lower: elif "minivan" in serie_lower:
return 'minivan.png' return "minivan.png"
elif 'van' in serie_lower: elif "van" in serie_lower:
if 'cargo' in serie_lower: if "cargo" in serie_lower:
return 'van_cargo.png' return "van_cargo.png"
else: else:
return 'van.png' return "van.png"
elif 'compactvan' in serie_lower: elif "compactvan" in serie_lower:
return 'van.png' return "van.png"
# Hatchback mapping # Hatchback mapping
elif 'hatchback' in serie_lower: elif "hatchback" in serie_lower:
return 'hatchback.png' return "hatchback.png"
# Wagon mapping # Wagon mapping
elif 'wagon' in serie_lower: elif "wagon" in serie_lower:
return 'van.png' # Closest match return "van.png" # Closest match
# Coupe/Sports mapping # Coupe/Sports mapping
elif 'cabriolet' in serie_lower: elif "cabriolet" in serie_lower:
return 'cabriolet.png' return "cabriolet.png"
elif 'coupe' in serie_lower: elif "coupe" in serie_lower:
return 'coupe.png' return "coupe.png"
elif 'speedster' in serie_lower: elif "speedster" in serie_lower:
return 'sport_car.png' return "sport_car.png"
# Liftback mapping # Liftback mapping
elif 'liftback' in serie_lower: elif "liftback" in serie_lower:
return 'hatchback.png' # Closest match return "hatchback.png" # Closest match
# Sedan mapping (including 2 doors) # Sedan mapping (including 2 doors)
elif 'sedan' in serie_lower: elif "sedan" in serie_lower:
if '2 doors' in serie_lower: if "2 doors" in serie_lower:
return 'coupe.png' return "coupe.png"
else: else:
return 'sedan.png' return "sedan.png"
# Default fallback # Default fallback
else: else:
return 'sedan.png' return "sedan.png"
@register.filter @register.filter
@ -510,24 +591,49 @@ def get_vehicle_type_name(car_serie):
Returns the vehicle type name for styling purposes Returns the vehicle type name for styling purposes
""" """
if not car_serie: if not car_serie:
return 'sedan' return "sedan"
serie_lower = car_serie.name.lower() serie_lower = car_serie.name.lower()
if 'suv' in serie_lower: if "suv" in serie_lower:
return 'suv' return "suv"
elif 'pickup' in serie_lower: elif "pickup" in serie_lower:
return 'pickup' return "pickup"
elif any(word in serie_lower for word in ['van', 'minivan']): elif any(word in serie_lower for word in ["van", "minivan"]):
return 'van' return "van"
elif 'hatchback' in serie_lower: elif "hatchback" in serie_lower:
return 'hatchback' return "hatchback"
elif 'wagon' in serie_lower: elif "wagon" in serie_lower:
return 'wagon' return "wagon"
elif any(word in serie_lower for word in ['coupe', 'cabriolet', 'speedster']): elif any(word in serie_lower for word in ["coupe", "cabriolet", "speedster"]):
return 'coupe' return "coupe"
elif 'liftback' in serie_lower: elif "liftback" in serie_lower:
return 'liftback' return "liftback"
else: else:
return 'sedan' return "sedan"
@register.filter
def status_badge_color(status):
color_map = {
'PENDING_APPROVAL': 'warning',
'APPROVED': 'info',
'IN_FINANCING': 'primary',
'PARTIALLY_PAID': 'success',
'FULLY_PAID': 'success',
'PENDING_DELIVERY': 'warning',
'DELIVERED': 'success',
'CANCELLED': 'danger',
}
return color_map.get(status, 'secondary')
@register.inclusion_tag('inventory/tags/inventory_table.html', takes_context=True)
def inventory_table(context, queryset):
ctx = {
'entity_slug': context['view'].kwargs['entity_slug'],
'inventory_list': queryset
}
ctx.update(queryset.aggregate(inventory_total_value=Sum('total_value')))
return ctx

View File

@ -3,9 +3,10 @@ from num2words import num2words
register = template.Library() register = template.Library()
@register.filter @register.filter
def num_to_words(value, lang='ar'): def num_to_words(value, lang="ar"):
try: try:
return num2words(value, lang=lang) return num2words(value, lang=lang)
except: except:
return value return value

View File

@ -25,9 +25,12 @@ from django_ledger.io.io_core import get_localdate
register = template.Library() register = template.Library()
@register.filter() @register.filter()
def to_int(value): def to_int(value):
return Decimal(value).quantize(Decimal("0.01")) return Decimal(value).quantize(Decimal("0.01"))
# @register.simple_tag(name='current_version') # @register.simple_tag(name='current_version')
# def current_version(): # def current_version():
# return __version__ # return __version__
@ -535,89 +538,96 @@ def to_int(value):
# } # }
@register.inclusion_tag('inventory/ledger/reports/components/period_navigator.html', takes_context=True) @register.inclusion_tag(
"inventory/ledger/reports/components/period_navigator.html", takes_context=True
)
def period_navigation(context, base_url: str): def period_navigation(context, base_url: str):
kwargs = dict() kwargs = dict()
entity_slug = context['view'].kwargs['entity_slug'] entity_slug = context["view"].kwargs["entity_slug"]
kwargs['entity_slug'] = entity_slug kwargs["entity_slug"] = entity_slug
if context['view'].kwargs.get('ledger_pk'): if context["view"].kwargs.get("ledger_pk"):
kwargs['ledger_pk'] = context['view'].kwargs.get('ledger_pk') kwargs["ledger_pk"] = context["view"].kwargs.get("ledger_pk")
if context['view'].kwargs.get('account_pk'): if context["view"].kwargs.get("account_pk"):
kwargs['account_pk'] = context['view'].kwargs.get('account_pk') kwargs["account_pk"] = context["view"].kwargs.get("account_pk")
if context['view'].kwargs.get('unit_slug'): if context["view"].kwargs.get("unit_slug"):
kwargs['unit_slug'] = context['view'].kwargs.get('unit_slug') kwargs["unit_slug"] = context["view"].kwargs.get("unit_slug")
if context['view'].kwargs.get('coa_slug'): if context["view"].kwargs.get("coa_slug"):
kwargs['coa_slug'] = context['view'].kwargs.get('coa_slug') kwargs["coa_slug"] = context["view"].kwargs.get("coa_slug")
ctx = dict() ctx = dict()
ctx['year'] = context['year'] ctx["year"] = context["year"]
ctx['has_year'] = context.get('has_year') ctx["has_year"] = context.get("has_year")
ctx['has_quarter'] = context.get('has_quarter') ctx["has_quarter"] = context.get("has_quarter")
ctx['has_month'] = context.get('has_month') ctx["has_month"] = context.get("has_month")
ctx['has_date'] = context.get('has_date') ctx["has_date"] = context.get("has_date")
ctx['previous_year'] = context['previous_year'] ctx["previous_year"] = context["previous_year"]
kwargs['year'] = context['previous_year'] kwargs["year"] = context["previous_year"]
ctx['previous_year_url'] = reverse(f'django_ledger:{base_url}-year', kwargs=kwargs) ctx["previous_year_url"] = reverse(f"django_ledger:{base_url}-year", kwargs=kwargs)
ctx['next_year'] = context['next_year'] ctx["next_year"] = context["next_year"]
kwargs['year'] = context['next_year'] kwargs["year"] = context["next_year"]
ctx['next_year_url'] = reverse(f'django_ledger:{base_url}-year', kwargs=kwargs) ctx["next_year_url"] = reverse(f"django_ledger:{base_url}-year", kwargs=kwargs)
kwargs['year'] = context['year'] kwargs["year"] = context["year"]
ctx['current_year_url'] = reverse(f'django_ledger:{base_url}-year', kwargs=kwargs) ctx["current_year_url"] = reverse(f"django_ledger:{base_url}-year", kwargs=kwargs)
dt = get_localdate() dt = get_localdate()
KWARGS_CURRENT_MONTH = { KWARGS_CURRENT_MONTH = {
'entity_slug': context['view'].kwargs['entity_slug'], "entity_slug": context["view"].kwargs["entity_slug"],
'year': dt.year, "year": dt.year,
'month': dt.month "month": dt.month,
} }
if 'unit_slug' in kwargs: if "unit_slug" in kwargs:
KWARGS_CURRENT_MONTH['unit_slug'] = kwargs['unit_slug'] KWARGS_CURRENT_MONTH["unit_slug"] = kwargs["unit_slug"]
if 'account_pk' in kwargs: if "account_pk" in kwargs:
KWARGS_CURRENT_MONTH['account_pk'] = kwargs['account_pk'] KWARGS_CURRENT_MONTH["account_pk"] = kwargs["account_pk"]
if 'ledger_pk' in kwargs: if "ledger_pk" in kwargs:
KWARGS_CURRENT_MONTH['ledger_pk'] = kwargs['ledger_pk'] KWARGS_CURRENT_MONTH["ledger_pk"] = kwargs["ledger_pk"]
if 'coa_slug' in kwargs: if "coa_slug" in kwargs:
KWARGS_CURRENT_MONTH['coa_slug'] = kwargs['coa_slug'] KWARGS_CURRENT_MONTH["coa_slug"] = kwargs["coa_slug"]
ctx['current_month_url'] = reverse(f'django_ledger:{base_url}-month', ctx["current_month_url"] = reverse(
kwargs=KWARGS_CURRENT_MONTH) f"django_ledger:{base_url}-month", kwargs=KWARGS_CURRENT_MONTH
)
quarter_urls = list() quarter_urls = list()
ctx['quarter'] = context.get('quarter') ctx["quarter"] = context.get("quarter")
for Q in range(1, 5): for Q in range(1, 5):
kwargs['quarter'] = Q kwargs["quarter"] = Q
quarter_urls.append({ quarter_urls.append(
'url': reverse(f'django_ledger:{base_url}-quarter', kwargs=kwargs), {
'quarter': Q, "url": reverse(f"django_ledger:{base_url}-quarter", kwargs=kwargs),
'quarter_name': f'Q{Q}' "quarter": Q,
}) "quarter_name": f"Q{Q}",
del kwargs['quarter'] }
ctx['quarter_urls'] = quarter_urls )
del kwargs["quarter"]
ctx["quarter_urls"] = quarter_urls
month_urls = list() month_urls = list()
ctx['month'] = context.get('month') ctx["month"] = context.get("month")
for M in range(1, 13): for M in range(1, 13):
kwargs['month'] = M kwargs["month"] = M
month_urls.append({ month_urls.append(
'url': reverse(f'django_ledger:{base_url}-month', kwargs=kwargs), {
'month': M, "url": reverse(f"django_ledger:{base_url}-month", kwargs=kwargs),
'month_abbr': month_abbr[M] "month": M,
}) "month_abbr": month_abbr[M],
ctx['month_urls'] = month_urls }
ctx['from_date'] = context['from_date'] )
ctx['to_date'] = context['to_date'] ctx["month_urls"] = month_urls
ctx["from_date"] = context["from_date"]
ctx["to_date"] = context["to_date"]
ctx.update(kwargs) ctx.update(kwargs)
ctx['date_navigation_url'] = context.get('date_navigation_url') ctx["date_navigation_url"] = context.get("date_navigation_url")
return ctx return ctx

View File

@ -8,7 +8,7 @@ from django_ledger.io.io_core import get_localdate
from django.core.exceptions import ObjectDoesNotExist from django.core.exceptions import ObjectDoesNotExist
from decimal import Decimal from decimal import Decimal
from unittest.mock import MagicMock from unittest.mock import MagicMock
from inventory.models import VatRate from inventory.models import VatRate
from inventory.utils import CarFinanceCalculator from inventory.utils import CarFinanceCalculator
@ -47,6 +47,7 @@ class ModelTest(TestCase):
:ivar car_finances: Car finance object for the car under test. :ivar car_finances: Car finance object for the car under test.
:type car_finances: CarFinance instance :type car_finances: CarFinance instance
""" """
def setUp(self): def setUp(self):
email = "RkzgO@example.com" email = "RkzgO@example.com"
name = "John Doe" name = "John Doe"
@ -178,46 +179,46 @@ class AuthenticationTest(TestCase):
:ivar url: URL for account signup endpoint used in the test cases. :ivar url: URL for account signup endpoint used in the test cases.
:type url: str :type url: str
""" """
def setUp(self): def setUp(self):
self.client = Client() self.client = Client()
self.url = reverse("account_signup") self.url = reverse("account_signup")
def test_login(self): def test_login(self):
url = reverse("account_login") url = reverse("account_login")
response = self.client.post(url, {"email": "RkzgO@example.com", "password": "password"}) response = self.client.post(
url, {"email": "RkzgO@example.com", "password": "password"}
)
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
def test_valid_data(self): def test_valid_data(self):
# Create valid JSON data # Create valid JSON data
data = { data = {
"wizardValidationForm1": { "wizardValidationForm1": {
"email": "test@example.com", "email": "test@example.com",
"password": "password123", "password": "password123",
"confirm_password": "password123" "confirm_password": "password123",
}, },
"wizardValidationForm2": { "wizardValidationForm2": {
"name": "John Doe", "name": "John Doe",
"arabic_name": "جون دو", "arabic_name": "جون دو",
"phone_number": "1234567890" "phone_number": "1234567890",
}, },
"wizardValidationForm3": { "wizardValidationForm3": {
"crn": "123456", "crn": "123456",
"vrn": "789012", "vrn": "789012",
"address": "123 Main St" "address": "123 Main St",
} },
} }
# Send a POST request with the JSON data # Send a POST request with the JSON data
response = self.client.post( response = self.client.post(
self.url, self.url, data=json.dumps(data), content_type="application/json"
data=json.dumps(data),
content_type='application/json'
) )
# Check the response # Check the response
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertEqual(response.json(), {'message': 'User created successfully.'}) self.assertEqual(response.json(), {"message": "User created successfully."})
def test_passwords_do_not_match(self): def test_passwords_do_not_match(self):
# Create JSON data with mismatched passwords # Create JSON data with mismatched passwords
@ -225,25 +226,23 @@ class AuthenticationTest(TestCase):
"wizardValidationForm1": { "wizardValidationForm1": {
"email": "test@example.com", "email": "test@example.com",
"password": "password123", "password": "password123",
"confirm_password": "differentpassword" "confirm_password": "differentpassword",
}, },
"wizardValidationForm2": { "wizardValidationForm2": {
"name": "John Doe", "name": "John Doe",
"arabic_name": "جون دو", "arabic_name": "جون دو",
"phone_number": "1234567890" "phone_number": "1234567890",
}, },
"wizardValidationForm3": { "wizardValidationForm3": {
"crn": "123456", "crn": "123456",
"vrn": "789012", "vrn": "789012",
"address": "123 Main St" "address": "123 Main St",
} },
} }
# Send a POST request with the JSON data # Send a POST request with the JSON data
response = self.client.post( response = self.client.post(
self.url, self.url, data=json.dumps(data), content_type="application/json"
data=json.dumps(data),
content_type='application/json'
) )
# Check the response # Check the response
@ -261,26 +260,26 @@ class AuthenticationTest(TestCase):
"wizardValidationForm2": { "wizardValidationForm2": {
"name": "John Doe", "name": "John Doe",
"arabic_name": "جون دو", "arabic_name": "جون دو",
"phone_number": "1234567890" "phone_number": "1234567890",
}, },
"wizardValidationForm3": { "wizardValidationForm3": {
"crn": "123456", "crn": "123456",
"vrn": "789012", "vrn": "789012",
"address": "123 Main St" "address": "123 Main St",
} },
} }
# Send a POST request with the JSON data # Send a POST request with the JSON data
response = self.client.post( response = self.client.post(
self.url, self.url, data=json.dumps(data), content_type="application/json"
data=json.dumps(data),
content_type='application/json'
) )
# Check the response # Check the response
self.assertEqual(response.status_code, 400) self.assertEqual(response.status_code, 400)
self.assertIn("error", response.json()) # Assuming the view returns an error for missing fields self.assertIn(
"error", response.json()
) # Assuming the view returns an error for missing fields
class CarFinanceCalculatorTests(TestCase): class CarFinanceCalculatorTests(TestCase):
""" """
@ -297,10 +296,11 @@ class CarFinanceCalculatorTests(TestCase):
:ivar vat_rate: Active VAT rate used for testing VAT rate retrieval. :ivar vat_rate: Active VAT rate used for testing VAT rate retrieval.
:type vat_rate: VatRate :type vat_rate: VatRate
""" """
def setUp(self): def setUp(self):
# Common setup for all tests # Common setup for all tests
self.mock_model = MagicMock() self.mock_model = MagicMock()
self.vat_rate = VatRate.objects.create(rate=Decimal('0.20'), is_active=True) self.vat_rate = VatRate.objects.create(rate=Decimal("0.20"), is_active=True)
def test_no_active_vat_rate_raises_error(self): def test_no_active_vat_rate_raises_error(self):
VatRate.objects.all().delete() # Ensure no active VAT VatRate.objects.all().delete() # Ensure no active VAT
@ -309,11 +309,13 @@ class CarFinanceCalculatorTests(TestCase):
def test_vat_rate_retrieval(self): def test_vat_rate_retrieval(self):
calculator = CarFinanceCalculator(self.mock_model) calculator = CarFinanceCalculator(self.mock_model)
self.assertEqual(calculator.vat_rate, Decimal('0.20')) self.assertEqual(calculator.vat_rate, Decimal("0.20"))
def test_item_transactions_retrieval(self): def test_item_transactions_retrieval(self):
mock_item = MagicMock() mock_item = MagicMock()
self.mock_model.get_itemtxs_data.return_value = [MagicMock(all=lambda: [mock_item])] self.mock_model.get_itemtxs_data.return_value = [
MagicMock(all=lambda: [mock_item])
]
calculator = CarFinanceCalculator(self.mock_model) calculator = CarFinanceCalculator(self.mock_model)
self.assertEqual(calculator.item_transactions, [mock_item]) self.assertEqual(calculator.item_transactions, [mock_item])
@ -321,136 +323,152 @@ class CarFinanceCalculatorTests(TestCase):
mock_item = MagicMock() mock_item = MagicMock()
mock_item.ce_quantity = 2 mock_item.ce_quantity = 2
mock_item.item_model = MagicMock() mock_item.item_model = MagicMock()
mock_item.item_model.item_number = '123' mock_item.item_model.item_number = "123"
mock_item.item_model.additional_info = { mock_item.item_model.additional_info = {
CarFinanceCalculator.CAR_FINANCE_KEY: { CarFinanceCalculator.CAR_FINANCE_KEY: {
'selling_price': '10000', "selling_price": "10000",
'cost_price': '8000', "cost_price": "8000",
'discount_amount': '500', "discount_amount": "500",
'total_vat': '2000' "total_vat": "2000",
}, },
CarFinanceCalculator.CAR_INFO_KEY: { CarFinanceCalculator.CAR_INFO_KEY: {
'vin': 'VIN123', "vin": "VIN123",
'make': 'Toyota', "make": "Toyota",
'model': 'Camry', "model": "Camry",
'year': 2020, "year": 2020,
'trim': 'LE', "trim": "LE",
'mileage': 15000 "mileage": 15000,
}, },
CarFinanceCalculator.ADDITIONAL_SERVICES_KEY: [ CarFinanceCalculator.ADDITIONAL_SERVICES_KEY: [
{'name': 'Service 1', 'price': '200', 'taxable': True, 'price_': '240'} {"name": "Service 1", "price": "200", "taxable": True, "price_": "240"}
] ],
} }
calculator = CarFinanceCalculator(self.mock_model) calculator = CarFinanceCalculator(self.mock_model)
car_data = calculator._get_car_data(mock_item) car_data = calculator._get_car_data(mock_item)
self.assertEqual(car_data['item_number'], '123') self.assertEqual(car_data["item_number"], "123")
self.assertEqual(car_data['vin'], 'VIN123') self.assertEqual(car_data["vin"], "VIN123")
self.assertEqual(car_data['make'], 'Toyota') self.assertEqual(car_data["make"], "Toyota")
self.assertEqual(car_data['selling_price'], '10000') self.assertEqual(car_data["selling_price"], "10000")
self.assertEqual(car_data['unit_price'], Decimal('10000')) self.assertEqual(car_data["unit_price"], Decimal("10000"))
self.assertEqual(car_data['quantity'], 2) self.assertEqual(car_data["quantity"], 2)
self.assertEqual(car_data['total'], Decimal('20000')) self.assertEqual(car_data["total"], Decimal("20000"))
self.assertEqual(car_data['total_vat'], '2000') self.assertEqual(car_data["total_vat"], "2000")
self.assertEqual(car_data['additional_services'], [{'name': 'Service 1', 'price': '200', 'taxable': True, 'price_': '240'}]) self.assertEqual(
car_data["additional_services"],
[{"name": "Service 1", "price": "200", "taxable": True, "price_": "240"}],
)
def test_get_additional_services(self): def test_get_additional_services(self):
mock_item1 = MagicMock() mock_item1 = MagicMock()
mock_item1.item_model.additional_info = { mock_item1.item_model.additional_info = {
CarFinanceCalculator.ADDITIONAL_SERVICES_KEY: [ CarFinanceCalculator.ADDITIONAL_SERVICES_KEY: [
{'name': 'Service 1', 'price': '100', 'taxable': True, 'price_': '120'} {"name": "Service 1", "price": "100", "taxable": True, "price_": "120"}
] ]
} }
mock_item2 = MagicMock() mock_item2 = MagicMock()
mock_item2.item_model.additional_info = { mock_item2.item_model.additional_info = {
CarFinanceCalculator.ADDITIONAL_SERVICES_KEY: [ CarFinanceCalculator.ADDITIONAL_SERVICES_KEY: [
{'name': 'Service 2', 'price': '200', 'taxable': False, 'price_': '200'} {"name": "Service 2", "price": "200", "taxable": False, "price_": "200"}
] ]
} }
self.mock_model.get_itemtxs_data.return_value = [MagicMock(all=lambda: [mock_item1, mock_item2])] self.mock_model.get_itemtxs_data.return_value = [
MagicMock(all=lambda: [mock_item1, mock_item2])
]
calculator = CarFinanceCalculator(self.mock_model) calculator = CarFinanceCalculator(self.mock_model)
services = calculator._get_additional_services() services = calculator._get_additional_services()
self.assertEqual(len(services), 2) self.assertEqual(len(services), 2)
self.assertEqual(services[0]['name'], 'Service 1') self.assertEqual(services[0]["name"], "Service 1")
self.assertEqual(services[1]['name'], 'Service 2') self.assertEqual(services[1]["name"], "Service 2")
self.assertEqual(services[0]['price_'], '120') self.assertEqual(services[0]["price_"], "120")
self.assertEqual(services[1]['price_'], '200') self.assertEqual(services[1]["price_"], "200")
def test_calculate_totals(self): def test_calculate_totals(self):
mock_item1 = MagicMock() mock_item1 = MagicMock()
mock_item1.ce_quantity = 2 mock_item1.ce_quantity = 2
mock_item1.item_model.additional_info = { mock_item1.item_model.additional_info = {
CarFinanceCalculator.CAR_FINANCE_KEY: { CarFinanceCalculator.CAR_FINANCE_KEY: {
'selling_price': '10000', "selling_price": "10000",
'discount_amount': '500' "discount_amount": "500",
}, },
CarFinanceCalculator.ADDITIONAL_SERVICES_KEY: [ CarFinanceCalculator.ADDITIONAL_SERVICES_KEY: [
{'price_': '100'}, {"price_": "100"},
{'price_': '200'} {"price_": "200"},
] ],
} }
mock_item2 = MagicMock() mock_item2 = MagicMock()
mock_item2.quantity = 3 mock_item2.quantity = 3
mock_item2.item_model.additional_info = { mock_item2.item_model.additional_info = {
CarFinanceCalculator.CAR_FINANCE_KEY: { CarFinanceCalculator.CAR_FINANCE_KEY: {
'selling_price': '20000', "selling_price": "20000",
'discount_amount': '1000' "discount_amount": "1000",
}, },
CarFinanceCalculator.ADDITIONAL_SERVICES_KEY: [ CarFinanceCalculator.ADDITIONAL_SERVICES_KEY: [{"price_": "300"}],
{'price_': '300'}
]
} }
self.mock_model.get_itemtxs_data.return_value = [MagicMock(all=lambda: [mock_item1, mock_item2])] self.mock_model.get_itemtxs_data.return_value = [
MagicMock(all=lambda: [mock_item1, mock_item2])
]
calculator = CarFinanceCalculator(self.mock_model) calculator = CarFinanceCalculator(self.mock_model)
totals = calculator.calculate_totals() totals = calculator.calculate_totals()
expected_total_price = (Decimal('10000') * 2 + Decimal('20000') * 3) - (Decimal('500') + Decimal('1000')) expected_total_price = (Decimal("10000") * 2 + Decimal("20000") * 3) - (
expected_vat = expected_total_price * Decimal('0.15') Decimal("500") + Decimal("1000")
expected_additionals = Decimal('100') + Decimal('200') + Decimal('300') )
expected_grand_total = (expected_total_price + expected_vat + expected_additionals).quantize(Decimal('0.00')) expected_vat = expected_total_price * Decimal("0.15")
expected_additionals = Decimal("100") + Decimal("200") + Decimal("300")
expected_grand_total = (
expected_total_price + expected_vat + expected_additionals
).quantize(Decimal("0.00"))
self.assertEqual(totals['total_price'], expected_total_price) self.assertEqual(totals["total_price"], expected_total_price)
self.assertEqual(totals['total_discount'], Decimal('1500')) self.assertEqual(totals["total_discount"], Decimal("1500"))
self.assertEqual(totals['total_vat_amount'], expected_vat) self.assertEqual(totals["total_vat_amount"], expected_vat)
self.assertEqual(totals['total_additionals'], expected_additionals) self.assertEqual(totals["total_additionals"], expected_additionals)
self.assertEqual(totals['grand_total'], expected_grand_total) self.assertEqual(totals["grand_total"], expected_grand_total)
def test_get_finance_data(self): def test_get_finance_data(self):
mock_item = MagicMock() mock_item = MagicMock()
mock_item.ce_quantity = 1 mock_item.ce_quantity = 1
mock_item.item_model = MagicMock() mock_item.item_model = MagicMock()
mock_item.item_model.item_number = '456' mock_item.item_model.item_number = "456"
mock_item.item_model.additional_info = { mock_item.item_model.additional_info = {
CarFinanceCalculator.CAR_FINANCE_KEY: { CarFinanceCalculator.CAR_FINANCE_KEY: {
'selling_price': '15000', "selling_price": "15000",
'discount_amount': '1000', "discount_amount": "1000",
'total_vat': '2800' "total_vat": "2800",
}, },
CarFinanceCalculator.CAR_INFO_KEY: { CarFinanceCalculator.CAR_INFO_KEY: {
'vin': 'VIN456', "vin": "VIN456",
'make': 'Honda', "make": "Honda",
'model': 'Civic', "model": "Civic",
'year': 2021 "year": 2021,
}, },
CarFinanceCalculator.ADDITIONAL_SERVICES_KEY: [ CarFinanceCalculator.ADDITIONAL_SERVICES_KEY: [
{'name': 'Service', 'price': '150', 'taxable': True, 'price_': '180'} {"name": "Service", "price": "150", "taxable": True, "price_": "180"}
] ],
} }
self.mock_model.get_itemtxs_data.return_value = [MagicMock(all=lambda: [mock_item])] self.mock_model.get_itemtxs_data.return_value = [
MagicMock(all=lambda: [mock_item])
]
calculator = CarFinanceCalculator(self.mock_model) calculator = CarFinanceCalculator(self.mock_model)
finance_data = calculator.get_finance_data() finance_data = calculator.get_finance_data()
self.assertEqual(len(finance_data['cars']), 1) self.assertEqual(len(finance_data["cars"]), 1)
self.assertEqual(finance_data['quantity'], 1) self.assertEqual(finance_data["quantity"], 1)
self.assertEqual(finance_data['total_price'], Decimal('14000')) # 15000 - 1000 self.assertEqual(finance_data["total_price"], Decimal("14000")) # 15000 - 1000
self.assertEqual(finance_data['total_vat'], Decimal('14000') + (Decimal('14000') * Decimal('0.20'))) self.assertEqual(
self.assertEqual(finance_data['total_vat_amount'], Decimal('14000') * Decimal('0.20')) finance_data["total_vat"],
self.assertEqual(finance_data['total_additionals'], Decimal('180')) Decimal("14000") + (Decimal("14000") * Decimal("0.20")),
self.assertEqual(finance_data['additionals'][0]['name'], 'Service') )
self.assertEqual(finance_data['vat'], Decimal('0.20')) self.assertEqual(
finance_data["total_vat_amount"], Decimal("14000") * Decimal("0.20")
)
self.assertEqual(finance_data["total_additionals"], Decimal("180"))
self.assertEqual(finance_data["additionals"][0]["name"], "Service")
self.assertEqual(finance_data["vat"], Decimal("0.20"))

View File

@ -1,4 +1,4 @@
from django.conf.urls import handler403,handler400,handler404,handler500 from django.conf.urls import handler403, handler400, handler404, handler500
from django.urls import path from django.urls import path
from django_tables2.export.export import TableExport from django_tables2.export.export import TableExport
@ -43,30 +43,39 @@ urlpatterns = [
# ), # ),
# ), # ),
# Tasks # Tasks
path("tasks/", views.task_list, name="task_list"),
path('tasks/', views.task_list, name='task_list'), path("legal/", views.terms_and_privacy, name="terms_and_privacy"),
path('legal/', views.terms_and_privacy, name='terms_and_privacy'),
# path('tasks/<int:task_id>/detail/', views.task_detail, name='task_detail'), # path('tasks/<int:task_id>/detail/', views.task_detail, name='task_detail'),
# Dashboards # Dashboards
# path("user/<int:pk>/settings/", views.UserSettingsView.as_view(), name="user_settings"), # path("user/<int:pk>/settings/", views.UserSettingsView.as_view(), name="user_settings"),
path("pricing/", views.pricing_page, name="pricing_page"), path("pricing/", views.pricing_page, name="pricing_page"),
path("submit_plan/", views.submit_plan, name="submit_plan"), path("submit_plan/", views.submit_plan, name="submit_plan"),
path('payment-callback/', views.payment_callback, name='payment_callback'), path("payment-callback/", views.payment_callback, name="payment_callback"),
# #
path( path(
"dealers/activity/", "dealers/activity/",
views.UserActivityLogListView.as_view(), views.UserActivityLogListView.as_view(),
name="dealer_activity", name="dealer_activity",
), ),
path("dealers/<slug:slug>/settings/", views.DealerSettingsView, name="dealer_settings"), path(
"dealers/<slug:slug>/settings/",
views.DealerSettingsView,
name="dealer_settings",
),
path("dealers/assign-car-makes/", views.assign_car_makes, name="assign_car_makes"), path("dealers/assign-car-makes/", views.assign_car_makes, name="assign_car_makes"),
path("dashboards/manager/", views.ManagerDashboard.as_view(), name="manager_dashboard"), path(
"dashboards/manager/",
views.ManagerDashboard.as_view(),
name="manager_dashboard",
),
path("dashboards/sales/", views.SalesDashboard.as_view(), name="sales_dashboard"), path("dashboards/sales/", views.SalesDashboard.as_view(), name="sales_dashboard"),
path("test/", views.TestView.as_view(), name="test"), path("test/", views.TestView.as_view(), name="test"),
path('cars/inventory/table/', views.CarListViewTable.as_view(), name="car_table"), path("cars/inventory/table/", views.CarListViewTable.as_view(), name="car_table"),
path("export/format/", TableExport, name="export"), path("export/format/", TableExport, name="export"),
# Dealer URLs # Dealer URLs
path("dealers/<slug:slug>/", views.DealerDetailView.as_view(), name="dealer_detail"), path(
"dealers/<slug:slug>/", views.DealerDetailView.as_view(), name="dealer_detail"
),
path( path(
"dealers/<slug:slug>/update/", "dealers/<slug:slug>/update/",
views.DealerUpdateView.as_view(), views.DealerUpdateView.as_view(),
@ -93,7 +102,9 @@ urlpatterns = [
views.CustomerUpdateView.as_view(), views.CustomerUpdateView.as_view(),
name="customer_update", name="customer_update",
), ),
path("customers/<slug:slug>/delete/", views.delete_customer, name="customer_delete"), path(
"customers/<slug:slug>/delete/", views.delete_customer, name="customer_delete"
),
path( path(
"customers/<slug:slug>/opportunities/create/", "customers/<slug:slug>/opportunities/create/",
views.OpportunityCreateView.as_view(), views.OpportunityCreateView.as_view(),
@ -101,19 +112,26 @@ urlpatterns = [
), ),
path("crm/leads/create/", views.lead_create, name="lead_create"), path("crm/leads/create/", views.lead_create, name="lead_create"),
path( path(
"crm/leads/<slug:slug>/view/", views.LeadDetailView.as_view(), name="lead_detail" "crm/leads/<slug:slug>/view/",
views.LeadDetailView.as_view(),
name="lead_detail",
), ),
path('update-lead-actions/', views.update_lead_actions, name='update_lead_actions'), path("update-lead-actions/", views.update_lead_actions, name="update_lead_actions"),
path('crm/leads/lead_tracking/', views.lead_tracking, name='lead_tracking'), path("crm/leads/lead_tracking/", views.lead_tracking, name="lead_tracking"),
path('crm/leads/lead_view/', views.lead_view, name='lead_view'), path("crm/leads/lead_view/", views.lead_view, name="lead_view"),
path("crm/leads/", views.LeadListView.as_view(), name="lead_list"), path("crm/leads/", views.LeadListView.as_view(), name="lead_list"),
path( path(
"crm/leads/<slug:slug>/update/", views.LeadUpdateView.as_view(), name="lead_update" "crm/leads/<slug:slug>/update/",
views.LeadUpdateView.as_view(),
name="lead_update",
), ),
path("crm/leads/<slug:slug>/delete/", views.LeadDeleteView, name="lead_delete"), path("crm/leads/<slug:slug>/delete/", views.LeadDeleteView, name="lead_delete"),
path("crm/leads/<slug:slug>/lead-convert/", views.lead_convert, name="lead_convert"), path(
path("crm/leads/<int:pk>/delete-note/", views.delete_note, name="delete_note_to_lead"), "crm/leads/<slug:slug>/lead-convert/", views.lead_convert, name="lead_convert"
),
path(
"crm/leads/<int:pk>/delete-note/", views.delete_note, name="delete_note_to_lead"
),
path( path(
"crm/<int:pk>/update-note/", "crm/<int:pk>/update-note/",
views.update_note, views.update_note,
@ -211,17 +229,22 @@ urlpatterns = [
), ),
# path('crm/opportunities/<int:pk>/logs/', views.OpportunityLogsView.as_view(), name='opportunity_logs'), # path('crm/opportunities/<int:pk>/logs/', views.OpportunityLogsView.as_view(), name='opportunity_logs'),
# ####################### # #######################
path('stream/', views.sse_stream, name='sse_stream'), path("stream/", views.sse_stream, name="sse_stream"),
path('fetch/', views.fetch_notifications, name='fetch_notifications'), path("fetch/", views.fetch_notifications, name="fetch_notifications"),
# Mark single notification as read # Mark single notification as read
path('<int:notification_id>/mark-read/', views.mark_notification_as_read, name='mark_notification_as_read'), path(
"<int:notification_id>/mark-read/",
views.mark_notification_as_read,
name="mark_notification_as_read",
),
# Mark all notifications as read # Mark all notifications as read
path('mark-all-read/', views.mark_all_notifications_as_read, name='mark_all_notifications_as_read'), path(
"mark-all-read/",
views.mark_all_notifications_as_read,
name="mark_all_notifications_as_read",
),
# Notification history # Notification history
path('history/', views.notifications_history, name='notifications_history'), path("history/", views.notifications_history, name="notifications_history"),
# ####################### # #######################
path( path(
"crm/notifications/", "crm/notifications/",
@ -238,7 +261,7 @@ urlpatterns = [
views.mark_notification_as_read, views.mark_notification_as_read,
name="mark_notification_as_read", name="mark_notification_as_read",
), ),
path('crm/calender/', views.EmployeeCalendarView.as_view(), name='calendar_list'), path("crm/calender/", views.EmployeeCalendarView.as_view(), name="calendar_list"),
# Vendor URLs # Vendor URLs
path("vendors/create/", views.VendorCreateView.as_view(), name="vendor_create"), path("vendors/create/", views.VendorCreateView.as_view(), name="vendor_create"),
path("vendors", views.VendorListView.as_view(), name="vendor_list"), path("vendors", views.VendorListView.as_view(), name="vendor_list"),
@ -254,8 +277,8 @@ urlpatterns = [
name="vendor_delete", name="vendor_delete",
), ),
# Car URLs # Car URLs
path('cars/upload_cars/', views.upload_cars, name='upload_cars'), path("cars/upload_cars/", views.upload_cars, name="upload_cars"),
path('cars/<uuid:pk>/upload_cars/', views.upload_cars, name='upload_cars'), path("cars/<uuid:pk>/upload_cars/", views.upload_cars, name="upload_cars"),
path("cars/add/", views.CarCreateView.as_view(), name="car_add"), path("cars/add/", views.CarCreateView.as_view(), name="car_add"),
path("cars/inventory/", views.CarInventory.as_view(), name="car_inventory_all"), path("cars/inventory/", views.CarInventory.as_view(), name="car_inventory_all"),
path( path(
@ -288,13 +311,17 @@ urlpatterns = [
path( path(
"cars/<slug:slug>/add-color/", views.CarColorCreate.as_view(), name="add_color" "cars/<slug:slug>/add-color/", views.CarColorCreate.as_view(), name="add_color"
), ),
path('car/colors/<slug:slug>/update/', views.CarColorsUpdateView.as_view(), name='car_colors_update'), path(
"car/colors/<slug:slug>/update/",
views.CarColorsUpdateView.as_view(),
name="car_colors_update",
),
path( path(
"cars/<slug:slug>/location/add/", "cars/<slug:slug>/location/add/",
views.CarLocationCreateView.as_view(), views.CarLocationCreateView.as_view(),
name="add_car_location", name="add_car_location",
), ),
path( path(
"cars/<slug:car_pk>/location/<int:pk>/update", "cars/<slug:car_pk>/location/<int:pk>/update",
views.CarLocationUpdateView.as_view(), views.CarLocationUpdateView.as_view(),
name="update_car_location", name="update_car_location",
@ -324,13 +351,9 @@ path(
views.CarTransferPreviewView, views.CarTransferPreviewView,
name="transfer_preview", name="transfer_preview",
), ),
path("cars/inventory/search/", path("cars/inventory/search/", views.SearchCodeView.as_view(), name="car_search"),
views.SearchCodeView.as_view(),
name="car_search"),
# path('cars/<int:car_pk>/colors/<int:pk>/update/',views.CarColorUpdateView.as_view(),name='color_update'), # path('cars/<int:car_pk>/colors/<int:pk>/update/',views.CarColorUpdateView.as_view(),name='color_update'),
path("cars/reserve/<slug:slug>/", path("cars/reserve/<slug:slug>/", views.reserve_car_view, name="reserve_car"),
views.reserve_car_view,
name="reserve_car"),
path( path(
"reservations/<int:reservation_id>/", "reservations/<int:reservation_id>/",
views.manage_reservation, views.manage_reservation,
@ -341,16 +364,20 @@ path(
views.CustomCardCreateView.as_view(), views.CustomCardCreateView.as_view(),
name="add_custom_card", name="add_custom_card",
), ),
path('cars/<slug:slug>/add-registration/',
views.CarRegistrationCreateView.as_view(),
name='add_registration'),
#sales list
path( path(
'sales/list/', "cars/<slug:slug>/add-registration/",
views.sales_list_view, views.CarRegistrationCreateView.as_view(),
name='sales_list', name="add_registration",
), ),
# sales list
path(
"sales/list/",
views.sales_list_view,
name="sales_list",
),
path('sale_orders/<int:pk>/', views.SaleOrderDetailView.as_view(), name='order_detail'),
path('inventory/<slug:entity_slug>/list/', views.InventoryListView.as_view(), name='inventort_list'),
# Sales URLs quotation_create # Sales URLs quotation_create
# path( # path(
# "sales/quotations/create/", # "sales/quotations/create/",
@ -401,21 +428,26 @@ path(
# views.payment_create, # views.payment_create,
# name="payment_create", # name="payment_create",
# ), # ),
# Users URLs # Users URLs
path("user/", views.UserListView.as_view(), name="user_list"), path("user/", views.UserListView.as_view(), name="user_list"),
path("user/create/", views.UserCreateView.as_view(), name="user_create"), path("user/create/", views.UserCreateView.as_view(), name="user_create"),
path("user/<slug:slug>/", views.UserDetailView.as_view(), name="user_detail"), path("user/<slug:slug>/", views.UserDetailView.as_view(), name="user_detail"),
path("user/<slug:slug>/groups/", views.UserGroupView, name="user_groups"), path("user/<slug:slug>/groups/", views.UserGroupView, name="user_groups"),
path("user/<slug:slug>/update/", views.UserUpdateView.as_view(), name="user_update"), path(
"user/<slug:slug>/update/", views.UserUpdateView.as_view(), name="user_update"
),
path("user/<slug:slug>/confirm/", views.UserDeleteview, name="user_delete"), path("user/<slug:slug>/confirm/", views.UserDeleteview, name="user_delete"),
# Group URLs # Group URLs
path("group/create/", views.GroupCreateView.as_view(), name="group_create"), path("group/create/", views.GroupCreateView.as_view(), name="group_create"),
path("group/<int:pk>/update/", views.GroupUpdateView.as_view(), name="group_update"), path(
"group/<int:pk>/update/", views.GroupUpdateView.as_view(), name="group_update"
),
path("group/<int:pk>/", views.GroupDetailView.as_view(), name="group_detail"), path("group/<int:pk>/", views.GroupDetailView.as_view(), name="group_detail"),
path("group/", views.GroupListView.as_view(), name="group_list"), path("group/", views.GroupListView.as_view(), name="group_list"),
path("group/<int:pk>/confirm/", views.GroupDeleteview, name="group_delete"), path("group/<int:pk>/confirm/", views.GroupDeleteview, name="group_delete"),
path("group/<int:pk>/permission/", views.GroupPermissionView, name="group_permission"), path(
"group/<int:pk>/permission/", views.GroupPermissionView, name="group_permission"
),
# Organization URLs # Organization URLs
path( path(
"organizations/create/", "organizations/create/",
@ -468,76 +500,109 @@ path(
), ),
# Ledger URLS # Ledger URLS
# Ledger # Ledger
path( path("ledgers/", views.LedgerModelListView.as_view(), name="ledger_list"),
"ledgers/", views.LedgerModelListView.as_view(), name="ledger_list"
),
path( path(
"ledgers/create/", views.LedgerModelCreateView.as_view(), name="ledger_create" "ledgers/create/", views.LedgerModelCreateView.as_view(), name="ledger_create"
), ),
path( path(
"ledgers/<slug:entity_slug>/detail/<uuid:pk>/", views.LedgerModelDetailView.as_view(), name="ledger_detail" "ledgers/<slug:entity_slug>/detail/<uuid:pk>/",
views.LedgerModelDetailView.as_view(),
name="ledger_detail",
), ),
path( path(
"ledgers/<slug:entity_slug>/lock_all_journals/<uuid:pk>/", views.ledger_lock_all_journals, name="lock_all_journals" "ledgers/<slug:entity_slug>/lock_all_journals/<uuid:pk>/",
views.ledger_lock_all_journals,
name="lock_all_journals",
), ),
path( path(
"ledgers/<slug:entity_slug>/unlock_all_journals/<uuid:pk>/", views.ledger_unlock_all_journals, name="unlock_all_journals" "ledgers/<slug:entity_slug>/unlock_all_journals/<uuid:pk>/",
views.ledger_unlock_all_journals,
name="unlock_all_journals",
), ),
path( path(
"ledgers/<slug:entity_slug>/post_all_journals/<uuid:pk>/", views.ledger_post_all_journals, name="post_all_journals" "ledgers/<slug:entity_slug>/post_all_journals/<uuid:pk>/",
views.ledger_post_all_journals,
name="post_all_journals",
), ),
path( path(
"ledgers/<slug:entity_slug>/unpost_all_journals/<uuid:pk>/", views.ledger_unpost_all_journals, name="unpost_all_journals" "ledgers/<slug:entity_slug>/unpost_all_journals/<uuid:pk>/",
views.ledger_unpost_all_journals,
name="unpost_all_journals",
), ),
# path( # path(
# "ledgers/create/", views.LedgerModelCreateView.as_view(), name="ledger_create" # "ledgers/create/", views.LedgerModelCreateView.as_view(), name="ledger_create"
# ), # ),
path( path(
"journalentries/<uuid:pk>/list/", views.JournalEntryListView.as_view(), name="journalentry_list" "journalentries/<uuid:pk>/list/",
views.JournalEntryListView.as_view(),
name="journalentry_list",
), ),
path( path(
"journalentries/<uuid:pk>/create/", views.JournalEntryCreateView.as_view(), name="journalentry_create" "journalentries/<uuid:pk>/create/",
views.JournalEntryCreateView.as_view(),
name="journalentry_create",
), ),
path( path(
"journalentries/<uuid:pk>/delete/", views.JournalEntryDeleteView, name="journalentry_delete" "journalentries/<uuid:pk>/delete/",
views.JournalEntryDeleteView,
name="journalentry_delete",
), ),
path( path(
"journalentries/<uuid:pk>/transactions/", "journalentries/<uuid:pk>/transactions/",
views.JournalEntryTransactionsView, views.JournalEntryTransactionsView,
name="journalentry_transactions", name="journalentry_transactions",
), ),
path('journalentries/<slug:entity_slug>/<uuid:ledger_pk>/detail/<uuid:je_pk>/txs/', path(
views.JournalEntryModelTXSDetailView.as_view(), "journalentries/<slug:entity_slug>/<uuid:ledger_pk>/detail/<uuid:je_pk>/txs/",
name='journalentry_txs'), views.JournalEntryModelTXSDetailView.as_view(),
name="journalentry_txs",
),
# ledger actions # ledger actions
path(
path('ledgers/<slug:entity_slug>/action/<uuid:ledger_pk>/post/', "ledgers/<slug:entity_slug>/action/<uuid:ledger_pk>/post/",
views.LedgerModelModelActionView.as_view(action_name='post'), views.LedgerModelModelActionView.as_view(action_name="post"),
name='ledger-action-post'), name="ledger-action-post",
path('ledgers/<slug:entity_slug>/action/<uuid:ledger_pk>/post-journal-entries/', ),
views.LedgerModelModelActionView.as_view(action_name='post_journal_entries'), path(
name='ledger-action-post-journal-entries'), "ledgers/<slug:entity_slug>/action/<uuid:ledger_pk>/post-journal-entries/",
path('ledgers/<slug:entity_slug>/action/<uuid:ledger_pk>/unpost/', views.LedgerModelModelActionView.as_view(action_name="post_journal_entries"),
views.LedgerModelModelActionView.as_view(action_name='unpost'), name="ledger-action-post-journal-entries",
name='ledger-action-unpost'), ),
path('ledgers/<slug:entity_slug>/action/<uuid:ledger_pk>/lock/', path(
views.LedgerModelModelActionView.as_view(action_name='lock'), "ledgers/<slug:entity_slug>/action/<uuid:ledger_pk>/unpost/",
name='ledger-action-lock'), views.LedgerModelModelActionView.as_view(action_name="unpost"),
path('ledgers/<slug:entity_slug>/action/<uuid:ledger_pk>/lock-journal-entries/', name="ledger-action-unpost",
views.LedgerModelModelActionView.as_view(action_name='lock_journal_entries'), ),
name='ledger-action-lock-journal-entries'), path(
path('ledgers/<slug:entity_slug>/action/<uuid:ledger_pk>/unlock/', "ledgers/<slug:entity_slug>/action/<uuid:ledger_pk>/lock/",
views.LedgerModelModelActionView.as_view(action_name='unlock'), views.LedgerModelModelActionView.as_view(action_name="lock"),
name='ledger-action-unlock'), name="ledger-action-lock",
path('ledgers/<slug:entity_slug>/action/<uuid:ledger_pk>/hide/', ),
views.LedgerModelModelActionView.as_view(action_name='hide'), path(
name='ledger-action-hide'), "ledgers/<slug:entity_slug>/action/<uuid:ledger_pk>/lock-journal-entries/",
path('ledgers/<slug:entity_slug>/action/<uuid:ledger_pk>/unhide/', views.LedgerModelModelActionView.as_view(action_name="lock_journal_entries"),
views.LedgerModelModelActionView.as_view(action_name='unhide'), name="ledger-action-lock-journal-entries",
name='ledger-action-unhide'), ),
path('ledgers/<slug:entity_slug>/delete/<uuid:ledger_pk>/', path(
views.LedgerModelDeleteView.as_view(), "ledgers/<slug:entity_slug>/action/<uuid:ledger_pk>/unlock/",
name='ledger-delete'), views.LedgerModelModelActionView.as_view(action_name="unlock"),
name="ledger-action-unlock",
),
path(
"ledgers/<slug:entity_slug>/action/<uuid:ledger_pk>/hide/",
views.LedgerModelModelActionView.as_view(action_name="hide"),
name="ledger-action-hide",
),
path(
"ledgers/<slug:entity_slug>/action/<uuid:ledger_pk>/unhide/",
views.LedgerModelModelActionView.as_view(action_name="unhide"),
name="ledger-action-unhide",
),
path(
"ledgers/<slug:entity_slug>/delete/<uuid:ledger_pk>/",
views.LedgerModelDeleteView.as_view(),
name="ledger-delete",
),
# Bank Account # Bank Account
path( path(
"bank_accounts/", views.BankAccountListView.as_view(), name="bank_account_list" "bank_accounts/", views.BankAccountListView.as_view(), name="bank_account_list"
@ -586,7 +651,11 @@ path(
name="estimate_detail", name="estimate_detail",
), ),
path("sales/estimates/create/", views.create_estimate, name="estimate_create"), path("sales/estimates/create/", views.create_estimate, name="estimate_create"),
path("sales/estimates/create/<slug:slug>/", views.create_estimate, name="estimate_create_from_opportunity"), path(
"sales/estimates/create/<slug:slug>/",
views.create_estimate,
name="estimate_create_from_opportunity",
),
path( path(
"sales/estimates/<uuid:pk>/estimate_mark_as/", "sales/estimates/<uuid:pk>/estimate_mark_as/",
views.estimate_mark_as, views.estimate_mark_as,
@ -605,10 +674,21 @@ path(
path( path(
"sales/estimates/<uuid:pk>/send_email", views.send_email_view, name="send_email" "sales/estimates/<uuid:pk>/send_email", views.send_email_view, name="send_email"
), ),
path('sales/estimates/<uuid:pk>/sale_order/', views.create_sale_order, name='create_sale_order'), path(
path('sales/estimates/<uuid:pk>/sale_order/<int:order_pk>/details/', views.SaleOrderDetail.as_view(), name='sale_order_details'), "sales/estimates/<uuid:pk>/sale_order/",
path('sales/estimates/<uuid:pk>/sale_order/preview/', views.preview_sale_order, name='preview_sale_order'), views.create_sale_order,
name="create_sale_order",
),
path(
"sales/estimates/<uuid:pk>/sale_order/<int:order_pk>/details/",
views.SaleOrderDetail.as_view(),
name="sale_order_details",
),
path(
"sales/estimates/<uuid:pk>/sale_order/preview/",
views.preview_sale_order,
name="preview_sale_order",
),
# Invoice # Invoice
path("sales/invoices/", views.InvoiceListView.as_view(), name="invoice_list"), path("sales/invoices/", views.InvoiceListView.as_view(), name="invoice_list"),
path( path(
@ -702,52 +782,82 @@ path(
# Bills # Bills
path("items/bills/", views.BillListView.as_view(), name="bill_list"), path("items/bills/", views.BillListView.as_view(), name="bill_list"),
# path("items/bills/create/", views.BillModelCreateViewView.as_view(), name="bill_create"), # path("items/bills/create/", views.BillModelCreateViewView.as_view(), name="bill_create"),
path('items/bills/<slug:entity_slug>/create/', path(
views.BillModelCreateView.as_view(), "items/bills/<slug:entity_slug>/create/",
name='bill-create'), views.BillModelCreateView.as_view(),
path('items/bills/<slug:entity_slug>/create/purchase-order/<uuid:po_pk>/', name="bill-create",
views.BillModelCreateView.as_view(for_purchase_order=True), ),
name='bill-create-po'), path(
path('items/bills/<slug:entity_slug>/create/estimate/<uuid:ce_pk>/', "items/bills/<slug:entity_slug>/create/purchase-order/<uuid:po_pk>/",
views.BillModelCreateView.as_view(for_estimate=True), views.BillModelCreateView.as_view(for_purchase_order=True),
name='bill-create-estimate'), name="bill-create-po",
path('items/bills/<slug:entity_slug>/detail/<uuid:bill_pk>/', ),
views.BillModelDetailViewView.as_view(), path(
name='bill-detail'), "items/bills/<slug:entity_slug>/create/estimate/<uuid:ce_pk>/",
path('items/bills/<slug:entity_slug>/update/<uuid:bill_pk>/', views.BillModelCreateView.as_view(for_estimate=True),
views.BillModelUpdateViewView.as_view(), name="bill-create-estimate",
name='bill-update'), ),
path('items/bills/<slug:entity_slug>/update/<uuid:bill_pk>/items/', path(
views.BillModelUpdateViewView.as_view(action_update_items=True), "items/bills/<slug:entity_slug>/detail/<uuid:bill_pk>/",
name='bill-update-items'), views.BillModelDetailViewView.as_view(),
############################################################ name="bill-detail",
path('items/bills/<slug:entity_slug>/actions/<uuid:bill_pk>/mark-as-draft/', ),
views.BillModelActionMarkAsDraftView.as_view(), path(
name='bill-action-mark-as-draft'), "items/bills/<slug:entity_slug>/update/<uuid:bill_pk>/",
path('items/bills/<slug:entity_slug>/actions/<uuid:bill_pk>/mark-as-review/', views.BillModelUpdateViewView.as_view(),
views.BillModelActionMarkAsInReviewView.as_view(), name="bill-update",
name='bill-action-mark-as-review'), ),
path('items/bills/<slug:entity_slug>/actions/<uuid:bill_pk>/mark-as-approved/', path(
views.BillModelActionMarkAsApprovedView.as_view(), "items/bills/<slug:entity_slug>/update/<uuid:bill_pk>/items/",
name='bill-action-mark-as-approved'), views.BillModelUpdateViewView.as_view(action_update_items=True),
path('items/bills/<slug:entity_slug>/actions/<uuid:bill_pk>/mark-as-paid/', name="bill-update-items",
views.BillModelActionMarkAsPaidView.as_view(), ),
name='bill-action-mark-as-paid'), ############################################################
path('items/bills/<slug:entity_slug>/actions/<uuid:bill_pk>/mark-as-void/', path(
views.BillModelActionVoidView.as_view(), "items/bills/<slug:entity_slug>/actions/<uuid:bill_pk>/mark-as-draft/",
name='bill-action-mark-as-void'), views.BillModelActionMarkAsDraftView.as_view(),
path('items/bills/<slug:entity_slug>/actions/<uuid:bill_pk>/mark-as-canceled/', name="bill-action-mark-as-draft",
views.BillModelActionCanceledView.as_view(), ),
name='bill-action-mark-as-canceled'), path(
path('items/bills/<slug:entity_slug>/actions/<uuid:bill_pk>/lock-ledger/', "items/bills/<slug:entity_slug>/actions/<uuid:bill_pk>/mark-as-review/",
views.BillModelActionLockLedgerView.as_view(), views.BillModelActionMarkAsInReviewView.as_view(),
name='bill-action-lock-ledger'), name="bill-action-mark-as-review",
path('items/bills/<slug:entity_slug>/actions/<uuid:bill_pk>/unlock-ledger/', ),
views.BillModelActionUnlockLedgerView.as_view(), path(
name='bill-action-unlock-ledger'), "items/bills/<slug:entity_slug>/actions/<uuid:bill_pk>/mark-as-approved/",
path('items/bills/<slug:entity_slug>/actions/<uuid:bill_pk>/force-migration/', views.BillModelActionMarkAsApprovedView.as_view(),
views.BillModelActionForceMigrateView.as_view(), name="bill-action-mark-as-approved",
name='bill-action-force-migrate'), ),
path(
"items/bills/<slug:entity_slug>/actions/<uuid:bill_pk>/mark-as-paid/",
views.BillModelActionMarkAsPaidView.as_view(),
name="bill-action-mark-as-paid",
),
path(
"items/bills/<slug:entity_slug>/actions/<uuid:bill_pk>/mark-as-void/",
views.BillModelActionVoidView.as_view(),
name="bill-action-mark-as-void",
),
path(
"items/bills/<slug:entity_slug>/actions/<uuid:bill_pk>/mark-as-canceled/",
views.BillModelActionCanceledView.as_view(),
name="bill-action-mark-as-canceled",
),
path(
"items/bills/<slug:entity_slug>/actions/<uuid:bill_pk>/lock-ledger/",
views.BillModelActionLockLedgerView.as_view(),
name="bill-action-lock-ledger",
),
path(
"items/bills/<slug:entity_slug>/actions/<uuid:bill_pk>/unlock-ledger/",
views.BillModelActionUnlockLedgerView.as_view(),
name="bill-action-unlock-ledger",
),
path(
"items/bills/<slug:entity_slug>/actions/<uuid:bill_pk>/force-migration/",
views.BillModelActionForceMigrateView.as_view(),
name="bill-action-force-migrate",
),
# path("items/bills/create/", views.bill_create, name="bill_create"), # path("items/bills/create/", views.bill_create, name="bill_create"),
path( path(
"items/bills/<uuid:pk>/bill_detail/", "items/bills/<uuid:pk>/bill_detail/",
@ -775,130 +885,228 @@ path(
views.bill_mark_as_paid, views.bill_mark_as_paid,
name="bill_mark_as_paid", name="bill_mark_as_paid",
), ),
# orders # orders
path("orders/", views.OrderListView.as_view(), name="order_list_view"), path("orders/", views.OrderListView.as_view(), name="order_list_view"),
# BALANCE SHEET Reports... # BALANCE SHEET Reports...
# Entities... # Entities...
path('entity/<slug:entity_slug>/balance-sheet/', path(
views.BaseBalanceSheetRedirectView.as_view(), "entity/<slug:entity_slug>/balance-sheet/",
name='entity-bs'), views.BaseBalanceSheetRedirectView.as_view(),
path('entity/<slug:entity_slug>/balance-sheet/year/<int:year>/', name="entity-bs",
views.FiscalYearBalanceSheetViewBase.as_view(), ),
name='entity-bs-year'), path(
path('entity/<slug:entity_slug>/balance-sheet/quarter/<int:year>/<int:quarter>/', "entity/<slug:entity_slug>/balance-sheet/year/<int:year>/",
views.QuarterlyBalanceSheetView.as_view(), views.FiscalYearBalanceSheetViewBase.as_view(),
name='entity-bs-quarter'), name="entity-bs-year",
path('entity/<slug:entity_slug>/balance-sheet/month/<int:year>/<int:month>/', ),
views.MonthlyBalanceSheetView.as_view(), path(
name='entity-bs-month'), "entity/<slug:entity_slug>/balance-sheet/quarter/<int:year>/<int:quarter>/",
path('entity/<slug:entity_slug>/balance-sheet/date/<int:year>/<int:month>/<int:day>/', views.QuarterlyBalanceSheetView.as_view(),
views.DateBalanceSheetView.as_view(), name="entity-bs-quarter",
name='entity-bs-date'), ),
path(
"entity/<slug:entity_slug>/balance-sheet/month/<int:year>/<int:month>/",
views.MonthlyBalanceSheetView.as_view(),
name="entity-bs-month",
),
path(
"entity/<slug:entity_slug>/balance-sheet/date/<int:year>/<int:month>/<int:day>/",
views.DateBalanceSheetView.as_view(),
name="entity-bs-date",
),
# INCOME STATEMENT Reports ---- # INCOME STATEMENT Reports ----
# Entity ..... # Entity .....
path('entity/<slug:entity_slug>/income-statement/', path(
views.BaseIncomeStatementRedirectViewBase.as_view(), "entity/<slug:entity_slug>/income-statement/",
name='entity-ic'), views.BaseIncomeStatementRedirectViewBase.as_view(),
path('entity/<slug:entity_slug>/income-statement/year/<int:year>/', name="entity-ic",
views.FiscalYearIncomeStatementViewBase.as_view(), ),
name='entity-ic-year'), path(
path('entity/<slug:entity_slug>/income-statement/quarter/<int:year>/<int:quarter>/', "entity/<slug:entity_slug>/income-statement/year/<int:year>/",
views.QuarterlyIncomeStatementView.as_view(), views.FiscalYearIncomeStatementViewBase.as_view(),
name='entity-ic-quarter'), name="entity-ic-year",
path('entity/<slug:entity_slug>/income-statement/month/<int:year>/<int:month>/', ),
views.MonthlyIncomeStatementView.as_view(), path(
name='entity-ic-month'), "entity/<slug:entity_slug>/income-statement/quarter/<int:year>/<int:quarter>/",
path('entity/<slug:entity_slug>/income-statement/date/<int:year>/<int:month>/<int:day>/', views.QuarterlyIncomeStatementView.as_view(),
views.MonthlyIncomeStatementView.as_view(), name="entity-ic-quarter",
name='entity-ic-date'), ),
# CASH FLOW STATEMENTS... path(
"entity/<slug:entity_slug>/income-statement/month/<int:year>/<int:month>/",
views.MonthlyIncomeStatementView.as_view(),
name="entity-ic-month",
),
path(
"entity/<slug:entity_slug>/income-statement/date/<int:year>/<int:month>/<int:day>/",
views.MonthlyIncomeStatementView.as_view(),
name="entity-ic-date",
),
# CASH FLOW STATEMENTS...
# Entities... # Entities...
path('entity/<slug:entity_slug>/cash-flow-statement/', path(
views.BaseCashFlowStatementRedirectViewBase.as_view(), "entity/<slug:entity_slug>/cash-flow-statement/",
name='entity-cf'), views.BaseCashFlowStatementRedirectViewBase.as_view(),
path('entity/<slug:entity_slug>/cash-flow-statement/year/<int:year>/', name="entity-cf",
views.FiscalYearCashFlowStatementViewBase.as_view(), ),
name='entity-cf-year'), path(
path('entity/<slug:entity_slug>/cash-flow-statement/quarter/<int:year>/<int:quarter>/', "entity/<slug:entity_slug>/cash-flow-statement/year/<int:year>/",
views.QuarterlyCashFlowStatementView.as_view(), views.FiscalYearCashFlowStatementViewBase.as_view(),
name='entity-cf-quarter'), name="entity-cf-year",
path('entity/<slug:entity_slug>/cash-flow-statement/month/<int:year>/<int:month>/', ),
views.MonthlyCashFlowStatementView.as_view(), path(
name='entity-cf-month'), "entity/<slug:entity_slug>/cash-flow-statement/quarter/<int:year>/<int:quarter>/",
path('entity/<slug:entity_slug>/cash-flow-statement/date/<int:year>/<int:month>/<int:day>/', views.QuarterlyCashFlowStatementView.as_view(),
views.DateCashFlowStatementView.as_view(), name="entity-cf-quarter",
name='entity-cf-date'), ),
#Dashboard path(
"entity/<slug:entity_slug>/cash-flow-statement/month/<int:year>/<int:month>/",
views.MonthlyCashFlowStatementView.as_view(),
name="entity-cf-month",
),
path(
"entity/<slug:entity_slug>/cash-flow-statement/date/<int:year>/<int:month>/<int:day>/",
views.DateCashFlowStatementView.as_view(),
name="entity-cf-date",
),
# Dashboard
# DASHBOARD Views... # DASHBOARD Views...
path('<slug:entity_slug>/dashboard/', path(
views.EntityModelDetailHandlerViewBase.as_view(), "<slug:entity_slug>/dashboard/",
name='entity-dashboard'), views.EntityModelDetailHandlerViewBase.as_view(),
path('<slug:entity_slug>/dashboard/year/<int:year>/', name="entity-dashboard",
views.FiscalYearEntityModelDashboardView.as_view(), ),
name='entity-dashboard-year'), path(
path('<slug:entity_slug>/dashboard/quarter/<int:year>/<int:quarter>/', "<slug:entity_slug>/dashboard/year/<int:year>/",
views.QuarterlyEntityDashboardView.as_view(), views.FiscalYearEntityModelDashboardView.as_view(),
name='entity-dashboard-quarter'), name="entity-dashboard-year",
path('<slug:entity_slug>/dashboard/month/<int:year>/<int:month>/', ),
views.MonthlyEntityDashboardView.as_view(), path(
name='entity-dashboard-month'), "<slug:entity_slug>/dashboard/quarter/<int:year>/<int:quarter>/",
path('<slug:entity_slug>/dashboard/date/<int:year>/<int:month>/<int:day>/', views.QuarterlyEntityDashboardView.as_view(),
views.DateEntityDashboardView.as_view(), name="entity-dashboard-quarter",
name='entity-dashboard-date'), ),
#dashboard api path(
path('entity/<slug:entity_slug>/data/net-payables/', "<slug:entity_slug>/dashboard/month/<int:year>/<int:month>/",
views.PayableNetAPIView.as_view(), views.MonthlyEntityDashboardView.as_view(),
name='entity-json-net-payables'), name="entity-dashboard-month",
path('entity/<slug:entity_slug>/data/net-receivables/', ),
views.ReceivableNetAPIView.as_view(), path(
name='entity-json-net-receivables'), "<slug:entity_slug>/dashboard/date/<int:year>/<int:month>/<int:day>/",
path('entity/<slug:entity_slug>/data/pnl/', views.DateEntityDashboardView.as_view(),
views.PnLAPIView.as_view(), name="entity-dashboard-date",
name='entity-json-pnl'), ),
# Admin Management... # dashboard api
path('management/', views.management_view, name='management'), path(
path('management/user_management/', views.user_management, name='user_management'), "entity/<slug:entity_slug>/data/net-payables/",
path('management/<str:content_type>/<slug:slug>/activate_account/', views.activate_account, name='activate_account'), views.PayableNetAPIView.as_view(),
path('management/<str:content_type>/<slug:slug>/permenant_delete_account/', views.permenant_delete_account, name='permenant_delete_account'), name="entity-json-net-payables",
path('management/audit_log_dashboard/', views.AuditLogDashboardView, name='audit_log_dashboard'), ),
path(
"entity/<slug:entity_slug>/data/net-receivables/",
views.ReceivableNetAPIView.as_view(),
name="entity-json-net-receivables",
),
path(
"entity/<slug:entity_slug>/data/pnl/",
views.PnLAPIView.as_view(),
name="entity-json-pnl",
),
# Admin Management...
path("management/", views.management_view, name="management"),
path("management/user_management/", views.user_management, name="user_management"),
path(
"management/<str:content_type>/<slug:slug>/activate_account/",
views.activate_account,
name="activate_account",
),
path(
"management/<str:content_type>/<slug:slug>/permenant_delete_account/",
views.permenant_delete_account,
name="permenant_delete_account",
),
path(
"management/audit_log_dashboard/",
views.AuditLogDashboardView,
name="audit_log_dashboard",
),
######### #########
# Purchase Order # Purchase Order
path('purchase_orders/', views.PurchaseOrderListView.as_view(), name='purchase_order_list'), path(
path('purchase_orders/new/', views.PurchaseOrderCreateView, name='purchase_order_create'), "purchase_orders/",
path('purchase_orders/<uuid:pk>/detail/', views.PurchaseOrderDetailView.as_view(), name='purchase_order_detail'), views.PurchaseOrderListView.as_view(),
path('purchase_orders/<slug:entity_slug>/<uuid:po_pk>/update/', views.PurchaseOrderUpdateView.as_view(), name='purchase_order_update'), name="purchase_order_list",
path('purchase_orders/<slug:entity_slug>/update/<uuid:po_pk>/update-items/', ),
views.PurchaseOrderUpdateView.as_view(action_update_items=True), path(
name='purchase_order_update_items'), "purchase_orders/new/",
path('purchase_orders/inventory_item/create/', views.InventoryItemCreateView, name='inventory_item_create'), views.PurchaseOrderCreateView,
path('purchase_orders/inventory_items_filter/', views.inventory_items_filter, name='inventory_items_filter'), name="purchase_order_create",
path('purchase_orders/<slug:entity_slug>/delete/<uuid:po_pk>/', ),
views.PurchaseOrderModelDeleteView.as_view(), path(
name='po-delete'), "purchase_orders/<uuid:pk>/detail/",
path('purchase_orders/<slug:entity_slug>/<uuid:po_pk>/upload/',view=views.view_items_inventory,name='view_items_inventory'), views.PurchaseOrderDetailView.as_view(),
name="purchase_order_detail",
),
path(
"purchase_orders/<slug:entity_slug>/<uuid:po_pk>/update/",
views.PurchaseOrderUpdateView.as_view(),
name="purchase_order_update",
),
path(
"purchase_orders/<slug:entity_slug>/update/<uuid:po_pk>/update-items/",
views.PurchaseOrderUpdateView.as_view(action_update_items=True),
name="purchase_order_update_items",
),
path(
"purchase_orders/inventory_item/create/",
views.InventoryItemCreateView,
name="inventory_item_create",
),
path(
"purchase_orders/inventory_items_filter/",
views.inventory_items_filter,
name="inventory_items_filter",
),
path(
"purchase_orders/<slug:entity_slug>/delete/<uuid:po_pk>/",
views.PurchaseOrderModelDeleteView.as_view(),
name="po-delete",
),
path(
"purchase_orders/<slug:entity_slug>/<uuid:po_pk>/upload/",
view=views.view_items_inventory,
name="view_items_inventory",
),
# Actions.... # Actions....
path('<slug:entity_slug>/action/<uuid:po_pk>/mark-as-draft/', path(
views.PurchaseOrderMarkAsDraftView.as_view(), "<slug:entity_slug>/action/<uuid:po_pk>/mark-as-draft/",
name='po-action-mark-as-draft'), views.PurchaseOrderMarkAsDraftView.as_view(),
path('<slug:entity_slug>/action/<uuid:po_pk>/mark-as-review/', name="po-action-mark-as-draft",
views.PurchaseOrderMarkAsReviewView.as_view(), ),
name='po-action-mark-as-review'), path(
path('<slug:entity_slug>/action/<uuid:po_pk>/mark-as-approved/', "<slug:entity_slug>/action/<uuid:po_pk>/mark-as-review/",
views.PurchaseOrderMarkAsApprovedView.as_view(), views.PurchaseOrderMarkAsReviewView.as_view(),
name='po-action-mark-as-approved'), name="po-action-mark-as-review",
path('<slug:entity_slug>/action/<uuid:po_pk>/mark-as-fulfilled/', ),
views.PurchaseOrderMarkAsFulfilledView.as_view(), path(
name='po-action-mark-as-fulfilled'), "<slug:entity_slug>/action/<uuid:po_pk>/mark-as-approved/",
path('<slug:entity_slug>/action/<uuid:po_pk>/mark-as-canceled/', views.PurchaseOrderMarkAsApprovedView.as_view(),
views.PurchaseOrderMarkAsCanceledView.as_view(), name="po-action-mark-as-approved",
name='po-action-mark-as-canceled'), ),
path('<slug:entity_slug>/action/<uuid:po_pk>/mark-as-void/', path(
views.PurchaseOrderMarkAsVoidView.as_view(), "<slug:entity_slug>/action/<uuid:po_pk>/mark-as-fulfilled/",
name='po-action-mark-as-void'), views.PurchaseOrderMarkAsFulfilledView.as_view(),
name="po-action-mark-as-fulfilled",
),
path(
"<slug:entity_slug>/action/<uuid:po_pk>/mark-as-canceled/",
views.PurchaseOrderMarkAsCanceledView.as_view(),
name="po-action-mark-as-canceled",
),
path(
"<slug:entity_slug>/action/<uuid:po_pk>/mark-as-void/",
views.PurchaseOrderMarkAsVoidView.as_view(),
name="po-action-mark-as-void",
),
] ]
handler404 = "inventory.views.custom_page_not_found_view" handler404 = "inventory.views.custom_page_not_found_view"

View File

@ -1,34 +1,41 @@
from decimal import Decimal from decimal import Decimal
from django.conf import settings from django.conf import settings
from django_ledger.models.items import ItemModel from django_ledger.models.items import ItemModel
def calculate_vat(value): def calculate_vat(value):
"""Helper to calculate VAT dynamically for a given value.""" """Helper to calculate VAT dynamically for a given value."""
vat_rate = getattr(settings, 'VAT_RATE', Decimal('0.15')) # Default VAT rate vat_rate = getattr(settings, "VAT_RATE", Decimal("0.15")) # Default VAT rate
return (value * vat_rate).quantize(Decimal('0.01')) return (value * vat_rate).quantize(Decimal("0.01"))
# def get_financial_value(instance,attribute,vat=False): # def get_financial_value(instance,attribute,vat=False):
# if vat: # if vat:
# return calculate_vat(getattr(instance, attribute, Decimal('0.00')) if instance else Decimal('0.00')) # return calculate_vat(getattr(instance, attribute, Decimal('0.00')) if instance else Decimal('0.00'))
# return getattr(instance, attribute, Decimal('0.00')) if instance else Decimal('0.00') # return getattr(instance, attribute, Decimal('0.00')) if instance else Decimal('0.00')
def get_financial_value(name,vat=False):
def get_financial_value(name, vat=False):
val = ItemModel.objects.filter(name=name).first() val = ItemModel.objects.filter(name=name).first()
if not val: if not val:
return 0 return 0
if vat: if vat:
return (val.price * settings.VAT_RATE).quantize(Decimal('0.01')) return (val.price * settings.VAT_RATE).quantize(Decimal("0.01"))
return val.price return val.price
def get_total_financials(instance,vat=False): def get_total_financials(instance, vat=False):
total = 0 total = 0
if instance.additional_services.count() != 0: if instance.additional_services.count() != 0:
total_additional_services = sum(x.price for x in instance.additional_services.all()) total_additional_services = sum(
x.price for x in instance.additional_services.all()
)
total = instance.selling_price + total_additional_services total = instance.selling_price + total_additional_services
if vat and total: if vat and total:
total = (total * settings.VAT_RATE).quantize(Decimal('0.01')) + total total = (total * settings.VAT_RATE).quantize(Decimal("0.01")) + total
return total return total
def get_total(instance): def get_total(instance):
total = get_total_financials(instance,vat=True) total = get_total_financials(instance, vat=True)
return total return total

View File

@ -4,7 +4,6 @@ from django.conf import settings
from plans.taxation import TaxationPolicy from plans.taxation import TaxationPolicy
class SaudiTaxationPolicy(TaxationPolicy): class SaudiTaxationPolicy(TaxationPolicy):
""" """
Represents the taxation policy specific to Saudi Arabia. Represents the taxation policy specific to Saudi Arabia.
@ -17,12 +16,13 @@ class SaudiTaxationPolicy(TaxationPolicy):
:ivar settings: Configuration settings of the application. :ivar settings: Configuration settings of the application.
:type settings: module :type settings: module
""" """
def get_default_tax(self): def get_default_tax(self):
return getattr(settings, 'PLANS_TAX', None) return getattr(settings, "PLANS_TAX", None)
def get_issuer_country_code(self): def get_issuer_country_code(self):
return getattr(settings, 'PLANS_TAX_COUNTRY', None) return getattr(settings, "PLANS_TAX_COUNTRY", None)
def get_tax_rate(self, tax_id, country_code, request=None): def get_tax_rate(self, tax_id, country_code, request=None):
rate = Decimal("15") rate = Decimal("15")
return rate return rate

View File

@ -1,7 +1,7 @@
import json import json
import datetime import datetime
from plans.models import AbstractOrder from plans.models import AbstractOrder
from django.contrib.auth.models import Group,Permission from django.contrib.auth.models import Group, Permission
from django.db import transaction from django.db import transaction
from django.urls import reverse from django.urls import reverse
import requests import requests
@ -29,8 +29,12 @@ from django.contrib.auth.models import User
import secrets import secrets
def make_random_password(length=10, allowed_chars='abcdefghjkmnpqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ23456789'):
return ''.join(secrets.choice(allowed_chars) for i in range(length)) def make_random_password(
length=10, allowed_chars="abcdefghjkmnpqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ23456789"
):
return "".join(secrets.choice(allowed_chars) for i in range(length))
def get_jwt_token(): def get_jwt_token():
""" """
@ -290,8 +294,11 @@ def get_car_finance_data(model):
"discount_amount" "discount_amount"
], ],
"quantity": x.ce_quantity or x.quantity, "quantity": x.ce_quantity or x.quantity,
"unit_price": Decimal(x.item_model.additional_info["car_finance"]["total"]), "unit_price": Decimal(
"total": Decimal(x.item_model.additional_info["car_finance"]["total"]) * Decimal(x.quantity or x.ce_quantity), x.item_model.additional_info["car_finance"]["total"]
),
"total": Decimal(x.item_model.additional_info["car_finance"]["total"])
* Decimal(x.quantity or x.ce_quantity),
"total_vat": x.item_model.additional_info["car_finance"]["total_vat"], "total_vat": x.item_model.additional_info["car_finance"]["total_vat"],
"additional_services": x.item_model.additional_info[ "additional_services": x.item_model.additional_info[
"additional_services" "additional_services"
@ -393,7 +400,7 @@ def get_financial_values(model):
if i.item_model.additional_info["additional_services"]: if i.item_model.additional_info["additional_services"]:
additional_services.extend( additional_services.extend(
[ [
{"name": x['name'], "price": x["price"]} {"name": x["name"], "price": x["price"]}
for x in i.item_model.additional_info["additional_services"] for x in i.item_model.additional_info["additional_services"]
] ]
) )
@ -446,43 +453,7 @@ def set_invoice_payment(dealer, entity, invoice, amount, payment_method):
calculator = CarFinanceCalculator(invoice) calculator = CarFinanceCalculator(invoice)
finance_data = calculator.get_finance_data() finance_data = calculator.get_finance_data()
# journal = JournalEntryModel.objects.create( handle_account_process(invoice, amount, finance_data)
# posted=False,
# description=f"Payment for Invoice {invoice.invoice_number}",
# ledger=invoice.ledger,
# locked=False,
# origin="Payment",
# )
# credit_account = entity.get_default_coa_accounts().get(name="Sales Revenue")
# debit_account = entity.get_default_coa_accounts().get(name="Cash", active=True)
# vat_payable_account = entity.get_default_coa_accounts().get(name="VAT Payable", active=True)
# TransactionModel.objects.create(
# journal_entry=journal,
# account=debit_account, # Debit Account
# amount=Decimal(finance_data["grand_total"]),
# tx_type="debit",
# description="Payment Received",
# )
# TransactionModel.objects.create(
# journal_entry=journal,
# account=credit_account, # Credit Accounts Receivable
# amount=Decimal(finance_data["total_price"] + finance_data["total_additionals"]),
# tx_type="credit",
# description="Payment Received",
# )
# TransactionModel.objects.create(
# journal_entry=journal,
# account=vat_payable_account, # Credit VAT Payable
# amount=finance_data.get("total_vat_amount"),
# tx_type="credit",
# description="VAT Payable on Invoice",
# )
handle_account_process(invoice,amount,finance_data)
invoice.make_payment(amount) invoice.make_payment(amount)
invoice.save() invoice.save()
@ -597,6 +568,7 @@ def transfer_to_dealer(request, cars, to_dealer, remarks=None):
car.dealer = to_dealer car.dealer = to_dealer
car.save() car.save()
class CarTransfer: class CarTransfer:
""" """
Handles the process of transferring a car between dealers, automating associated tasks Handles the process of transferring a car between dealers, automating associated tasks
@ -620,6 +592,7 @@ class CarTransfer:
:ivar vendor: Vendor entity related to the transferring dealer. :ivar vendor: Vendor entity related to the transferring dealer.
:ivar bill: Bill entity created for the receiving dealer. :ivar bill: Bill entity created for the receiving dealer.
""" """
def __init__(self, car, transfer): def __init__(self, car, transfer):
self.car = car self.car = car
self.transfer = transfer self.transfer = transfer
@ -647,7 +620,11 @@ class CarTransfer:
self.customer = self._create_new_customer() self.customer = self._create_new_customer()
def _find_or_create_customer(self): def _find_or_create_customer(self):
return self.from_dealer.entity.get_customers().filter(email=self.to_dealer.user.email).first() return (
self.from_dealer.entity.get_customers()
.filter(email=self.to_dealer.user.email)
.first()
)
def _create_new_customer(self): def _create_new_customer(self):
customer = self.from_dealer.entity.create_customer( customer = self.from_dealer.entity.create_customer(
@ -666,8 +643,12 @@ class CarTransfer:
self.invoice = self.from_dealer.entity.create_invoice( self.invoice = self.from_dealer.entity.create_invoice(
customer_model=self.customer, customer_model=self.customer,
terms=InvoiceModel.TERMS_NET_30, terms=InvoiceModel.TERMS_NET_30,
cash_account=self.from_dealer.entity.get_default_coa_accounts().get(name="Cash", active=True), cash_account=self.from_dealer.entity.get_default_coa_accounts().get(
prepaid_account=self.from_dealer.entity.get_default_coa_accounts().get(name="Accounts Receivable", active=True), name="Cash", active=True
),
prepaid_account=self.from_dealer.entity.get_default_coa_accounts().get(
name="Accounts Receivable", active=True
),
coa_model=self.from_dealer.entity.get_default_coa(), coa_model=self.from_dealer.entity.get_default_coa(),
) )
@ -680,7 +661,11 @@ class CarTransfer:
self._add_car_item_to_invoice() self._add_car_item_to_invoice()
def _add_car_item_to_invoice(self): def _add_car_item_to_invoice(self):
self.item = self.from_dealer.entity.get_items_products().filter(name=self.car.vin).first() self.item = (
self.from_dealer.entity.get_items_products()
.filter(name=self.car.vin)
.first()
)
if not self.item: if not self.item:
return return
@ -700,11 +685,15 @@ class CarTransfer:
if self.invoice.can_review(): if self.invoice.can_review():
self.invoice.mark_as_review() self.invoice.mark_as_review()
self.invoice.mark_as_approved(self.from_dealer.entity.slug, self.from_dealer.entity.admin) self.invoice.mark_as_approved(
self.from_dealer.entity.slug, self.from_dealer.entity.admin
)
self.invoice.save() self.invoice.save()
def _create_product_in_receiver_ledger(self): def _create_product_in_receiver_ledger(self):
uom = self.to_dealer.entity.get_uom_all().filter(name=self.item.uom.name).first() uom = (
self.to_dealer.entity.get_uom_all().filter(name=self.item.uom.name).first()
)
self.product = self.to_dealer.entity.create_item_product( self.product = self.to_dealer.entity.create_item_product(
name=self.item.name, name=self.item.name,
uom_model=uom, uom_model=uom,
@ -721,15 +710,23 @@ class CarTransfer:
self.bill = self.to_dealer.entity.create_bill( self.bill = self.to_dealer.entity.create_bill(
vendor_model=self.vendor, vendor_model=self.vendor,
terms=BillModel.TERMS_NET_30, terms=BillModel.TERMS_NET_30,
cash_account=self.to_dealer.entity.get_default_coa_accounts().get(name="Cash", active=True), cash_account=self.to_dealer.entity.get_default_coa_accounts().get(
prepaid_account=self.to_dealer.entity.get_default_coa_accounts().get(name="Prepaid Expenses", active=True), name="Cash", active=True
),
prepaid_account=self.to_dealer.entity.get_default_coa_accounts().get(
name="Prepaid Expenses", active=True
),
coa_model=self.to_dealer.entity.get_default_coa(), coa_model=self.to_dealer.entity.get_default_coa(),
) )
self._add_car_item_to_bill() self._add_car_item_to_bill()
def _find_or_create_vendor(self): def _find_or_create_vendor(self):
vendor = self.to_dealer.entity.get_vendors().filter(vendor_name=self.from_dealer.name).first() vendor = (
self.to_dealer.entity.get_vendors()
.filter(vendor_name=self.from_dealer.name)
.first()
)
if not vendor: if not vendor:
vendor = VendorModel.objects.create( vendor = VendorModel.objects.create(
entity_model=self.to_dealer.entity, entity_model=self.to_dealer.entity,
@ -755,7 +752,9 @@ class CarTransfer:
self.bill.additional_info.update({"car_finance": self.car.finances.to_dict()}) self.bill.additional_info.update({"car_finance": self.car.finances.to_dict()})
self.bill.mark_as_review() self.bill.mark_as_review()
self.bill.mark_as_approved(self.to_dealer.entity.slug, self.to_dealer.entity.admin) self.bill.mark_as_approved(
self.to_dealer.entity.slug, self.to_dealer.entity.admin
)
self.bill.save() self.bill.save()
def _finalize_car_transfer(self): def _finalize_car_transfer(self):
@ -950,6 +949,7 @@ def to_dict(obj):
obj_dict[key] = str(value) obj_dict[key] = str(value)
return obj_dict return obj_dict
class CarFinanceCalculator: class CarFinanceCalculator:
""" """
Class responsible for calculating car financing details. Class responsible for calculating car financing details.
@ -969,10 +969,11 @@ class CarFinanceCalculator:
:ivar additional_services: A list of additional services with details (e.g., name, price, taxable status). :ivar additional_services: A list of additional services with details (e.g., name, price, taxable status).
:type additional_services: list :type additional_services: list
""" """
VAT_OBJ_NAME = 'vat_rate'
CAR_FINANCE_KEY = 'car_finance' VAT_OBJ_NAME = "vat_rate"
CAR_INFO_KEY = 'car_info' CAR_FINANCE_KEY = "car_finance"
ADDITIONAL_SERVICES_KEY = 'additional_services' CAR_INFO_KEY = "car_info"
ADDITIONAL_SERVICES_KEY = "additional_services"
def __init__(self, model): def __init__(self, model):
self.model = model self.model = model
@ -1003,75 +1004,92 @@ class CarFinanceCalculator:
quantity = self._get_quantity(item) quantity = self._get_quantity(item)
car_finance = self._get_nested_value(item, self.CAR_FINANCE_KEY) car_finance = self._get_nested_value(item, self.CAR_FINANCE_KEY)
car_info = self._get_nested_value(item, self.CAR_INFO_KEY) car_info = self._get_nested_value(item, self.CAR_INFO_KEY)
unit_price = Decimal(car_finance.get('selling_price', 0)) unit_price = Decimal(car_finance.get("selling_price", 0))
return { return {
"item_number": item.item_model.item_number, "item_number": item.item_model.item_number,
"vin": car_info.get('vin'), "vin": car_info.get("vin"),
"make": car_info.get('make'), "make": car_info.get("make"),
"model": car_info.get('model'), "model": car_info.get("model"),
"year": car_info.get('year'), "year": car_info.get("year"),
"logo": item.item_model.car.id_car_make.logo.url, "logo": item.item_model.car.id_car_make.logo.url,
"trim": car_info.get('trim'), "trim": car_info.get("trim"),
"mileage": car_info.get('mileage'), "mileage": car_info.get("mileage"),
"cost_price": car_finance.get('cost_price'), "cost_price": car_finance.get("cost_price"),
"selling_price": car_finance.get('selling_price'), "selling_price": car_finance.get("selling_price"),
"discount": car_finance.get('discount_amount'), "discount": car_finance.get("discount_amount"),
"quantity": quantity, "quantity": quantity,
"unit_price": unit_price, "unit_price": unit_price,
"total": unit_price * Decimal(quantity), "total": unit_price * Decimal(quantity),
"total_vat": car_finance.get('total_vat'), "total_vat": car_finance.get("total_vat"),
"additional_services": self._get_nested_value(item, self.ADDITIONAL_SERVICES_KEY), "additional_services": self._get_nested_value(
item, self.ADDITIONAL_SERVICES_KEY
),
} }
def _get_additional_services(self): def _get_additional_services(self):
return [ return [
{"name": service.get('name'), "price": service.get('price'), "taxable": service.get('taxable'),"price_": service.get('price_')} {
"name": service.get("name"),
"price": service.get("price"),
"taxable": service.get("taxable"),
"price_": service.get("price_"),
}
for item in self.item_transactions for item in self.item_transactions
for service in self._get_nested_value(item, self.ADDITIONAL_SERVICES_KEY) or [] for service in self._get_nested_value(item, self.ADDITIONAL_SERVICES_KEY)
or []
] ]
def calculate_totals(self): def calculate_totals(self):
total_price = sum( total_price = sum(
Decimal(self._get_nested_value(item, self.CAR_FINANCE_KEY, 'selling_price')) * int(self._get_quantity(item)) Decimal(self._get_nested_value(item, self.CAR_FINANCE_KEY, "selling_price"))
* int(self._get_quantity(item))
for item in self.item_transactions for item in self.item_transactions
) )
total_additionals = sum(Decimal(x.get('price_')) for x in self._get_additional_services()) total_additionals = sum(
Decimal(x.get("price_")) for x in self._get_additional_services()
)
total_discount = sum( total_discount = sum(
Decimal(self._get_nested_value(item, self.CAR_FINANCE_KEY, 'discount_amount')) Decimal(
self._get_nested_value(item, self.CAR_FINANCE_KEY, "discount_amount")
)
for item in self.item_transactions for item in self.item_transactions
) )
total_price_discounted = total_price - total_discount total_price_discounted = total_price - total_discount
total_vat_amount = total_price_discounted * self.vat_rate total_vat_amount = total_price_discounted * self.vat_rate
return { return {
"total_price_before_discount": round(total_price, 2), # total_price_before_discount, "total_price_before_discount": round(
total_price, 2
), # total_price_before_discount,
"total_price": round(total_price_discounted, 2), # total_price_discounted, "total_price": round(total_price_discounted, 2), # total_price_discounted,
"total_vat_amount": round(total_vat_amount, 2), # total_vat_amount, "total_vat_amount": round(total_vat_amount, 2), # total_vat_amount,
"total_discount": round(total_discount,2), "total_discount": round(total_discount, 2),
"total_additionals": round(total_additionals, 2), # total_additionals, "total_additionals": round(total_additionals, 2), # total_additionals,
"grand_total": round(total_price_discounted + total_vat_amount + total_additionals, 2) "grand_total": round(
total_price_discounted + total_vat_amount + total_additionals, 2
),
} }
def get_finance_data(self): def get_finance_data(self):
totals = self.calculate_totals() totals = self.calculate_totals()
return { return {
"cars": [self._get_car_data(item) for item in self.item_transactions], "cars": [self._get_car_data(item) for item in self.item_transactions],
"quantity": sum(self._get_quantity(item) for item in self.item_transactions), "quantity": sum(
"total_price": totals['total_price'], self._get_quantity(item) for item in self.item_transactions
"total_price_before_discount": totals['total_price_before_discount'], ),
"total_vat": totals['total_vat_amount'] + totals['total_price'], "total_price": totals["total_price"],
"total_vat_amount": totals['total_vat_amount'], "total_price_before_discount": totals["total_price_before_discount"],
"total_discount": totals['total_discount'], "total_vat": totals["total_vat_amount"] + totals["total_price"],
"total_additionals": totals['total_additionals'], "total_vat_amount": totals["total_vat_amount"],
"grand_total": totals['grand_total'], "total_discount": totals["total_discount"],
"total_additionals": totals["total_additionals"],
"grand_total": totals["grand_total"],
"additionals": self.additional_services, "additionals": self.additional_services,
"vat": self.vat_rate, "vat": self.vat_rate,
} }
def get_item_transactions(txs): def get_item_transactions(txs):
""" """
Extracts and compiles relevant transaction details from a list of transactions, Extracts and compiles relevant transaction details from a list of transactions,
@ -1086,10 +1104,10 @@ def get_item_transactions(txs):
transactions = [] transactions = []
for tx in txs: for tx in txs:
data = {} data = {}
if tx.item_model.additional_info.get('car_info'): if tx.item_model.additional_info.get("car_info"):
data["info"] = tx.item_model.additional_info.get('car_info') data["info"] = tx.item_model.additional_info.get("car_info")
if tx.item_model.additional_info.get('car_finance'): if tx.item_model.additional_info.get("car_finance"):
data["finance"] = tx.item_model.additional_info.get('car_finance') data["finance"] = tx.item_model.additional_info.get("car_finance")
if tx.has_estimate(): if tx.has_estimate():
data["estimate"] = tx.ce_model data["estimate"] = tx.ce_model
data["has_estimate"] = True data["has_estimate"] = True
@ -1122,12 +1140,12 @@ def get_local_name(self):
`arabic_name` for Arabic ('ar') language, or the value of `name` for any `arabic_name` for Arabic ('ar') language, or the value of `name` for any
other language or if `arabic_name` is not defined. other language or if `arabic_name` is not defined.
""" """
if get_language() == 'ar': if get_language() == "ar":
return getattr(self, 'arabic_name', None) return getattr(self, "arabic_name", None)
return getattr(self, 'name', None) return getattr(self, "name", None)
def handle_account_process(invoice,amount,finance_data): def handle_account_process(invoice, amount, finance_data):
""" """
Processes accounting transactions based on an invoice, financial data, Processes accounting transactions based on an invoice, financial data,
and related entity accounts configuration. This function handles the and related entity accounts configuration. This function handles the
@ -1149,7 +1167,11 @@ def handle_account_process(invoice,amount,finance_data):
entity = invoice.ledger.entity entity = invoice.ledger.entity
coa = entity.get_default_coa() coa = entity.get_default_coa()
cash_account = entity.get_all_accounts().filter(name="Cash", role=roles.ASSET_CA_CASH).first() cash_account = (
entity.get_all_accounts()
.filter(role_default=True, role=roles.ASSET_CA_CASH)
.first()
)
inventory_account = car.get_inventory_account() inventory_account = car.get_inventory_account()
revenue_account = car.get_revenue_account() revenue_account = car.get_revenue_account()
cogs_account = car.get_cogs_account() cogs_account = car.get_cogs_account()
@ -1193,7 +1215,6 @@ def handle_account_process(invoice,amount,finance_data):
# vat_payable_account = entity.get_default_coa_accounts().get(name="VAT Payable", active=True) # vat_payable_account = entity.get_default_coa_accounts().get(name="VAT Payable", active=True)
journal = JournalEntryModel.objects.create( journal = JournalEntryModel.objects.create(
posted=False, posted=False,
description=f"Payment for Invoice {invoice.invoice_number}", description=f"Payment for Invoice {invoice.invoice_number}",
@ -1241,9 +1262,9 @@ def handle_account_process(invoice,amount,finance_data):
description="", description="",
) )
try: try:
car.item_model.for_inventory = False car.item_model.for_inventory = False
except Exception as e: except Exception as e:
print(e) print(e)
car.finances.is_sold = True car.finances.is_sold = True
car.finances.save() car.finances.save()
car.item_model.save() car.item_model.save()
@ -1272,6 +1293,7 @@ def handle_account_process(invoice,amount,finance_data):
# description="VAT Payable on Invoice", # description="VAT Payable on Invoice",
# ) # )
def create_make_accounts(dealer): def create_make_accounts(dealer):
""" """
Creates accounts for dealer's car makes and associates them with a default Creates accounts for dealer's car makes and associates them with a default
@ -1293,13 +1315,20 @@ def create_make_accounts(dealer):
for make in makes: for make in makes:
account_name = f"{make.car_make.name} Inventory Account" account_name = f"{make.car_make.name} Inventory Account"
account = entity.get_all_accounts().filter(coa_model=coa,name=account_name).first() account = (
entity.get_all_accounts().filter(coa_model=coa, name=account_name).first()
)
if not account: if not account:
last_account = entity.get_all_accounts().filter(role=roles.ASSET_CA_INVENTORY).order_by('-created').first() last_account = (
entity.get_all_accounts()
.filter(role=roles.ASSET_CA_INVENTORY)
.order_by("-created")
.first()
)
if len(last_account.code) == 4: if len(last_account.code) == 4:
code = f"{int(last_account.code)}{1:03d}" code = f"{int(last_account.code)}{1:03d}"
elif len(last_account.code) > 4: elif len(last_account.code) > 4:
code = f"{int(last_account.code)+1}" code = f"{int(last_account.code) + 1}"
account = entity.create_account( account = entity.create_account(
name=account_name, name=account_name,
@ -1307,7 +1336,7 @@ def create_make_accounts(dealer):
role=roles.ASSET_CA_INVENTORY, role=roles.ASSET_CA_INVENTORY,
coa_model=coa, coa_model=coa,
balance_type="credit", balance_type="credit",
active=True active=True,
) )
@ -1335,17 +1364,16 @@ def create_user_dealer(email, password, name, arabic_name, phone, crn, vrn, addr
) )
def handle_payment(request, order):
def handle_payment(request,order):
url = "https://api.moyasar.com/v1/payments" url = "https://api.moyasar.com/v1/payments"
callback_url = request.build_absolute_uri(reverse("payment_callback")) callback_url = request.build_absolute_uri(reverse("payment_callback"))
if request.user.is_authenticated: if request.user.is_authenticated:
# email = request.user.email # email = request.user.email
# first_name = request.user.first_name # first_name = request.user.first_name
# last_name = request.user.last_name # last_name = request.user.last_name
# phone = request.user.phone # phone = request.user.phone
# else: # else:
email = request.POST["email"] email = request.POST["email"]
first_name = request.POST["first_name"] first_name = request.POST["first_name"]
last_name = request.POST["last_name"] last_name = request.POST["last_name"]
@ -1412,4 +1440,3 @@ def handle_payment(request,order):
# def get_user_quota(user): # def get_user_quota(user):
# return user.dealer.quota # return user.dealer.quota

View File

@ -1,9 +1,10 @@
from django.core.validators import RegexValidator from django.core.validators import RegexValidator
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
class SaudiPhoneNumberValidator(RegexValidator): class SaudiPhoneNumberValidator(RegexValidator):
def __init__(self): def __init__(self):
super().__init__( super().__init__(
regex=r'^(\+9665|05)[0-9]{8}$', regex=r"^(\+9665|05)[0-9]{8}$",
message=_("Enter a valid Saudi phone number (05XXXXXXXX or +9665XXXXXXXX)") message=_("Enter a valid Saudi phone number (05XXXXXXXX or +9665XXXXXXXX)"),
) )

File diff suppressed because it is too large Load Diff

View File

@ -7,8 +7,15 @@ os.environ.setdefault("DJANGO_SETTINGS_MODULE", "car_inventory.settings")
django.setup() django.setup()
from inventory.models import ( from inventory.models import (
CarMake, CarModel, CarSerie, CarTrim, CarEquipment, CarMake,
CarSpecification, CarSpecificationValue, CarOption, CarOptionValue CarModel,
CarSerie,
CarTrim,
CarEquipment,
CarSpecification,
CarSpecificationValue,
CarOption,
CarOptionValue,
) )
@ -72,7 +79,9 @@ def run():
# Step 5: Insert CarEquipment # Step 5: Insert CarEquipment
for item in tqdm(data["car_equipment"], desc="Inserting CarEquipment"): for item in tqdm(data["car_equipment"], desc="Inserting CarEquipment"):
if not CarEquipment.objects.filter(id_car_equipment=item["id_car_equipment"]).exists(): if not CarEquipment.objects.filter(
id_car_equipment=item["id_car_equipment"]
).exists():
# Check if related trim exists # Check if related trim exists
if CarTrim.objects.filter(id_car_trim=item["id_car_trim"]).exists(): if CarTrim.objects.filter(id_car_trim=item["id_car_trim"]).exists():
CarEquipment.objects.create( CarEquipment.objects.create(
@ -83,36 +92,53 @@ def run():
) )
# Step 6: Insert CarSpecification (Parent specifications first) # Step 6: Insert CarSpecification (Parent specifications first)
parent_specs = [item for item in data["car_specification"] if item["id_parent"] is None] parent_specs = [
child_specs = [item for item in data["car_specification"] if item["id_parent"] is not None] item for item in data["car_specification"] if item["id_parent"] is None
]
child_specs = [
item for item in data["car_specification"] if item["id_parent"] is not None
]
for item in tqdm(parent_specs, desc="Inserting Parent CarSpecifications"): for item in tqdm(parent_specs, desc="Inserting Parent CarSpecifications"):
if not CarSpecification.objects.filter(id_car_specification=item["id_car_specification"]).exists(): if not CarSpecification.objects.filter(
id_car_specification=item["id_car_specification"]
).exists():
CarSpecification.objects.create( CarSpecification.objects.create(
id_car_specification=item["id_car_specification"], id_car_specification=item["id_car_specification"],
name=item["name"], name=item["name"],
# arabic_name=item.get("arabic_name", ""), # arabic_name=item.get("arabic_name", ""),
id_parent_id=None id_parent_id=None,
) )
for item in tqdm(child_specs, desc="Inserting Child CarSpecifications"): for item in tqdm(child_specs, desc="Inserting Child CarSpecifications"):
if not CarSpecification.objects.filter(id_car_specification=item["id_car_specification"]).exists(): if not CarSpecification.objects.filter(
id_car_specification=item["id_car_specification"]
).exists():
# Check if parent exists # Check if parent exists
if CarSpecification.objects.filter(id_car_specification=item["id_parent"]).exists(): if CarSpecification.objects.filter(
id_car_specification=item["id_parent"]
).exists():
CarSpecification.objects.create( CarSpecification.objects.create(
id_car_specification=item["id_car_specification"], id_car_specification=item["id_car_specification"],
name=item["name"], name=item["name"],
# arabic_name=item.get("arabic_name", ""), # arabic_name=item.get("arabic_name", ""),
id_parent_id=item["id_parent"] id_parent_id=item["id_parent"],
) )
# Step 7: Insert CarSpecificationValue # Step 7: Insert CarSpecificationValue
for item in tqdm(data["car_specification_value"], desc="Inserting CarSpecificationValue"): for item in tqdm(
data["car_specification_value"], desc="Inserting CarSpecificationValue"
):
if not CarSpecificationValue.objects.filter( if not CarSpecificationValue.objects.filter(
id_car_specification_value=item["id_car_specification_value"]).exists(): id_car_specification_value=item["id_car_specification_value"]
).exists():
# Check if related objects exist # Check if related objects exist
if (CarTrim.objects.filter(id_car_trim=item["id_car_trim"]).exists() and if (
CarSpecification.objects.filter(id_car_specification=item["id_car_specification"]).exists()): CarTrim.objects.filter(id_car_trim=item["id_car_trim"]).exists()
and CarSpecification.objects.filter(
id_car_specification=item["id_car_specification"]
).exists()
):
CarSpecificationValue.objects.create( CarSpecificationValue.objects.create(
id_car_specification_value=item["id_car_specification_value"], id_car_specification_value=item["id_car_specification_value"],
id_car_trim_id=item["id_car_trim"], id_car_trim_id=item["id_car_trim"],
@ -123,7 +149,9 @@ def run():
# Step 8: Insert CarOption (Parent options first) # Step 8: Insert CarOption (Parent options first)
parent_options = [item for item in data["car_option"] if item["id_parent"] is None] parent_options = [item for item in data["car_option"] if item["id_parent"] is None]
child_options = [item for item in data["car_option"] if item["id_parent"] is not None] child_options = [
item for item in data["car_option"] if item["id_parent"] is not None
]
for item in tqdm(parent_options, desc="Inserting Parent CarOptions"): for item in tqdm(parent_options, desc="Inserting Parent CarOptions"):
if not CarOption.objects.filter(id_car_option=item["id_car_option"]).exists(): if not CarOption.objects.filter(id_car_option=item["id_car_option"]).exists():
@ -131,7 +159,7 @@ def run():
id_car_option=item["id_car_option"], id_car_option=item["id_car_option"],
name=item["name"], name=item["name"],
# arabic_name=item.get("arabic_name", ""), # arabic_name=item.get("arabic_name", ""),
id_parent_id=None id_parent_id=None,
) )
for item in tqdm(child_options, desc="Inserting Child CarOptions"): for item in tqdm(child_options, desc="Inserting Child CarOptions"):
@ -142,15 +170,23 @@ def run():
id_car_option=item["id_car_option"], id_car_option=item["id_car_option"],
name=item["name"], name=item["name"],
# arabic_name=item.get("arabic_name", ""), # arabic_name=item.get("arabic_name", ""),
id_parent_id=item["id_parent"] id_parent_id=item["id_parent"],
) )
# Step 9: Insert CarOptionValue # Step 9: Insert CarOptionValue
for item in tqdm(data["car_option_value"], desc="Inserting CarOptionValue"): for item in tqdm(data["car_option_value"], desc="Inserting CarOptionValue"):
if not CarOptionValue.objects.filter(id_car_option_value=item["id_car_option_value"]).exists(): if not CarOptionValue.objects.filter(
id_car_option_value=item["id_car_option_value"]
).exists():
# Check if related objects exist # Check if related objects exist
if (CarEquipment.objects.filter(id_car_equipment=item["id_car_equipment"]).exists() and if (
CarOption.objects.filter(id_car_option=item["id_car_option"]).exists()): CarEquipment.objects.filter(
id_car_equipment=item["id_car_equipment"]
).exists()
and CarOption.objects.filter(
id_car_option=item["id_car_option"]
).exists()
):
CarOptionValue.objects.create( CarOptionValue.objects.create(
id_car_option_value=item["id_car_option_value"], id_car_option_value=item["id_car_option_value"],
id_car_option_id=item["id_car_option"], id_car_option_id=item["id_car_option"],

View File

@ -1,12 +1,13 @@
#!/usr/bin/env python #!/usr/bin/env python
"""Django's command-line utility for administrative tasks.""" """Django's command-line utility for administrative tasks."""
import os import os
import sys import sys
def main(): def main():
"""Run administrative tasks.""" """Run administrative tasks."""
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'car_inventory.settings') os.environ.setdefault("DJANGO_SETTINGS_MODULE", "car_inventory.settings")
try: try:
from django.core.management import execute_from_command_line from django.core.management import execute_from_command_line
except ImportError as exc: except ImportError as exc:
@ -18,5 +19,5 @@ def main():
execute_from_command_line(sys.argv) execute_from_command_line(sys.argv)
if __name__ == '__main__': if __name__ == "__main__":
main() main()

View File

@ -14,21 +14,34 @@ try:
car_serie_df = pd.read_sql(car_serie_query, engine) car_serie_df = pd.read_sql(car_serie_query, engine)
# Perform a LEFT JOIN to keep all car series and merge with car generations # Perform a LEFT JOIN to keep all car series and merge with car generations
merged_df = pd.merge(car_serie_df, car_generation_df, on="id_car_generation", how="left") merged_df = pd.merge(
car_serie_df, car_generation_df, on="id_car_generation", how="left"
)
# Select and rename the relevant columns # Select and rename the relevant columns
final_df = merged_df.rename(columns={ final_df = merged_df.rename(
"id_car_serie": "id_car_serie", columns={
"id_car_model_x": "id_car_model", # Ensure correct column selection "id_car_serie": "id_car_serie",
"name_y": "generation_name", # Car generation name "id_car_model_x": "id_car_model", # Ensure correct column selection
"name_x": "serie_name", # Car series name "name_y": "generation_name", # Car generation name
"year_begin": "year_begin", "name_x": "serie_name", # Car series name
"year_end": "year_end" "year_begin": "year_begin",
})[["id_car_serie", "id_car_model", "generation_name", "serie_name", "year_begin", "year_end"]] "year_end": "year_end",
}
)[
[
"id_car_serie",
"id_car_model",
"generation_name",
"serie_name",
"year_begin",
"year_end",
]
]
# Save the filtered data to a JSON file # Save the filtered data to a JSON file
final_df.to_json("merged_car_data.json", orient="records", indent=4) final_df.to_json("merged_car_data.json", orient="records", indent=4)
print("Filtered merged data saved to 'merged_car_data.json'.") print("Filtered merged data saved to 'merged_car_data.json'.")
except Exception as e: except Exception as e:
print("Error:", e) print("Error:", e)

View File

@ -1,5 +1,4 @@
from datetime import datetime from datetime import datetime
from zoneinfo import ZoneInfo from zoneinfo import ZoneInfo
START_DTTM = datetime(year=2022, month=10, day=1, tzinfo=ZoneInfo('Asia/Riyadh')) START_DTTM = datetime(year=2022, month=10, day=1, tzinfo=ZoneInfo("Asia/Riyadh"))

View File

@ -43,10 +43,7 @@ Provide a clear step-by-step guide with numbered instructions. Include:
Helpful Step-by-Step Instructions:""" Helpful Step-by-Step Instructions:"""
PROMPT = PromptTemplate( PROMPT = PromptTemplate(template=template, input_variables=["context", "question"])
template=template,
input_variables=["context", "question"]
)
# Setup QA chain # Setup QA chain
qa = RetrievalQA.from_chain_type( qa = RetrievalQA.from_chain_type(
@ -54,23 +51,25 @@ qa = RetrievalQA.from_chain_type(
chain_type="stuff", chain_type="stuff",
retriever=index.vectorstore.as_retriever(), retriever=index.vectorstore.as_retriever(),
return_source_documents=True, return_source_documents=True,
chain_type_kwargs={"prompt": PROMPT} chain_type_kwargs={"prompt": PROMPT},
) )
# Function to run a query # Function to run a query
def ask_haikal(query): def ask_haikal(query):
response = qa.invoke({"query": query}) response = qa.invoke({"query": query})
print("\n" + "="*50) print("\n" + "=" * 50)
print(f"Question: {query}") print(f"Question: {query}")
print("="*50) print("=" * 50)
print("\nAnswer:") print("\nAnswer:")
print(response["result"]) print(response["result"])
print("\nSources:") print("\nSources:")
for doc in response["source_documents"]: for doc in response["source_documents"]:
print(f"- {doc.metadata.get('source', 'Unknown source')}") print(f"- {doc.metadata.get('source', 'Unknown source')}")
print("="*50) print("=" * 50)
return response["result"] return response["result"]
# Example query # Example query
if __name__ == "__main__": if __name__ == "__main__":
query = "How do I add a new car to the inventory? answer in Arabic" query = "How do I add a new car to the inventory? answer in Arabic"

View File

@ -2,9 +2,9 @@ import requests
import csv import csv
# Replace with your actual API token and secret key # Replace with your actual API token and secret key
API_TOKEN = 'f5204a00-6f31-4de2-96d8-ed998e0d230c' API_TOKEN = "f5204a00-6f31-4de2-96d8-ed998e0d230c"
SECRET_KEY = 'ae430502a5c66e818d9722919c8b5584' SECRET_KEY = "ae430502a5c66e818d9722919c8b5584"
BASE_URL = 'https://carapi.app/api/v1' BASE_URL = "https://carapi.app/api/v1"
def fetch_and_save_car_makes_models(api_url, headers, output_csv): def fetch_and_save_car_makes_models(api_url, headers, output_csv):
@ -25,17 +25,17 @@ def fetch_and_save_car_makes_models(api_url, headers, output_csv):
# Process the batch of responses # Process the batch of responses
for data in responses: for data in responses:
if 'data' not in data: if "data" not in data:
continue continue
for item in data['data']: for item in data["data"]:
make_name = item['make_model']['make']['name'] make_name = item["make_model"]["make"]["name"]
model_name = item['make_model']['name'] model_name = item["make_model"]["name"]
# Create dictionary for each make and model combination # Create dictionary for each make and model combination
translation_entry = { translation_entry = {
'make': f"{make_name} ", "make": f"{make_name} ",
'model': f"{model_name} " # Replace with actual Arabic translation if available "model": f"{model_name} ", # Replace with actual Arabic translation if available
} }
TRANSLATION.append(translation_entry) TRANSLATION.append(translation_entry)
@ -43,12 +43,15 @@ def fetch_and_save_car_makes_models(api_url, headers, output_csv):
page += pages_per_batch page += pages_per_batch
# Check if there are more pages to fetch # Check if there are more pages to fetch
if not responses or ('next' in responses[-1]['collection'] and not responses[-1]['collection']['next']): if not responses or (
"next" in responses[-1]["collection"]
and not responses[-1]["collection"]["next"]
):
break break
# Save the TRANSLATION data to a CSV file # Save the TRANSLATION data to a CSV file
with open(output_csv, 'w', newline='', encoding='utf-8') as csvfile: with open(output_csv, "w", newline="", encoding="utf-8") as csvfile:
fieldnames = ['make', 'model'] fieldnames = ["make", "model"]
writer = csv.DictWriter(csvfile, fieldnames=fieldnames) writer = csv.DictWriter(csvfile, fieldnames=fieldnames)
writer.writeheader() writer.writeheader()
@ -61,9 +64,9 @@ def fetch_and_save_car_makes_models(api_url, headers, output_csv):
# Example usage: # Example usage:
api_url = "https://carapi.app/api/trims?sort=make_model_id&direction=asc&verbose=yes" api_url = "https://carapi.app/api/trims?sort=make_model_id&direction=asc&verbose=yes"
headers = { headers = {
'Authorization': 'Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJjYXJhcGkuYXBwIiwic3ViIjoiYjU1OGYzMDMtODI0Ni00NjgzLTkwYTQtZmYwMGQxYWNmNGU3IiwiYXVkIjoiYjU1OGYzMDMtODI0Ni00NjgzLTkwYTQtZmYwMGQxYWNmNGU3IiwiZXhwIjoxNzIzNzMxODMyLCJpYXQiOjE3MjMxMjcwMzIsImp0aSI6IjNkMGJhMzA4LWUzZTAtNGJhZC1iZmMxLTBiMDA3YzNmMmE2NSIsInVzZXIiOnsic3Vic2NyaWJlZCI6dHJ1ZSwic3Vic2NyaXB0aW9uIjoic3RhcnRlciIsInJhdGVfbGltaXRfdHlwZSI6ImhhcmQiLCJhZGRvbnMiOnsiYW50aXF1ZV92ZWhpY2xlcyI6ZmFsc2UsImRhdGFfZmVlZCI6ZmFsc2V9fX0.t__L53yN0OndnOP3_YxaAbrwgQXSYwVUgEqE1IwH8Nk', # Replace with actual token "Authorization": "Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJjYXJhcGkuYXBwIiwic3ViIjoiYjU1OGYzMDMtODI0Ni00NjgzLTkwYTQtZmYwMGQxYWNmNGU3IiwiYXVkIjoiYjU1OGYzMDMtODI0Ni00NjgzLTkwYTQtZmYwMGQxYWNmNGU3IiwiZXhwIjoxNzIzNzMxODMyLCJpYXQiOjE3MjMxMjcwMzIsImp0aSI6IjNkMGJhMzA4LWUzZTAtNGJhZC1iZmMxLTBiMDA3YzNmMmE2NSIsInVzZXIiOnsic3Vic2NyaWJlZCI6dHJ1ZSwic3Vic2NyaXB0aW9uIjoic3RhcnRlciIsInJhdGVfbGltaXRfdHlwZSI6ImhhcmQiLCJhZGRvbnMiOnsiYW50aXF1ZV92ZWhpY2xlcyI6ZmFsc2UsImRhdGFfZmVlZCI6ZmFsc2V9fX0.t__L53yN0OndnOP3_YxaAbrwgQXSYwVUgEqE1IwH8Nk", # Replace with actual token
'Content-Type': 'application/json' "Content-Type": "application/json",
} }
output_csv = 'car_makes_models.csv' output_csv = "car_makes_models.csv"
fetch_and_save_car_makes_models(api_url, headers, output_csv) fetch_and_save_car_makes_models(api_url, headers, output_csv)

View File

@ -3,10 +3,7 @@ import json
# Connect to MySQL # Connect to MySQL
mysql_conn = mysql.connector.connect( mysql_conn = mysql.connector.connect(
host='localhost', host="localhost", user="root", password="Kfsh&rc9788", database="trucks2db"
user='root',
password='Kfsh&rc9788',
database='trucks2db'
) )
cursor = mysql_conn.cursor() cursor = mysql_conn.cursor()
@ -25,5 +22,5 @@ for table in tables:
data = [dict(zip(columns, row)) for row in rows] data = [dict(zip(columns, row)) for row in rows]
# Write to JSON file # Write to JSON file
with open(f'{table_name}.json', 'w') as f: with open(f"{table_name}.json", "w") as f:
json.dump(data, f) json.dump(data, f)

View File

@ -1,5 +1,6 @@
from inventory.models import * from inventory.models import *
from django_ledger.models import VendorModel from django_ledger.models import VendorModel
# from rich import print # from rich import print
import random import random
import datetime import datetime
@ -10,7 +11,10 @@ from inventory.services import decodevin
def run(): def run():
# car = Car.objects.filter(vin='2C3HD46R4WH170267') # car = Car.objects.filter(vin='2C3HD46R4WH170267')
dealer = Dealer.objects.first() dealer = Dealer.objects.first()
vendors = [VendorModel.objects.create(vendor_name=f'vendor{i}',entity_model=dealer.entity) for i in range(1, 5)] vendors = [
VendorModel.objects.create(vendor_name=f"vendor{i}", entity_model=dealer.entity)
for i in range(1, 5)
]
vin_list = [ vin_list = [
"1B3ES56C13D120225", "1B3ES56C13D120225",
@ -29,7 +33,6 @@ def run():
for vin in vin_list: for vin in vin_list:
try: try:
for _ in range(5): for _ in range(5):
vin = f"{vin[:-4]}{random.randint(0, 9)}{random.randint(0, 9)}{random.randint(0, 9)}{random.randint(0, 9)}" vin = f"{vin[:-4]}{random.randint(0, 9)}{random.randint(0, 9)}{random.randint(0, 9)}{random.randint(0, 9)}"
result = decodevin(vin) result = decodevin(vin)
make = CarMake.objects.get(name=result["maker"]) make = CarMake.objects.get(name=result["maker"])

View File

@ -1,17 +1,15 @@
from django.core.mail import send_mail from django.core.mail import send_mail
def send_test_email(): def send_test_email():
subject = 'Test Email from Django' subject = "Test Email from Django"
message = 'Hello, this is a test email sent from Django!' message = "Hello, this is a test email sent from Django!"
from_email = 'your-email@example.com' from_email = "your-email@example.com"
recipient_list = ['recipient-email@example.com'] recipient_list = ["recipient-email@example.com"]
send_mail(subject, message, from_email, recipient_list) send_mail(subject, message, from_email, recipient_list)
print('Email sent successfully!') print("Email sent successfully!")
def run(): def run():
send_test_email() send_test_email()

View File

@ -1,4 +1,3 @@
from vininfo.utils import merge_wmi from vininfo.utils import merge_wmi
@ -1332,14 +1331,13 @@ wmi_manufacturer_mapping = {
"MM0": "Mazda", "MM0": "Mazda",
"MM6": "Mazda", "MM6": "Mazda",
"MM7": "Mazda", "MM7": "Mazda",
"MM8": "Mazda" "MM8": "Mazda",
} }
# merge_wmi(wmi) # merge_wmi(wmi)
new_keys, wmi_source_code = merge_wmi(wmi_manufacturer_mapping) new_keys, wmi_source_code = merge_wmi(wmi_manufacturer_mapping)
print("New keys added:", new_keys) print("New keys added:", new_keys)
print("\nUpdated WMI dictionary source code:\n", wmi_source_code) print("\nUpdated WMI dictionary source code:\n", wmi_source_code)

View File

@ -1,6 +1,7 @@
import os import os
import pymysql import pymysql
import django import django
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "car_inventory.settings") os.environ.setdefault("DJANGO_SETTINGS_MODULE", "car_inventory.settings")
django.setup() django.setup()
@ -8,10 +9,18 @@ import json
from datetime import datetime from datetime import datetime
from tqdm import tqdm from tqdm import tqdm
from inventory.models import ( from inventory.models import (
CarMake, CarModel, CarSerie, CarTrim, CarEquipment, CarMake,
CarSpecification, CarSpecificationValue, CarOption, CarOptionValue CarModel,
CarSerie,
CarTrim,
CarEquipment,
CarSpecification,
CarSpecificationValue,
CarOption,
CarOptionValue,
) )
# Step 1: Perform MySQL Dump # Step 1: Perform MySQL Dump
def dump_mysql_database(): def dump_mysql_database():
print("Starting MySQL dump...") print("Starting MySQL dump...")
@ -22,14 +31,15 @@ def dump_mysql_database():
os.system(f"mysqldump -u {db_user} -p{db_password} {db_name} > {dump_file}") os.system(f"mysqldump -u {db_user} -p{db_password} {db_name} > {dump_file}")
print(f"MySQL dump completed: {dump_file}") print(f"MySQL dump completed: {dump_file}")
# Step 2: Connect to MySQL and export data to JSON # Step 2: Connect to MySQL and export data to JSON
def export_database_to_json(): def export_database_to_json():
print("Starting export to JSON...") print("Starting export to JSON...")
db_config = { db_config = {
'host': 'localhost', "host": "localhost",
'user': 'root', "user": "root",
'password': 'Kfsh&rc9788', "password": "Kfsh&rc9788",
'database': 'trucks2db', "database": "trucks2db",
} }
connection = pymysql.connect(**db_config) connection = pymysql.connect(**db_config)
@ -53,6 +63,7 @@ def export_database_to_json():
print(f"Database exported to JSON successfully: {json_file_name}") print(f"Database exported to JSON successfully: {json_file_name}")
return json_file_name return json_file_name
# Step 3: Run the JSON processing script (previously created) # Step 3: Run the JSON processing script (previously created)
def process_json_data(json_file_name): def process_json_data(json_file_name):
print(f"Starting JSON processing for file: {json_file_name}") print(f"Starting JSON processing for file: {json_file_name}")
@ -69,24 +80,28 @@ def process_json_data(json_file_name):
"arabic_name": item.get("arabic_name", ""), "arabic_name": item.get("arabic_name", ""),
"logo": item.get("Logo", ""), "logo": item.get("Logo", ""),
"is_sa_import": item.get("is_sa_import", False), "is_sa_import": item.get("is_sa_import", False),
} },
) )
# Step 2: Populate CarModel # Step 2: Populate CarModel
for item in tqdm(data["car_model"], desc="Populating CarModel"): for item in tqdm(data["car_model"], desc="Populating CarModel"):
CarMake.objects.get(id_car_make=item["id_car_make"]) # Ensures foreign key exists CarMake.objects.get(
id_car_make=item["id_car_make"]
) # Ensures foreign key exists
CarModel.objects.update_or_create( CarModel.objects.update_or_create(
id_car_model=item["id_car_model"], id_car_model=item["id_car_model"],
defaults={ defaults={
"id_car_make_id": item["id_car_make"], "id_car_make_id": item["id_car_make"],
"name": item["name"], "name": item["name"],
"arabic_name": item.get("arabic_name", ""), "arabic_name": item.get("arabic_name", ""),
} },
) )
# Step 3: Populate CarSerie # Step 3: Populate CarSerie
for item in tqdm(data["car_serie"], desc="Populating CarSerie"): for item in tqdm(data["car_serie"], desc="Populating CarSerie"):
CarModel.objects.get(id_car_model=item["id_car_model"]) # Ensures foreign key exists CarModel.objects.get(
id_car_model=item["id_car_model"]
) # Ensures foreign key exists
CarSerie.objects.update_or_create( CarSerie.objects.update_or_create(
id_car_serie=item["id_car_serie"], id_car_serie=item["id_car_serie"],
defaults={ defaults={
@ -96,12 +111,14 @@ def process_json_data(json_file_name):
"year_begin": item.get("year_begin"), "year_begin": item.get("year_begin"),
"year_end": item.get("year_end"), "year_end": item.get("year_end"),
"generation_name": item.get("generation_name", ""), "generation_name": item.get("generation_name", ""),
} },
) )
# Step 4: Populate CarTrim # Step 4: Populate CarTrim
for item in tqdm(data["car_trim"], desc="Populating CarTrim"): for item in tqdm(data["car_trim"], desc="Populating CarTrim"):
CarSerie.objects.get(id_car_serie=item["id_car_serie"]) # Ensures foreign key exists CarSerie.objects.get(
id_car_serie=item["id_car_serie"]
) # Ensures foreign key exists
CarTrim.objects.update_or_create( CarTrim.objects.update_or_create(
id_car_trim=item["id_car_trim"], id_car_trim=item["id_car_trim"],
defaults={ defaults={
@ -113,19 +130,21 @@ def process_json_data(json_file_name):
"date_create": item["date_create"], "date_create": item["date_create"],
"date_update": item["date_update"], "date_update": item["date_update"],
"id_car_type": item.get("id_car_type", 1), "id_car_type": item.get("id_car_type", 1),
} },
) )
# Step 5: Populate CarEquipment # Step 5: Populate CarEquipment
for item in tqdm(data["car_equipment"], desc="Populating CarEquipment"): for item in tqdm(data["car_equipment"], desc="Populating CarEquipment"):
CarTrim.objects.get(id_car_trim=item["id_car_trim"]) # Ensures foreign key exists CarTrim.objects.get(
id_car_trim=item["id_car_trim"]
) # Ensures foreign key exists
CarEquipment.objects.update_or_create( CarEquipment.objects.update_or_create(
id_car_equipment=item["id_car_equipment"], id_car_equipment=item["id_car_equipment"],
defaults={ defaults={
"id_car_trim_id": item["id_car_trim"], "id_car_trim_id": item["id_car_trim"],
"name": item["name"], "name": item["name"],
"year_begin": item.get("year"), "year_begin": item.get("year"),
} },
) )
# Step 6: Populate CarSpecification # Step 6: Populate CarSpecification
@ -136,13 +155,19 @@ def process_json_data(json_file_name):
"name": item["name"], "name": item["name"],
"arabic_name": item.get("arabic_name", ""), "arabic_name": item.get("arabic_name", ""),
"id_parent_id": item.get("id_parent"), "id_parent_id": item.get("id_parent"),
} },
) )
# Step 7: Populate CarSpecificationValue # Step 7: Populate CarSpecificationValue
for item in tqdm(data["car_specification_value"], desc="Populating CarSpecificationValue"): for item in tqdm(
CarTrim.objects.get(id_car_trim=item["id_car_trim"]) # Ensures foreign key exists data["car_specification_value"], desc="Populating CarSpecificationValue"
CarSpecification.objects.get(id_car_specification=item["id_car_specification"]) # Ensures foreign key exists ):
CarTrim.objects.get(
id_car_trim=item["id_car_trim"]
) # Ensures foreign key exists
CarSpecification.objects.get(
id_car_specification=item["id_car_specification"]
) # Ensures foreign key exists
CarSpecificationValue.objects.update_or_create( CarSpecificationValue.objects.update_or_create(
id_car_specification_value=item["id_car_specification_value"], id_car_specification_value=item["id_car_specification_value"],
defaults={ defaults={
@ -150,7 +175,7 @@ def process_json_data(json_file_name):
"id_car_specification_id": item["id_car_specification"], "id_car_specification_id": item["id_car_specification"],
"value": item["value"], "value": item["value"],
"unit": item.get("unit", ""), "unit": item.get("unit", ""),
} },
) )
# Step 8: Populate CarOption # Step 8: Populate CarOption
@ -161,20 +186,24 @@ def process_json_data(json_file_name):
"name": item["name"], "name": item["name"],
"arabic_name": item.get("arabic_name", ""), "arabic_name": item.get("arabic_name", ""),
"id_parent_id": item.get("id_parent"), "id_parent_id": item.get("id_parent"),
} },
) )
# Step 9: Populate CarOptionValue # Step 9: Populate CarOptionValue
for item in tqdm(data["car_option_value"], desc="Populating CarOptionValue"): for item in tqdm(data["car_option_value"], desc="Populating CarOptionValue"):
CarEquipment.objects.get(id_car_equipment=item["id_car_equipment"]) # Ensures foreign key exists CarEquipment.objects.get(
CarOption.objects.get(id_car_option=item["id_car_option"]) # Ensures foreign key exists id_car_equipment=item["id_car_equipment"]
) # Ensures foreign key exists
CarOption.objects.get(
id_car_option=item["id_car_option"]
) # Ensures foreign key exists
CarOptionValue.objects.update_or_create( CarOptionValue.objects.update_or_create(
id_car_option_value=item["id_car_option_value"], id_car_option_value=item["id_car_option_value"],
defaults={ defaults={
"id_car_option_id": item["id_car_option"], "id_car_option_id": item["id_car_option"],
"id_car_equipment_id": item["id_car_equipment"], "id_car_equipment_id": item["id_car_equipment"],
"is_base": item["is_base"], "is_base": item["is_base"],
} },
) )
print("Data population completed.") print("Data population completed.")
@ -182,11 +211,13 @@ def process_json_data(json_file_name):
os.system(f"python process_car_data.py {json_file_name}") os.system(f"python process_car_data.py {json_file_name}")
print("JSON processing completed.") print("JSON processing completed.")
# Main function to run all steps # Main function to run all steps
def main(): def main():
dump_mysql_database() # Step 1: Dump the database dump_mysql_database() # Step 1: Dump the database
json_file_name = export_database_to_json() # Step 2: Export to JSON json_file_name = export_database_to_json() # Step 2: Export to JSON
# process_json_data(json_file_name) # Step 3: Process the JSON # process_json_data(json_file_name) # Step 3: Process the JSON
if __name__ == "__main__": if __name__ == "__main__":
main() main()

Some files were not shown because too many files have changed in this diff Show More