diff --git a/api/apps.py b/api/apps.py
index 66656fd2..878e7d54 100644
--- a/api/apps.py
+++ b/api/apps.py
@@ -2,5 +2,5 @@ from django.apps import AppConfig
class ApiConfig(AppConfig):
- default_auto_field = 'django.db.models.BigAutoField'
- name = 'api'
+ default_auto_field = "django.db.models.BigAutoField"
+ name = "api"
diff --git a/api/consumers.py b/api/consumers.py
index 8b8ac4ae..d32da48e 100644
--- a/api/consumers.py
+++ b/api/consumers.py
@@ -32,4 +32,4 @@
# await self.send(text_data=json.dumps({
# 'message': 'VIN received',
# 'vin': vin
-# }))
\ No newline at end of file
+# }))
diff --git a/api/migrations/0001_initial.py b/api/migrations/0001_initial.py
index 7441cd65..fa6dd7f1 100644
--- a/api/migrations/0001_initial.py
+++ b/api/migrations/0001_initial.py
@@ -4,19 +4,28 @@ from django.db import migrations, models
class Migration(migrations.Migration):
-
initial = True
- dependencies = [
- ]
+ dependencies = []
operations = [
migrations.CreateModel(
- name='CarVIN',
+ name="CarVIN",
fields=[
- ('id', 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')),
+ (
+ "id",
+ 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"),
+ ),
],
),
]
diff --git a/api/models.py b/api/models.py
index 810bb254..1c91d741 100644
--- a/api/models.py
+++ b/api/models.py
@@ -8,4 +8,3 @@ class CarVIN(models.Model):
def __str__(self):
return self.vin
-
diff --git a/api/serializers.py b/api/serializers.py
index 80eba627..6bddc1cd 100644
--- a/api/serializers.py
+++ b/api/serializers.py
@@ -6,78 +6,93 @@ from inventory import models as inventory_models
class CarVINSerializer(serializers.ModelSerializer):
class Meta:
model = models.CarVIN
- fields = ['vin']
+ fields = ["vin"]
def create(self, validated_data):
- vin = validated_data.pop('vin')
+ vin = validated_data.pop("vin")
return models.CarVIN.objects.create(vin=vin, **validated_data)
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:
model = inventory_models.CarMake
- fields = '__all__'
+ fields = "__all__"
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:
model = inventory_models.CarModel
- fields = '__all__'
+ fields = "__all__"
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:
model = inventory_models.CarSerie
- fields = '__all__'
+ fields = "__all__"
class CarTrimSerializer(serializers.ModelSerializer):
- car_equipments = serializers.PrimaryKeyRelatedField(many=True, read_only=True, source='carequipment_set')
- car_specification_values = serializers.PrimaryKeyRelatedField(many=True, read_only=True,
- source='carspecificationvalue_set')
+ car_equipments = serializers.PrimaryKeyRelatedField(
+ many=True, read_only=True, source="carequipment_set"
+ )
+ car_specification_values = serializers.PrimaryKeyRelatedField(
+ many=True, read_only=True, source="carspecificationvalue_set"
+ )
class Meta:
model = inventory_models.CarTrim
- fields = '__all__'
+ fields = "__all__"
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:
model = inventory_models.CarEquipment
- fields = '__all__'
+ fields = "__all__"
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:
model = inventory_models.CarSpecification
- fields = '__all__'
+ fields = "__all__"
class CarSpecificationValueSerializer(serializers.ModelSerializer):
class Meta:
model = inventory_models.CarSpecificationValue
- fields = '__all__'
+ fields = "__all__"
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:
model = inventory_models.CarOption
- fields = '__all__'
+ fields = "__all__"
class CarOptionValueSerializer(serializers.ModelSerializer):
class Meta:
model = inventory_models.CarOptionValue
- fields = '__all__'
\ No newline at end of file
+ fields = "__all__"
diff --git a/api/services.py b/api/services.py
index 7284a869..55be18c9 100644
--- a/api/services.py
+++ b/api/services.py
@@ -2,19 +2,12 @@ import requests
def get_bearer():
-
api_token = "f5204a00-6f31-4de2-96d8-ed998e0d230c"
api_secret = "8c11320781a5b8f4f327b6937e6f8241"
url = "https://carapi.app/api/auth/login"
- headers = {
- "accept": "text/plain",
- "Content-Type": "application/json"
- }
- data = {
- "api_token": api_token,
- "api_secret": api_secret
- }
+ headers = {"accept": "text/plain", "Content-Type": "application/json"}
+ data = {"api_token": api_token, "api_secret": api_secret}
response = requests.post(url, headers=headers, json=data)
@@ -26,11 +19,8 @@ def get_bearer():
def get_car_data(vin):
-
url = f"https://carapi.app/api/vin/{vin}?verbose=no&all_trims=no"
- headers = {
- "Authorization": f"Bearer {get_bearer()}"
- }
+ headers = {"Authorization": f"Bearer {get_bearer()}"}
try:
response = requests.get(url, headers=headers)
@@ -52,10 +42,8 @@ def get_from_cardatabase(vin):
url = "https://api.vehicledatabases.com/premium/vin-decode/{vin}"
payload = {}
- headers = {
- 'x-AuthKey': '3cefdfd4272445f1929b5801c55d8fa5'
- }
+ headers = {"x-AuthKey": "3cefdfd4272445f1929b5801c55d8fa5"}
response = requests.request("GET", url, headers=headers, data=payload)
- print(response.text)
\ No newline at end of file
+ print(response.text)
diff --git a/api/tests.py b/api/tests.py
index 42ddf071..a39b155a 100644
--- a/api/tests.py
+++ b/api/tests.py
@@ -1,2 +1 @@
-
-# Create your tests here.
\ No newline at end of file
+# Create your tests here.
diff --git a/api/urls.py b/api/urls.py
index d48e0173..c94c955b 100644
--- a/api/urls.py
+++ b/api/urls.py
@@ -5,20 +5,20 @@ from api import views
router = routers.DefaultRouter()
-router.register(r'car-makes', views.CarMakeViewSet)
-router.register(r'car-models', views.CarModelViewSet)
-router.register(r'car-series', views.CarSerieViewSet)
-router.register(r'car-trims', views.CarTrimViewSet)
-router.register(r'car-equipments', views.CarEquipmentViewSet)
-router.register(r'car-specifications', views.CarSpecificationViewSet)
-router.register(r'car-specification-values', views.CarSpecificationValueViewSet)
-router.register(r'car-options', views.CarOptionViewSet)
-router.register(r'car-option-values', views.CarOptionValueViewSet)
+router.register(r"car-makes", views.CarMakeViewSet)
+router.register(r"car-models", views.CarModelViewSet)
+router.register(r"car-series", views.CarSerieViewSet)
+router.register(r"car-trims", views.CarTrimViewSet)
+router.register(r"car-equipments", views.CarEquipmentViewSet)
+router.register(r"car-specifications", views.CarSpecificationViewSet)
+router.register(r"car-specification-values", views.CarSpecificationValueViewSet)
+router.register(r"car-options", views.CarOptionViewSet)
+router.register(r"car-option-values", views.CarOptionValueViewSet)
urlpatterns = [
- path('', include(router.urls)),
- path('cars/vin/', views.CarVINViewSet.as_view(), name='car_vin'),
+ path("", include(router.urls)),
+ path("cars/vin/", views.CarVINViewSet.as_view(), name="car_vin"),
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"),
]
diff --git a/api/views.py b/api/views.py
index 3891d188..0c4bd145 100644
--- a/api/views.py
+++ b/api/views.py
@@ -16,7 +16,9 @@ logger = logging.getLogger(__name__)
class LoginView(APIView):
- permission_classes = [permissions.AllowAny,]
+ permission_classes = [
+ permissions.AllowAny,
+ ]
# def post(self, request, *args, **kwargs):
# username = request.data.get('username')
@@ -35,7 +37,7 @@ class LoginView(APIView):
class CarVINViewSet(APIView):
- queryset = models.CarVIN.objects.all().order_by('-created')
+ queryset = models.CarVIN.objects.all().order_by("-created")
serializer_class = serializers.CarVINSerializer
def get(self, request):
@@ -100,30 +102,27 @@ class CarOptionValueViewSet(viewsets.ModelViewSet):
serializer_class = serializers.CarOptionValueSerializer
-
def car_list(request):
dealer = get_user_type(request)
page = request.GET.get("page", 1)
per_page = 10
cars = inventory_models.Car.objects.all().values(
- "vin",
- "year",
- "id_car_make__name",
- "id_car_model__name",
- "status"
+ "vin", "year", "id_car_make__name", "id_car_model__name", "status"
)
paginator = Paginator(cars, per_page)
page_obj = paginator.get_page(page)
- return JsonResponse({
- "data": list(page_obj), # Convert QuerySet to list
- "page": page_obj.number, # Current page number
- "per_page": per_page,
- "total_pages": paginator.num_pages, # Total pages
- "total_items": paginator.count # Total records
- })
+ return JsonResponse(
+ {
+ "data": list(page_obj), # Convert QuerySet to list
+ "page": page_obj.number, # Current page number
+ "per_page": per_page,
+ "total_pages": paginator.num_pages, # Total pages
+ "total_items": paginator.count, # Total records
+ }
+ )
class VinDecodeAPIView(APIView):
@@ -162,4 +161,4 @@ class VinDecodeAPIView(APIView):
"modelYear": result.get("year_model"),
}
- return Response({"success": True, "data": vin_data}, status=status.HTTP_200_OK)
\ No newline at end of file
+ return Response({"success": True, "data": vin_data}, status=status.HTTP_200_OK)
diff --git a/car_inventory/asgi.py b/car_inventory/asgi.py
index e7563904..206c8884 100644
--- a/car_inventory/asgi.py
+++ b/car_inventory/asgi.py
@@ -15,13 +15,11 @@ from channels.routing import ProtocolTypeRouter, URLRouter
from channels.auth import AuthMiddlewareStack
from api import routing
-os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'car_inventory.settings')
+os.environ.setdefault("DJANGO_SETTINGS_MODULE", "car_inventory.settings")
-application = ProtocolTypeRouter({
- "http": get_asgi_application(),
- "websocket": AuthMiddlewareStack(
- URLRouter(
- routing.websocket_urlpatterns
- )
- ),
-})
+application = ProtocolTypeRouter(
+ {
+ "http": get_asgi_application(),
+ "websocket": AuthMiddlewareStack(URLRouter(routing.websocket_urlpatterns)),
+ }
+)
diff --git a/car_inventory/urls.py b/car_inventory/urls.py
index 7fa793f5..681dfa60 100644
--- a/car_inventory/urls.py
+++ b/car_inventory/urls.py
@@ -4,6 +4,7 @@ from django.conf.urls.static import static
from django.conf import settings
from django.conf.urls.i18n import i18n_patterns
from inventory import views
+
# import debug_toolbar
from schema_graph.views import Schema
# from two_factor.urls import urlpatterns as tf_urls
@@ -11,24 +12,22 @@ from schema_graph.views import Schema
urlpatterns = [
# path('__debug__/', include(debug_toolbar.urls)),
# path('silk/', include('silk.urls', namespace='silk')),
- path('api-auth/', include('rest_framework.urls')),
- path('api/', include('api.urls')),
+ path("api-auth/", include("rest_framework.urls")),
+ path("api/", include("api.urls")),
# path('dj-rest-auth/', include('dj_rest_auth.urls')),
-
-
]
urlpatterns += i18n_patterns(
- path('admin/', admin.site.urls),
- path('switch_language/', views.switch_language, name='switch_language'),
- path('accounts/', include('allauth.urls')),
+ path("admin/", admin.site.urls),
+ path("switch_language/", views.switch_language, name="switch_language"),
+ path("accounts/", include("allauth.urls")),
# path('prometheus/', include('django_prometheus.urls')),
- path('', include('inventory.urls')),
- path('ledger/', include('django_ledger.urls', namespace='django_ledger')),
+ path("", include("inventory.urls")),
+ path("ledger/", include("django_ledger.urls", namespace="django_ledger")),
path("haikalbot/", include("haikalbot.urls")),
- path('appointment/', include('appointment.urls')),
- path('plans/', include('plans.urls')),
+ path("appointment/", include("appointment.urls")),
+ path("plans/", include("plans.urls")),
path("schema/", Schema.as_view()),
- path('tours/', include('tours.urls')),
+ path("tours/", include("tours.urls")),
# path('', include(tf_urls)),
)
diff --git a/car_inventory/wsgi.py b/car_inventory/wsgi.py
index 1e65e538..7c3b10f3 100644
--- a/car_inventory/wsgi.py
+++ b/car_inventory/wsgi.py
@@ -11,6 +11,6 @@ import os
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()
diff --git a/docs/source/conf.py b/docs/source/conf.py
index 2efff906..a0ecbe19 100644
--- a/docs/source/conf.py
+++ b/docs/source/conf.py
@@ -6,23 +6,23 @@
# -- Project information -----------------------------------------------------
# https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information
-project = 'Haikal'
-copyright = '2024, Marwan Alwali'
-author = 'Marwan Alwali'
-release = '01/11/2024'
+project = "Haikal"
+copyright = "2024, Marwan Alwali"
+author = "Marwan Alwali"
+release = "01/11/2024"
# -- General configuration ---------------------------------------------------
# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration
extensions = []
-templates_path = ['_templates']
+templates_path = ["_templates"]
exclude_patterns = []
-language = '[en,ar]'
+language = "[en,ar]"
# -- Options for HTML output -------------------------------------------------
# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output
-html_theme = 'alabaster'
-html_static_path = ['_static']
+html_theme = "alabaster"
+html_static_path = ["_static"]
diff --git a/generate.py b/generate.py
index b06c6c2d..81bd1ef4 100644
--- a/generate.py
+++ b/generate.py
@@ -5,6 +5,7 @@ os.environ.setdefault("DJANGO_SETTINGS_MODULE", "car_inventory.settings")
django.setup()
from inventory.models import *
+
# from rich import print
import random
import datetime
@@ -31,7 +32,6 @@ def run():
"LGJE1EE0XSM333551",
"LGJE1EE02SM333561",
"LGJE1EE0XSM333565",
-
]
for vin in vin_list:
try:
@@ -69,5 +69,6 @@ def run():
except Exception as e:
print(e)
+
if __name__ == "__main__":
- run()
\ No newline at end of file
+ run()
diff --git a/haikalbot/admin.py b/haikalbot/admin.py
index 5acba54b..e9e7e1b1 100644
--- a/haikalbot/admin.py
+++ b/haikalbot/admin.py
@@ -4,10 +4,16 @@ from .models import AnalysisCache
@admin.register(AnalysisCache)
class AnalysisCacheAdmin(admin.ModelAdmin):
- list_display = ('prompt_hash', 'dealer_id', 'created_at', 'expires_at', 'is_expired')
- list_filter = ('dealer_id', 'created_at')
- search_fields = ('prompt_hash',)
- readonly_fields = ('prompt_hash', 'created_at', 'updated_at')
+ list_display = (
+ "prompt_hash",
+ "dealer_id",
+ "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):
return obj.is_expired()
diff --git a/haikalbot/ai_agent.py b/haikalbot/ai_agent.py
index 083adb7a..1ed46368 100644
--- a/haikalbot/ai_agent.py
+++ b/haikalbot/ai_agent.py
@@ -20,10 +20,10 @@ from sqlalchemy.orm import relationship
logger = logging.getLogger(__name__)
# Configuration settings
-LLM_MODEL = getattr(settings, 'MODEL_ANALYZER_LLM_MODEL', 'qwen3:8b')
-LLM_TEMPERATURE = getattr(settings, 'MODEL_ANALYZER_LLM_TEMPERATURE', 0.3)
-LLM_MAX_TOKENS = getattr(settings, 'MODEL_ANALYZER_LLM_MAX_TOKENS', 2048)
-CACHE_TIMEOUT = getattr(settings, 'MODEL_ANALYZER_CACHE_TIMEOUT', 3600)
+LLM_MODEL = getattr(settings, "MODEL_ANALYZER_LLM_MODEL", "qwen3:8b")
+LLM_TEMPERATURE = getattr(settings, "MODEL_ANALYZER_LLM_TEMPERATURE", 0.3)
+LLM_MAX_TOKENS = getattr(settings, "MODEL_ANALYZER_LLM_MAX_TOKENS", 2048)
+CACHE_TIMEOUT = getattr(settings, "MODEL_ANALYZER_CACHE_TIMEOUT", 3600)
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:
@@ -91,52 +91,55 @@ class ModelAnalysis:
class DjangoModelAnalyzer:
def __init__(self):
self.analysis_patterns = {
- 'count': {
- 'patterns': [r'\b(count|number|how many)\b'],
- 'fields': ['id'],
- 'weight': 1.0
+ "count": {
+ "patterns": [r"\b(count|number|how many)\b"],
+ "fields": ["id"],
+ "weight": 1.0,
},
- 'aggregate': {
- 'patterns': [r'\b(average|avg|mean|sum|total)\b'],
- 'fields': ['price', 'amount', 'value', 'cost', 'quantity'],
- 'weight': 0.8
+ "aggregate": {
+ "patterns": [r"\b(average|avg|mean|sum|total)\b"],
+ "fields": ["price", "amount", "value", "cost", "quantity"],
+ "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:
# Initialize LLM
- llm = ChatOllama(
- model=LLM_MODEL,
- temperature=LLM_TEMPERATURE
- )
+ llm = ChatOllama(model=LLM_MODEL, temperature=LLM_TEMPERATURE)
# Get model analysis from LLM
messages = [
SystemMessage(content=system_instruction),
- HumanMessage(content=prompt)
+ HumanMessage(content=prompt),
]
try:
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")
analysis_requirements = self._parse_llm_response(response.content)
except Exception as 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)
def _parse_llm_response(self, response: str) -> Dict:
try:
- json_match = re.search(r'({.*})', response.replace('\n', ' '), re.DOTALL)
+ json_match = re.search(r"({.*})", response.replace("\n", " "), re.DOTALL)
if json_match:
return json.loads(json_match.group(1))
return {}
@@ -149,20 +152,22 @@ class DjangoModelAnalyzer:
relevant_fields = []
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()):
- relevant_fields.extend(config['fields'])
+ relevant_fields.extend(config["fields"])
analysis_type = analysis_name
break
if analysis_type:
break
return {
- 'analysis_type': analysis_type or 'basic',
- 'fields': list(set(relevant_fields)) or ['id', 'name']
+ "analysis_type": analysis_type or "basic",
+ "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")
model_name = requirements.get("analysis_requirements", {}).get("model_name")
fields = requirements.get("analysis_requirements", {}).get("fields") or []
@@ -185,26 +190,31 @@ class DjangoModelAnalyzer:
field_analysis = FieldAnalysis(
name=field_name,
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,
- related_model=field.related_model.__name__ if field.is_relation and hasattr(field,
- 'related_model') and field.related_model else None
+ related_model=field.related_model.__name__
+ 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,
- requirements.get('analysis_type', 'basic')
+ field_analysis, requirements.get("analysis_type", "basic")
)
relevant_fields.append(field_analysis)
if field.is_relation:
- relationships.append({
- 'field': field_name,
- 'type': field.get_internal_type(),
- 'to': field.related_model.__name__ if hasattr(field,
- 'related_model') and field.related_model else ''
- })
+ relationships.append(
+ {
+ "field": field_name,
+ "type": field.get_internal_type(),
+ "to": field.related_model.__name__
+ if hasattr(field, "related_model") and field.related_model
+ else "",
+ }
+ )
except FieldDoesNotExist:
logger.warning(f"Field {field_name} not found in {model_name}")
@@ -212,16 +222,20 @@ class DjangoModelAnalyzer:
return ModelAnalysis(
app_label=app_label,
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,
- 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
if analysis_type in self.analysis_patterns:
- if field.name in self.analysis_patterns[analysis_type]['fields']:
- base_score += self.analysis_patterns[analysis_type]['weight']
+ if field.name in self.analysis_patterns[analysis_type]["fields"]:
+ base_score += self.analysis_patterns[analysis_type]["weight"]
if field.is_required:
base_score += 0.2
if field.is_relation:
@@ -257,26 +271,32 @@ def get_all_model_structures(filtered_apps: Optional[List[str]] = None) -> List[
if field.is_relation:
# Get related model name safely
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__
- elif hasattr(field, 'model') and field.model:
+ elif hasattr(field, "model") and field.model:
related_model_name = field.model.__name__
- if related_model_name: # Only add relationship if we have a valid related model
- relationships.append({
- "field": field.name,
- "type": field.get_internal_type(),
- "to": related_model_name
- })
+ if (
+ related_model_name
+ ): # Only add relationship if we have a valid related model
+ relationships.append(
+ {
+ "field": field.name,
+ "type": field.get_internal_type(),
+ "to": related_model_name,
+ }
+ )
else:
fields[field.name] = field.get_internal_type()
- structures.append({
- "app_label": app_label,
- "model_name": model.__name__,
- "fields": fields,
- "relationships": relationships
- })
+ structures.append(
+ {
+ "app_label": app_label,
+ "model_name": model.__name__,
+ "fields": fields,
+ "relationships": relationships,
+ }
+ )
return structures
@@ -332,25 +352,42 @@ def apply_filters(queryset: QuerySet, filters: Dict[str, Any]) -> QuerySet:
for key, value in filters.items():
if isinstance(value, dict):
# Handle complex filters
- operation = value.get('operation', 'exact')
- filter_value = value.get('value')
+ operation = value.get("operation", "exact")
+ filter_value = value.get("value")
- if not filter_value and operation != 'isnull':
+ if not filter_value and operation != "isnull":
continue
- if operation == 'contains':
+ if operation == "contains":
q_objects.append(Q(**{f"{key}__icontains": filter_value}))
- elif operation == 'in':
+ elif operation == "in":
if isinstance(filter_value, list) and 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}))
- elif operation == 'between' and isinstance(filter_value, list) and len(filter_value) >= 2:
- q_objects.append(Q(**{
- f"{key}__gte": filter_value[0],
- f"{key}__lte": filter_value[1]
- }))
- elif operation == 'isnull':
+ elif (
+ operation == "between"
+ and isinstance(filter_value, list)
+ and len(filter_value) >= 2
+ ):
+ 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)}))
else:
# Simple exact match
@@ -363,10 +400,10 @@ def apply_filters(queryset: QuerySet, filters: Dict[str, Any]) -> QuerySet:
def process_aggregation(
- queryset: QuerySet,
- aggregation: str,
- fields: List[str],
- group_by: Optional[List[str]] = None
+ queryset: QuerySet,
+ aggregation: str,
+ fields: List[str],
+ group_by: Optional[List[str]] = None,
) -> Union[Dict[str, Any], List[Dict[str, Any]]]:
"""
Process aggregation queries with support for grouping.
@@ -383,13 +420,7 @@ def process_aggregation(
if not fields:
return {"error": "No fields specified for aggregation"}
- agg_func_map = {
- "sum": Sum,
- "avg": Avg,
- "count": Count,
- "max": Max,
- "min": Min
- }
+ agg_func_map = {"sum": Sum, "avg": Avg, "count": Count, "max": Max, "min": Min}
agg_func = agg_func_map.get(aggregation.lower())
if not agg_func:
@@ -404,22 +435,25 @@ def process_aggregation(
agg_dict[f"{aggregation}_{field}"] = agg_func(field)
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
return list(queryset.values(*group_by).annotate(**agg_dict))
else:
# Simple aggregation without grouping
- return queryset.aggregate(**{
- f"{aggregation}_{field}": agg_func(field)
- for field in fields
- })
+ return queryset.aggregate(
+ **{f"{aggregation}_{field}": agg_func(field) for field in fields}
+ )
except Exception as e:
logger.error(f"Aggregation error: {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.
@@ -449,15 +483,18 @@ def prepare_chart_data(data: List[Dict], fields: List[str], chart_type: str) ->
return {
"type": chart_type,
"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": [
"rgba(54, 162, 235, 0.6)",
"rgba(255, 99, 132, 0.6)",
"rgba(255, 206, 86, 0.6)",
"rgba(75, 192, 192, 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
@@ -483,13 +520,14 @@ def prepare_chart_data(data: List[Dict], fields: List[str], chart_type: str) ->
"labels": labels,
"data": data_values,
"backgroundColor": [
- "rgba(54, 162, 235, 0.6)",
- "rgba(255, 99, 132, 0.6)",
- "rgba(255, 206, 86, 0.6)",
- "rgba(75, 192, 192, 0.6)",
- "rgba(153, 102, 255, 0.6)",
- "rgba(255, 159, 64, 0.6)"
- ] * (len(data_values) // 6 + 1) # Repeat colors as needed
+ "rgba(54, 162, 235, 0.6)",
+ "rgba(255, 99, 132, 0.6)",
+ "rgba(255, 206, 86, 0.6)",
+ "rgba(75, 192, 192, 0.6)",
+ "rgba(153, 102, 255, 0.6)",
+ "rgba(255, 159, 64, 0.6)",
+ ]
+ * (len(data_values) // 6 + 1), # Repeat colors as needed
}
else:
# 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],
"backgroundColor": f"rgba({50 + i * 50}, {100 + i * 40}, 235, 0.6)",
"borderColor": f"rgba({50 + i * 50}, {100 + i * 40}, 235, 1.0)",
- "borderWidth": 1
+ "borderWidth": 1,
}
datasets.append(dataset)
except (ValueError, TypeError) as e:
logger.warning(f"Error processing field {field} for chart: {e}")
- return {
- "type": chart_type,
- "labels": labels,
- "datasets": datasets
- }
+ return {"type": chart_type, "labels": labels, "datasets": datasets}
except Exception as e:
logger.error(f"Error preparing chart data: {e}")
return None
@@ -556,7 +590,7 @@ def query_django_model(parsed: Dict[str, Any]) -> Dict[str, Any]:
return {
"status": "error",
"error": "app_label and model are required",
- "language": language
+ "language": language,
}
# Get model class
@@ -566,7 +600,7 @@ def query_django_model(parsed: Dict[str, Any]) -> Dict[str, Any]:
return {
"status": "error",
"error": f"Model '{model_name}' not found in app '{app_label}'",
- "language": language
+ "language": language,
}
# Validate fields against model
@@ -592,7 +626,7 @@ def query_django_model(parsed: Dict[str, Any]) -> Dict[str, Any]:
return {
"status": "error",
"error": f"Invalid filters: {str(e)}",
- "language": language
+ "language": language,
}
# Handle aggregations
@@ -603,7 +637,7 @@ def query_django_model(parsed: Dict[str, Any]) -> Dict[str, Any]:
return {
"status": "error",
"error": result["error"],
- "language": language
+ "language": language,
}
chart_data = None
@@ -614,7 +648,7 @@ def query_django_model(parsed: Dict[str, Any]) -> Dict[str, Any]:
"status": "success",
"data": result,
"chart": chart_data,
- "language": language
+ "language": language,
}
# Handle regular queries
@@ -649,9 +683,9 @@ def query_django_model(parsed: Dict[str, Any]) -> Dict[str, Any]:
"total_count": len(data),
"fields": fields,
"model": model_name,
- "app": app_label
+ "app": app_label,
},
- "language": language
+ "language": language,
}
except Exception as e:
@@ -659,7 +693,7 @@ def query_django_model(parsed: Dict[str, Any]) -> Dict[str, Any]:
return {
"status": "error",
"error": f"Query execution failed: {str(e)}",
- "language": language
+ "language": language,
}
except Exception as e:
@@ -667,11 +701,13 @@ def query_django_model(parsed: Dict[str, Any]) -> Dict[str, Any]:
return {
"status": "error",
"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.
@@ -682,21 +718,39 @@ def determine_aggregation_type(prompt: str, fields: List[FieldAnalysis]) -> Opti
Returns:
Aggregation type or None
"""
- if any(pattern in prompt.lower() for pattern in ['average', 'avg', 'mean', 'معدل', 'متوسط']):
- return 'avg'
- elif any(pattern in prompt.lower() for pattern in ['sum', 'total', 'مجموع', 'إجمالي']):
- return 'sum'
- 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'
+ if any(
+ pattern in prompt.lower()
+ for pattern in ["average", "avg", "mean", "معدل", "متوسط"]
+ ):
+ return "avg"
+ elif any(
+ pattern in prompt.lower() for pattern in ["sum", "total", "مجموع", "إجمالي"]
+ ):
+ return "sum"
+ 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
- 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:
- return 'sum' # Default to sum for numeric fields
+ return "sum" # Default to sum for numeric fields
return None
@@ -713,32 +767,47 @@ def determine_chart_type(prompt: str, fields: List[FieldAnalysis]) -> Optional[s
Chart type or None
"""
# Check for explicit chart type mentions in prompt
- if any(term in prompt.lower() for term in ['line chart', 'time series', 'trend', 'رسم خطي', 'اتجاه']):
- return 'line'
- elif any(term in prompt.lower() for term in ['bar chart', 'histogram', 'column', 'رسم شريطي', 'أعمدة']):
- 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'
+ if any(
+ term in prompt.lower()
+ for term in ["line chart", "time series", "trend", "رسم خطي", "اتجاه"]
+ ):
+ return "line"
+ elif any(
+ term in prompt.lower()
+ for term in ["bar chart", "histogram", "column", "رسم شريطي", "أعمدة"]
+ ):
+ 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
- date_fields = [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']]
+ date_fields = [
+ 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:
- return 'line' # Time series data
+ return "line" # Time series data
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):
- return 'pie' # Single dimension data
+ return "pie" # Single dimension data
elif len(fields) > 2:
- return 'bar' # Multi-dimensional data
+ return "bar" # Multi-dimensional data
# Default
- return 'bar'
+ return "bar"
def analyze_prompt(prompt: str) -> Dict[str, Any]:
@@ -752,8 +821,8 @@ def analyze_prompt(prompt: str) -> Dict[str, Any]:
Dictionary with query results
"""
# Detect language
- language = "ar" if bool(re.search(r'[\u0600-\u06FF]', prompt)) else "en"
- filtered_apps = ['inventory']
+ language = "ar" if bool(re.search(r"[\u0600-\u06FF]", prompt)) else "en"
+ filtered_apps = ["inventory"]
try:
analyzer = DjangoModelAnalyzer()
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)
print(analysis)
-
if not analysis or not analysis.app_label or not analysis.model_name:
return {
"status": "error",
- "message": "تعذر العثور على النموذج المطلوب" if language == "ar" else "Missing model information",
- "language": language
+ "message": "تعذر العثور على النموذج المطلوب"
+ if language == "ar"
+ else "Missing model information",
+ "language": language,
}
query_params = {
"app_label": analysis.app_label,
"model_name": analysis.model_name,
"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": {},
"aggregation": determine_aggregation_type(prompt, analysis.relevant_fields),
"chart": determine_chart_type(prompt, analysis.relevant_fields),
- "language": language
+ "language": language,
}
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}")
return {
"status": "error",
- "error": "حدث خطأ أثناء تحليل الاستعلام" if language == "ar" else f"Error analyzing prompt: {str(e)}",
- "language": language
- }
\ No newline at end of file
+ "error": "حدث خطأ أثناء تحليل الاستعلام"
+ if language == "ar"
+ else f"Error analyzing prompt: {str(e)}",
+ "language": language,
+ }
diff --git a/haikalbot/apps.py b/haikalbot/apps.py
index 1034b56b..c0c9b3c4 100644
--- a/haikalbot/apps.py
+++ b/haikalbot/apps.py
@@ -2,5 +2,5 @@ from django.apps import AppConfig
class HaikalbotConfig(AppConfig):
- default_auto_field = 'django.db.models.BigAutoField'
- name = 'haikalbot'
+ default_auto_field = "django.db.models.BigAutoField"
+ name = "haikalbot"
diff --git a/haikalbot/haikal_agent.py b/haikalbot/haikal_agent.py
index f09c9e52..b2ccffd2 100644
--- a/haikalbot/haikal_agent.py
+++ b/haikalbot/haikal_agent.py
@@ -63,7 +63,6 @@ class DatabaseType(Enum):
MYSQL = "mysql"
-
@dataclass
class DatabaseConnection:
db_type: DatabaseType
@@ -95,18 +94,23 @@ class DatabaseSchema(BaseModel):
description="Database schema with table names as keys and column info as values"
)
relationships: Optional[List[Dict[str, Any]]] = Field(
- default=None,
- description="Foreign key relationships between tables"
+ default=None, description="Foreign key relationships between tables"
)
class InsightRequest(BaseModel):
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")
limit: Optional[int] = Field(default=1000, description="Maximum number of results")
- language: Optional[str] = Field(default="auto", description="Response language preference")
- use_django: Optional[bool] = Field(default=True, description="Use Django database if available")
+ language: Optional[str] = Field(
+ default="auto", description="Response language preference"
+ )
+ use_django: Optional[bool] = Field(
+ default=True, description="Use Django database if available"
+ )
class DatabaseInsightSystem:
@@ -114,7 +118,7 @@ class DatabaseInsightSystem:
self.config = config or DatabaseConfig()
self.model = OpenAIModel(
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._setup_agents()
@@ -148,7 +152,7 @@ class DatabaseInsightSystem:
}
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]:
@@ -158,27 +162,31 @@ class DatabaseInsightSystem:
try:
# Get default database configuration
- db_config = settings.DATABASES.get('default', {})
+ db_config = settings.DATABASES.get("default", {})
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
- engine = db_config.get('ENGINE', '')
- db_name = db_config.get('NAME', '')
- host = db_config.get('HOST', 'localhost')
- port = db_config.get('PORT', None)
- user = db_config.get('USER', '')
- password = db_config.get('PASSWORD', '')
+ engine = db_config.get("ENGINE", "")
+ db_name = db_config.get("NAME", "")
+ host = db_config.get("HOST", "localhost")
+ port = db_config.get("PORT", None)
+ user = db_config.get("USER", "")
+ password = db_config.get("PASSWORD", "")
# Determine database type from engine
- if 'sqlite' in engine.lower():
+ if "sqlite" in engine.lower():
db_type = DatabaseType.SQLITE
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
port = port or 5432
- connection_string = f"postgresql://{user}:{password}@{host}:{port}/{db_name}"
- elif 'mysql' in engine.lower():
+ connection_string = (
+ f"postgresql://{user}:{password}@{host}:{port}/{db_name}"
+ )
+ elif "mysql" in engine.lower():
db_type = DatabaseType.MYSQL
port = port or 3306
connection_string = f"mysql://{user}:{password}@{host}:{port}/{db_name}"
@@ -193,7 +201,7 @@ class DatabaseInsightSystem:
host=host,
port=port,
user=user,
- password=password
+ password=password,
)
except Exception as e:
@@ -218,8 +226,7 @@ class DatabaseInsightSystem:
if request.database_path:
# Assume SQLite for direct file path
self.db_connection = DatabaseConnection(
- db_type=DatabaseType.SQLITE,
- connection_string=request.database_path
+ db_type=DatabaseType.SQLITE, connection_string=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})")
columns = []
for col in cursor.fetchall():
- columns.append({
- "name": col[1],
- "type": col[2],
- "notnull": bool(col[3]),
- "default_value": col[4],
- "primary_key": bool(col[5])
- })
+ columns.append(
+ {
+ "name": col[1],
+ "type": col[2],
+ "notnull": bool(col[3]),
+ "default_value": col[4],
+ "primary_key": bool(col[5]),
+ }
+ )
schema_data[table] = columns
# Get foreign key relationships
cursor.execute(f"PRAGMA foreign_key_list({table})")
for fk in cursor.fetchall():
- relationships.append({
- "from_table": table,
- "from_column": fk[3],
- "to_table": fk[2],
- "to_column": fk[4]
- })
+ relationships.append(
+ {
+ "from_table": table,
+ "from_column": fk[3],
+ "to_table": fk[2],
+ "to_column": fk[4],
+ }
+ )
conn.close()
return DatabaseSchema(tables=schema_data, relationships=relationships)
@@ -287,27 +298,33 @@ class DatabaseInsightSystem:
for field in model._meta.get_fields():
if not field.is_relation:
- columns.append({
- "name": field.name,
- "type": field.get_internal_type(),
- "notnull": not getattr(field, 'null', True),
- "primary_key": getattr(field, 'primary_key', False)
- })
+ columns.append(
+ {
+ "name": field.name,
+ "type": field.get_internal_type(),
+ "notnull": not getattr(field, "null", True),
+ "primary_key": getattr(field, "primary_key", False),
+ }
+ )
else:
# Handle relationships
- if hasattr(field, 'related_model') and field.related_model:
- relationships.append({
- "from_table": table_name,
- "from_column": field.name,
- "to_table": field.related_model._meta.db_table,
- "relationship_type": field.get_internal_type()
- })
+ if hasattr(field, "related_model") and field.related_model:
+ relationships.append(
+ {
+ "from_table": table_name,
+ "from_column": field.name,
+ "to_table": field.related_model._meta.db_table,
+ "relationship_type": field.get_internal_type(),
+ }
+ )
schema_data[table_name] = columns
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."""
if not POSTGRESQL_AVAILABLE:
raise ImportError("psycopg2 is not available")
@@ -325,47 +342,56 @@ class DatabaseInsightSystem:
FROM information_schema.tables
WHERE table_schema = 'public'
""")
- tables = [row['table_name'] for row in cursor.fetchall()]
+ tables = [row["table_name"] for row in cursor.fetchall()]
schema_data = {}
relationships = []
for table in tables:
# Get column information
- cursor.execute("""
+ cursor.execute(
+ """
SELECT column_name, data_type, is_nullable, column_default
FROM information_schema.columns
WHERE table_name = %s
ORDER BY ordinal_position
- """, (table,))
+ """,
+ (table,),
+ )
columns = []
for col in cursor.fetchall():
- columns.append({
- "name": col['column_name'],
- "type": col['data_type'],
- "notnull": col['is_nullable'] == 'NO',
- "default_value": col['column_default'],
- "primary_key": False # Will be updated below
- })
+ columns.append(
+ {
+ "name": col["column_name"],
+ "type": col["data_type"],
+ "notnull": col["is_nullable"] == "NO",
+ "default_value": col["column_default"],
+ "primary_key": False, # Will be updated below
+ }
+ )
# Get primary key information
- cursor.execute("""
+ cursor.execute(
+ """
SELECT column_name
FROM information_schema.key_column_usage
WHERE table_name = %s
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:
- if col['name'] in pk_columns:
- col['primary_key'] = True
+ if col["name"] in pk_columns:
+ col["primary_key"] = True
schema_data[table] = columns
# Get foreign key relationships
- cursor.execute("""
+ cursor.execute(
+ """
SELECT kcu.column_name,
ccu.table_name AS foreign_table_name,
ccu.column_name AS foreign_column_name
@@ -376,15 +402,19 @@ class DatabaseInsightSystem:
ON ccu.constraint_name = tc.constraint_name
WHERE tc.constraint_type = 'FOREIGN KEY'
AND tc.table_name = %s
- """, (table,))
+ """,
+ (table,),
+ )
for fk in cursor.fetchall():
- relationships.append({
- "from_table": table,
- "from_column": fk['column_name'],
- "to_table": fk['foreign_table_name'],
- "to_column": fk['foreign_column_name']
- })
+ relationships.append(
+ {
+ "from_table": table,
+ "from_column": fk["column_name"],
+ "to_table": fk["foreign_table_name"],
+ "to_column": fk["foreign_column_name"],
+ }
+ )
conn.close()
return DatabaseSchema(tables=schema_data, relationships=relationships)
@@ -404,6 +434,7 @@ class DatabaseInsightSystem:
# Parse connection string to get connection parameters
# Format: mysql://user:password@host:port/database
import urllib.parse
+
parsed = urllib.parse.urlparse(connection_string)
conn = pymysql.connect(
@@ -412,7 +443,7 @@ class DatabaseInsightSystem:
user=parsed.username,
password=parsed.password,
database=parsed.path[1:], # Remove leading slash
- cursorclass=pymysql.cursors.DictCursor
+ cursorclass=pymysql.cursors.DictCursor,
)
cursor = conn.cursor()
@@ -429,13 +460,15 @@ class DatabaseInsightSystem:
cursor.execute(f"DESCRIBE {table}")
columns = []
for col in cursor.fetchall():
- columns.append({
- "name": col['Field'],
- "type": col['Type'],
- "notnull": col['Null'] == 'NO',
- "default_value": col['Default'],
- "primary_key": col['Key'] == 'PRI'
- })
+ columns.append(
+ {
+ "name": col["Field"],
+ "type": col["Type"],
+ "notnull": col["Null"] == "NO",
+ "default_value": col["Default"],
+ "primary_key": col["Key"] == "PRI",
+ }
+ )
schema_data[table] = columns
@@ -451,12 +484,14 @@ class DatabaseInsightSystem:
""")
for fk in cursor.fetchall():
- relationships.append({
- "from_table": table,
- "from_column": fk['COLUMN_NAME'],
- "to_table": fk['REFERENCED_TABLE_NAME'],
- "to_column": fk['REFERENCED_COLUMN_NAME']
- })
+ relationships.append(
+ {
+ "from_table": table,
+ "from_column": fk["COLUMN_NAME"],
+ "to_table": fk["REFERENCED_TABLE_NAME"],
+ "to_column": fk["REFERENCED_COLUMN_NAME"],
+ }
+ )
conn.close()
return DatabaseSchema(tables=schema_data, relationships=relationships)
@@ -467,7 +502,7 @@ class DatabaseInsightSystem:
def _detect_language(self, text: str) -> str:
"""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"
def _execute_query_sync(self, query: str) -> List[Dict]:
@@ -480,13 +515,19 @@ class DatabaseInsightSystem:
raise ValueError("No database connection established")
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:
# return await self._execute_django_query(query)
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:
- 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:
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}")
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."""
try:
import psycopg2
@@ -548,7 +591,9 @@ class DatabaseInsightSystem:
logger.error(f"PostgreSQL query execution failed: {e}")
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."""
try:
import pymysql
@@ -562,7 +607,7 @@ class DatabaseInsightSystem:
user=parsed.username,
password=parsed.password,
database=parsed.path[1:],
- cursorclass=pymysql.cursors.DictCursor
+ cursorclass=pymysql.cursors.DictCursor,
)
cursor = conn.cursor()
@@ -576,7 +621,9 @@ class DatabaseInsightSystem:
logger.error(f"MySQL query execution failed: {e}")
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."""
if not data or not fields:
return None
@@ -613,7 +660,7 @@ class DatabaseInsightSystem:
"backgroundColor": [
f"rgba({50 + i * 30}, {100 + i * 25}, {200 + i * 20}, 0.7)"
for i in range(len(values))
- ]
+ ],
}
else:
# Multiple datasets for other chart types
@@ -627,21 +674,19 @@ class DatabaseInsightSystem:
value = 0
dataset_values.append(value)
- datasets.append({
- "label": field,
- "data": dataset_values,
- "backgroundColor": f"rgba({50 + i * 40}, {100 + i * 30}, 235, 0.6)",
- "borderColor": f"rgba({50 + i * 40}, {100 + i * 30}, 235, 1.0)",
- "borderWidth": 2
- })
+ datasets.append(
+ {
+ "label": field,
+ "data": dataset_values,
+ "backgroundColor": f"rgba({50 + i * 40}, {100 + i * 30}, 235, 0.6)",
+ "borderColor": f"rgba({50 + i * 40}, {100 + i * 30}, 235, 1.0)",
+ "borderWidth": 2,
+ }
+ )
except Exception as e:
logger.warning(f"Error processing field {field}: {e}")
- return {
- "type": chart_type,
- "labels": labels,
- "datasets": datasets
- }
+ return {"type": chart_type, "labels": labels, "datasets": datasets}
except Exception as e:
logger.error(f"Chart preparation failed: {e}")
@@ -659,14 +704,18 @@ class DatabaseInsightSystem:
"data": [],
"metadata": {},
"error": str(e),
- "language": "en"
+ "language": "en",
}
async def get_insights(self, request: InsightRequest) -> QueryResult:
"""Main method to get database insights from natural language prompt."""
try:
# 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
schema = await self.analyze_database_schema(request)
@@ -674,7 +723,7 @@ class DatabaseInsightSystem:
# Generate query plan using AI
query_response = await self.query_agent.run(
f"User prompt: {request.prompt}\nLanguage: {language}",
- database_schema=schema
+ database_schema=schema,
)
# Parse AI response
@@ -682,13 +731,15 @@ class DatabaseInsightSystem:
query_plan = json.loads(query_response.output)
except json.JSONDecodeError:
# 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:
query_plan = {
"sql_query": sql_match.group(0),
"chart_suggestion": "bar",
"expected_fields": [],
- "language": language
+ "language": language,
}
else:
raise ValueError("Could not parse AI response")
@@ -710,21 +761,25 @@ class DatabaseInsightSystem:
elif data:
# Use first few fields if no specific fields suggested
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
return QueryResult(
status="success",
- data=data[:request.limit] if data else [],
+ data=data[: request.limit] if data else [],
metadata={
"total_count": len(data) if data else 0,
"query": sql_query,
"analysis": query_plan.get("analysis", ""),
"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,
- language=language
+ language=language,
)
except Exception as e:
@@ -734,7 +789,7 @@ class DatabaseInsightSystem:
data=[],
metadata={},
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
@@ -798,4 +853,4 @@ def analyze_prompt_sync(prompt: str, **kwargs) -> Dict[str, Any]:
"""
system = DatabaseInsightSystem()
request = InsightRequest(prompt=prompt, **kwargs)
- return system.get_insights_sync(request)
\ No newline at end of file
+ return system.get_insights_sync(request)
diff --git a/haikalbot/management/commands/generate_support_yaml.py b/haikalbot/management/commands/generate_support_yaml.py
index cfdee27e..a844fe4b 100644
--- a/haikalbot/management/commands/generate_support_yaml.py
+++ b/haikalbot/management/commands/generate_support_yaml.py
@@ -9,7 +9,9 @@ from django.template.loaders.app_directories import get_app_template_dirs
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):
output_file = "haikal_kb.yaml"
@@ -22,7 +24,7 @@ class Command(BaseCommand):
"features": {},
"user_workflows": {}, # New section for step-by-step instructions
"templates": {},
- "glossary": {}
+ "glossary": {},
}
def extract_doc(item):
@@ -42,31 +44,48 @@ class Command(BaseCommand):
def get_all_model_classes():
all_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
def get_all_templates():
- template_dirs = get_app_template_dirs('templates')
+ template_dirs = get_app_template_dirs("templates")
templates = []
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 file in files:
- if file.endswith(('.html', '.htm', '.txt')):
- rel_path = os.path.relpath(os.path.join(root, file), template_dir)
- with open(os.path.join(root, file), 'r', encoding='utf-8', errors='ignore') as f:
+ if file.endswith((".html", ".htm", ".txt")):
+ rel_path = os.path.relpath(
+ os.path.join(root, file), template_dir
+ )
+ with open(
+ os.path.join(root, file),
+ "r",
+ encoding="utf-8",
+ errors="ignore",
+ ) as f:
try:
content = f.read()
# Extract template comment documentation if it exists
doc = ""
- if '{# DOC:' in content and '#}' in content:
- doc_parts = content.split('{# DOC:')
+ if "{# DOC:" in content and "#}" in content:
+ doc_parts = content.split("{# DOC:")
for part in doc_parts[1:]:
- if '#}' in part:
- doc += part.split('#}')[0].strip() + "\n"
+ if "#}" in part:
+ doc += (
+ part.split("#}")[0].strip() + "\n"
+ )
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
templates.append((app_name, rel_path, doc.strip()))
@@ -75,17 +94,24 @@ class Command(BaseCommand):
# Look for workflow documentation files
def get_workflow_docs():
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):
for file in os.listdir(workflow_dir):
- if file.endswith('.yaml') or file.endswith('.yml'):
+ if file.endswith(".yaml") or file.endswith(".yml"):
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)
- for workflow_name, workflow_info in workflow_data.items():
+ for (
+ workflow_name,
+ workflow_info,
+ ) in workflow_data.items():
workflows[workflow_name] = workflow_info
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
# Extract views
@@ -96,26 +122,28 @@ class Command(BaseCommand):
kb["features"][name] = {
"description": doc,
"source": f"{app}.views.{name}",
- "type": "view_function"
+ "type": "view_function",
}
# Look for @workflow decorator or WORKFLOW tag in docstring
- if hasattr(obj, 'workflow_steps') or 'WORKFLOW:' in doc:
- workflow_name = name.replace('_', ' ').title()
+ if hasattr(obj, "workflow_steps") or "WORKFLOW:" in doc:
+ workflow_name = name.replace("_", " ").title()
steps = []
- if hasattr(obj, 'workflow_steps'):
+ if hasattr(obj, "workflow_steps"):
steps = obj.workflow_steps
- elif 'WORKFLOW:' in doc:
- workflow_section = doc.split('WORKFLOW:')[1].strip()
- steps_text = workflow_section.split('\n')
- steps = [step.strip() for step in steps_text if step.strip()]
+ elif "WORKFLOW:" in doc:
+ workflow_section = doc.split("WORKFLOW:")[1].strip()
+ steps_text = workflow_section.split("\n")
+ steps = [
+ step.strip() for step in steps_text if step.strip()
+ ]
if steps:
kb["user_workflows"][workflow_name] = {
"description": f"How to {name.replace('_', ' ')}",
"steps": steps,
- "source": f"{app}.views.{name}"
+ "source": f"{app}.views.{name}",
}
# Extract models
@@ -124,7 +152,7 @@ class Command(BaseCommand):
kb["features"][name] = {
"description": doc,
"source": f"{app}.models.{name}",
- "type": "model_class"
+ "type": "model_class",
}
# Extract templates
@@ -134,7 +162,7 @@ class Command(BaseCommand):
kb["templates"][template_id] = {
"description": doc,
"path": template_path,
- "app": app
+ "app": app,
}
# Add workflow documentation
@@ -153,9 +181,9 @@ class Command(BaseCommand):
"Select the car series from the available options",
"Select the trim level for the car",
"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": {
"description": "How to create a new invoice",
@@ -167,13 +195,15 @@ class Command(BaseCommand):
"Select the car(s) to include in the invoice",
"Add any additional services or parts by clicking 'Add Item'",
"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:
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}")
+ )
diff --git a/haikalbot/migrations/0001_initial.py b/haikalbot/migrations/0001_initial.py
index 6a1928ce..9f5ebf66 100644
--- a/haikalbot/migrations/0001_initial.py
+++ b/haikalbot/migrations/0001_initial.py
@@ -7,44 +7,90 @@ from django.db import migrations, models
class Migration(migrations.Migration):
-
initial = True
dependencies = [
- ('inventory', '__first__'),
+ ("inventory", "__first__"),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
- name='AnalysisCache',
+ name="AnalysisCache",
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)),
- ('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)),
+ (
+ "id",
+ models.BigAutoField(
+ auto_created=True,
+ primary_key=True,
+ serialize=False,
+ verbose_name="ID",
+ ),
+ ),
+ ("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={
- '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')],
+ "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"
+ ),
+ ],
},
),
migrations.CreateModel(
- name='ChatLog',
+ name="ChatLog",
fields=[
- ('id', models.BigAutoField(auto_created=True, 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')),
+ (
+ "id",
+ models.BigAutoField(
+ auto_created=True,
+ 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={
- 'ordering': ['-timestamp'],
- 'indexes': [models.Index(fields=['dealer', 'timestamp'], name='haikalbot_c_dealer__6f8d63_idx')],
+ "ordering": ["-timestamp"],
+ "indexes": [
+ models.Index(
+ fields=["dealer", "timestamp"],
+ name="haikalbot_c_dealer__6f8d63_idx",
+ )
+ ],
},
),
]
diff --git a/haikalbot/models.py b/haikalbot/models.py
index fed58f8e..6637aafd 100644
--- a/haikalbot/models.py
+++ b/haikalbot/models.py
@@ -24,15 +24,18 @@ class ChatLog(models.Model):
:ivar timestamp: The date and time when the chat log entry was created.
: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()
chatbot_response = models.TextField()
timestamp = models.DateTimeField(auto_now_add=True, db_index=True)
class Meta:
- ordering = ['-timestamp']
+ ordering = ["-timestamp"]
indexes = [
- models.Index(fields=['dealer', 'timestamp']),
+ models.Index(fields=["dealer", "timestamp"]),
]
def __str__(self):
@@ -62,8 +65,11 @@ class AnalysisCache(models.Model):
:ivar result: The cached analysis result
:type result: dict
"""
+
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)
created_at = models.DateTimeField(default=timezone.now)
updated_at = models.DateTimeField(auto_now=True)
@@ -72,8 +78,8 @@ class AnalysisCache(models.Model):
class Meta:
indexes = [
- models.Index(fields=['prompt_hash', 'dealer_id']),
- models.Index(fields=['expires_at']),
+ models.Index(fields=["prompt_hash", "dealer_id"]),
+ models.Index(fields=["expires_at"]),
]
verbose_name_plural = "Analysis caches"
diff --git a/haikalbot/tests.py b/haikalbot/tests.py
index 49290204..a39b155a 100644
--- a/haikalbot/tests.py
+++ b/haikalbot/tests.py
@@ -1,2 +1 @@
-
# Create your tests here.
diff --git a/haikalbot/utils/ask_haikalbot.py b/haikalbot/utils/ask_haikalbot.py
index bcf11aea..cb8ebf12 100644
--- a/haikalbot/utils/ask_haikalbot.py
+++ b/haikalbot/utils/ask_haikalbot.py
@@ -43,10 +43,7 @@ Provide a clear step-by-step guide with numbered instructions. Include:
Helpful Step-by-Step Instructions:"""
-PROMPT = PromptTemplate(
- template=template,
- input_variables=["context", "question"]
-)
+PROMPT = PromptTemplate(template=template, input_variables=["context", "question"])
# Setup QA chain
qa = RetrievalQA.from_chain_type(
@@ -54,23 +51,25 @@ qa = RetrievalQA.from_chain_type(
chain_type="stuff",
retriever=index.vectorstore.as_retriever(),
return_source_documents=True,
- chain_type_kwargs={"prompt": PROMPT}
+ chain_type_kwargs={"prompt": PROMPT},
)
+
# Function to run a query
def ask_haikal(query):
response = qa.invoke({"query": query})
- print("\n" + "="*50)
+ print("\n" + "=" * 50)
print(f"Question: {query}")
- print("="*50)
+ print("=" * 50)
print("\nAnswer:")
print(response["result"])
print("\nSources:")
for doc in response["source_documents"]:
print(f"- {doc.metadata.get('source', 'Unknown source')}")
- print("="*50)
+ print("=" * 50)
return response["result"]
+
# # Example query
# if __name__ == "__main__":
# query = "How do I add a new car to the inventory? answer in Arabic"
diff --git a/haikalbot/utils/export.py b/haikalbot/utils/export.py
index f2701671..41cb1341 100644
--- a/haikalbot/utils/export.py
+++ b/haikalbot/utils/export.py
@@ -15,17 +15,16 @@ def export_to_excel(self, data, filename):
HttpResponse: Response with Excel file
"""
-
# Convert data to DataFrame
df = pd.DataFrame(data)
# Create Excel file in memory
excel_file = BytesIO()
- with pd.ExcelWriter(excel_file, engine='xlsxwriter') as writer:
- df.to_excel(writer, sheet_name='Model Analysis', index=False)
+ with pd.ExcelWriter(excel_file, engine="xlsxwriter") as writer:
+ df.to_excel(writer, sheet_name="Model Analysis", index=False)
# Auto-adjust columns width
- worksheet = writer.sheets['Model Analysis']
+ worksheet = writer.sheets["Model Analysis"]
for i, col in enumerate(df.columns):
max_width = max(df[col].astype(str).map(len).max(), len(col)) + 2
worksheet.set_column(i, i, max_width)
@@ -34,9 +33,9 @@ def export_to_excel(self, data, filename):
excel_file.seek(0)
response = HttpResponse(
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
@@ -52,7 +51,6 @@ def export_to_csv(self, data, filename):
HttpResponse: Response with CSV file
"""
-
# Convert data to DataFrame
df = pd.DataFrame(data)
@@ -61,6 +59,6 @@ def export_to_csv(self, data, filename):
df.to_csv(csv_file, index=False)
# Set up response
- response = HttpResponse(csv_file.getvalue(), content_type='text/csv')
- response['Content-Disposition'] = f'attachment; filename="{filename}.csv"'
+ response = HttpResponse(csv_file.getvalue(), content_type="text/csv")
+ response["Content-Disposition"] = f'attachment; filename="{filename}.csv"'
return response
diff --git a/haikalbot/utils/response_formatter.py b/haikalbot/utils/response_formatter.py
index ea9e6ea1..438485a3 100644
--- a/haikalbot/utils/response_formatter.py
+++ b/haikalbot/utils/response_formatter.py
@@ -1,10 +1,10 @@
def format_response(prompt, language, request_id, timestamp):
"""
Format a standardized response structure based on language.
-
+
This utility creates a consistent response structure with the appropriate
keys based on the specified language.
-
+
:param prompt: The original user prompt
:type prompt: str
:param language: Language code ('en' or 'ar')
@@ -16,28 +16,28 @@ def format_response(prompt, language, request_id, timestamp):
:return: Formatted response structure
:rtype: dict
"""
- if language == 'ar':
+ if language == "ar":
return {
- 'حالة': "نجاح",
- 'معرف_الطلب': request_id,
- 'الطابع_الزمني': timestamp,
- 'الاستعلام': prompt,
- 'التحليلات': []
+ "حالة": "نجاح",
+ "معرف_الطلب": request_id,
+ "الطابع_الزمني": timestamp,
+ "الاستعلام": prompt,
+ "التحليلات": [],
}
else:
return {
- 'status': "success",
- 'request_id': request_id,
- 'timestamp': timestamp,
- 'prompt': prompt,
- 'insights': []
+ "status": "success",
+ "request_id": request_id,
+ "timestamp": timestamp,
+ "prompt": prompt,
+ "insights": [],
}
-def format_error_response(message, status_code, language='en'):
+def format_error_response(message, status_code, language="en"):
"""
Format a standardized error response.
-
+
:param message: Error message
:type message: str
:param status_code: HTTP status code
@@ -47,24 +47,16 @@ def format_error_response(message, status_code, language='en'):
:return: Formatted error response
:rtype: dict
"""
- if language == 'ar':
- return {
- 'حالة': "خطأ",
- 'رسالة': message,
- 'رمز_الحالة': status_code
- }
+ if language == "ar":
+ return {"حالة": "خطأ", "رسالة": message, "رمز_الحالة": status_code}
else:
- return {
- 'status': "error",
- 'message': message,
- 'status_code': status_code
- }
+ return {"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.
-
+
:param insights: Raw insights data
:type insights: dict
:param language: Language code ('en' or 'ar')
@@ -73,47 +65,55 @@ def format_insights_for_display(insights, language='en'):
:rtype: str
"""
formatted_text = ""
-
+
# Determine keys based on language
- insights_key = 'التحليلات' if language == 'ar' else 'insights'
- recs_key = 'التوصيات' if language == 'ar' else 'recommendations'
-
+ insights_key = "التحليلات" if language == "ar" else "insights"
+ recs_key = "التوصيات" if language == "ar" else "recommendations"
+
# Format insights
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
-
+
for insight in insights[insights_key]:
if isinstance(insight, dict):
# Add insight type as header
- if 'type' in insight or 'نوع' in insight:
- type_key = 'نوع' if language == 'ar' else 'type'
- insight_type = insight.get(type_key, insight.get('type', insight.get('نوع', '')))
+ if "type" in insight or "نوع" in insight:
+ type_key = "نوع" if language == "ar" else "type"
+ insight_type = insight.get(
+ type_key, insight.get("type", insight.get("نوع", ""))
+ )
formatted_text += f"### {insight_type}\n\n"
-
+
# Format results if present
- results_key = 'النتائج' if language == 'ar' else 'results'
+ results_key = "النتائج" if language == "ar" else "results"
if results_key in insight:
for result in insight[results_key]:
- model_key = 'النموذج' if language == 'ar' else 'model'
- error_key = 'خطأ' if language == 'ar' else 'error'
- count_key = 'العدد' if language == 'ar' else 'count'
-
- model_name = result.get(model_key, result.get('model', ''))
-
+ model_key = "النموذج" if language == "ar" else "model"
+ error_key = "خطأ" if language == "ar" else "error"
+ count_key = "العدد" if language == "ar" else "count"
+
+ model_name = result.get(model_key, result.get("model", ""))
+
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:
- formatted_text += f"- **{model_name}**: {result[count_key]}\n"
-
+ formatted_text += (
+ f"- **{model_name}**: {result[count_key]}\n"
+ )
+
formatted_text += "\n"
-
+
# Format recommendations
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
-
+
for rec in insights[recs_key]:
formatted_text += f"- {rec}\n"
-
+
return formatted_text
diff --git a/haikalbot/views.py b/haikalbot/views.py
index 9467391d..9451560d 100644
--- a/haikalbot/views.py
+++ b/haikalbot/views.py
@@ -5,20 +5,22 @@ from django.utils.translation import gettext as _
from django.views import View
import logging
from .ai_agent import analyze_prompt
+
# from .haikal_agent import DatabaseInsightSystem, analyze_prompt_sync
from .utils.export import export_to_excel, export_to_csv
logger = logging.getLogger(__name__)
# analyze_prompt_ai = DatabaseInsightSystem
+
class HaikalBot(LoginRequiredMixin, View):
def get(self, request, *args, **kwargs):
"""
Render the chat interface.
"""
context = {
- 'dark_mode': request.session.get('dark_mode', False),
- 'page_title': _('AI Assistant')
+ "dark_mode": request.session.get("dark_mode", False),
+ "page_title": _("AI Assistant"),
}
return render(request, "haikalbot/chat.html", context)
@@ -31,7 +33,9 @@ class HaikalBot(LoginRequiredMixin, View):
language = request.POST.get("language", request.LANGUAGE_CODE)
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)
try:
result = analyze_prompt(prompt)
@@ -54,8 +58,11 @@ class HaikalBot(LoginRequiredMixin, View):
if language == "ar":
error_msg = "حدث خطأ أثناء معالجة طلبك."
- return JsonResponse({
- "status": "error",
- "error": error_msg,
- "details": str(e) if request.user.is_staff else None
- }, status=500)
+ return JsonResponse(
+ {
+ "status": "error",
+ "error": error_msg,
+ "details": str(e) if request.user.is_staff else None,
+ },
+ status=500,
+ )
diff --git a/inventory/admin.py b/inventory/admin.py
index 6983bb84..306c3a98 100644
--- a/inventory/admin.py
+++ b/inventory/admin.py
@@ -2,6 +2,7 @@
from django.contrib import admin
from . import models
from django_ledger import models as ledger_models
+
# from django_pdf_actions.actions import export_to_pdf_landscape, export_to_pdf_portrait
# from appointment import models as appointment_models
from import_export.admin import ExportMixin
@@ -18,10 +19,12 @@ from import_export.resources import ModelResource
# class CarSeriesAdmin(ExportMixin, admin.ModelAdmin):
# resource_class = CarSerieResource
+
class CarTrimResource(ModelResource):
class Meta:
model = models.CarTrim
+
@admin.register(models.CarTrim)
class CarTrimAdmin(ExportMixin, admin.ModelAdmin):
resource_class = CarTrimResource
@@ -71,44 +74,55 @@ admin.site.register(models.DealersMake)
@admin.register(models.Car)
class CarAdmin(admin.ModelAdmin):
- search_fields = ('vin',)
+ search_fields = ("vin",)
# actions = [export_to_pdf_landscape, export_to_pdf_portrait]
+
@admin.register(models.CarMake)
class CarMakeAdmin(admin.ModelAdmin):
- list_display = ('name', 'arabic_name', 'is_sa_import')
- search_fields = ('name', 'arabic_name')
- list_filter = ('is_sa_import', 'name',)
+ list_display = ("name", "arabic_name", "is_sa_import")
+ search_fields = ("name", "arabic_name")
+ list_filter = (
+ "is_sa_import",
+ "name",
+ )
# actions = [export_to_pdf_landscape, export_to_pdf_portrait]
class Meta:
verbose_name = "Car Make"
- ordering = ('name',)
+ ordering = ("name",)
@admin.register(models.CarModel)
class CarModelAdmin(admin.ModelAdmin):
- list_display = ('name', 'arabic_name', 'id_car_make', 'get_is_sa_import')
- search_fields = ('id_car_model', 'name', 'arabic_name')
- list_filter = ('id_car_make__is_sa_import', 'id_car_make')
- sortable_by = ['name', 'arabic_name', 'id_car_make']
+ list_display = ("name", "arabic_name", "id_car_make", "get_is_sa_import")
+ search_fields = ("id_car_model", "name", "arabic_name")
+ list_filter = ("id_car_make__is_sa_import", "id_car_make")
+ sortable_by = ["name", "arabic_name", "id_car_make"]
def get_is_sa_import(self, obj):
return obj.id_car_make.is_sa_import
+
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:
verbose_name = "Car Model"
- ordering = ('name',)
+ ordering = ("name",)
@admin.register(models.CarSerie)
class CarSeriesAdmin(admin.ModelAdmin):
- list_display = ('name', 'arabic_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',)
+ list_display = (
+ "name",
+ "arabic_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:
verbose_name = "Car Series"
@@ -130,9 +144,9 @@ class CarSeriesAdmin(admin.ModelAdmin):
@admin.register(models.CarSpecification)
class CarSpecificationAdmin(admin.ModelAdmin):
- list_display = ('name', 'arabic_name', 'id_parent')
- search_fields = ('name', 'id_parent')
- list_filter = ('id_parent',)
+ list_display = ("name", "arabic_name", "id_parent")
+ search_fields = ("name", "id_parent")
+ list_filter = ("id_parent",)
class Meta:
verbose_name = "Car Specification"
@@ -140,12 +154,14 @@ class CarSpecificationAdmin(admin.ModelAdmin):
@admin.register(models.CarOption)
class CarOptionAdmin(admin.ModelAdmin):
- list_display = ('name', 'arabic_name', 'id_parent')
- search_fields = ('name', 'id_parent')
+ list_display = ("name", "arabic_name", "id_parent")
+ search_fields = ("name", "id_parent")
+
# list_filter = ('id_parent',)
class Meta:
verbose_name = "Car Option"
+
# @admin.register(models.UserActivityLog)
# class UserActivityLogAdmin(admin.ModelAdmin):
# list_display = ('user', 'action', 'timestamp')
@@ -156,5 +172,3 @@ class CarOptionAdmin(admin.ModelAdmin):
# class ItemTransactionModelAdmin(admin.ModelAdmin):
# actions = [export_to_pdf_landscape, export_to_pdf_portrait]
-
-
diff --git a/inventory/apps.py b/inventory/apps.py
index ad2bcf1a..b13bce27 100644
--- a/inventory/apps.py
+++ b/inventory/apps.py
@@ -1,8 +1,9 @@
from django.apps import AppConfig
+
class InventoryConfig(AppConfig):
- default_auto_field = 'django.db.models.BigAutoField'
- name = 'inventory'
+ default_auto_field = "django.db.models.BigAutoField"
+ name = "inventory"
def ready(self):
import inventory.signals
diff --git a/inventory/context_processors.py b/inventory/context_processors.py
index 55cff5ef..7f749d6a 100644
--- a/inventory/context_processors.py
+++ b/inventory/context_processors.py
@@ -1,5 +1,6 @@
from django.conf import settings
+
def currency_context(request):
"""
Provides a context dictionary containing the currency setting. This is typically
@@ -14,9 +15,7 @@ def currency_context(request):
project's CURRENCY setting.
:rtype: dict
"""
- return {
- 'CURRENCY': settings.CURRENCY
- }
+ return {"CURRENCY": settings.CURRENCY}
def breadcrumbs(request):
@@ -37,8 +36,8 @@ def breadcrumbs(request):
:rtype: dict
"""
breadcrumbs = []
- path = request.path.strip('/').split('/')
+ path = request.path.strip("/").split("/")
for i in range(len(path)):
- url = '/' + '/'.join(path[:i+1]) + '/'
- breadcrumbs.append({'name': path[i].capitalize(), 'url': url})
- return {'breadcrumbs': breadcrumbs}
\ No newline at end of file
+ url = "/" + "/".join(path[: i + 1]) + "/"
+ breadcrumbs.append({"name": path[i].capitalize(), "url": url})
+ return {"breadcrumbs": breadcrumbs}
diff --git a/inventory/filters.py b/inventory/filters.py
index 8fdb0206..cced1175 100644
--- a/inventory/filters.py
+++ b/inventory/filters.py
@@ -1,6 +1,7 @@
from django_ledger.models import AccountModel
import django_filters
+
class AccountModelFilter(django_filters.FilterSet):
"""
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.
:type fields: list(str)
"""
+
class Meta:
model = AccountModel
- fields = ['code', 'name','role']
\ No newline at end of file
+ fields = ["code", "name", "role"]
diff --git a/inventory/forms.py b/inventory/forms.py
index 7ace0c01..3debc3bf 100644
--- a/inventory/forms.py
+++ b/inventory/forms.py
@@ -1270,6 +1270,7 @@ class OpportunityForm(forms.ModelForm):
widgets = {
"expected_revenue": forms.NumberInput(attrs={"readonly": "readonly"}),
}
+
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# Add a visible number input to display the current value
@@ -1354,19 +1355,27 @@ class SaleOrderForm(forms.ModelForm):
class Meta:
model = SaleOrder
fields = [
- "estimate",
- "payment_method",
- "opportunity",
- "agreed_price",
- "down_payment_amount",
- "loan_amount",
+ "customer",
"expected_delivery_date",
+ "estimate",
+ "opportunity",
"comments",
- "status"
+ "order_date",
+ "status",
]
widgets = {
- "comments": forms.Textarea(attrs={"rows": 3}),
- "expected_delivery_date": forms.DateInput(attrs={"type": "date"}),
+ "expected_delivery_date": forms.DateInput(
+ attrs={"type": "date", "label": _("Expected Delivery Date")}
+ ),
+ "order_date": forms.DateInput(
+ attrs={"type": "date", "label": _("Order Date")}
+ ),
+ "customer": forms.Select(
+ attrs={
+ "class": "form-control",
+ "label": _("Customer"),
+ }
+ ),
}
@@ -1909,6 +1918,7 @@ class StaffTaskForm(forms.ModelForm):
#############################################################
+
class ItemInventoryForm(forms.Form):
make = forms.ModelChoiceField(
queryset=CarMake.objects.all(),
@@ -1932,65 +1942,64 @@ class ItemInventoryForm(forms.Form):
)
-
-
#####################################################################
+
class CSVUploadForm(forms.Form):
dealer = forms.ModelChoiceField(
- queryset=Dealer.objects.all(),
- label=_('Dealer'),
- widget=forms.HiddenInput()
+ queryset=Dealer.objects.all(), label=_("Dealer"), widget=forms.HiddenInput()
)
vendor = forms.ModelChoiceField(
queryset=Vendor.objects.all(),
- label=_('Vendor'),
- widget=forms.Select(attrs={'class': 'form-select'}),
- required=True
+ label=_("Vendor"),
+ widget=forms.Select(attrs={"class": "form-select"}),
+ required=True,
)
year = forms.IntegerField(
- label=_('Year'),
- widget=forms.NumberInput(attrs={'class': 'form-control'}),
- required=True
+ label=_("Year"),
+ widget=forms.NumberInput(attrs={"class": "form-control"}),
+ required=True,
)
exterior = forms.ModelChoiceField(
queryset=ExteriorColors.objects.all(),
- label=_('Exterior Color'),
- widget=forms.RadioSelect(attrs={'class': 'form-select'}),
- required=True
+ label=_("Exterior Color"),
+ widget=forms.RadioSelect(attrs={"class": "form-select"}),
+ required=True,
)
interior = forms.ModelChoiceField(
queryset=InteriorColors.objects.all(),
- label=_('Interior Color'),
- widget=forms.RadioSelect(attrs={'class': 'form-select'}),
- required=True
+ label=_("Interior Color"),
+ widget=forms.RadioSelect(attrs={"class": "form-select"}),
+ required=True,
)
receiving_date = forms.DateField(
- label=_('Receiving Date'),
- widget=forms.DateInput(attrs={'type': 'date', 'class': 'form-control'}),
- required=True
+ label=_("Receiving Date"),
+ widget=forms.DateInput(attrs={"type": "date", "class": "form-control"}),
+ required=True,
)
def clean_csv_file(self):
- csv_file = self.cleaned_data['csv_file']
- if not csv_file.name.endswith('.csv'):
- raise forms.ValidationError(_('File is not a CSV file'))
+ csv_file = self.cleaned_data["csv_file"]
+ if not csv_file.name.endswith(".csv"):
+ raise forms.ValidationError(_("File is not a CSV file"))
# Read and validate CSV structure
try:
- csv_data = TextIOWrapper(csv_file.file, encoding='utf-8')
+ csv_data = TextIOWrapper(csv_file.file, encoding="utf-8")
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):
missing = set(required_fields) - set(reader.fieldnames)
raise forms.ValidationError(
- _('CSV is missing required columns: %(missing)s'),
- params={'missing': ', '.join(missing)}
+ _("CSV is missing required columns: %(missing)s"),
+ params={"missing": ", ".join(missing)},
)
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
csv_file.file.seek(0)
- return csv_file
\ No newline at end of file
+ return csv_file
diff --git a/inventory/haikalna.py b/inventory/haikalna.py
index 7d4e9297..a66d40d4 100644
--- a/inventory/haikalna.py
+++ b/inventory/haikalna.py
@@ -3,15 +3,57 @@ import re
def vin_year(vin_char: str) -> int:
YEAR_MAPPING = {
- 'A': 1980, 'B': 1981, 'C': 1982, 'D': 1983, 'E': 1984, 'F': 1985,
- 'G': 1986, 'H': 1987, '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,
+ "A": 1980,
+ "B": 1981,
+ "C": 1982,
+ "D": 1983,
+ "E": 1984,
+ "F": 1985,
+ "G": 1986,
+ "H": 1987,
+ "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
@@ -19,7 +61,9 @@ def vin_year(vin_char: str) -> int:
# Check if the character is valid
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 YEAR_MAPPING[vin_char]
@@ -1355,452 +1399,425 @@ wmi_manufacturer_mapping = {
"MM0": "Mazda",
"MM6": "Mazda",
"MM7": "Mazda",
- "MM8": "Mazda"
+ "MM8": "Mazda",
}
def decode_vds(manufacturer, vds):
# Mapping of manufacturers to their VDS positions and corresponding models
vds_model_mapping = {
- 'Honda': {
+ "Honda": {
1: {
- 'F': 'Agila',
- 'G': 'Insignia',
- 'J': 'Mokka',
- 'L': 'Antara',
- 'M': 'Movano',
- 'P': ['Astra J', 'Zafira C'],
- 'R': 'Astra GTC J',
- 'S': 'Meriva',
- 'V': 'Combo II',
- 'W': 'Cascada',
+ "F": "Agila",
+ "G": "Insignia",
+ "J": "Mokka",
+ "L": "Antara",
+ "M": "Movano",
+ "P": ["Astra J", "Zafira C"],
+ "R": "Astra GTC J",
+ "S": "Meriva",
+ "V": "Combo II",
+ "W": "Cascada",
}
},
-
- 'Kia': {
+ "Kia": {
1: {
- 'A': ['Rio', 'EV9'],
- 'C': ['Niro', 'EV6'],
- 'D': 'Rio',
- 'E': ['Stinger', 'Seltos'],
- 'F': ['Forte', 'K4'],
- 'G': ['Optima', 'Magentis', 'K5'],
- 'H': 'Rondo',
- 'J': 'Soul',
- 'K': ['Mohave', 'Sorento', 'Sportage'],
- 'L': ['Cadenza', 'K9'],
- 'M': 'Sedona',
- 'N': 'Carnival',
- 'P': ['Sportage', 'Sorento', 'Telluride'],
- 'R': 'Sorento',
- 'S': 'K9'
+ "A": ["Rio", "EV9"],
+ "C": ["Niro", "EV6"],
+ "D": "Rio",
+ "E": ["Stinger", "Seltos"],
+ "F": ["Forte", "K4"],
+ "G": ["Optima", "Magentis", "K5"],
+ "H": "Rondo",
+ "J": "Soul",
+ "K": ["Mohave", "Sorento", "Sportage"],
+ "L": ["Cadenza", "K9"],
+ "M": "Sedona",
+ "N": "Carnival",
+ "P": ["Sportage", "Sorento", "Telluride"],
+ "R": "Sorento",
+ "S": "K9",
}
},
-
- 'Peugeot': {
+ "Peugeot": {
1: {
- 'A': '604',
- '2': '206',
- '8': ['406', '508'],
- '6': '407',
- '9': '308',
- "4": '308',
- 'B': 'Expert',
- 'C': ['208', '504'],
- 'D': '301',
- '3': '307',
- '7': ['306', 'Partner'],
- 'U': '2008',
- 'M': '3008',
+ "A": "604",
+ "2": "206",
+ "8": ["406", "508"],
+ "6": "407",
+ "9": "308",
+ "4": "308",
+ "B": "Expert",
+ "C": ["208", "504"],
+ "D": "301",
+ "3": "307",
+ "7": ["306", "Partner"],
+ "U": "2008",
+ "M": "3008",
}
},
-
- 'Toyota': {
+ "Toyota": {
1: { # 5th character in VDS
- '0': 'MR2 Spyder',
- '1': 'Tundra',
- '3': ['Echo', 'Yaris'],
- 'A': ['Highlander', 'Sequoia', 'Celica', 'Supra'],
- 'B': 'Avalon',
- 'C': ['Sienna', 'Previa'],
- 'D': 'T100',
- 'E': ['Corolla', 'Matrix'],
- 'F': 'FJ Cruiser',
- 'G': 'Hilux',
- 'H': 'Highlander',
- 'J': 'Land Cruiser',
- 'K': 'Camry',
- 'L': ['Tercel', 'Paseo'],
- 'M': 'Previa',
- 'N': 'Tacoma',
- 'P': 'Camry',
- 'R': ['4Runner', 'Corolla'],
- 'T': 'Celica FWD',
- 'U': 'Prius',
- 'V': 'RAV4',
- 'W': 'MR2 non Spyder',
- 'X': 'Cressida',
+ "0": "MR2 Spyder",
+ "1": "Tundra",
+ "3": ["Echo", "Yaris"],
+ "A": ["Highlander", "Sequoia", "Celica", "Supra"],
+ "B": "Avalon",
+ "C": ["Sienna", "Previa"],
+ "D": "T100",
+ "E": ["Corolla", "Matrix"],
+ "F": "FJ Cruiser",
+ "G": "Hilux",
+ "H": "Highlander",
+ "J": "Land Cruiser",
+ "K": "Camry",
+ "L": ["Tercel", "Paseo"],
+ "M": "Previa",
+ "N": "Tacoma",
+ "P": "Camry",
+ "R": ["4Runner", "Corolla"],
+ "T": "Celica FWD",
+ "U": "Prius",
+ "V": "RAV4",
+ "W": "MR2 non Spyder",
+ "X": "Cressida",
}
},
-
- 'Nissan': {
+ "Nissan": {
2: {
- 'A': ['Armada', 'Titan', 'Maxima'],
- 'B': 'Sentra',
- 'C': 'Versa (07-11)',
- 'D': ['Truck', 'Xterra (00-04)', 'Frontier'],
- 'J': 'Maxima',
- 'L': 'Altima',
- 'N': 'Xterra (05-11)',
- 'P': 'Kicks',
- 'R': 'Pathfinder',
- 'S': ['240SX', 'Rogue (08-11)'],
- 'T': 'X Trail',
- 'U': 'Altima',
- 'Y': 'Patrol',
- 'Z': ['300Z', '350Z', 'Murano'],
+ "A": ["Armada", "Titan", "Maxima"],
+ "B": "Sentra",
+ "C": "Versa (07-11)",
+ "D": ["Truck", "Xterra (00-04)", "Frontier"],
+ "J": "Maxima",
+ "L": "Altima",
+ "N": "Xterra (05-11)",
+ "P": "Kicks",
+ "R": "Pathfinder",
+ "S": ["240SX", "Rogue (08-11)"],
+ "T": "X Trail",
+ "U": "Altima",
+ "Y": "Patrol",
+ "Z": ["300Z", "350Z", "Murano"],
}
},
-
- 'Renault': {
+ "Renault": {
2: { # 5th character in VDS
- '0': 'Twingo',
- '1': 'R4',
- '2': 'R25',
- '3': 'R4',
- '4': ['R21', 'Express'],
- '5': ['Clio I', 'Laguna', 'R19', 'Safrane'],
- 'A': ['Megane I', 'Master'],
- 'B': 'Clio II',
- 'C': 'Kangoo',
- 'D': 'Master',
- 'E': ['Espace III', 'Avantime'],
- 'G': 'Laguna II',
- 'H': 'Master Propulsion',
- 'J': ['Vel Satis', 'New Trafic'],
- 'K': 'Espace IV',
- 'L': 'Trafic',
- 'M': 'Megan II',
- 'P': 'Modus',
- 'S': ['Logan', 'Sandero', 'Duster', 'Dokker', 'Lodgy'],
- 'Y': 'Koleos',
+ "0": "Twingo",
+ "1": "R4",
+ "2": "R25",
+ "3": "R4",
+ "4": ["R21", "Express"],
+ "5": ["Clio I", "Laguna", "R19", "Safrane"],
+ "A": ["Megane I", "Master"],
+ "B": "Clio II",
+ "C": "Kangoo",
+ "D": "Master",
+ "E": ["Espace III", "Avantime"],
+ "G": "Laguna II",
+ "H": "Master Propulsion",
+ "J": ["Vel Satis", "New Trafic"],
+ "K": "Espace IV",
+ "L": "Trafic",
+ "M": "Megan II",
+ "P": "Modus",
+ "S": ["Logan", "Sandero", "Duster", "Dokker", "Lodgy"],
+ "Y": "Koleos",
}
},
-
# New Manufacturers Added Below
- 'Ford': {
+ "Ford": {
3: {
- 'A': ['Fiesta', 'Focus'],
- 'B': ['Mustang', 'Explorer'],
- 'C': 'Ranger',
- 'D': ['Escape', 'Edge'],
- 'E': 'F-150',
- 'F': 'Transit',
- 'G': 'Bronco',
- 'H': 'Expedition',
+ "A": ["Fiesta", "Focus"],
+ "B": ["Mustang", "Explorer"],
+ "C": "Ranger",
+ "D": ["Escape", "Edge"],
+ "E": "F-150",
+ "F": "Transit",
+ "G": "Bronco",
+ "H": "Expedition",
}
},
-
- 'BMW': {
+ "BMW": {
4: {
- '1': '1 Series',
- '2': '2 Series',
- '3': '3 Series',
- '4': '4 Series',
- '5': '5 Series',
- '6': '6 Series',
- '7': '7 Series',
- '8': '8 Series',
- 'X': 'X Series (SUV)',
- 'Z': 'Z Series (Roadster)',
+ "1": "1 Series",
+ "2": "2 Series",
+ "3": "3 Series",
+ "4": "4 Series",
+ "5": "5 Series",
+ "6": "6 Series",
+ "7": "7 Series",
+ "8": "8 Series",
+ "X": "X Series (SUV)",
+ "Z": "Z Series (Roadster)",
}
},
-
- 'Mercedes-Benz': {
+ "Mercedes-Benz": {
3: {
- 'A': 'A-Class',
- 'B': 'B-Class',
- 'C': 'C-Class',
- 'E': 'E-Class',
- 'G': 'G-Class',
- 'S': 'S-Class',
- 'V': 'V-Class',
- 'X': 'GL-Class',
+ "A": "A-Class",
+ "B": "B-Class",
+ "C": "C-Class",
+ "E": "E-Class",
+ "G": "G-Class",
+ "S": "S-Class",
+ "V": "V-Class",
+ "X": "GL-Class",
}
},
-
- 'Volkswagen': {
+ "Volkswagen": {
2: {
- '1': 'Golf',
- '2': 'Jetta',
- '3': 'Passat',
- '4': 'Tiguan',
- '5': 'Polo',
- '6': 'Arteon',
- '7': 'Atlas',
- '8': 'Touareg',
- '9': 'Beetle',
+ "1": "Golf",
+ "2": "Jetta",
+ "3": "Passat",
+ "4": "Tiguan",
+ "5": "Polo",
+ "6": "Arteon",
+ "7": "Atlas",
+ "8": "Touareg",
+ "9": "Beetle",
}
},
-
- 'Hyundai': {
+ "Hyundai": {
1: {
- 'A': 'Accent',
- 'B': 'Elantra',
- 'C': 'Sonata',
- 'D': 'Tucson',
- 'E': 'Santa Fe',
- 'F': 'Kona',
- 'G': 'Palisade',
- 'H': 'Veloster',
- 'J': 'Genesis',
+ "A": "Accent",
+ "B": "Elantra",
+ "C": "Sonata",
+ "D": "Tucson",
+ "E": "Santa Fe",
+ "F": "Kona",
+ "G": "Palisade",
+ "H": "Veloster",
+ "J": "Genesis",
}
},
-
- 'Chevrolet': {
+ "Chevrolet": {
2: {
- 'A': 'Camaro',
- 'B': 'Corvette',
- 'C': 'Cruze',
- 'D': 'Malibu',
- 'E': 'Equinox',
- 'F': 'Traverse',
- 'G': 'Silverado',
- 'H': 'Tahoe',
- 'J': 'Suburban',
+ "A": "Camaro",
+ "B": "Corvette",
+ "C": "Cruze",
+ "D": "Malibu",
+ "E": "Equinox",
+ "F": "Traverse",
+ "G": "Silverado",
+ "H": "Tahoe",
+ "J": "Suburban",
}
},
-
- 'Audi': {
+ "Audi": {
3: {
- '1': 'A1',
- '2': 'A2',
- '3': 'A3',
- '4': 'A4',
- '5': 'A5',
- '6': 'A6',
- '7': 'A7',
- '8': 'A8',
- 'Q': 'Q Series (SUV)',
- 'T': 'TT',
+ "1": "A1",
+ "2": "A2",
+ "3": "A3",
+ "4": "A4",
+ "5": "A5",
+ "6": "A6",
+ "7": "A7",
+ "8": "A8",
+ "Q": "Q Series (SUV)",
+ "T": "TT",
}
},
-
- 'Subaru': {
+ "Subaru": {
2: {
- 'A': 'Impreza',
- 'B': 'Legacy',
- 'C': 'Outback',
- 'D': 'Forester',
- 'E': 'Crosstrek',
- 'F': 'BRZ',
- 'G': 'Ascent',
+ "A": "Impreza",
+ "B": "Legacy",
+ "C": "Outback",
+ "D": "Forester",
+ "E": "Crosstrek",
+ "F": "BRZ",
+ "G": "Ascent",
}
},
-
- 'Mazda': {
+ "Mazda": {
1: {
- 'A': 'Mazda3',
- 'B': 'Mazda6',
- 'C': 'CX-5',
- 'D': 'CX-9',
- 'E': 'MX-5 Miata',
- 'F': 'CX-30',
- 'G': 'RX-8',
+ "A": "Mazda3",
+ "B": "Mazda6",
+ "C": "CX-5",
+ "D": "CX-9",
+ "E": "MX-5 Miata",
+ "F": "CX-30",
+ "G": "RX-8",
}
},
-
- 'Dongfeng': {
+ "Dongfeng": {
1: {
- 'A': 'A-Series',
- 'B': 'SHINE',
- 'C': 'C-Series',
- 'D': 'MAGE',
- 'E': ['CAPTAIN E', 'E32'],
- 'F': 'CAPTAIN C',
- 'G': 'S50',
- 'H': 'Dongfeng Fengshen AX3',
- 'J': 'Dongfeng Joyear SUV',
- 'K': 'Dongfeng Rich 6',
- 'L': 'Dongfeng Sokon',
- 'M': 'Dongfeng Glory 580',
+ "A": "A-Series",
+ "B": "SHINE",
+ "C": "C-Series",
+ "D": "MAGE",
+ "E": ["CAPTAIN E", "E32"],
+ "F": "CAPTAIN C",
+ "G": "S50",
+ "H": "Dongfeng Fengshen AX3",
+ "J": "Dongfeng Joyear SUV",
+ "K": "Dongfeng Rich 6",
+ "L": "Dongfeng Sokon",
+ "M": "Dongfeng Glory 580",
},
-
2: {
- '3': 'C35', # Specific models in C-Series
- '1': 'C31',
- '2': 'C32',
- '7': 'C72',
- '6': 'A60', # Specific models in A-Series
- '3': 'A30',
- 'X': ['AX7', 'AX4'],
+ "3": "C35", # Specific models in C-Series
+ "1": "C31",
+ "2": "C32",
+ "7": "C72",
+ "6": "A60", # Specific models in A-Series
+ "3": "A30",
+ "X": ["AX7", "AX4"],
},
3: { # Third character (for AX models)
- '7': 'AX7', # Resolves AX7
- '4': 'AX4', # Resolves AX4
- }
-
+ "7": "AX7", # Resolves AX7
+ "4": "AX4", # Resolves AX4
+ },
},
-
- 'Changan': {
+ "Changan": {
1: {
- 'A': 'Changan CS35',
- 'B': 'Changan CS55',
- 'C': 'Changan CS75',
- 'D': 'Changan CS85',
- 'E': 'Changan CS95',
- 'F': 'Changan Eado',
- 'G': 'Changan Raeton',
- 'H': 'Changan Alsvin',
- 'J': 'Changan UNI-T',
- 'K': 'Changan UNI-K',
- 'L': 'Changan Oushang',
- 'M': 'Changan Benni',
+ "A": "Changan CS35",
+ "B": "Changan CS55",
+ "C": "Changan CS75",
+ "D": "Changan CS85",
+ "E": "Changan CS95",
+ "F": "Changan Eado",
+ "G": "Changan Raeton",
+ "H": "Changan Alsvin",
+ "J": "Changan UNI-T",
+ "K": "Changan UNI-K",
+ "L": "Changan Oushang",
+ "M": "Changan Benni",
}
},
-
- 'Chery': {
+ "Chery": {
1: {
- 'A': 'Chery Arrizo 5',
- 'B': 'Chery Arrizo 7',
- 'C': 'Chery Tiggo 3',
- 'D': 'Chery Tiggo 5',
- 'E': 'Chery Tiggo 7',
- 'F': 'Chery Tiggo 8',
- 'G': 'Chery QQ',
- 'H': 'Chery Fulwin',
- 'J': 'Chery Cowin',
- 'K': 'Chery eQ1',
- 'L': 'Chery Exeed TX',
- 'M': 'Chery Exeed LX',
+ "A": "Chery Arrizo 5",
+ "B": "Chery Arrizo 7",
+ "C": "Chery Tiggo 3",
+ "D": "Chery Tiggo 5",
+ "E": "Chery Tiggo 7",
+ "F": "Chery Tiggo 8",
+ "G": "Chery QQ",
+ "H": "Chery Fulwin",
+ "J": "Chery Cowin",
+ "K": "Chery eQ1",
+ "L": "Chery Exeed TX",
+ "M": "Chery Exeed LX",
}
},
-
- 'MG': {
+ "MG": {
1: {
- 'A': 'MG 3',
- 'B': 'MG 5',
- 'C': 'MG 6',
- 'D': 'MG ZS',
- 'E': 'MG HS',
- 'F': 'MG RX5',
- 'G': 'MG Marvel R',
- 'H': 'MG EZS',
- 'J': 'MG GT',
- 'K': 'MG TF',
- 'L': 'MG Cyberster',
+ "A": "MG 3",
+ "B": "MG 5",
+ "C": "MG 6",
+ "D": "MG ZS",
+ "E": "MG HS",
+ "F": "MG RX5",
+ "G": "MG Marvel R",
+ "H": "MG EZS",
+ "J": "MG GT",
+ "K": "MG TF",
+ "L": "MG Cyberster",
}
},
-
- 'JMC': {
+ "JMC": {
1: {
- 'A': 'JMC Yusheng',
- 'B': 'JMC Vigus',
- 'C': 'JMC Baodian',
- 'D': 'JMC Ford Transit',
- 'E': 'JMC S350',
- 'F': 'JMC Teshun',
- 'G': 'JMC Realm',
- 'H': 'JMC Yuhu',
- 'J': 'JMC E200',
- 'K': 'JMC E400',
+ "A": "JMC Yusheng",
+ "B": "JMC Vigus",
+ "C": "JMC Baodian",
+ "D": "JMC Ford Transit",
+ "E": "JMC S350",
+ "F": "JMC Teshun",
+ "G": "JMC Realm",
+ "H": "JMC Yuhu",
+ "J": "JMC E200",
+ "K": "JMC E400",
}
},
-
- 'JAC': {
+ "JAC": {
1: {
- 'A': 'JAC J3',
- 'B': 'JAC J4',
- 'C': 'JAC J5',
- 'D': 'JAC J6',
- 'E': 'JAC J7',
- 'F': 'JAC S2',
- 'G': 'JAC S3',
- 'H': 'JAC S4',
- 'J': 'JAC S5',
- 'K': 'JAC S7',
- 'L': 'JAC iEV7S',
- 'M': 'JAC iEVS4',
+ "A": "JAC J3",
+ "B": "JAC J4",
+ "C": "JAC J5",
+ "D": "JAC J6",
+ "E": "JAC J7",
+ "F": "JAC S2",
+ "G": "JAC S3",
+ "H": "JAC S4",
+ "J": "JAC S5",
+ "K": "JAC S7",
+ "L": "JAC iEV7S",
+ "M": "JAC iEVS4",
}
},
-
- 'BYD': {
+ "BYD": {
1: {
- 'A': 'BYD F3',
- 'B': 'BYD F6',
- 'C': 'BYD S6',
- 'D': 'BYD Tang',
- 'E': 'BYD Song',
- 'F': 'BYD Yuan',
- 'G': 'BYD Qin',
- 'H': 'BYD Han',
- 'J': 'BYD e5',
- 'K': 'BYD e6',
- 'L': 'BYD Dolphin',
- 'M': 'BYD Seal',
+ "A": "BYD F3",
+ "B": "BYD F6",
+ "C": "BYD S6",
+ "D": "BYD Tang",
+ "E": "BYD Song",
+ "F": "BYD Yuan",
+ "G": "BYD Qin",
+ "H": "BYD Han",
+ "J": "BYD e5",
+ "K": "BYD e6",
+ "L": "BYD Dolphin",
+ "M": "BYD Seal",
}
},
-
- 'Geely': {
+ "Geely": {
1: {
- 'A': 'Geely Emgrand EC7',
- 'B': 'Geely Emgrand GS',
- 'C': 'Geely Emgrand GL',
- 'D': 'Geely Boyue',
- 'E': 'Geely Xingyue',
- 'F': 'Geely Binrui',
- 'G': 'Geely Borui',
- 'H': 'Geely Vision',
- 'J': 'Geely Coolray',
- 'K': 'Geely Monjaro',
- 'L': 'Geely Geometry A',
- 'M': 'Geely Geometry C',
+ "A": "Geely Emgrand EC7",
+ "B": "Geely Emgrand GS",
+ "C": "Geely Emgrand GL",
+ "D": "Geely Boyue",
+ "E": "Geely Xingyue",
+ "F": "Geely Binrui",
+ "G": "Geely Borui",
+ "H": "Geely Vision",
+ "J": "Geely Coolray",
+ "K": "Geely Monjaro",
+ "L": "Geely Geometry A",
+ "M": "Geely Geometry C",
}
},
-
- 'Great Wall Motors (GWM)': {
+ "Great Wall Motors (GWM)": {
1: {
- 'A': 'GWM Haval H6',
- 'B': 'GWM Haval H9',
- 'C': 'GWM Haval Jolion',
- 'D': 'GWM WEY VV5',
- 'E': 'GWM WEY VV6',
- 'F': 'GWM WEY VV7',
- 'G': 'GWM Ora R1',
- 'H': 'GWM Ora Good Cat',
- 'J': 'GWM Poer',
- 'K': 'GWM Tank 300',
- 'L': 'GWM Tank 500',
+ "A": "GWM Haval H6",
+ "B": "GWM Haval H9",
+ "C": "GWM Haval Jolion",
+ "D": "GWM WEY VV5",
+ "E": "GWM WEY VV6",
+ "F": "GWM WEY VV7",
+ "G": "GWM Ora R1",
+ "H": "GWM Ora Good Cat",
+ "J": "GWM Poer",
+ "K": "GWM Tank 300",
+ "L": "GWM Tank 500",
}
},
-
- 'FAW': {
+ "FAW": {
1: {
- 'A': 'FAW Besturn B50',
- 'B': 'FAW Besturn X40',
- 'C': 'FAW Besturn X80',
- 'D': 'FAW Hongqi H5',
- 'E': 'FAW Hongqi H7',
- 'F': 'FAW Hongqi HS5',
- 'G': 'FAW Hongqi HS7',
- 'H': 'FAW Jiefang Truck',
- 'J': 'FAW Oley',
- 'K': 'FAW Vita',
+ "A": "FAW Besturn B50",
+ "B": "FAW Besturn X40",
+ "C": "FAW Besturn X80",
+ "D": "FAW Hongqi H5",
+ "E": "FAW Hongqi H7",
+ "F": "FAW Hongqi HS5",
+ "G": "FAW Hongqi HS7",
+ "H": "FAW Jiefang Truck",
+ "J": "FAW Oley",
+ "K": "FAW Vita",
}
},
-
- 'SAIC Motor': {
+ "SAIC Motor": {
1: {
- 'A': 'SAIC Maxus G10',
- 'B': 'SAIC Maxus G50',
- 'C': 'SAIC Maxus T60',
- 'D': 'SAIC Roewe RX5',
- 'E': 'SAIC Roewe i5',
- 'F': 'SAIC Roewe i6',
- 'G': 'SAIC MG ZS',
- 'H': 'SAIC MG HS',
- 'J': 'SAIC IM LS7',
- 'K': 'SAIC IM Marvel R',
+ "A": "SAIC Maxus G10",
+ "B": "SAIC Maxus G50",
+ "C": "SAIC Maxus T60",
+ "D": "SAIC Roewe RX5",
+ "E": "SAIC Roewe i5",
+ "F": "SAIC Roewe i6",
+ "G": "SAIC MG ZS",
+ "H": "SAIC MG HS",
+ "J": "SAIC IM LS7",
+ "K": "SAIC IM Marvel R",
}
},
}
@@ -1827,7 +1844,9 @@ def decode_vin_haikalna(vin):
pattern = r"^[A-HJ-NPR-Z0-9]{17}$"
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()
@@ -1842,10 +1861,9 @@ def decode_vin_haikalna(vin):
model = decode_vds(manufacturer, vds)
data = {
- 'maker': manufacturer,
- 'model': model,
- 'modelYear': year,
-
+ "maker": manufacturer,
+ "model": model,
+ "modelYear": year,
}
print(data)
- return data
\ No newline at end of file
+ return data
diff --git a/inventory/management/commands/add_id_car_model.py b/inventory/management/commands/add_id_car_model.py
index 482fc876..bfc87cd1 100644
--- a/inventory/management/commands/add_id_car_model.py
+++ b/inventory/management/commands/add_id_car_model.py
@@ -6,51 +6,71 @@ from inventory.models import CarTrim
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):
# Define the file path relative to the project directory
- base_dir = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
- file_path = os.path.join(base_dir, 'data/mappings.csv')
+ base_dir = os.path.dirname(
+ 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
with connection.cursor() as cursor:
try:
- cursor.execute("ALTER TABLE inventory_cartrim ADD COLUMN id_car_model INTEGER")
- self.stdout.write(self.style.SUCCESS("Column 'id_car_model' added successfully."))
+ cursor.execute(
+ "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:
- 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
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)
for row in reader:
# Extract id_car_serie and id_car_model from the current row
- id_car_serie = row.get('id_car_serie')
- id_car_model = row.get('id_car_model')
+ id_car_serie = row.get("id_car_serie")
+ id_car_model = row.get("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
# 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
if updated_count > 0:
- self.stdout.write(self.style.SUCCESS(
- f"Updated {updated_count} rows for id_car_serie={id_car_serie} with id_car_model={id_car_model}."
- ))
+ self.stdout.write(
+ self.style.SUCCESS(
+ f"Updated {updated_count} rows for id_car_serie={id_car_serie} with id_car_model={id_car_model}."
+ )
+ )
else:
- self.stdout.write(self.style.WARNING(
- f"No rows found for id_car_serie={id_car_serie}."
- ))
+ self.stdout.write(
+ 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:
self.stdout.write(self.style.ERROR(f"File not found: {file_path}"))
except Exception as e:
- self.stdout.write(self.style.ERROR(f"An error occurred: {e}"))
\ No newline at end of file
+ self.stdout.write(self.style.ERROR(f"An error occurred: {e}"))
diff --git a/inventory/management/commands/analyze_car_hierarchy.py b/inventory/management/commands/analyze_car_hierarchy.py
index bb7a35b0..405d8460 100644
--- a/inventory/management/commands/analyze_car_hierarchy.py
+++ b/inventory/management/commands/analyze_car_hierarchy.py
@@ -7,24 +7,24 @@ from django.conf import settings
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):
parser.add_argument(
- '--export',
- action='store_true',
- help='Export results to CSV files',
+ "--export",
+ action="store_true",
+ help="Export results to CSV files",
)
parser.add_argument(
- '--export-path',
+ "--export-path",
type=str,
- default='exports',
+ default="exports",
help='Directory to export CSV files (default: "exports")',
)
def handle(self, *args, **options):
- export = options['export']
- export_path = options['export_path']
+ export = options["export"]
+ export_path = options["export_path"]
# Create export directory if needed
if export:
@@ -35,14 +35,18 @@ class Command(BaseCommand):
# Analyze makes without models
all_makes = CarMake.objects.all()
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()
self.stdout.write(self.style.SUCCESS(f"Total car makes: {total_makes}"))
- self.stdout.write(self.style.SUCCESS(
- f"Car makes without models: {makes_without_models_count} "
- f"({makes_without_models_count/total_makes*100:.2f}% of all makes)"
- ))
+ self.stdout.write(
+ self.style.SUCCESS(
+ 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:
self.stdout.write("\nSample of car makes without models:")
@@ -52,14 +56,18 @@ class Command(BaseCommand):
# Analyze models without series
all_models = CarModel.objects.all()
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()
self.stdout.write(self.style.SUCCESS(f"\nTotal car models: {total_models}"))
- self.stdout.write(self.style.SUCCESS(
- f"Car models without series: {models_without_series_count} "
- f"({models_without_series_count/total_models*100:.2f}% of all models)"
- ))
+ self.stdout.write(
+ self.style.SUCCESS(
+ 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:
self.stdout.write("\nSample of car models without series:")
@@ -69,14 +77,18 @@ class Command(BaseCommand):
# Analyze series without trims
all_series = CarSerie.objects.all()
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()
self.stdout.write(self.style.SUCCESS(f"\nTotal car series: {total_series}"))
- self.stdout.write(self.style.SUCCESS(
- f"Car series without trims: {series_without_trims_count} "
- f"({series_without_trims_count/total_series*100:.2f}% of all series)"
- ))
+ self.stdout.write(
+ self.style.SUCCESS(
+ 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:
self.stdout.write("\nSample of car series without trims:")
@@ -90,50 +102,77 @@ class Command(BaseCommand):
if export:
# Export makes without models
if makes_without_models_count > 0:
- filepath = os.path.join(export_dir, 'makes_without_models.csv')
- with open(filepath, 'w', newline='') as csvfile:
+ filepath = os.path.join(export_dir, "makes_without_models.csv")
+ with open(filepath, "w", newline="") as 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:
- writer.writerow([make.id_car_make, make.name, make.is_sa_import])
- self.stdout.write(self.style.SUCCESS(f"Exported makes without models to {filepath}"))
+ writer.writerow(
+ [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
if models_without_series_count > 0:
- filepath = os.path.join(export_dir, 'models_without_series.csv')
- with open(filepath, 'w', newline='') as csvfile:
+ filepath = os.path.join(export_dir, "models_without_series.csv")
+ with open(filepath, "w", newline="") as 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:
- writer.writerow([
- model.id_car_model,
- model.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}"))
+ writer.writerow(
+ [
+ model.id_car_model,
+ model.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}")
+ )
# Export series without trims
if series_without_trims_count > 0:
- filepath = os.path.join(export_dir, 'series_without_trims.csv')
- with open(filepath, 'w', newline='') as csvfile:
+ filepath = os.path.join(export_dir, "series_without_trims.csv")
+ with open(filepath, "w", newline="") as 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:
- writer.writerow([
- serie.id_car_serie,
- serie.name,
- serie.id_car_model.id_car_model,
- serie.id_car_model.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}"))
+ writer.writerow(
+ [
+ serie.id_car_serie,
+ serie.name,
+ serie.id_car_model.id_car_model,
+ serie.id_car_model.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}")
+ )
# Summary
- self.stdout.write("\n" + "="*50)
+ self.stdout.write("\n" + "=" * 50)
self.stdout.write(self.style.SUCCESS("SUMMARY"))
- 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(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}%)")
+ 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(
+ 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}%)"
+ )
diff --git a/inventory/management/commands/check_vin.py b/inventory/management/commands/check_vin.py
index 43d44d93..98c43a26 100644
--- a/inventory/management/commands/check_vin.py
+++ b/inventory/management/commands/check_vin.py
@@ -1,26 +1,56 @@
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):
- help = 'Seed the Customer model with 20 records'
+ help = "Seed the Customer model with 20 records"
def handle(self, *args, **kwargs):
# vin,description = self.generate_vin()
result = decodevin("1HGCM82633A123456")
- self.stdout.write(self.style.SUCCESS('####################################################################################################'))
- self.stdout.write(self.style.SUCCESS('####################################################################################################'))
+ self.stdout.write(
+ 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'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:"1HGCM82633A123456" - Make {make} - Model {model} - Model Year {year_model}'))
+ 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:"1HGCM82633A123456" - Make {make} - Model {model} - Model Year {year_model}'
+ )
+ )
make = get_make(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('####################################################################################################'))
+ 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(
+ "####################################################################################################"
+ )
+ )
diff --git a/inventory/management/commands/claude.py b/inventory/management/commands/claude.py
index bfb14a05..d4491bad 100644
--- a/inventory/management/commands/claude.py
+++ b/inventory/management/commands/claude.py
@@ -1,11 +1,11 @@
from django_ledger.io import roles
from django.core.management.base import BaseCommand
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):
- help = 'Creates Chart of Accounts for Deepseek entity'
+ help = "Creates Chart of Accounts for Deepseek entity"
def handle(self, *args, **options):
"""
@@ -13,9 +13,7 @@ class Command(BaseCommand):
"""
# Create Chart of Accounts
- entity_model = EntityModel.objects.get(
- name="Claude"
- )
+ entity_model = EntityModel.objects.get(name="Claude")
coa_model = entity_model.get_default_coa()
# entity_model.get_all_accounts().delete()
# coa_model, created = ChartOfAccountModel.objects.get_or_create(
@@ -1130,389 +1128,382 @@ class Command(BaseCommand):
# }
# ]
-
accounts_data = [
# Current Assets (must start with 1)
{
- 'code': '1010',
- 'name': 'Cash on Hand',
- 'role': roles.ASSET_CA_CASH,
- 'balance_type': roles.DEBIT,
- 'locked': True,
- 'default': True # Default for ASSET_CA_CASH
+ "code": "1010",
+ "name": "Cash on Hand",
+ "role": roles.ASSET_CA_CASH,
+ "balance_type": roles.DEBIT,
+ "locked": True,
+ "default": True, # Default for ASSET_CA_CASH
},
{
- 'code': '1020',
- 'name': 'Bank',
- 'role': roles.ASSET_CA_CASH,
- 'balance_type': roles.DEBIT,
- 'locked': True,
- 'default': False
+ "code": "1020",
+ "name": "Bank",
+ "role": roles.ASSET_CA_CASH,
+ "balance_type": roles.DEBIT,
+ "locked": True,
+ "default": False,
},
{
- 'code': '1030',
- 'name': 'Accounts Receivable',
- 'role': roles.ASSET_CA_RECEIVABLES,
- 'balance_type': roles.DEBIT,
- 'locked': True,
- 'default': True # Default for ASSET_CA_RECEIVABLES
+ "code": "1030",
+ "name": "Accounts Receivable",
+ "role": roles.ASSET_CA_RECEIVABLES,
+ "balance_type": roles.DEBIT,
+ "locked": True,
+ "default": True, # Default for ASSET_CA_RECEIVABLES
},
{
- 'code': '1040',
- 'name': 'Inventory (Cars)',
- 'role': roles.ASSET_CA_INVENTORY,
- 'balance_type': roles.DEBIT,
- 'locked': True,
- 'default': True # Default for ASSET_CA_INVENTORY
+ "code": "1040",
+ "name": "Inventory (Cars)",
+ "role": roles.ASSET_CA_INVENTORY,
+ "balance_type": roles.DEBIT,
+ "locked": True,
+ "default": True, # Default for ASSET_CA_INVENTORY
},
{
- 'code': '1045',
- 'name': 'Spare Parts Inventory',
- 'role': roles.ASSET_CA_INVENTORY,
- 'balance_type': roles.DEBIT,
- 'locked': False,
- 'default': False
+ "code": "1045",
+ "name": "Spare Parts Inventory",
+ "role": roles.ASSET_CA_INVENTORY,
+ "balance_type": roles.DEBIT,
+ "locked": False,
+ "default": False,
},
{
- 'code': '1050',
- 'name': 'Employee Advances',
- 'role': roles.ASSET_CA_RECEIVABLES,
- 'balance_type': roles.DEBIT,
- 'locked': False,
- 'default': False
+ "code": "1050",
+ "name": "Employee Advances",
+ "role": roles.ASSET_CA_RECEIVABLES,
+ "balance_type": roles.DEBIT,
+ "locked": False,
+ "default": False,
},
{
- 'code': '1060',
- 'name': 'Prepaid Expenses',
- 'role': roles.ASSET_CA_PREPAID,
- 'balance_type': roles.DEBIT,
- 'locked': False,
- 'default': True # Default for ASSET_CA_PREPAID
+ "code": "1060",
+ "name": "Prepaid Expenses",
+ "role": roles.ASSET_CA_PREPAID,
+ "balance_type": roles.DEBIT,
+ "locked": False,
+ "default": True, # Default for ASSET_CA_PREPAID
},
{
- 'code': '1070',
- 'name': 'Notes Receivable',
- 'role': roles.ASSET_LTI_NOTES_RECEIVABLE,
- 'balance_type': roles.DEBIT,
- 'locked': False,
- 'default': True # Default for ASSET_LTI_NOTES_RECEIVABLE
+ "code": "1070",
+ "name": "Notes Receivable",
+ "role": roles.ASSET_LTI_NOTES_RECEIVABLE,
+ "balance_type": roles.DEBIT,
+ "locked": False,
+ "default": True, # Default for ASSET_LTI_NOTES_RECEIVABLE
},
-
# Fixed Assets (must also start with 1)
{
- 'code': '1110',
- 'name': 'Lands',
- 'role': roles.ASSET_LTI_LAND,
- 'balance_type': roles.DEBIT,
- 'locked': False,
- 'default': True # Default for ASSET_LTI_LAND
+ "code": "1110",
+ "name": "Lands",
+ "role": roles.ASSET_LTI_LAND,
+ "balance_type": roles.DEBIT,
+ "locked": False,
+ "default": True, # Default for ASSET_LTI_LAND
},
{
- 'code': '1111',
- 'name': 'Buildings',
- 'role': roles.ASSET_PPE_BUILDINGS,
- 'balance_type': roles.DEBIT,
- 'locked': False,
- 'default': True # Default for ASSET_PPE_BUILDINGS
+ "code": "1111",
+ "name": "Buildings",
+ "role": roles.ASSET_PPE_BUILDINGS,
+ "balance_type": roles.DEBIT,
+ "locked": False,
+ "default": True, # Default for ASSET_PPE_BUILDINGS
},
{
- 'code': '1112',
- 'name': 'Company Vehicles',
- 'role': roles.ASSET_PPE_EQUIPMENT,
- 'balance_type': roles.DEBIT,
- 'locked': False,
- 'default': True # Default for ASSET_PPE_EQUIPMENT
+ "code": "1112",
+ "name": "Company Vehicles",
+ "role": roles.ASSET_PPE_EQUIPMENT,
+ "balance_type": roles.DEBIT,
+ "locked": False,
+ "default": True, # Default for ASSET_PPE_EQUIPMENT
},
{
- 'code': '1113',
- 'name': 'Equipment & Tools',
- 'role': roles.ASSET_PPE_EQUIPMENT,
- 'balance_type': roles.DEBIT,
- 'locked': False,
- 'default': False
+ "code": "1113",
+ "name": "Equipment & Tools",
+ "role": roles.ASSET_PPE_EQUIPMENT,
+ "balance_type": roles.DEBIT,
+ "locked": False,
+ "default": False,
},
{
- 'code': '1114',
- 'name': 'Furniture & Fixtures',
- 'role': roles.ASSET_PPE_EQUIPMENT,
- 'balance_type': roles.DEBIT,
- 'locked': False,
- 'default': False
+ "code": "1114",
+ "name": "Furniture & Fixtures",
+ "role": roles.ASSET_PPE_EQUIPMENT,
+ "balance_type": roles.DEBIT,
+ "locked": False,
+ "default": False,
},
{
- 'code': '1115',
- 'name': 'Other Fixed Assets',
- 'role': roles.ASSET_PPE_EQUIPMENT,
- 'balance_type': roles.DEBIT,
- 'locked': False,
- 'default': False
+ "code": "1115",
+ "name": "Other Fixed Assets",
+ "role": roles.ASSET_PPE_EQUIPMENT,
+ "balance_type": roles.DEBIT,
+ "locked": False,
+ "default": False,
},
{
- 'code': '1120',
- 'name': 'Long-term Investments',
- 'role': roles.ASSET_LTI_SECURITIES,
- 'balance_type': roles.DEBIT,
- 'locked': False,
- 'default': True # Default for ASSET_LTI_SECURITIES
+ "code": "1120",
+ "name": "Long-term Investments",
+ "role": roles.ASSET_LTI_SECURITIES,
+ "balance_type": roles.DEBIT,
+ "locked": False,
+ "default": True, # Default for ASSET_LTI_SECURITIES
},
{
- 'code': '1130',
- 'name': 'Intangible Assets',
- 'role': roles.ASSET_INTANGIBLE_ASSETS,
- 'balance_type': roles.DEBIT,
- 'locked': False,
- 'default': True # Default for ASSET_INTANGIBLE_ASSETS
+ "code": "1130",
+ "name": "Intangible Assets",
+ "role": roles.ASSET_INTANGIBLE_ASSETS,
+ "balance_type": roles.DEBIT,
+ "locked": False,
+ "default": True, # Default for ASSET_INTANGIBLE_ASSETS
},
-
# Current Liabilities (must start with 2)
{
- 'code': '2010',
- 'name': 'Accounts Payable',
- 'role': roles.LIABILITY_CL_ACC_PAYABLE,
- 'balance_type': roles.CREDIT,
- 'locked': True,
- 'default': True # Default for LIABILITY_CL_ACC_PAYABLE
+ "code": "2010",
+ "name": "Accounts Payable",
+ "role": roles.LIABILITY_CL_ACC_PAYABLE,
+ "balance_type": roles.CREDIT,
+ "locked": True,
+ "default": True, # Default for LIABILITY_CL_ACC_PAYABLE
},
{
- 'code': '2020',
- 'name': 'Notes Payable',
- 'role': roles.LIABILITY_CL_ST_NOTES_PAYABLE,
- 'balance_type': roles.CREDIT,
- 'locked': False,
- 'default': True # Default for LIABILITY_CL_ST_NOTES_PAYABLE
+ "code": "2020",
+ "name": "Notes Payable",
+ "role": roles.LIABILITY_CL_ST_NOTES_PAYABLE,
+ "balance_type": roles.CREDIT,
+ "locked": False,
+ "default": True, # Default for LIABILITY_CL_ST_NOTES_PAYABLE
},
{
- 'code': '2030',
- 'name': 'Short-term Loans',
- 'role': roles.LIABILITY_CL_ST_NOTES_PAYABLE,
- 'balance_type': roles.CREDIT,
- 'locked': False,
- 'default': False
+ "code": "2030",
+ "name": "Short-term Loans",
+ "role": roles.LIABILITY_CL_ST_NOTES_PAYABLE,
+ "balance_type": roles.CREDIT,
+ "locked": False,
+ "default": False,
},
{
- 'code': '2040',
- 'name': 'Employee Payables',
- 'role': roles.LIABILITY_CL_WAGES_PAYABLE,
- 'balance_type': roles.CREDIT,
- 'locked': False,
- 'default': True # Default for LIABILITY_CL_WAGES_PAYABLE
+ "code": "2040",
+ "name": "Employee Payables",
+ "role": roles.LIABILITY_CL_WAGES_PAYABLE,
+ "balance_type": roles.CREDIT,
+ "locked": False,
+ "default": True, # Default for LIABILITY_CL_WAGES_PAYABLE
},
{
- 'code': '2050',
- 'name': 'Accrued Expenses',
- 'role': roles.LIABILITY_CL_OTHER,
- 'balance_type': roles.CREDIT,
- 'locked': False,
- 'default': True # Default for LIABILITY_CL_OTHER
+ "code": "2050",
+ "name": "Accrued Expenses",
+ "role": roles.LIABILITY_CL_OTHER,
+ "balance_type": roles.CREDIT,
+ "locked": False,
+ "default": True, # Default for LIABILITY_CL_OTHER
},
{
- 'code': '2060',
- 'name': 'Accrued Taxes',
- 'role': roles.LIABILITY_CL_TAXES_PAYABLE,
- 'balance_type': roles.CREDIT,
- 'locked': False,
- 'default': True # Default for LIABILITY_CL_TAXES_PAYABLE
+ "code": "2060",
+ "name": "Accrued Taxes",
+ "role": roles.LIABILITY_CL_TAXES_PAYABLE,
+ "balance_type": roles.CREDIT,
+ "locked": False,
+ "default": True, # Default for LIABILITY_CL_TAXES_PAYABLE
},
{
- 'code': '2070',
- 'name': 'Provisions',
- 'role': roles.LIABILITY_CL_OTHER,
- 'balance_type': roles.CREDIT,
- 'locked': False,
- 'default': False
+ "code": "2070",
+ "name": "Provisions",
+ "role": roles.LIABILITY_CL_OTHER,
+ "balance_type": roles.CREDIT,
+ "locked": False,
+ "default": False,
},
-
# Long-term Liabilities (must also start with 2)
{
- 'code': '2210',
- 'name': 'Long-term Bank Loans',
- 'role': roles.LIABILITY_LTL_NOTES_PAYABLE,
- 'balance_type': roles.CREDIT,
- 'locked': False,
- 'default': True # Default for LIABILITY_LTL_NOTES_PAYABLE
+ "code": "2210",
+ "name": "Long-term Bank Loans",
+ "role": roles.LIABILITY_LTL_NOTES_PAYABLE,
+ "balance_type": roles.CREDIT,
+ "locked": False,
+ "default": True, # Default for LIABILITY_LTL_NOTES_PAYABLE
},
{
- 'code': '2220',
- 'name': 'Lease Liabilities',
- 'role': roles.LIABILITY_LTL_NOTES_PAYABLE,
- 'balance_type': roles.CREDIT,
- 'locked': False,
- 'default': False
+ "code": "2220",
+ "name": "Lease Liabilities",
+ "role": roles.LIABILITY_LTL_NOTES_PAYABLE,
+ "balance_type": roles.CREDIT,
+ "locked": False,
+ "default": False,
},
{
- 'code': '2230',
- 'name': 'Other Long-term Liabilities',
- 'role': roles.LIABILITY_LTL_NOTES_PAYABLE,
- 'balance_type': roles.CREDIT,
- 'locked': False,
- 'default': False
+ "code": "2230",
+ "name": "Other Long-term Liabilities",
+ "role": roles.LIABILITY_LTL_NOTES_PAYABLE,
+ "balance_type": roles.CREDIT,
+ "locked": False,
+ "default": False,
},
-
# Equity (must start with 3)
{
- 'code': '3010',
- 'name': 'Capital',
- 'role': roles.EQUITY_CAPITAL,
- 'balance_type': roles.CREDIT,
- 'locked': True,
- 'default': True # Default for EQUITY_CAPITAL
+ "code": "3010",
+ "name": "Capital",
+ "role": roles.EQUITY_CAPITAL,
+ "balance_type": roles.CREDIT,
+ "locked": True,
+ "default": True, # Default for EQUITY_CAPITAL
},
{
- 'code': '3020',
- 'name': 'Statutory Reserve',
- 'role': roles.EQUITY_ADJUSTMENT,
- 'balance_type': roles.CREDIT,
- 'locked': False,
- 'default': True # Default for EQUITY_ADJUSTMENT
+ "code": "3020",
+ "name": "Statutory Reserve",
+ "role": roles.EQUITY_ADJUSTMENT,
+ "balance_type": roles.CREDIT,
+ "locked": False,
+ "default": True, # Default for EQUITY_ADJUSTMENT
},
{
- 'code': '3030',
- 'name': 'Retained Earnings',
- 'role': roles.EQUITY_ADJUSTMENT,
- 'balance_type': roles.CREDIT,
- 'locked': False,
- 'default': False
+ "code": "3030",
+ "name": "Retained Earnings",
+ "role": roles.EQUITY_ADJUSTMENT,
+ "balance_type": roles.CREDIT,
+ "locked": False,
+ "default": False,
},
{
- 'code': '3040',
- 'name': 'Profit & Loss for the Period',
- 'role': roles.EQUITY_ADJUSTMENT,
- 'balance_type': roles.CREDIT,
- 'locked': False,
- 'default': False
+ "code": "3040",
+ "name": "Profit & Loss for the Period",
+ "role": roles.EQUITY_ADJUSTMENT,
+ "balance_type": roles.CREDIT,
+ "locked": False,
+ "default": False,
},
-
# Revenue (must start with 4)
{
- 'code': '4010',
- 'name': 'Car Sales',
- 'role': roles.INCOME_OPERATIONAL,
- 'balance_type': roles.CREDIT,
- 'locked': True,
- 'default': True # Default for INCOME_OPERATIONAL
+ "code": "4010",
+ "name": "Car Sales",
+ "role": roles.INCOME_OPERATIONAL,
+ "balance_type": roles.CREDIT,
+ "locked": True,
+ "default": True, # Default for INCOME_OPERATIONAL
},
{
- 'code': '4020',
- 'name': 'After-Sales Services',
- 'role': roles.INCOME_OPERATIONAL,
- 'balance_type': roles.CREDIT,
- 'locked': False,
- 'default': False
+ "code": "4020",
+ "name": "After-Sales Services",
+ "role": roles.INCOME_OPERATIONAL,
+ "balance_type": roles.CREDIT,
+ "locked": False,
+ "default": False,
},
{
- 'code': '4030',
- 'name': 'Car Rental Income',
- 'role': roles.INCOME_PASSIVE,
- 'balance_type': roles.CREDIT,
- 'locked': False,
- 'default': True # Default for INCOME_PASSIVE
+ "code": "4030",
+ "name": "Car Rental Income",
+ "role": roles.INCOME_PASSIVE,
+ "balance_type": roles.CREDIT,
+ "locked": False,
+ "default": True, # Default for INCOME_PASSIVE
},
{
- 'code': '4040',
- 'name': 'Other Income',
- 'role': roles.INCOME_OTHER,
- 'balance_type': roles.CREDIT,
- 'locked': False,
- 'default': True # Default for INCOME_OTHER
+ "code": "4040",
+ "name": "Other Income",
+ "role": roles.INCOME_OTHER,
+ "balance_type": roles.CREDIT,
+ "locked": False,
+ "default": True, # Default for INCOME_OTHER
},
-
# Expenses (must start with 5 for COGS, 6 for others)
{
- 'code': '5010',
- 'name': 'Cost of Goods Sold',
- 'role': roles.COGS,
- 'balance_type': roles.DEBIT,
- 'locked': True,
- 'default': True # Default for COGS
+ "code": "5010",
+ "name": "Cost of Goods Sold",
+ "role": roles.COGS,
+ "balance_type": roles.DEBIT,
+ "locked": True,
+ "default": True, # Default for COGS
},
{
- 'code': '5015',
- 'name': 'Spare Parts Cost Consumed',
- 'role': roles.COGS,
- 'balance_type': roles.DEBIT,
- 'locked': False,
- 'default': False
+ "code": "5015",
+ "name": "Spare Parts Cost Consumed",
+ "role": roles.COGS,
+ "balance_type": roles.DEBIT,
+ "locked": False,
+ "default": False,
},
{
- 'code': '6010',
- 'name': 'Salaries & Wages',
- 'role': roles.EXPENSE_OPERATIONAL,
- 'balance_type': roles.DEBIT,
- 'locked': False,
- 'default': True # Default for EXPENSE_OPERATIONAL
+ "code": "6010",
+ "name": "Salaries & Wages",
+ "role": roles.EXPENSE_OPERATIONAL,
+ "balance_type": roles.DEBIT,
+ "locked": False,
+ "default": True, # Default for EXPENSE_OPERATIONAL
},
{
- 'code': '6020',
- 'name': 'Rent',
- 'role': roles.EXPENSE_OPERATIONAL,
- 'balance_type': roles.DEBIT,
- 'locked': False,
- 'default': False
+ "code": "6020",
+ "name": "Rent",
+ "role": roles.EXPENSE_OPERATIONAL,
+ "balance_type": roles.DEBIT,
+ "locked": False,
+ "default": False,
},
{
- 'code': '6030',
- 'name': 'Utilities',
- 'role': roles.EXPENSE_OPERATIONAL,
- 'balance_type': roles.DEBIT,
- 'locked': False,
- 'default': False
+ "code": "6030",
+ "name": "Utilities",
+ "role": roles.EXPENSE_OPERATIONAL,
+ "balance_type": roles.DEBIT,
+ "locked": False,
+ "default": False,
},
{
- 'code': '6040',
- 'name': 'Advertising & Marketing',
- 'role': roles.EXPENSE_OPERATIONAL,
- 'balance_type': roles.DEBIT,
- 'locked': False,
- 'default': False
+ "code": "6040",
+ "name": "Advertising & Marketing",
+ "role": roles.EXPENSE_OPERATIONAL,
+ "balance_type": roles.DEBIT,
+ "locked": False,
+ "default": False,
},
{
- 'code': '6050',
- 'name': 'Maintenance',
- 'role': roles.EXPENSE_OPERATIONAL,
- 'balance_type': roles.DEBIT,
- 'locked': False,
- 'default': False
+ "code": "6050",
+ "name": "Maintenance",
+ "role": roles.EXPENSE_OPERATIONAL,
+ "balance_type": roles.DEBIT,
+ "locked": False,
+ "default": False,
},
{
- 'code': '6060',
- 'name': 'Operating Expenses',
- 'role': roles.EXPENSE_OPERATIONAL,
- 'balance_type': roles.DEBIT,
- 'locked': False,
- 'default': False
+ "code": "6060",
+ "name": "Operating Expenses",
+ "role": roles.EXPENSE_OPERATIONAL,
+ "balance_type": roles.DEBIT,
+ "locked": False,
+ "default": False,
},
{
- 'code': '6070',
- 'name': 'Depreciation',
- 'role': roles.EXPENSE_DEPRECIATION,
- 'balance_type': roles.DEBIT,
- 'locked': False,
- 'default': True # Default for EXPENSE_DEPRECIATION
+ "code": "6070",
+ "name": "Depreciation",
+ "role": roles.EXPENSE_DEPRECIATION,
+ "balance_type": roles.DEBIT,
+ "locked": False,
+ "default": True, # Default for EXPENSE_DEPRECIATION
},
{
- 'code': '6080',
- 'name': 'Fees & Taxes',
- 'role': roles.EXPENSE_OPERATIONAL,
- 'balance_type': roles.DEBIT,
- 'locked': False,
- 'default': False
+ "code": "6080",
+ "name": "Fees & Taxes",
+ "role": roles.EXPENSE_OPERATIONAL,
+ "balance_type": roles.DEBIT,
+ "locked": False,
+ "default": False,
},
{
- 'code': '6090',
- 'name': 'Bank Charges',
- 'role': roles.EXPENSE_OPERATIONAL,
- 'balance_type': roles.DEBIT,
- 'locked': False,
- 'default': False
+ "code": "6090",
+ "name": "Bank Charges",
+ "role": roles.EXPENSE_OPERATIONAL,
+ "balance_type": roles.DEBIT,
+ "locked": False,
+ "default": False,
},
{
- 'code': '6100',
- 'name': 'Other Expenses',
- 'role': roles.EXPENSE_OTHER,
- 'balance_type': roles.DEBIT,
- 'locked': False,
- 'default': True # Default for EXPENSE_OTHER
- }
+ "code": "6100",
+ "name": "Other Expenses",
+ "role": roles.EXPENSE_OTHER,
+ "balance_type": roles.DEBIT,
+ "locked": False,
+ "default": True, # Default for EXPENSE_OTHER
+ },
]
created_accounts = []
@@ -1521,23 +1512,29 @@ class Command(BaseCommand):
try:
account = entity_model.create_account(
coa_model=coa_model,
- code=account_data['code'],
- name=_(account_data['name']),
- role=_(account_data['role']),
- balance_type=_(account_data['balance_type']),
- active=True
+ code=account_data["code"],
+ name=_(account_data["name"]),
+ role=_(account_data["role"]),
+ balance_type=_(account_data["balance_type"]),
+ active=True,
)
- account.role_default = account_data['default']
+ account.role_default = account_data["default"]
account.save()
created_accounts.append(account)
- self.stdout.write(self.style.SUCCESS(
- f"Created account: {account.code} - {account.name}"
- ))
+ self.stdout.write(
+ self.style.SUCCESS(
+ f"Created account: {account.code} - {account.name}"
+ )
+ )
except Exception as e:
- self.stdout.write(self.style.ERROR(
- f"Error creating account {account_data['code']}: {str(e)}"
- ))
+ self.stdout.write(
+ self.style.ERROR(
+ f"Error creating account {account_data['code']}: {str(e)}"
+ )
+ )
- self.stdout.write(self.style.SUCCESS(
- f"\nSuccessfully created {len(created_accounts)} accounts in Chart of Accounts"
- ))
+ self.stdout.write(
+ self.style.SUCCESS(
+ f"\nSuccessfully created {len(created_accounts)} accounts in Chart of Accounts"
+ )
+ )
diff --git a/inventory/management/commands/db2json.py b/inventory/management/commands/db2json.py
index 11dfd41a..cfb514c6 100644
--- a/inventory/management/commands/db2json.py
+++ b/inventory/management/commands/db2json.py
@@ -7,14 +7,15 @@ from tqdm import tqdm # Progress bar support
# Database connection details
db_config = {
- 'host': 'localhost',
- 'user': 'root',
- 'password': "Kfsh&rc9788",
- 'database': 'car2db_june'
+ "host": "localhost",
+ "user": "root",
+ "password": "Kfsh&rc9788",
+ "database": "car2db_june",
}
EXCLUDED_TABLES = {"car_serie", "car_generation"} # Tables to exclude from direct dump
+
class Command(BaseCommand):
help = "Merge car_serie with car_generation, include in final JSON dump with a progress bar."
@@ -22,7 +23,9 @@ class Command(BaseCommand):
try:
self.stdout.write(self.style.SUCCESS("Connecting to database..."))
# 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
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)
# 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..."))
- merged_df = pd.merge(car_serie_df, car_generation_df, on="id_car_generation", how="left")
+ self.stdout.write(
+ 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
- final_df = merged_df.rename(columns={
- "id_car_serie": "id_car_serie",
- "id_car_model_x": "id_car_model",
- "name_y": "generation_name",
- "name_x": "serie_name",
- "year_begin": "year_begin",
- "year_end": "year_end"
- })[["id_car_serie", "id_car_model", "generation_name", "serie_name", "year_begin", "year_end"]]
+ final_df = merged_df.rename(
+ columns={
+ "id_car_serie": "id_car_serie",
+ "id_car_model_x": "id_car_model",
+ "name_y": "generation_name",
+ "name_x": "serie_name",
+ "year_begin": "year_begin",
+ "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
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
self.export_database_to_json(car_serie_json)
@@ -59,7 +79,7 @@ class Command(BaseCommand):
self.stdout.write(self.style.ERROR(f"Error: {e}"))
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:
self.stdout.write(self.style.SUCCESS("Exporting database to JSON..."))
# Connect to the database using pymysql
@@ -90,13 +110,17 @@ class Command(BaseCommand):
# Save the JSON to a file
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)
- 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:
self.stdout.write(self.style.ERROR(f"Error exporting database: {e}"))
finally:
if connection:
- connection.close()
\ No newline at end of file
+ connection.close()
diff --git a/inventory/management/commands/deactivate_expired_plans.py b/inventory/management/commands/deactivate_expired_plans.py
index 4087e158..6e420a48 100644
--- a/inventory/management/commands/deactivate_expired_plans.py
+++ b/inventory/management/commands/deactivate_expired_plans.py
@@ -2,17 +2,17 @@ from django.core.management.base import BaseCommand
from django.utils import timezone
from plans.models import UserPlan
+
class Command(BaseCommand):
- help = 'Deactivates expired user plans'
+ help = "Deactivates expired user plans"
def handle(self, *args, **options):
- expired_plans = UserPlan.objects.filter(
- active=True,
- expire__lt=timezone.now()
- )
+ expired_plans = UserPlan.objects.filter(active=True, expire__lt=timezone.now())
count = expired_plans.count()
for plan in expired_plans:
plan.expire_account()
- self.stdout.write(self.style.SUCCESS(f'Successfully deactivated {count} expired plans'))
\ No newline at end of file
+ self.stdout.write(
+ self.style.SUCCESS(f"Successfully deactivated {count} expired plans")
+ )
diff --git a/inventory/management/commands/generate_slugs.py b/inventory/management/commands/generate_slugs.py
index 6c9dfdfa..f043c1fc 100644
--- a/inventory/management/commands/generate_slugs.py
+++ b/inventory/management/commands/generate_slugs.py
@@ -4,69 +4,72 @@ from django.db import transaction, models
from django.utils.text import slugify
from django.db.models import Case, When, Value
+
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):
parser.add_argument(
- '--model',
+ "--model",
type=str,
required=True,
- help='Model name (format: "app_label.ModelName")'
+ help='Model name (format: "app_label.ModelName")',
)
parser.add_argument(
- '--field',
+ "--field",
type=str,
- default='name',
- help='Field to use as slug source (default: "name")'
+ default="name",
+ help='Field to use as slug source (default: "name")',
)
parser.add_argument(
- '--batch-size',
+ "--batch-size",
type=int,
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(
- '--dry-run',
- action='store_true',
- help='Test without actually saving changes'
+ "--dry-run",
+ action="store_true",
+ help="Test without actually saving changes",
)
parser.add_argument(
- '--fill-empty',
- action='store_true',
- help='Fill empty slugs with model-ID when source field is empty'
+ "--fill-empty",
+ action="store_true",
+ help="Fill empty slugs with model-ID when source field is empty",
)
def handle(self, *args, **options):
- model = self.get_model(options['model'])
- source_field = options['field']
- batch_size = options['batch_size']
- dry_run = options['dry_run']
- fill_empty = options['fill_empty']
+ model = self.get_model(options["model"])
+ source_field = options["field"]
+ batch_size = options["batch_size"]
+ dry_run = options["dry_run"]
+ 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()
processed = 0
empty_source = 0
self.stdout.write(
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})'
)
)
with transaction.atomic():
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)
for offset in range(0, total_count, batch_size):
- batch = queryset[offset:offset + batch_size]
+ batch = queryset[offset : offset + batch_size]
updates = []
for obj in batch:
- source_value = getattr(obj, source_field, '')
+ source_value = getattr(obj, source_field, "")
if not source_value:
if fill_empty:
@@ -76,12 +79,14 @@ class Command(BaseCommand):
else:
self.stdout.write(
self.style.WARNING(
- f'Skipping {obj} (empty {source_field})'
+ f"Skipping {obj} (empty {source_field})"
)
)
continue
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
updates.append((obj.pk, new_slug))
@@ -94,27 +99,26 @@ class Command(BaseCommand):
)
self.stdout.write(
- f'Processed batch {offset//batch_size + 1}: '
- f'{min(offset + batch_size, total_count)}/{total_count}'
+ f"Processed batch {offset // batch_size + 1}: "
+ f"{min(offset + batch_size, total_count)}/{total_count}"
)
stats = [
f"Total processed: {processed}",
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.style.SUCCESS('\n'.join(stats))
- )
+ self.stdout.write(self.style.SUCCESS("\n".join(stats)))
def get_model(self, model_path):
"""Get model class from 'app_label.ModelName' string"""
from django.apps import apps
+
try:
- app_label, model_name = model_path.split('.')
+ app_label, model_name = model_path.split(".")
return apps.get_model(app_label, model_name)
except ValueError:
raise self.style.ERROR('Model must be specified as "app_label.ModelName"')
except LookupError as e:
- raise self.style.ERROR(f'Model not found: {e}')
\ No newline at end of file
+ raise self.style.ERROR(f"Model not found: {e}")
diff --git a/inventory/management/commands/generate_vin.py b/inventory/management/commands/generate_vin.py
index bb1a4186..36dc6132 100644
--- a/inventory/management/commands/generate_vin.py
+++ b/inventory/management/commands/generate_vin.py
@@ -1,32 +1,59 @@
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
import requests
+
class Command(BaseCommand):
- help = 'Seed the Customer model with 20 records'
+ help = "Seed the Customer model with 20 records"
def handle(self, *args, **kwargs):
- vin,description = self.generate_vin()
+ vin, description = self.generate_vin()
result = decodevin(vin)
- self.stdout.write(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'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}'))
+ self.stdout.write(
+ 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"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)
- self.stdout.write(self.style.SUCCESS(f'Make: {m.id_car_make} - Model: {m}'))
- self.stdout.write(self.style.SUCCESS('####################################################################################################'))
- self.stdout.write(self.style.SUCCESS('####################################################################################################'))
+ self.stdout.write(self.style.SUCCESS(f"Make: {m.id_car_make} - Model: {m}"))
+ self.stdout.write(
+ self.style.SUCCESS(
+ "####################################################################################################"
+ )
+ )
+ self.stdout.write(
+ self.style.SUCCESS(
+ "####################################################################################################"
+ )
+ )
-
-
def generate_vin(self):
# url = "https://www.vindecoder.org/vin-decoder"
url = "https://vingenerator.org/"
@@ -34,5 +61,5 @@ class Command(BaseCommand):
soup = BeautifulSoup(response.content, "html.parser")
vin = soup.find("input", {"name": "vin"})["value"]
description = soup.find("div", {"class": "description"}).text
-
- return vin,description
+
+ return vin, description
diff --git a/inventory/management/commands/import_data.py b/inventory/management/commands/import_data.py
index 7e27b24c..3c33cef9 100644
--- a/inventory/management/commands/import_data.py
+++ b/inventory/management/commands/import_data.py
@@ -8,8 +8,15 @@ django.setup()
import json
from tqdm import tqdm
from inventory.models import (
- CarMake, CarModel, CarSerie, CarTrim, CarEquipment,
- CarSpecification, CarSpecificationValue, CarOption, CarOptionValue
+ CarMake,
+ CarModel,
+ CarSerie,
+ CarTrim,
+ CarEquipment,
+ CarSpecification,
+ CarSpecificationValue,
+ CarOption,
+ CarOptionValue,
)
# 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", ""),
# "logo": item.get("Logo", ""),
# "is_sa_import": item.get("is_sa_import", False),
- }
+ },
)
# 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"],
"name": item["name"],
# "arabic_name": item.get("arabic_name", ""),
- }
+ },
)
# 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_end": item.get("year_end"),
"generation_name": item.get("generation_name", ""),
- }
+ },
)
# Step 4: Insert CarTrim
@@ -67,7 +74,7 @@ for item in tqdm(data["car_trim"], desc="Inserting CarTrim"):
# "arabic_name": item.get("arabic_name", ""),
"start_production_year": item["start_production_year"],
"end_production_year": item["end_production_year"],
- }
+ },
)
# 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"],
"name": item["name"],
"year_begin": item.get("year"),
- }
+ },
)
# Step 6: Insert CarSpecification (Parent specifications first)
parent_specs = [item for item in data["car_specification"] if item["id_parent"] is None]
-child_specs = [item for item in data["car_specification"] if item["id_parent"] is not None]
+child_specs = [
+ item for item in data["car_specification"] if item["id_parent"] is not None
+]
for item in tqdm(parent_specs, desc="Inserting Parent CarSpecifications"):
CarSpecification.objects.update_or_create(
@@ -92,8 +101,8 @@ for item in tqdm(parent_specs, desc="Inserting Parent CarSpecifications"):
defaults={
"name": item["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"):
@@ -103,12 +112,14 @@ for item in tqdm(child_specs, desc="Inserting Child CarSpecifications"):
defaults={
"name": item["name"],
# "arabic_name": item.get("arabic_name", ""),
- "id_parent_id": item["id_parent"]
- }
+ "id_parent_id": item["id_parent"],
+ },
)
# 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"])
CarSpecification.objects.get(id_car_specification=item["id_car_specification"])
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"],
"value": item["value"],
"unit": item.get("unit", ""),
- }
+ },
)
# Step 8: Insert CarOption (Parent options first)
@@ -131,8 +142,8 @@ for item in tqdm(parent_options, desc="Inserting Parent CarOptions"):
defaults={
"name": item["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"):
@@ -142,8 +153,8 @@ for item in tqdm(child_options, desc="Inserting Child CarOptions"):
defaults={
"name": item["name"],
# "arabic_name": item.get("arabic_name", ""),
- "id_parent_id": item["id_parent"]
- }
+ "id_parent_id": item["id_parent"],
+ },
)
# 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_equipment_id": item["id_car_equipment"],
"is_base": item["is_base"],
- }
+ },
)
-print("Data population completed successfully.")
\ No newline at end of file
+print("Data population completed successfully.")
diff --git a/inventory/management/commands/initial_services_offered.py b/inventory/management/commands/initial_services_offered.py
index 1838b082..1624712b 100644
--- a/inventory/management/commands/initial_services_offered.py
+++ b/inventory/management/commands/initial_services_offered.py
@@ -2,11 +2,31 @@
from django.core.management.base import BaseCommand
from appointment.models import Service
import datetime
+
+
class Command(BaseCommand):
- help = 'create initial services offered'
+ help = "create initial services offered"
def handle(self, *args, **options):
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(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')
\ No newline at end of file
+ Service.objects.create(
+ name="call",
+ 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",
+ )
diff --git a/inventory/management/commands/populate_colors.py b/inventory/management/commands/populate_colors.py
index f03a0f0f..70e25662 100644
--- a/inventory/management/commands/populate_colors.py
+++ b/inventory/management/commands/populate_colors.py
@@ -48,7 +48,9 @@ class Command(BaseCommand):
if created:
self.stdout.write(f"Added Exterior Color: {obj.name} ({obj.rgb})")
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...")
for color in self.interior_colors:
@@ -58,6 +60,8 @@ class Command(BaseCommand):
if created:
self.stdout.write(f"Added Interior Color: {obj.name} ({obj.rgb})")
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.")
\ No newline at end of file
+ self.stdout.write("Finished populating colors.")
diff --git a/inventory/management/commands/seed_customer.py b/inventory/management/commands/seed_customer.py
index 4ef2518b..71c43f26 100644
--- a/inventory/management/commands/seed_customer.py
+++ b/inventory/management/commands/seed_customer.py
@@ -2,24 +2,27 @@ from django.core.management.base import BaseCommand
from faker import Faker
from inventory.models import Customer, Dealer
+
class Command(BaseCommand):
- help = 'Seed the Customer model with 20 records'
+ help = "Seed the Customer model with 20 records"
def handle(self, *args, **kwargs):
fake = Faker()
dealers = Dealer.objects.all()
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
for _ in range(20):
dealer = fake.random_element(elements=dealers)
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()
email = fake.unique.email()
- national_id = fake.unique.bothify(text='##########')
+ national_id = fake.unique.bothify(text="##########")
phone_number = fake.unique.phone_number()
address = fake.address()
@@ -31,7 +34,7 @@ class Command(BaseCommand):
email=email,
national_id=national_id,
phone_number=phone_number,
- address=address
+ address=address,
)
- self.stdout.write(self.style.SUCCESS('Successfully seeded 20 customers.'))
\ No newline at end of file
+ self.stdout.write(self.style.SUCCESS("Successfully seeded 20 customers."))
diff --git a/inventory/management/commands/serie_translate.py b/inventory/management/commands/serie_translate.py
index 6660ccaf..3af1afc3 100644
--- a/inventory/management/commands/serie_translate.py
+++ b/inventory/management/commands/serie_translate.py
@@ -3,7 +3,6 @@ from inventory.models import CarSerie
TRANSLATIONS = {
"Liftback 5-doors": "ليفت باك - خمسة أبواب",
-
}
@@ -18,9 +17,15 @@ class Command(BaseCommand):
car_serie.arabic_name = arabic_translation
car_serie.save()
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:
- 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:
self.stdout.write(self.style.WARNING("No updates were made."))
diff --git a/inventory/management/commands/serie_up.py b/inventory/management/commands/serie_up.py
index 4f2582d1..298ca372 100644
--- a/inventory/management/commands/serie_up.py
+++ b/inventory/management/commands/serie_up.py
@@ -3,38 +3,53 @@ import csv
from django.core.management.base import BaseCommand
from inventory.models import CarSerie, CarModel
+
class Command(BaseCommand):
help = "Update or add CarSerie entries from a merged CSV file"
def handle(self, *args, **kwargs):
# Path to the merged CSV 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):
self.stdout.write(self.style.ERROR(f"File not found: {file_path}"))
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)
for row in reader:
try:
- car_model = CarModel.objects.get(pk=row['id_car_model'])
+ car_model = CarModel.objects.get(pk=row["id_car_model"])
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
car_serie, created = CarSerie.objects.update_or_create(
- id_car_serie=row['id_car_serie'],
+ id_car_serie=row["id_car_serie"],
defaults={
- 'id_car_model': car_model,
- 'name': row['name'],
- 'arabic_name': "-",
- 'year_begin': int(float(row['year_begin'])) if row['year_begin'] else None,
- 'year_end': int(float(row['year_end'])) if row['year_end'] else None,
- 'generation_name': row['generation_name'],
+ "id_car_model": car_model,
+ "name": row["name"],
+ "arabic_name": "-",
+ "year_begin": int(float(row["year_begin"]))
+ if row["year_begin"]
+ 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"
- self.stdout.write(self.style.SUCCESS(f"{action} CarSerie with ID {car_serie.id_car_serie}"))
\ No newline at end of file
+ self.stdout.write(
+ self.style.SUCCESS(
+ f"{action} CarSerie with ID {car_serie.id_car_serie}"
+ )
+ )
diff --git a/inventory/management/commands/set_vat.py b/inventory/management/commands/set_vat.py
index 5460cc1d..d80c9293 100644
--- a/inventory/management/commands/set_vat.py
+++ b/inventory/management/commands/set_vat.py
@@ -1,6 +1,8 @@
from django.core.management.base import BaseCommand
from inventory.models import VatRate
from decimal import Decimal
+
+
class Command(BaseCommand):
def handle(self, *args, **kwargs):
- VatRate.objects.get_or_create(rate=Decimal('0.15'), is_active=True)
\ No newline at end of file
+ VatRate.objects.get_or_create(rate=Decimal("0.15"), is_active=True)
diff --git a/inventory/management/commands/setplan.py b/inventory/management/commands/setplan.py
index e168d5ef..9bbfa2a0 100644
--- a/inventory/management/commands/setplan.py
+++ b/inventory/management/commands/setplan.py
@@ -4,24 +4,23 @@ from plans.models import Plan, Quota, Pricing, PlanPricing
from decimal import Decimal
from django.db.models import Q
+
class Command(BaseCommand):
- help = 'Create basic subscription plans structure'
+ help = "Create basic subscription plans structure"
def add_arguments(self, parser):
parser.add_argument(
- '--reset',
- action='store_true',
- help='Delete existing plans and quotas before creating new ones'
+ "--reset",
+ action="store_true",
+ help="Delete existing plans and quotas before creating new ones",
)
def handle(self, *args, **options):
- if options['reset']:
- self.stdout.write(self.style.WARNING('Resetting existing plans data...'))
+ if options["reset"]:
+ self.stdout.write(self.style.WARNING("Resetting existing plans data..."))
Plan.objects.all().delete()
Quota.objects.filter(
- Q(codename='basic') |
- Q(codename='pro') |
- Q(codename='premium')
+ Q(codename="basic") | Q(codename="pro") | Q(codename="premium")
).delete()
# Ensure no existing plans are marked as default
@@ -32,93 +31,89 @@ class Command(BaseCommand):
# Create core quotas
basic_quota, _ = Quota.objects.update_or_create(
- codename='basic',
+ codename="basic",
defaults={
- 'name': 'Basic Features',
- 'description': 'Essential platform access',
- 'is_boolean': True
- }
+ "name": "Basic Features",
+ "description": "Essential platform access",
+ "is_boolean": True,
+ },
)
pro_quota, _ = Quota.objects.update_or_create(
- codename='pro',
+ codename="pro",
defaults={
- 'name': 'Pro Features',
- 'description': 'Advanced functionality',
- 'is_boolean': True
- }
+ "name": "Pro Features",
+ "description": "Advanced functionality",
+ "is_boolean": True,
+ },
)
premium_quota, _ = Quota.objects.update_or_create(
- codename='premium',
+ codename="premium",
defaults={
- 'name': 'Premium Features',
- 'description': 'Full platform access',
- 'is_boolean': True
- }
+ "name": "Premium Features",
+ "description": "Full platform access",
+ "is_boolean": True,
+ },
)
# Create pricing period
monthly_pricing, _ = Pricing.objects.update_or_create(
- name='Monthly',
- defaults={'period': 30}
+ name="Monthly", defaults={"period": 30}
)
# Define plan structure
plans = [
{
- 'name': 'Basic',
- 'description': 'Entry-level plan',
- 'price': Decimal('0.00'),
- 'period': None,
- 'quotas': [basic_quota],
- 'default': True
+ "name": "Basic",
+ "description": "Entry-level plan",
+ "price": Decimal("0.00"),
+ "period": None,
+ "quotas": [basic_quota],
+ "default": True,
},
{
- 'name': 'Pro',
- 'description': 'Professional plan',
- 'price': Decimal('29.00'),
- 'period': 30,
- 'quotas': [basic_quota, pro_quota],
- 'default': False
+ "name": "Pro",
+ "description": "Professional plan",
+ "price": Decimal("29.00"),
+ "period": 30,
+ "quotas": [basic_quota, pro_quota],
+ "default": False,
},
{
- 'name': 'Premium',
- 'description': 'Full access plan',
- 'price': Decimal('99.00'),
- 'period': 30,
- 'quotas': [basic_quota, pro_quota, premium_quota],
- 'default': None
- }
+ "name": "Premium",
+ "description": "Full access plan",
+ "price": Decimal("99.00"),
+ "period": 30,
+ "quotas": [basic_quota, pro_quota, premium_quota],
+ "default": None,
+ },
]
# Create plans and associations
for plan_data in plans:
plan, created = Plan.objects.update_or_create(
- name=plan_data['name'],
+ name=plan_data["name"],
defaults={
- 'description': plan_data['description'],
- 'default': plan_data.get('default', False),
- 'available': True,
- 'visible': True
- }
+ "description": plan_data["description"],
+ "default": plan_data.get("default", False),
+ "available": True,
+ "visible": True,
+ },
)
# Set quotas
- plan.quotas.set(plan_data['quotas'])
+ plan.quotas.set(plan_data["quotas"])
# Create pricing if applicable
- if plan_data['price'] > 0:
+ if plan_data["price"] > 0:
PlanPricing.objects.update_or_create(
plan=plan,
pricing=monthly_pricing,
- defaults={
- 'price': plan_data['price'],
- 'visible': True
- }
+ defaults={"price": plan_data["price"], "visible": True},
)
- status = 'Created' if created else 'Updated'
- self.stdout.write(self.style.SUCCESS(f'{status} {plan.name} plan'))
+ status = "Created" if created else "Updated"
+ self.stdout.write(self.style.SUCCESS(f"{status} {plan.name} plan"))
- self.stdout.write(self.style.SUCCESS('Successfully created plans structure'))
\ No newline at end of file
+ self.stdout.write(self.style.SUCCESS("Successfully created plans structure"))
diff --git a/inventory/management/commands/tenhal_plan.py b/inventory/management/commands/tenhal_plan.py
index acd3d39b..a301a6ad 100644
--- a/inventory/management/commands/tenhal_plan.py
+++ b/inventory/management/commands/tenhal_plan.py
@@ -2,14 +2,16 @@
from decimal import Decimal
from django.core.management.base import BaseCommand
from plans.models import Plan, Quota, PlanQuota, Pricing, PlanPricing
+
+
class Command(BaseCommand):
- help = 'Create basic subscription plans structure'
+ help = "Create basic subscription plans structure"
def add_arguments(self, parser):
parser.add_argument(
- '--reset',
- action='store_true',
- help='Delete existing plans and quotas before creating new ones'
+ "--reset",
+ action="store_true",
+ help="Delete existing plans and quotas before creating new ones",
)
def handle(self, *args, **options):
@@ -22,13 +24,23 @@ class Command(BaseCommand):
# Order.objects.all().delete()
# BillingInfo.objects.all().delete()
-
- users_quota = Quota.objects.create(name='Users', codename='Users', unit='number')
- cars_quota = Quota.objects.create(name='Cars', codename='Cars', unit='number')
+ users_quota = Quota.objects.create(
+ name="Users", codename="Users", unit="number"
+ )
+ cars_quota = Quota.objects.create(name="Cars", codename="Cars", unit="number")
# Create plans
- basic_plan = Plan.objects.create(name='Basic', description='basic 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)
+ basic_plan = Plan.objects.create(
+ name="Basic", description="basic 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
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)
# Define pricing
- basic_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)
+ basic_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)
- PlanPricing.objects.create(plan=basic_plan, pricing=basic_pricing, price=Decimal('9.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'))
+ PlanPricing.objects.create(
+ plan=basic_plan, pricing=basic_pricing, price=Decimal("9.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
# 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)
# pro = Pricing.objects.create(name='Monthly', period=30)
-
# 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'))
# Create users
# user = User.objects.first()
- # # Create user plans
- # billing_info = BillingInfo.objects.create(
- # user=user,
- # tax_number='123456789',
- # name='John Doe',
- # street='123 Main St',
- # zipcode='12345',
- # city='Anytown',
- # country='US',
- # )
+ # # Create user plans
+ # billing_info = BillingInfo.objects.create(
+ # user=user,
+ # tax_number='123456789',
+ # name='John Doe',
+ # street='123 Main St',
+ # zipcode='12345',
+ # city='Anytown',
+ # country='US',
+ # )
- # order = Order.objects.create(
- # user=user,
- # plan=pro_plan,
- # pricing=pro_pricing,
- # amount=pro_pricing.price,
- # currency="SAR",
- # )
+ # order = Order.objects.create(
+ # user=user,
+ # plan=pro_plan,
+ # pricing=pro_pricing,
+ # amount=pro_pricing.price,
+ # currency="SAR",
+ # )
- # UserPlan.objects.create(
- # user=user,
- # plan=pro_plan,
- # expire=timezone.now() + timedelta(days=2),
- # active=True,
- # )
+ # UserPlan.objects.create(
+ # user=user,
+ # plan=pro_plan,
+ # expire=timezone.now() + timedelta(days=2),
+ # active=True,
+ # )
diff --git a/inventory/management/commands/test.py b/inventory/management/commands/test.py
index 2f2b7326..e16ba9a2 100644
--- a/inventory/management/commands/test.py
+++ b/inventory/management/commands/test.py
@@ -4,8 +4,9 @@ from inventory.tasks import create_coa_accounts
from inventory.models import Dealer
User = get_user_model()
-class Command(BaseCommand):
+
+class Command(BaseCommand):
def handle(self, *args, **kwargs):
# user = User.objects.last()
# print(user.email)
@@ -17,4 +18,4 @@ class Command(BaseCommand):
# result = re.match(r'^05\d{8}$', '0625252522')
# print(result)
dealer = Dealer.objects.last()
- create_coa_accounts(dealer.pk)
\ No newline at end of file
+ create_coa_accounts(dealer.pk)
diff --git a/inventory/management/commands/test_task_process_running.py b/inventory/management/commands/test_task_process_running.py
index 8603c5b3..3eed3551 100644
--- a/inventory/management/commands/test_task_process_running.py
+++ b/inventory/management/commands/test_task_process_running.py
@@ -1,4 +1,5 @@
from django.core.management.base import BaseCommand
+
# from background_task.models import Task
# from background_task import background
# from django_q.tasks import async_task
@@ -6,7 +7,5 @@ from inventory.tasks import send_email
class Command(BaseCommand):
-
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")
diff --git a/inventory/management/commands/translate.py b/inventory/management/commands/translate.py
index fb1a0a22..5d593e47 100644
--- a/inventory/management/commands/translate.py
+++ b/inventory/management/commands/translate.py
@@ -5,17 +5,19 @@ from django.conf import settings
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):
client = OpenAI(api_key=settings.OPENAI_API_KEY)
car_models = CarModel.objects.all()
total = car_models.count()
- print(f'Translating {total} names...')
+ print(f"Translating {total} names...")
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):
car_model.arabic_name = car_model.name
car_model.save()
@@ -31,12 +33,9 @@ class Command(BaseCommand):
"You are an assistant that translates English car names to Arabic."
"If the name is purely numeric, keep it as is."
"For mixed names like 'D9', translate them as 'دي 9'."
- )
+ ),
},
- {
- "role": "user",
- "content": car_model.name
- }
+ {"role": "user", "content": car_model.name},
],
temperature=0.2,
)
@@ -45,4 +44,4 @@ class Command(BaseCommand):
car_model.save()
print(f"[{index}/{total}] .. Done")
except Exception as e:
- print(f"Error translating '{car_model.name}': {e}")
\ No newline at end of file
+ print(f"Error translating '{car_model.name}': {e}")
diff --git a/inventory/management/commands/update_car_make.py b/inventory/management/commands/update_car_make.py
index 6da22408..6f8ede65 100644
--- a/inventory/management/commands/update_car_make.py
+++ b/inventory/management/commands/update_car_make.py
@@ -2,32 +2,37 @@ from django.core.management.base import BaseCommand
from inventory.models import CarMake
import json
+
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):
# 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)
# Iterate over the data and update the CarMake model
for car_make_data in car_makes_data:
- pk = car_make_data['pk']
- fields = car_make_data['fields']
+ pk = car_make_data["pk"]
+ fields = car_make_data["fields"]
# Get or create the CarMake instance
car_make, created = CarMake.objects.get_or_create(pk=pk)
# Update the fields
- car_make.name = fields['name']
- car_make.arabic_name = fields['arabic_name']
- car_make.logo = fields['logo']
- car_make.is_sa_import = fields['is_sa_import']
+ car_make.name = fields["name"]
+ car_make.arabic_name = fields["arabic_name"]
+ car_make.logo = fields["logo"]
+ car_make.is_sa_import = fields["is_sa_import"]
# Save the updated instance
car_make.save()
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:
- self.stdout.write(self.style.SUCCESS(f'Updated CarMake: {car_make.name}'))
\ No newline at end of file
+ self.stdout.write(
+ self.style.SUCCESS(f"Updated CarMake: {car_make.name}")
+ )
diff --git a/inventory/management/commands/update_car_model.py b/inventory/management/commands/update_car_model.py
index 84b18c86..723a3157 100644
--- a/inventory/management/commands/update_car_model.py
+++ b/inventory/management/commands/update_car_model.py
@@ -3,35 +3,46 @@ import csv
from django.core.management.base import BaseCommand
from inventory.models import CarModel, CarMake
+
class Command(BaseCommand):
help = "Update or add CarModel entries from a CSV file"
def handle(self, *args, **kwargs):
# Path to the car_model CSV 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):
self.stdout.write(self.style.ERROR(f"File not found: {file_path}"))
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)
for row in reader:
try:
- car_make = CarMake.objects.get(pk=row['id_car_make'])
+ car_make = CarMake.objects.get(pk=row["id_car_make"])
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
car_model, created = CarModel.objects.update_or_create(
- id_car_model=row['id_car_model'],
+ id_car_model=row["id_car_model"],
defaults={
- 'id_car_make': car_make,
- 'name': row['name'],
- 'arabic_name': row.get('arabic_name', ''),
+ "id_car_make": car_make,
+ "name": row["name"],
+ "arabic_name": row.get("arabic_name", ""),
},
)
action = "Created" if created else "Updated"
- self.stdout.write(self.style.SUCCESS(f"{action} CarModel with ID {car_model.id_car_model}"))
\ No newline at end of file
+ self.stdout.write(
+ self.style.SUCCESS(
+ f"{action} CarModel with ID {car_model.id_car_model}"
+ )
+ )
diff --git a/inventory/management/commands/update_car_specification_value.py b/inventory/management/commands/update_car_specification_value.py
index 6d321f7c..4cc61907 100644
--- a/inventory/management/commands/update_car_specification_value.py
+++ b/inventory/management/commands/update_car_specification_value.py
@@ -3,42 +3,61 @@ import csv
from django.core.management.base import BaseCommand
from inventory.models import CarSpecificationValue, CarSpecification, CarTrim
+
class Command(BaseCommand):
help = "Update or add CarSpecificationValue entries from a CSV file"
def handle(self, *args, **kwargs):
# Path to the car_specification_value CSV 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):
self.stdout.write(self.style.ERROR(f"File not found: {file_path}"))
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)
for row in reader:
try:
- car_trim = CarTrim.objects.get(pk=row['id_car_trim'])
+ car_trim = CarTrim.objects.get(pk=row["id_car_trim"])
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
try:
- car_specification = CarSpecification.objects.get(pk=row['id_car_specification'])
+ car_specification = CarSpecification.objects.get(
+ pk=row["id_car_specification"]
+ )
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
- car_specification_value, created = CarSpecificationValue.objects.update_or_create(
- id_car_specification_value=row['id_car_specification_value'],
- defaults={
- 'id_car_trim': car_trim,
- 'id_car_specification': car_specification,
- 'value': row['value'],
- 'unit': row.get('unit', ''),
- },
+ car_specification_value, created = (
+ CarSpecificationValue.objects.update_or_create(
+ id_car_specification_value=row["id_car_specification_value"],
+ defaults={
+ "id_car_trim": car_trim,
+ "id_car_specification": car_specification,
+ "value": row["value"],
+ "unit": row.get("unit", ""),
+ },
+ )
)
action = "Created" if created else "Updated"
- self.stdout.write(self.style.SUCCESS(f"{action} CarSpecificationValue with ID {car_specification_value.id_car_specification_value}"))
\ No newline at end of file
+ self.stdout.write(
+ self.style.SUCCESS(
+ f"{action} CarSpecificationValue with ID {car_specification_value.id_car_specification_value}"
+ )
+ )
diff --git a/inventory/management/commands/update_car_trim.py b/inventory/management/commands/update_car_trim.py
index 8a50d86e..f76e3766 100644
--- a/inventory/management/commands/update_car_trim.py
+++ b/inventory/management/commands/update_car_trim.py
@@ -3,45 +3,66 @@ import csv
from django.core.management.base import BaseCommand
from inventory.models import CarTrim, CarSerie, CarModel
+
class Command(BaseCommand):
help = "Update or add CarTrim entries from a CSV file"
def handle(self, *args, **kwargs):
# Path to the car_trim CSV 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):
self.stdout.write(self.style.ERROR(f"File not found: {file_path}"))
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)
for row in reader:
try:
- car_serie = CarSerie.objects.get(pk=row['id_car_serie'])
+ car_serie = CarSerie.objects.get(pk=row["id_car_serie"])
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
car_model = None
- if row['id_car_model']:
+ if row["id_car_model"]:
try:
- car_model = CarModel.objects.get(pk=row['id_car_model'])
+ car_model = CarModel.objects.get(pk=row["id_car_model"])
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(
- id_car_trim=row['id_car_trim'],
+ id_car_trim=row["id_car_trim"],
defaults={
- 'id_car_serie': car_serie,
- 'id_car_model': car_model,
- 'name': row['name'],
- 'arabic_name': row.get('arabic_name', ''),
- 'start_production_year': int(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,
+ "id_car_serie": car_serie,
+ "id_car_model": car_model,
+ "name": row["name"],
+ "arabic_name": row.get("arabic_name", ""),
+ "start_production_year": int(
+ 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"
- self.stdout.write(self.style.SUCCESS(f"{action} CarTrim with ID {car_trim.id_car_trim}"))
\ No newline at end of file
+ self.stdout.write(
+ self.style.SUCCESS(
+ f"{action} CarTrim with ID {car_trim.id_car_trim}"
+ )
+ )
diff --git a/inventory/middleware.py b/inventory/middleware.py
index 8a65685b..3ee697b8 100644
--- a/inventory/middleware.py
+++ b/inventory/middleware.py
@@ -4,7 +4,7 @@ from django.utils import timezone
from inventory.utils import get_user_type
-logger = logging.getLogger('user_activity')
+logger = logging.getLogger("user_activity")
class LogUserActivityMiddleware:
@@ -20,6 +20,7 @@ class LogUserActivityMiddleware:
chain.
:type get_response: Callable
"""
+
def __init__(self, get_response):
self.get_response = get_response
@@ -29,18 +30,16 @@ class LogUserActivityMiddleware:
if request.user.is_authenticated:
action = f"{request.method} {request.path}"
models.UserActivityLog.objects.create(
- user=request.user,
- action=action,
- timestamp=timezone.now()
+ user=request.user, action=action, timestamp=timezone.now()
)
return response
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:
- return x_forwarded_for.split(',')[0]
- return request.META.get('REMOTE_ADDR')
-
+ return x_forwarded_for.split(",")[0]
+ return request.META.get("REMOTE_ADDR")
+
class InjectParamsMiddleware:
"""
@@ -54,15 +53,16 @@ class InjectParamsMiddleware:
:ivar get_response: The callable to get the next middleware or view response.
:type get_response: Callable
"""
+
def __init__(self, get_response):
self.get_response = get_response
- def __call__(self, request):
- try:
+ def __call__(self, request):
+ try:
# request.entity = request.user.dealer.entity
request.dealer = get_user_type(request)
except Exception:
- pass
+ pass
response = self.get_response(request)
return response
@@ -81,22 +81,24 @@ class InjectDealerMiddleware:
to process the next middleware or the view in the request-response cycle.
:type get_response: Callable
"""
+
def __init__(self, get_response):
self.get_response = get_response
- def __call__(self, request):
- try:
- request.is_dealer = False
- request.is_staff = False
+ def __call__(self, request):
+ try:
+ request.is_dealer = False
+ request.is_staff = False
if hasattr(request.user, "dealer"):
- request.is_dealer = True
- if hasattr(request.user, "staffmember"):
+ request.is_dealer = True
+ if hasattr(request.user, "staffmember"):
request.is_staff = True
except Exception:
- pass
+ pass
response = self.get_response(request)
return response
+
# class OTPVerificationMiddleware:
# def __init__(self, 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):
# return redirect(reverse('verify_otp'))
# return self.get_response(request)
-
diff --git a/inventory/migrations/0001_initial.py b/inventory/migrations/0001_initial.py
new file mode 100644
index 00000000..eecd0e39
--- /dev/null
+++ b/inventory/migrations/0001_initial.py
@@ -0,0 +1,3059 @@
+# 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"
+ ),
+ ],
+ },
+ ),
+ ]
diff --git a/inventory/migrations/0002_remove_saleorder_journal_entry.py b/inventory/migrations/0002_remove_saleorder_journal_entry.py
new file mode 100644
index 00000000..78d5b484
--- /dev/null
+++ b/inventory/migrations/0002_remove_saleorder_journal_entry.py
@@ -0,0 +1,16 @@
+# 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",
+ ),
+ ]
diff --git a/inventory/migrations/0003_saleorder_journal_entry.py b/inventory/migrations/0003_saleorder_journal_entry.py
new file mode 100644
index 00000000..c0134202
--- /dev/null
+++ b/inventory/migrations/0003_saleorder_journal_entry.py
@@ -0,0 +1,24 @@
+# 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",
+ ),
+ ),
+ ]
diff --git a/inventory/migrations/0004_remove_saleorder_agreed_price_and_more.py b/inventory/migrations/0004_remove_saleorder_agreed_price_and_more.py
new file mode 100644
index 00000000..51a90490
--- /dev/null
+++ b/inventory/migrations/0004_remove_saleorder_agreed_price_and_more.py
@@ -0,0 +1,60 @@
+# 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",
+ ),
+ ]
diff --git a/inventory/migrations/0005_remove_saleorder_car.py b/inventory/migrations/0005_remove_saleorder_car.py
new file mode 100644
index 00000000..5aa3c25f
--- /dev/null
+++ b/inventory/migrations/0005_remove_saleorder_car.py
@@ -0,0 +1,16 @@
+# 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",
+ ),
+ ]
diff --git a/inventory/migrations/0006_saleorder_dealer.py b/inventory/migrations/0006_saleorder_dealer.py
new file mode 100644
index 00000000..40eafea4
--- /dev/null
+++ b/inventory/migrations/0006_saleorder_dealer.py
@@ -0,0 +1,24 @@
+# Generated by Django 5.2.1 on 2025-06-19 13:51
+
+import django.db.models.deletion
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+ dependencies = [
+ ("inventory", "0005_remove_saleorder_car"),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name="saleorder",
+ name="dealer",
+ field=models.ForeignKey(
+ default=1,
+ on_delete=django.db.models.deletion.CASCADE,
+ related_name="sale_orders",
+ to="inventory.dealer",
+ ),
+ preserve_default=False,
+ ),
+ ]
diff --git a/inventory/migrations/0007_saleorder_estimate.py b/inventory/migrations/0007_saleorder_estimate.py
new file mode 100644
index 00000000..6e32e257
--- /dev/null
+++ b/inventory/migrations/0007_saleorder_estimate.py
@@ -0,0 +1,26 @@
+# 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",
+ ),
+ ),
+ ]
diff --git a/inventory/migrations/0008_saleorder_opportunity.py b/inventory/migrations/0008_saleorder_opportunity.py
new file mode 100644
index 00000000..ad5caf00
--- /dev/null
+++ b/inventory/migrations/0008_saleorder_opportunity.py
@@ -0,0 +1,25 @@
+# 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",
+ ),
+ ),
+ ]
diff --git a/inventory/migrations/0009_saleorder_customer.py b/inventory/migrations/0009_saleorder_customer.py
new file mode 100644
index 00000000..47eddd24
--- /dev/null
+++ b/inventory/migrations/0009_saleorder_customer.py
@@ -0,0 +1,25 @@
+# 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",
+ ),
+ ),
+ ]
diff --git a/inventory/migrations/0010_alter_saleorder_created_by_and_more.py b/inventory/migrations/0010_alter_saleorder_created_by_and_more.py
new file mode 100644
index 00000000..95c30c5c
--- /dev/null
+++ b/inventory/migrations/0010_alter_saleorder_created_by_and_more.py
@@ -0,0 +1,39 @@
+# 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,
+ ),
+ ),
+ ]
diff --git a/inventory/migrations/0015_intendedvehicle_purchaseorder_deliveryreceipt.py b/inventory/migrations/0015_intendedvehicle_purchaseorder_deliveryreceipt.py
deleted file mode 100644
index 26a7c129..00000000
--- a/inventory/migrations/0015_intendedvehicle_purchaseorder_deliveryreceipt.py
+++ /dev/null
@@ -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')),
- ],
- ),
- ]
diff --git a/inventory/migrations/0016_purchaseorderitem_sale.py b/inventory/migrations/0016_purchaseorderitem_sale.py
deleted file mode 100644
index 295a2e43..00000000
--- a/inventory/migrations/0016_purchaseorderitem_sale.py
+++ /dev/null
@@ -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')),
- ],
- ),
- ]
diff --git a/inventory/migrations/0017_intendedvehicle_purchase_order.py b/inventory/migrations/0017_intendedvehicle_purchase_order.py
deleted file mode 100644
index feed14dc..00000000
--- a/inventory/migrations/0017_intendedvehicle_purchase_order.py
+++ /dev/null
@@ -1,20 +0,0 @@
-# Generated by Django 5.2.1 on 2025-06-03 11:36
-
-import django.db.models.deletion
-from django.db import migrations, models
-
-
-class Migration(migrations.Migration):
-
- dependencies = [
- ('inventory', '0016_purchaseorderitem_sale'),
- ]
-
- operations = [
- migrations.AddField(
- model_name='intendedvehicle',
- name='purchase_order',
- field=models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, related_name='intended_vehicles', to='inventory.purchaseorder'),
- preserve_default=False,
- ),
- ]
diff --git a/inventory/migrations/0018_intendedvehicle_quantity.py b/inventory/migrations/0018_intendedvehicle_quantity.py
deleted file mode 100644
index d8336b71..00000000
--- a/inventory/migrations/0018_intendedvehicle_quantity.py
+++ /dev/null
@@ -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),
- ),
- ]
diff --git a/inventory/migrations/0019_alter_intendedvehicle_make_and_more.py b/inventory/migrations/0019_alter_intendedvehicle_make_and_more.py
deleted file mode 100644
index 81564bb1..00000000
--- a/inventory/migrations/0019_alter_intendedvehicle_make_and_more.py
+++ /dev/null
@@ -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),
- ),
- ]
diff --git a/inventory/migrations/0020_remove_intendedvehicle_purchase_order_and_more.py b/inventory/migrations/0020_remove_intendedvehicle_purchase_order_and_more.py
deleted file mode 100644
index bb76f0d8..00000000
--- a/inventory/migrations/0020_remove_intendedvehicle_purchase_order_and_more.py
+++ /dev/null
@@ -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',
- ),
- ]
diff --git a/inventory/migrations/__init__.py b/inventory/migrations/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/inventory/mixins.py b/inventory/mixins.py
index 9a6f9c77..ae55f329 100644
--- a/inventory/mixins.py
+++ b/inventory/mixins.py
@@ -1,5 +1,6 @@
from django.utils.translation import get_language
+
class AddClassMixin:
"""
Provides functionality for automatically adding CSS classes to form field widgets.
@@ -10,6 +11,7 @@ class AddClassMixin:
apply different CSS classes.
"""
+
def add_class_to_fields(self):
"""
Adds the class to the fields of the model.
@@ -20,8 +22,10 @@ class AddClassMixin:
# existing_classes = field.widget.attrs.get('class', '')
# field.widget.attrs['class'] = f"{existing_classes} form-select form-select-sm".strip()
# else:
- existing_classes = field.widget.attrs.get('class', '')
- field.widget.attrs['class'] = f"{existing_classes} form-control form-control-sm".strip()
+ existing_classes = field.widget.attrs.get("class", "")
+ field.widget.attrs["class"] = (
+ f"{existing_classes} form-control form-control-sm".strip()
+ )
class LocalizedNameMixin:
@@ -38,13 +42,14 @@ class LocalizedNameMixin:
:ivar name: Default name used for non-Arabic languages.
:type name: Optional[str]
"""
+
def get_local_name(self):
"""
Returns the localized name based on the current language.
"""
- if get_language() == 'ar':
- return getattr(self, 'arabic_name', None)
- return getattr(self, 'name', None)
+ if get_language() == "ar":
+ return getattr(self, "arabic_name", None)
+ return getattr(self, "name", None)
# class AddDealerInstanceMixin:
diff --git a/inventory/models.py b/inventory/models.py
index 8fab2192..ade6dd21 100644
--- a/inventory/models.py
+++ b/inventory/models.py
@@ -16,6 +16,7 @@ from django_ledger.models import (
EntityModel,
ItemModel,
CustomerModel,
+ JournalEntryModel,
)
from django_ledger.io.io_core import get_localdate
from django.core.exceptions import ValidationError
@@ -29,7 +30,7 @@ from django_ledger.models import (
EstimateModel,
InvoiceModel,
AccountModel,
- EntityManagementModel
+ EntityManagementModel,
)
from django.contrib.contenttypes.fields import GenericForeignKey
from django.contrib.contenttypes.models import ContentType
@@ -630,6 +631,7 @@ class Car(Base):
[
self.colors,
self.finances,
+ self.finances.selling_price > 0,
]
)
except Exception:
@@ -712,10 +714,14 @@ class Car(Base):
.filter(name=f"Cogs:{self.id_car_make.name}")
.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()
+
class CarTransfer(models.Model):
car = models.ForeignKey(
"Car",
@@ -1368,7 +1374,7 @@ class Customer(models.Model):
def full_name(self):
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 = self.dealer.entity.create_customer(
commit=False,
@@ -1411,7 +1417,7 @@ class Customer(models.Model):
customer.save()
return customer
- def create_user_model(self,for_lead=False):
+ def create_user_model(self, for_lead=False):
user = User.objects.create_user(
username=self.email,
email=self.email,
@@ -1501,7 +1507,7 @@ class Organization(models.Model, LocalizedNameMixin):
def __str__(self):
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 = self.dealer.entity.create_customer(
commit=False,
@@ -1543,7 +1549,7 @@ class Organization(models.Model, LocalizedNameMixin):
customer.save()
return customer
- def create_user_model(self,for_lead=False):
+ def create_user_model(self, for_lead=False):
user = User.objects.create_user(
username=self.email,
email=self.email,
@@ -1777,6 +1783,7 @@ class Lead(models.Model):
def get_opportunities(self):
return Opportunity.objects.filter(lead=self)
+
@property
def get_current_action(self):
return (
@@ -1912,7 +1919,10 @@ class Opportunity(models.Model):
max_digits=10, decimal_places=2, verbose_name=_("Salary"), blank=True, null=True
)
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(
max_length=20, choices=Stage.choices, verbose_name=_("Stage")
@@ -1933,10 +1943,16 @@ class Opportunity(models.Model):
)
probability = models.PositiveIntegerField(validators=[validate_probability])
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(
- 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_model = models.CharField(max_length=100, blank=True, null=True)
@@ -1961,6 +1977,7 @@ class Opportunity(models.Model):
def get_notes(self):
return self._get_filter(Notes).order_by("-created")
+
def get_activities(self):
return self._get_filter(Activity)
@@ -1969,17 +1986,21 @@ class Opportunity(models.Model):
def get_meetings(self):
return self.lead.get_meetings()
+
def get_calls(self):
return self.lead.get_calls()
+
def get_schedules(self):
- return self.lead.get_all_schedules().filter(
- scheduled_at__gt=timezone.now()
- ).order_by("scheduled_at")
+ return (
+ self.lead.get_all_schedules()
+ .filter(scheduled_at__gt=timezone.now())
+ .order_by("scheduled_at")
+ )
def get_emails(self):
return self._get_filter(Email)
- def _get_filter(self,Model):
+ def _get_filter(self, Model):
objects = Model.objects.filter(
content_type__model="opportunity", object_id=self.id
)
@@ -2001,9 +2022,7 @@ class Opportunity(models.Model):
opportinity_for = self.organization.name
if not self.slug:
- self.slug = slugify(
- f"opportunity {opportinity_for}"
- )
+ self.slug = slugify(f"opportunity {opportinity_for}")
super().save(*args, **kwargs)
class Meta:
@@ -2012,7 +2031,9 @@ class Opportunity(models.Model):
def __str__(self):
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}"
@@ -2340,12 +2361,16 @@ class SaleOrder(models.Model):
("DELIVERED", "Delivered"),
("CANCELLED", "Cancelled"),
]
-
+ dealer = models.ForeignKey(
+ Dealer, on_delete=models.CASCADE, related_name="sale_orders"
+ )
estimate = models.ForeignKey(
EstimateModel,
on_delete=models.CASCADE,
related_name="sale_orders",
verbose_name=_("Estimate"),
+ null=True,
+ blank=True,
)
invoice = models.ForeignKey(
InvoiceModel,
@@ -2355,93 +2380,25 @@ class SaleOrder(models.Model):
null=True,
blank=True,
)
- payment_method = models.CharField(
- max_length=20,
- choices=[
- ("cash", _("Cash")),
- ("finance", _("Finance")),
- ("lease", _("Lease")),
- ("credit_card", _("Credit Card")),
- ("bank_transfer", _("Bank Transfer")),
- ("sadad", _("SADAD")),
- ],
+ customer = models.ForeignKey(
+ Customer,
+ on_delete=models.CASCADE,
+ related_name="sale_orders",
+ verbose_name=_("Customer"),
+ null=True,
+ blank=True,
+ )
+ 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)
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 = models.CharField(
max_length=20,
@@ -2469,13 +2426,13 @@ class SaleOrder(models.Model):
blank=True, null=True, help_text="Reason for cancellation, if applicable."
)
- # Audit fields
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
created_by = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.SET_NULL,
null=True,
+ blank=True,
related_name="created_sales_orders",
help_text="The user who created this sales order.",
)
@@ -2483,6 +2440,7 @@ class SaleOrder(models.Model):
settings.AUTH_USER_MODEL,
on_delete=models.SET_NULL,
null=True,
+ blank=True,
related_name="modified_sales_orders",
help_text="The user who last modified this sales order.",
)
@@ -2501,14 +2459,7 @@ class SaleOrder(models.Model):
next_id = 1
year = get_localdate().year
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)
def __str__(self):
@@ -2524,13 +2475,15 @@ class SaleOrder(models.Model):
@property
def items(self):
- if self.estimate.get_itemtxs_data():
- return self.estimate.get_itemtxs_data()[0]
+ if self.invoice.get_itemtxs_data():
+ return self.invoice.get_itemtxs_data()[0]
return []
@property
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):
@@ -2540,6 +2493,10 @@ class CustomGroup(models.Model):
"auth.Group", verbose_name=_("Group"), on_delete=models.CASCADE
)
+ @property
+ def entity(self):
+ return self.invoice.entity
+
@property
def users(self):
return self.group.user_set.all()
@@ -2859,7 +2816,6 @@ class PaymentHistory(models.Model):
return self.status == self.COMPLETED
-
######################################################################################################
######################################################################################################
######################################################################################################
diff --git a/inventory/services.py b/inventory/services.py
index e22883d2..ccc26e2b 100644
--- a/inventory/services.py
+++ b/inventory/services.py
@@ -27,11 +27,12 @@ def get_make(item):
if not data:
r = item.split(" ")
for i in r:
- if data:= CarMake.objects.filter(name__iexact=i).first():
+ if data := CarMake.objects.filter(name__iexact=i).first():
break
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
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:
r = item.split(" ")
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
return data
+
def normalize_name(name):
"""
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):
-
"""
Decodes a Vehicle Identification Number (VIN) using multiple decoding functions
and returns the decoded result. This function attempts to decode the VIN using
@@ -81,9 +82,9 @@ def decodevin(vin):
all decoding attempts fail.
:rtype: dict | None
"""
- if result:=decode_vin(vin):
+ if result := decode_vin(vin):
return result
- elif result:=elm(vin):
+ elif result := elm(vin):
return result
# elif result:=decode_vin_haikalna(vin):
# return result
@@ -158,6 +159,3 @@ def elm(vin):
}
print([x for x in data.values()])
return data if all([x for x in data.values()]) else None
-
-
-
diff --git a/inventory/signals.py b/inventory/signals.py
index 20306bf9..a37a1b4c 100644
--- a/inventory/signals.py
+++ b/inventory/signals.py
@@ -12,7 +12,7 @@ from django_ledger.models import (
JournalEntryModel,
TransactionModel,
LedgerModel,
- AccountModel
+ AccountModel,
)
from . import models
from django.utils.timezone import now
@@ -59,7 +59,10 @@ User = get_user_model()
@receiver(post_save, sender=models.Car)
def create_dealers_make(sender, instance, created, **kwargs):
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)
def create_car_location(sender, instance, created, **kwargs):
@@ -94,6 +97,7 @@ def create_car_location(sender, instance, created, **kwargs):
except Exception as e:
print(f"Failed to create CarLocation for car {instance.vin}: {e}")
+
# Create Entity
@receiver(post_save, sender=models.Dealer)
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_accounts_for_make(instance.pk)
+
@receiver(post_save, sender=models.Dealer)
def create_dealer_groups(sender, instance, created, **kwargs):
"""
@@ -162,11 +167,18 @@ def create_dealer_groups(sender, instance, created, **kwargs):
def create_groups():
for group_name in group_names:
- group, created = Group.objects.get_or_create(name=f"{instance.pk}_{group_name}")
- group_manager,created = models.CustomGroup.objects.get_or_create(name=group_name, dealer=instance, group=group)
+ group, created = Group.objects.get_or_create(
+ 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()
instance.user.groups.add(group)
+
transaction.on_commit(create_groups)
+
+
# Create Vendor
@receiver(post_save, sender=models.Vendor)
def create_ledger_vendor(sender, instance, created, **kwargs):
@@ -189,6 +201,7 @@ def create_ledger_vendor(sender, instance, created, **kwargs):
else:
instance.update_vendor_model()
+
# Create Item
@receiver(post_save, sender=models.Car)
def create_item_model(sender, instance, created, **kwargs):
@@ -228,12 +241,13 @@ def create_item_model(sender, instance, created, **kwargs):
# )
instance.item_model = inventory
inventory.additional_info = {}
- inventory.additional_info.update({'car_info': instance.to_dict()})
+ inventory.additional_info.update({"car_info": instance.to_dict()})
inventory.save()
else:
- instance.item_model.additional_info.update({'car_info': instance.to_dict()})
+ instance.item_model.additional_info.update({"car_info": instance.to_dict()})
instance.item_model.save()
+
# # update price - CarFinance
@receiver(post_save, sender=models.CarFinance)
def update_item_model_cost(sender, instance, created, **kwargs):
@@ -252,20 +266,57 @@ def update_item_model_cost(sender, instance, created, **kwargs):
if created and not instance.is_sold:
entity = instance.car.dealer.entity
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:
- 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:
- cogs = create_make_accounts(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()
+ cogs = create_make_accounts(
+ 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:
- 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(
ledger=ledger,
description=f"Acquired {instance.car} for inventory",
@@ -289,8 +340,14 @@ def update_item_model_cost(sender, instance, created, **kwargs):
instance.car.item_model.default_amount = instance.selling_price
if not isinstance(instance.car.item_model.additional_info, dict):
instance.car.item_model.additional_info = {}
- instance.car.item_model.additional_info.update({"car_finance":instance.to_dict()})
- instance.car.item_model.additional_info.update({"additional_services": [service.to_dict() for service in instance.additional_services.all()]})
+ instance.car.item_model.additional_info.update({"car_finance": instance.to_dict()})
+ instance.car.item_model.additional_info.update(
+ {
+ "additional_services": [
+ service.to_dict() for service in instance.additional_services.all()
+ ]
+ }
+ )
instance.car.item_model.save()
print(f"Inventory item updated with CarFinance data for Car: {instance.car}")
@@ -316,6 +373,7 @@ def update_item_model_cost(sender, instance, created, **kwargs):
# quotation.status = 'pending'
# quotation.save()
+
@receiver(post_save, sender=models.CarColors)
def update_car_when_color_changed(sender, instance, **kwargs):
"""
@@ -335,6 +393,7 @@ def update_car_when_color_changed(sender, instance, **kwargs):
car = instance.car
car.save()
+
@receiver(post_save, sender=models.Opportunity)
def notify_staff_on_deal_stage_change(sender, instance, **kwargs):
"""
@@ -409,7 +468,11 @@ def create_item_service(sender, instance, created, **kwargs):
if created:
entity = instance.dealer.entity
uom = entity.get_uom_all().get(unit_abbr=instance.uom)
- cogs = entity.get_all_accounts().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(
name=instance.name,
@@ -456,7 +519,7 @@ def track_lead_status_change(sender, instance, **kwargs):
lead=instance,
old_status=old_lead.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:
pass # Ignore if the lead doesn't exist (e.g., during initial creation)
@@ -479,7 +542,7 @@ def notify_assigned_staff(sender, instance, created, **kwargs):
if instance.staff: # Check if the lead is assigned
models.Notification.objects.create(
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 +564,7 @@ def update_car_status_on_reservation_create(sender, instance, created, **kwargs)
car.status = models.CarStatusChoices.RESERVED
car.save()
+
@receiver(post_delete, sender=models.CarReservation)
def update_car_status_on_reservation_delete(sender, instance, **kwargs):
"""
@@ -523,6 +587,7 @@ def update_car_status_on_reservation_delete(sender, instance, **kwargs):
car.status = models.CarStatusChoices.AVAILABLE
car.save()
+
@receiver(post_save, sender=models.CarReservation)
def update_car_status_on_reservation_update(sender, instance, **kwargs):
"""
@@ -545,6 +610,7 @@ def update_car_status_on_reservation_update(sender, instance, **kwargs):
car.status = models.CarStatusChoices.AVAILABLE
car.save()
+
@receiver(post_save, sender=models.Dealer)
def create_dealer_settings(sender, instance, created, **kwargs):
"""
@@ -567,13 +633,26 @@ def create_dealer_settings(sender, instance, created, **kwargs):
if created:
models.DealerSettings.objects.create(
dealer=instance,
- invoice_cash_account=instance.entity.get_all_accounts().filter(role=roles.ASSET_CA_CASH).first(),
- invoice_prepaid_account=instance.entity.get_all_accounts().filter(role=roles.ASSET_CA_RECEIVABLES).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()
- )
+ invoice_cash_account=instance.entity.get_all_accounts()
+ .filter(role=roles.ASSET_CA_CASH)
+ .first(),
+ invoice_prepaid_account=instance.entity.get_all_accounts()
+ .filter(role=roles.ASSET_CA_RECEIVABLES)
+ .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)
# def update_estimate_status(sender, instance,created, **kwargs):
@@ -618,6 +697,7 @@ def create_dealer_settings(sender, instance, created, **kwargs):
# """
# VatRate.objects.get_or_create(rate=Decimal('0.15'), is_active=True)
+
@receiver(post_save, sender=models.Dealer)
def create_make_ledger_accounts(sender, instance, created, **kwargs):
"""
@@ -653,7 +733,6 @@ def create_make_ledger_accounts(sender, instance, created, **kwargs):
# )
-
# @receiver(post_save, sender=VendorModel)
# def create_vendor_accounts(sender, instance, created, **kwargs):Dealer)
# if created:
@@ -674,7 +753,8 @@ def create_make_ledger_accounts(sender, instance, created, **kwargs):
# 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.
@@ -708,32 +788,43 @@ def save_journal(car_finance,ledger,vendor):
ledger.additional_info["je_number"] = journal.je_number
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()
if not vendor_account:
- last_account = entity.get_all_accounts().filter(role=roles.LIABILITY_CL_ACC_PAYABLE).order_by('-created').first()
- if len(last_account.code) == 4:
- code = f"{int(last_account.code)}{1:03d}"
- elif len(last_account.code) > 4:
- code = f"{int(last_account.code)+1}"
+ last_account = (
+ entity.get_all_accounts()
+ .filter(role=roles.LIABILITY_CL_ACC_PAYABLE)
+ .order_by("-created")
+ .first()
+ )
+ if len(last_account.code) == 4:
+ code = f"{int(last_account.code)}{1:03d}"
+ elif len(last_account.code) > 4:
+ code = f"{int(last_account.code) + 1}"
- vendor_account = entity.create_account(
- name=vendor.name,
- code=code,
- role=roles.LIABILITY_CL_ACC_PAYABLE,
- coa_model=coa,
- balance_type="credit",
- active=True
- )
- additional_services_account = entity.get_default_coa_accounts().filter(name="Additional Services",role=roles.COGS).first()
+ vendor_account = entity.create_account(
+ name=vendor.name,
+ code=code,
+ role=roles.LIABILITY_CL_ACC_PAYABLE,
+ coa_model=coa,
+ balance_type="credit",
+ active=True,
+ )
+ additional_services_account = (
+ entity.get_default_coa_accounts()
+ .filter(name="Additional Services", role=roles.COGS)
+ .first()
+ )
# Debit Inventory Account
TransactionModel.objects.create(
- journal_entry=journal,
- account=inventory_account,
- amount=car_finance.cost_price,
- tx_type='debit'
+ journal_entry=journal,
+ account=inventory_account,
+ amount=car_finance.cost_price,
+ tx_type="debit",
)
# Credit Vendor Account
@@ -741,9 +832,10 @@ def save_journal(car_finance,ledger,vendor):
journal_entry=journal,
account=vendor_account,
amount=car_finance.cost_price,
- tx_type='credit',
+ tx_type="credit",
)
+
@receiver(post_save, sender=models.CarFinance)
def update_finance_cost(sender, instance, created, **kwargs):
"""
@@ -776,8 +868,8 @@ def update_finance_cost(sender, instance, created, **kwargs):
vendor_name = vendor.name if vendor else ""
name = f"{vin}-{make}-{model}-{year}-{vendor_name}"
- ledger,_ = LedgerModel.objects.get_or_create(name=name, entity=entity)
- save_journal(instance,ledger,vendor)
+ ledger, _ = LedgerModel.objects.get_or_create(name=name, entity=entity)
+ save_journal(instance, ledger, vendor)
# if not created:
# if ledger.additional_info.get("je_number"):
diff --git a/inventory/tables.py b/inventory/tables.py
index 84881c70..570c6d64 100644
--- a/inventory/tables.py
+++ b/inventory/tables.py
@@ -47,17 +47,29 @@ class CarTable(tables.Table):
with badge-style formatting based on the status value.
:type status: tables.Column
"""
+
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_model = tables.Column(verbose_name=_("Model"))
year = tables.Column(verbose_name=_("Year"))
id_car_serie = tables.Column(verbose_name=_("Series"))
id_car_trim = tables.Column(verbose_name=_("Trim"))
mileage = tables.Column(verbose_name=_("Mileage"))
- selling_price = tables.Column(accessor="finances.selling_price", verbose_name=_("Price"))
- exterior_color = tables.Column(accessor="colors.exterior.name", verbose_name=_("Exterior Color"))
- interior_color = tables.Column(accessor="colors.interior.name", verbose_name=_("Interior Color"))
+ selling_price = tables.Column(
+ accessor="finances.selling_price", verbose_name=_("Price")
+ )
+ 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"))
status = tables.Column(verbose_name=_("Status"))
@@ -111,7 +123,9 @@ class CarTable(tables.Table):
"transfer": "badge-phoenix-warning",
}
badge_class = status_badges.get(value.lower(), "badge-secondary")
- return format_html('{}', badge_class, value)
+ return format_html(
+ '{}', badge_class, value
+ )
def render_stock_type(self, value):
type_badges = {
@@ -119,4 +133,6 @@ class CarTable(tables.Table):
"used": "badge-phoenix-warning",
}
badge_class = type_badges.get(value.lower(), "badge-secondary")
- return format_html('{}', badge_class, value)
\ No newline at end of file
+ return format_html(
+ '{}', badge_class, value
+ )
diff --git a/inventory/tasks.py b/inventory/tasks.py
index 821f3e44..c04577ec 100644
--- a/inventory/tasks.py
+++ b/inventory/tasks.py
@@ -4,8 +4,7 @@ from django_ledger.io import roles
from django.core.mail import send_mail
from background_task import background
from django.utils.translation import gettext_lazy as _
-from inventory.models import DealerSettings,Dealer
-
+from inventory.models import DealerSettings, Dealer
# @background
@@ -13,13 +12,25 @@ def create_settings(pk):
instance = Dealer.objects.get(pk=pk)
DealerSettings.objects.create(
- dealer=instance,
- invoice_cash_account=instance.entity.get_all_accounts().filter(role=roles.ASSET_CA_CASH).first(),
- invoice_prepaid_account=instance.entity.get_all_accounts().filter(role=roles.ASSET_CA_RECEIVABLES).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()
+ dealer=instance,
+ invoice_cash_account=instance.entity.get_all_accounts()
+ .filter(role=roles.ASSET_CA_CASH)
+ .first(),
+ invoice_prepaid_account=instance.entity.get_all_accounts()
+ .filter(role=roles.ASSET_CA_RECEIVABLES)
+ .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(),
)
@@ -371,402 +382,397 @@ def create_coa_accounts(pk):
accounts_data = [
# Current Assets (must start with 1)
{
- 'code': '1010',
- 'name': 'Cash on Hand',
- 'role': roles.ASSET_CA_CASH,
- 'balance_type': roles.DEBIT,
- 'locked': True,
- 'default': True # Default for ASSET_CA_CASH
+ "code": "1010",
+ "name": "Cash on Hand",
+ "role": roles.ASSET_CA_CASH,
+ "balance_type": roles.DEBIT,
+ "locked": True,
+ "default": True, # Default for ASSET_CA_CASH
},
{
- 'code': '1020',
- 'name': 'Bank',
- 'role': roles.ASSET_CA_CASH,
- 'balance_type': roles.DEBIT,
- 'locked': True,
- 'default': False
+ "code": "1020",
+ "name": "Bank",
+ "role": roles.ASSET_CA_CASH,
+ "balance_type": roles.DEBIT,
+ "locked": True,
+ "default": False,
},
{
- 'code': '1030',
- 'name': 'Accounts Receivable',
- 'role': roles.ASSET_CA_RECEIVABLES,
- 'balance_type': roles.DEBIT,
- 'locked': True,
- 'default': True # Default for ASSET_CA_RECEIVABLES
+ "code": "1030",
+ "name": "Accounts Receivable",
+ "role": roles.ASSET_CA_RECEIVABLES,
+ "balance_type": roles.DEBIT,
+ "locked": True,
+ "default": True, # Default for ASSET_CA_RECEIVABLES
},
{
- 'code': '1040',
- 'name': 'Inventory (Cars)',
- 'role': roles.ASSET_CA_INVENTORY,
- 'balance_type': roles.DEBIT,
- 'locked': True,
- 'default': True # Default for ASSET_CA_INVENTORY
+ "code": "1040",
+ "name": "Inventory (Cars)",
+ "role": roles.ASSET_CA_INVENTORY,
+ "balance_type": roles.DEBIT,
+ "locked": True,
+ "default": True, # Default for ASSET_CA_INVENTORY
},
{
- 'code': '1045',
- 'name': 'Spare Parts Inventory',
- 'role': roles.ASSET_CA_INVENTORY,
- 'balance_type': roles.DEBIT,
- 'locked': False,
- 'default': False
+ "code": "1045",
+ "name": "Spare Parts Inventory",
+ "role": roles.ASSET_CA_INVENTORY,
+ "balance_type": roles.DEBIT,
+ "locked": False,
+ "default": False,
},
{
- 'code': '1050',
- 'name': 'Employee Advances',
- 'role': roles.ASSET_CA_RECEIVABLES,
- 'balance_type': roles.DEBIT,
- 'locked': False,
- 'default': False
+ "code": "1050",
+ "name": "Employee Advances",
+ "role": roles.ASSET_CA_RECEIVABLES,
+ "balance_type": roles.DEBIT,
+ "locked": False,
+ "default": False,
},
{
- 'code': '1060',
- 'name': 'Prepaid Expenses',
- 'role': roles.ASSET_CA_PREPAID,
- 'balance_type': roles.DEBIT,
- 'locked': False,
- 'default': True # Default for ASSET_CA_PREPAID
+ "code": "1060",
+ "name": "Prepaid Expenses",
+ "role": roles.ASSET_CA_PREPAID,
+ "balance_type": roles.DEBIT,
+ "locked": False,
+ "default": True, # Default for ASSET_CA_PREPAID
},
{
- 'code': '1070',
- 'name': 'Notes Receivable',
- 'role': roles.ASSET_LTI_NOTES_RECEIVABLE,
- 'balance_type': roles.DEBIT,
- 'locked': False,
- 'default': True # Default for ASSET_LTI_NOTES_RECEIVABLE
+ "code": "1070",
+ "name": "Notes Receivable",
+ "role": roles.ASSET_LTI_NOTES_RECEIVABLE,
+ "balance_type": roles.DEBIT,
+ "locked": False,
+ "default": True, # Default for ASSET_LTI_NOTES_RECEIVABLE
},
-
# Fixed Assets (must also start with 1)
{
- 'code': '1110',
- 'name': 'Lands',
- 'role': roles.ASSET_LTI_LAND,
- 'balance_type': roles.DEBIT,
- 'locked': False,
- 'default': True # Default for ASSET_LTI_LAND
+ "code": "1110",
+ "name": "Lands",
+ "role": roles.ASSET_LTI_LAND,
+ "balance_type": roles.DEBIT,
+ "locked": False,
+ "default": True, # Default for ASSET_LTI_LAND
},
{
- 'code': '1111',
- 'name': 'Buildings',
- 'role': roles.ASSET_PPE_BUILDINGS,
- 'balance_type': roles.DEBIT,
- 'locked': False,
- 'default': True # Default for ASSET_PPE_BUILDINGS
+ "code": "1111",
+ "name": "Buildings",
+ "role": roles.ASSET_PPE_BUILDINGS,
+ "balance_type": roles.DEBIT,
+ "locked": False,
+ "default": True, # Default for ASSET_PPE_BUILDINGS
},
{
- 'code': '1112',
- 'name': 'Company Vehicles',
- 'role': roles.ASSET_PPE_EQUIPMENT,
- 'balance_type': roles.DEBIT,
- 'locked': False,
- 'default': True # Default for ASSET_PPE_EQUIPMENT
+ "code": "1112",
+ "name": "Company Vehicles",
+ "role": roles.ASSET_PPE_EQUIPMENT,
+ "balance_type": roles.DEBIT,
+ "locked": False,
+ "default": True, # Default for ASSET_PPE_EQUIPMENT
},
{
- 'code': '1113',
- 'name': 'Equipment & Tools',
- 'role': roles.ASSET_PPE_EQUIPMENT,
- 'balance_type': roles.DEBIT,
- 'locked': False,
- 'default': False
+ "code": "1113",
+ "name": "Equipment & Tools",
+ "role": roles.ASSET_PPE_EQUIPMENT,
+ "balance_type": roles.DEBIT,
+ "locked": False,
+ "default": False,
},
{
- 'code': '1114',
- 'name': 'Furniture & Fixtures',
- 'role': roles.ASSET_PPE_EQUIPMENT,
- 'balance_type': roles.DEBIT,
- 'locked': False,
- 'default': False
+ "code": "1114",
+ "name": "Furniture & Fixtures",
+ "role": roles.ASSET_PPE_EQUIPMENT,
+ "balance_type": roles.DEBIT,
+ "locked": False,
+ "default": False,
},
{
- 'code': '1115',
- 'name': 'Other Fixed Assets',
- 'role': roles.ASSET_PPE_EQUIPMENT,
- 'balance_type': roles.DEBIT,
- 'locked': False,
- 'default': False
+ "code": "1115",
+ "name": "Other Fixed Assets",
+ "role": roles.ASSET_PPE_EQUIPMENT,
+ "balance_type": roles.DEBIT,
+ "locked": False,
+ "default": False,
},
{
- 'code': '1120',
- 'name': 'Long-term Investments',
- 'role': roles.ASSET_LTI_SECURITIES,
- 'balance_type': roles.DEBIT,
- 'locked': False,
- 'default': True # Default for ASSET_LTI_SECURITIES
+ "code": "1120",
+ "name": "Long-term Investments",
+ "role": roles.ASSET_LTI_SECURITIES,
+ "balance_type": roles.DEBIT,
+ "locked": False,
+ "default": True, # Default for ASSET_LTI_SECURITIES
},
{
- 'code': '1130',
- 'name': 'Intangible Assets',
- 'role': roles.ASSET_INTANGIBLE_ASSETS,
- 'balance_type': roles.DEBIT,
- 'locked': False,
- 'default': True # Default for ASSET_INTANGIBLE_ASSETS
+ "code": "1130",
+ "name": "Intangible Assets",
+ "role": roles.ASSET_INTANGIBLE_ASSETS,
+ "balance_type": roles.DEBIT,
+ "locked": False,
+ "default": True, # Default for ASSET_INTANGIBLE_ASSETS
},
-
# Current Liabilities (must start with 2)
{
- 'code': '2010',
- 'name': 'Accounts Payable',
- 'role': roles.LIABILITY_CL_ACC_PAYABLE,
- 'balance_type': roles.CREDIT,
- 'locked': True,
- 'default': True # Default for LIABILITY_CL_ACC_PAYABLE
+ "code": "2010",
+ "name": "Accounts Payable",
+ "role": roles.LIABILITY_CL_ACC_PAYABLE,
+ "balance_type": roles.CREDIT,
+ "locked": True,
+ "default": True, # Default for LIABILITY_CL_ACC_PAYABLE
},
{
- 'code': '2020',
- 'name': 'Notes Payable',
- 'role': roles.LIABILITY_CL_ST_NOTES_PAYABLE,
- 'balance_type': roles.CREDIT,
- 'locked': False,
- 'default': True # Default for LIABILITY_CL_ST_NOTES_PAYABLE
+ "code": "2020",
+ "name": "Notes Payable",
+ "role": roles.LIABILITY_CL_ST_NOTES_PAYABLE,
+ "balance_type": roles.CREDIT,
+ "locked": False,
+ "default": True, # Default for LIABILITY_CL_ST_NOTES_PAYABLE
},
{
- 'code': '2030',
- 'name': 'Short-term Loans',
- 'role': roles.LIABILITY_CL_ST_NOTES_PAYABLE,
- 'balance_type': roles.CREDIT,
- 'locked': False,
- 'default': False
+ "code": "2030",
+ "name": "Short-term Loans",
+ "role": roles.LIABILITY_CL_ST_NOTES_PAYABLE,
+ "balance_type": roles.CREDIT,
+ "locked": False,
+ "default": False,
},
{
- 'code': '2040',
- 'name': 'Employee Payables',
- 'role': roles.LIABILITY_CL_WAGES_PAYABLE,
- 'balance_type': roles.CREDIT,
- 'locked': False,
- 'default': True # Default for LIABILITY_CL_WAGES_PAYABLE
+ "code": "2040",
+ "name": "Employee Payables",
+ "role": roles.LIABILITY_CL_WAGES_PAYABLE,
+ "balance_type": roles.CREDIT,
+ "locked": False,
+ "default": True, # Default for LIABILITY_CL_WAGES_PAYABLE
},
{
- 'code': '2050',
- 'name': 'Accrued Expenses',
- 'role': roles.LIABILITY_CL_OTHER,
- 'balance_type': roles.CREDIT,
- 'locked': False,
- 'default': True # Default for LIABILITY_CL_OTHER
+ "code": "2050",
+ "name": "Accrued Expenses",
+ "role": roles.LIABILITY_CL_OTHER,
+ "balance_type": roles.CREDIT,
+ "locked": False,
+ "default": True, # Default for LIABILITY_CL_OTHER
},
{
- 'code': '2060',
- 'name': 'Accrued Taxes',
- 'role': roles.LIABILITY_CL_TAXES_PAYABLE,
- 'balance_type': roles.CREDIT,
- 'locked': False,
- 'default': True # Default for LIABILITY_CL_TAXES_PAYABLE
+ "code": "2060",
+ "name": "Accrued Taxes",
+ "role": roles.LIABILITY_CL_TAXES_PAYABLE,
+ "balance_type": roles.CREDIT,
+ "locked": False,
+ "default": True, # Default for LIABILITY_CL_TAXES_PAYABLE
},
{
- 'code': '2070',
- 'name': 'Provisions',
- 'role': roles.LIABILITY_CL_OTHER,
- 'balance_type': roles.CREDIT,
- 'locked': False,
- 'default': False
+ "code": "2070",
+ "name": "Provisions",
+ "role": roles.LIABILITY_CL_OTHER,
+ "balance_type": roles.CREDIT,
+ "locked": False,
+ "default": False,
},
-
# Long-term Liabilities (must also start with 2)
{
- 'code': '2210',
- 'name': 'Long-term Bank Loans',
- 'role': roles.LIABILITY_LTL_NOTES_PAYABLE,
- 'balance_type': roles.CREDIT,
- 'locked': False,
- 'default': True # Default for LIABILITY_LTL_NOTES_PAYABLE
+ "code": "2210",
+ "name": "Long-term Bank Loans",
+ "role": roles.LIABILITY_LTL_NOTES_PAYABLE,
+ "balance_type": roles.CREDIT,
+ "locked": False,
+ "default": True, # Default for LIABILITY_LTL_NOTES_PAYABLE
},
{
- 'code': '2220',
- 'name': 'Lease Liabilities',
- 'role': roles.LIABILITY_LTL_NOTES_PAYABLE,
- 'balance_type': roles.CREDIT,
- 'locked': False,
- 'default': False
+ "code": "2220",
+ "name": "Lease Liabilities",
+ "role": roles.LIABILITY_LTL_NOTES_PAYABLE,
+ "balance_type": roles.CREDIT,
+ "locked": False,
+ "default": False,
},
{
- 'code': '2230',
- 'name': 'Other Long-term Liabilities',
- 'role': roles.LIABILITY_LTL_NOTES_PAYABLE,
- 'balance_type': roles.CREDIT,
- 'locked': False,
- 'default': False
+ "code": "2230",
+ "name": "Other Long-term Liabilities",
+ "role": roles.LIABILITY_LTL_NOTES_PAYABLE,
+ "balance_type": roles.CREDIT,
+ "locked": False,
+ "default": False,
},
-
# Equity (must start with 3)
{
- 'code': '3010',
- 'name': 'Capital',
- 'role': roles.EQUITY_CAPITAL,
- 'balance_type': roles.CREDIT,
- 'locked': True,
- 'default': True # Default for EQUITY_CAPITAL
+ "code": "3010",
+ "name": "Capital",
+ "role": roles.EQUITY_CAPITAL,
+ "balance_type": roles.CREDIT,
+ "locked": True,
+ "default": True, # Default for EQUITY_CAPITAL
},
{
- 'code': '3020',
- 'name': 'Statutory Reserve',
- 'role': roles.EQUITY_ADJUSTMENT,
- 'balance_type': roles.CREDIT,
- 'locked': False,
- 'default': True # Default for EQUITY_ADJUSTMENT
+ "code": "3020",
+ "name": "Statutory Reserve",
+ "role": roles.EQUITY_ADJUSTMENT,
+ "balance_type": roles.CREDIT,
+ "locked": False,
+ "default": True, # Default for EQUITY_ADJUSTMENT
},
{
- 'code': '3030',
- 'name': 'Retained Earnings',
- 'role': roles.EQUITY_ADJUSTMENT,
- 'balance_type': roles.CREDIT,
- 'locked': False,
- 'default': False
+ "code": "3030",
+ "name": "Retained Earnings",
+ "role": roles.EQUITY_ADJUSTMENT,
+ "balance_type": roles.CREDIT,
+ "locked": False,
+ "default": False,
},
{
- 'code': '3040',
- 'name': 'Profit & Loss for the Period',
- 'role': roles.EQUITY_ADJUSTMENT,
- 'balance_type': roles.CREDIT,
- 'locked': False,
- 'default': False
+ "code": "3040",
+ "name": "Profit & Loss for the Period",
+ "role": roles.EQUITY_ADJUSTMENT,
+ "balance_type": roles.CREDIT,
+ "locked": False,
+ "default": False,
},
-
# Revenue (must start with 4)
{
- 'code': '4010',
- 'name': 'Car Sales',
- 'role': roles.INCOME_OPERATIONAL,
- 'balance_type': roles.CREDIT,
- 'locked': True,
- 'default': True # Default for INCOME_OPERATIONAL
+ "code": "4010",
+ "name": "Car Sales",
+ "role": roles.INCOME_OPERATIONAL,
+ "balance_type": roles.CREDIT,
+ "locked": True,
+ "default": True, # Default for INCOME_OPERATIONAL
},
{
- 'code': '4020',
- 'name': 'After-Sales Services',
- 'role': roles.INCOME_OPERATIONAL,
- 'balance_type': roles.CREDIT,
- 'locked': False,
- 'default': False
+ "code": "4020",
+ "name": "After-Sales Services",
+ "role": roles.INCOME_OPERATIONAL,
+ "balance_type": roles.CREDIT,
+ "locked": False,
+ "default": False,
},
{
- 'code': '4030',
- 'name': 'Car Rental Income',
- 'role': roles.INCOME_PASSIVE,
- 'balance_type': roles.CREDIT,
- 'locked': False,
- 'default': True # Default for INCOME_PASSIVE
+ "code": "4030",
+ "name": "Car Rental Income",
+ "role": roles.INCOME_PASSIVE,
+ "balance_type": roles.CREDIT,
+ "locked": False,
+ "default": True, # Default for INCOME_PASSIVE
},
{
- 'code': '4040',
- 'name': 'Other Income',
- 'role': roles.INCOME_OTHER,
- 'balance_type': roles.CREDIT,
- 'locked': False,
- 'default': True # Default for INCOME_OTHER
+ "code": "4040",
+ "name": "Other Income",
+ "role": roles.INCOME_OTHER,
+ "balance_type": roles.CREDIT,
+ "locked": False,
+ "default": True, # Default for INCOME_OTHER
},
-
# Expenses (must start with 5 for COGS, 6 for others)
{
- 'code': '5010',
- 'name': 'Cost of Goods Sold',
- 'role': roles.COGS,
- 'balance_type': roles.DEBIT,
- 'locked': True,
- 'default': True # Default for COGS
+ "code": "5010",
+ "name": "Cost of Goods Sold",
+ "role": roles.COGS,
+ "balance_type": roles.DEBIT,
+ "locked": True,
+ "default": True, # Default for COGS
},
{
- 'code': '5015',
- 'name': 'Spare Parts Cost Consumed',
- 'role': roles.COGS,
- 'balance_type': roles.DEBIT,
- 'locked': False,
- 'default': False
+ "code": "5015",
+ "name": "Spare Parts Cost Consumed",
+ "role": roles.COGS,
+ "balance_type": roles.DEBIT,
+ "locked": False,
+ "default": False,
},
{
- 'code': '6010',
- 'name': 'Salaries & Wages',
- 'role': roles.EXPENSE_OPERATIONAL,
- 'balance_type': roles.DEBIT,
- 'locked': False,
- 'default': True # Default for EXPENSE_OPERATIONAL
+ "code": "6010",
+ "name": "Salaries & Wages",
+ "role": roles.EXPENSE_OPERATIONAL,
+ "balance_type": roles.DEBIT,
+ "locked": False,
+ "default": True, # Default for EXPENSE_OPERATIONAL
},
{
- 'code': '6020',
- 'name': 'Rent',
- 'role': roles.EXPENSE_OPERATIONAL,
- 'balance_type': roles.DEBIT,
- 'locked': False,
- 'default': False
+ "code": "6020",
+ "name": "Rent",
+ "role": roles.EXPENSE_OPERATIONAL,
+ "balance_type": roles.DEBIT,
+ "locked": False,
+ "default": False,
},
{
- 'code': '6030',
- 'name': 'Utilities',
- 'role': roles.EXPENSE_OPERATIONAL,
- 'balance_type': roles.DEBIT,
- 'locked': False,
- 'default': False
+ "code": "6030",
+ "name": "Utilities",
+ "role": roles.EXPENSE_OPERATIONAL,
+ "balance_type": roles.DEBIT,
+ "locked": False,
+ "default": False,
},
{
- 'code': '6040',
- 'name': 'Advertising & Marketing',
- 'role': roles.EXPENSE_OPERATIONAL,
- 'balance_type': roles.DEBIT,
- 'locked': False,
- 'default': False
+ "code": "6040",
+ "name": "Advertising & Marketing",
+ "role": roles.EXPENSE_OPERATIONAL,
+ "balance_type": roles.DEBIT,
+ "locked": False,
+ "default": False,
},
{
- 'code': '6050',
- 'name': 'Maintenance',
- 'role': roles.EXPENSE_OPERATIONAL,
- 'balance_type': roles.DEBIT,
- 'locked': False,
- 'default': False
+ "code": "6050",
+ "name": "Maintenance",
+ "role": roles.EXPENSE_OPERATIONAL,
+ "balance_type": roles.DEBIT,
+ "locked": False,
+ "default": False,
},
{
- 'code': '6060',
- 'name': 'Operating Expenses',
- 'role': roles.EXPENSE_OPERATIONAL,
- 'balance_type': roles.DEBIT,
- 'locked': False,
- 'default': False
+ "code": "6060",
+ "name": "Operating Expenses",
+ "role": roles.EXPENSE_OPERATIONAL,
+ "balance_type": roles.DEBIT,
+ "locked": False,
+ "default": False,
},
{
- 'code': '6070',
- 'name': 'Depreciation',
- 'role': roles.EXPENSE_DEPRECIATION,
- 'balance_type': roles.DEBIT,
- 'locked': False,
- 'default': True # Default for EXPENSE_DEPRECIATION
+ "code": "6070",
+ "name": "Depreciation",
+ "role": roles.EXPENSE_DEPRECIATION,
+ "balance_type": roles.DEBIT,
+ "locked": False,
+ "default": True, # Default for EXPENSE_DEPRECIATION
},
{
- 'code': '6080',
- 'name': 'Fees & Taxes',
- 'role': roles.EXPENSE_OPERATIONAL,
- 'balance_type': roles.DEBIT,
- 'locked': False,
- 'default': False
+ "code": "6080",
+ "name": "Fees & Taxes",
+ "role": roles.EXPENSE_OPERATIONAL,
+ "balance_type": roles.DEBIT,
+ "locked": False,
+ "default": False,
},
{
- 'code': '6090',
- 'name': 'Bank Charges',
- 'role': roles.EXPENSE_OPERATIONAL,
- 'balance_type': roles.DEBIT,
- 'locked': False,
- 'default': False
+ "code": "6090",
+ "name": "Bank Charges",
+ "role": roles.EXPENSE_OPERATIONAL,
+ "balance_type": roles.DEBIT,
+ "locked": False,
+ "default": False,
},
{
- 'code': '6100',
- 'name': 'Other Expenses',
- 'role': roles.EXPENSE_OTHER,
- 'balance_type': roles.DEBIT,
- 'locked': False,
- 'default': True # Default for EXPENSE_OTHER
- }
+ "code": "6100",
+ "name": "Other Expenses",
+ "role": roles.EXPENSE_OTHER,
+ "balance_type": roles.DEBIT,
+ "locked": False,
+ "default": True, # Default for EXPENSE_OTHER
+ },
]
for account_data in accounts_data:
try:
account = entity.create_account(
- coa_model=coa,
- code=account_data['code'],
- name=_(account_data['name']),
- role=_(account_data['role']),
- balance_type=_(account_data['balance_type']),
- active=True
+ coa_model=coa,
+ code=account_data["code"],
+ name=_(account_data["name"]),
+ role=_(account_data["role"]),
+ balance_type=_(account_data["balance_type"]),
+ active=True,
)
- account.role_default = account_data['default']
+ account.role_default = account_data["default"]
account.save()
except Exception as e:
print(e)
+
@background
def create_coa_accounts1(pk):
with transaction.atomic():
@@ -810,7 +816,6 @@ def create_coa_accounts1(pk):
asset_ca_inventory.role_default = True
asset_ca_inventory.save()
-
# Prepaid Expenses Account
asset_ca_prepaid = entity.create_account(
coa_model=coa,
@@ -833,8 +838,7 @@ def create_coa_accounts1(pk):
active=True,
)
-
- # VAT Payable Account
+ # VAT Payable Account
liability_ltl_vat_receivable = entity.create_account(
coa_model=coa,
code="1106",
@@ -904,7 +908,6 @@ def create_coa_accounts1(pk):
# asset_lti_land.role_default = True
# asset_lti_land.save()
-
# Buildings Account
asset_ppe_buildings = entity.create_account(
coa_model=coa,
@@ -917,8 +920,6 @@ def create_coa_accounts1(pk):
asset_ppe_buildings.role_default = True
asset_ppe_buildings.save()
-
-
# Accounts Payable Account
liability_cl_acc_payable = entity.create_account(
coa_model=coa,
@@ -998,7 +999,14 @@ def create_coa_accounts1(pk):
)
# End of Service Benefits
- entity.create_account(coa_model=coa, code="2202", role=roles.LIABILITY_LTL_NOTES_PAYABLE, name=_("End of Service Benefits"), balance_type="credit", active=True)
+ entity.create_account(
+ coa_model=coa,
+ code="2202",
+ role=roles.LIABILITY_LTL_NOTES_PAYABLE,
+ name=_("End of Service Benefits"),
+ balance_type="credit",
+ active=True,
+ )
# Mortgage Payable Account
liability_ltl_mortgage_payable = entity.create_account(
@@ -1013,21 +1021,56 @@ def create_coa_accounts1(pk):
liability_ltl_mortgage_payable.save()
# Capital
- equity_capital = entity.create_account(coa_model=coa, code="3101", role=roles.EQUITY_CAPITAL, name=_("Registered Capital"), balance_type="credit", active=True)
+ equity_capital = entity.create_account(
+ coa_model=coa,
+ code="3101",
+ role=roles.EQUITY_CAPITAL,
+ name=_("Registered Capital"),
+ balance_type="credit",
+ active=True,
+ )
equity_capital.role_default = True
equity_capital.save()
- entity.create_account(coa_model=coa, code="3102", role=roles.EQUITY_CAPITAL, name=_("Additional Paid-In Capital"), balance_type="credit", active=True)
+ entity.create_account(
+ coa_model=coa,
+ code="3102",
+ role=roles.EQUITY_CAPITAL,
+ name=_("Additional Paid-In Capital"),
+ balance_type="credit",
+ active=True,
+ )
# Other Equity
- other_equity = entity.create_account(coa_model=coa, code="3201", role=roles.EQUITY_COMMON_STOCK, name=_("Opening Balances"), balance_type="credit", active=True)
+ other_equity = entity.create_account(
+ coa_model=coa,
+ code="3201",
+ role=roles.EQUITY_COMMON_STOCK,
+ name=_("Opening Balances"),
+ balance_type="credit",
+ active=True,
+ )
other_equity.role_default = True
other_equity.save()
# Reserves
- reserve = entity.create_account(coa_model=coa, code="3301", role=roles.EQUITY_ADJUSTMENT, name=_("Statutory Reserve"), balance_type="credit", active=True)
+ reserve = entity.create_account(
+ coa_model=coa,
+ code="3301",
+ role=roles.EQUITY_ADJUSTMENT,
+ name=_("Statutory Reserve"),
+ balance_type="credit",
+ active=True,
+ )
reserve.role_default = True
reserve.save()
- entity.create_account(coa_model=coa, code="3302", role=roles.EQUITY_ADJUSTMENT, name=_("Foreign Currency Translation Reserve"), balance_type="credit", active=True)
+ entity.create_account(
+ coa_model=coa,
+ code="3302",
+ role=roles.EQUITY_ADJUSTMENT,
+ name=_("Foreign Currency Translation Reserve"),
+ balance_type="credit",
+ active=True,
+ )
# Retained Earnings Account
equity_retained_earnings = entity.create_account(
@@ -1085,11 +1128,24 @@ def create_coa_accounts1(pk):
)
# Operating Revenues
- entity.create_account(coa_model=coa, code="4104", role=roles.INCOME_OPERATIONAL, name=_("Sales/Service Revenue"), balance_type="credit", active=True)
-
- #Non-Operating Revenues
- entity.create_account(coa_model=coa, code="4201", role=roles.INCOME_OTHER, name=_("Non-Operating Revenues"), balance_type="credit", active=True)
+ entity.create_account(
+ coa_model=coa,
+ code="4104",
+ role=roles.INCOME_OPERATIONAL,
+ name=_("Sales/Service Revenue"),
+ balance_type="credit",
+ active=True,
+ )
+ # Non-Operating Revenues
+ entity.create_account(
+ coa_model=coa,
+ code="4201",
+ role=roles.INCOME_OTHER,
+ name=_("Non-Operating Revenues"),
+ balance_type="credit",
+ active=True,
+ )
# Cost of Goods Sold (COGS) Account
expense_cogs = entity.create_account(
@@ -1103,7 +1159,6 @@ def create_coa_accounts1(pk):
expense_cogs.role_default = True
expense_cogs.save()
-
# accrued Expenses Account
expense_cogs = entity.create_account(
coa_model=coa,
@@ -1277,19 +1332,76 @@ def create_coa_accounts1(pk):
)
# 5.1 Direct Costs
- entity.create_account(coa_model=coa, code="6201", role=roles.EXPENSE_OPERATIONAL, name=_("Cost of Goods Sold"), balance_type="debit", active=True)
- entity.create_account(coa_model=coa, code="6202", role=roles.EXPENSE_OPERATIONAL, name=_("Salaries and Wages"), balance_type="debit", active=True)
- entity.create_account(coa_model=coa, code="6203", role=roles.EXPENSE_OPERATIONAL, name=_("Sales Commissions"), balance_type="debit", active=True)
- entity.create_account(coa_model=coa, code="6204", role=roles.EXPENSE_OPERATIONAL, name=_("Shipping and Customs Clearance"), balance_type="debit", active=True)
+ entity.create_account(
+ coa_model=coa,
+ code="6201",
+ role=roles.EXPENSE_OPERATIONAL,
+ name=_("Cost of Goods Sold"),
+ balance_type="debit",
+ active=True,
+ )
+ entity.create_account(
+ coa_model=coa,
+ code="6202",
+ role=roles.EXPENSE_OPERATIONAL,
+ name=_("Salaries and Wages"),
+ balance_type="debit",
+ active=True,
+ )
+ entity.create_account(
+ coa_model=coa,
+ code="6203",
+ role=roles.EXPENSE_OPERATIONAL,
+ name=_("Sales Commissions"),
+ balance_type="debit",
+ active=True,
+ )
+ entity.create_account(
+ coa_model=coa,
+ code="6204",
+ role=roles.EXPENSE_OPERATIONAL,
+ name=_("Shipping and Customs Clearance"),
+ balance_type="debit",
+ active=True,
+ )
# 5.3 Non-Operating Expenses
- entity.create_account(coa_model=coa, code="6301", role=roles.EXPENSE_OTHER, name=_("Zakat"), balance_type="debit", active=True)
- entity.create_account(coa_model=coa, code="6302", role=roles.EXPENSE_OTHER, name=_("Taxes"), balance_type="debit", active=True)
- entity.create_account(coa_model=coa, code="6303", role=roles.EXPENSE_OTHER, name=_("Foreign Currency Translation"), balance_type="debit", active=True)
- entity.create_account(coa_model=coa, code="6304", role=roles.EXPENSE_OTHER, name=_("Interest Expenses"), balance_type="debit", active=True)
+ entity.create_account(
+ coa_model=coa,
+ code="6301",
+ role=roles.EXPENSE_OTHER,
+ name=_("Zakat"),
+ balance_type="debit",
+ active=True,
+ )
+ entity.create_account(
+ coa_model=coa,
+ code="6302",
+ role=roles.EXPENSE_OTHER,
+ name=_("Taxes"),
+ balance_type="debit",
+ active=True,
+ )
+ entity.create_account(
+ coa_model=coa,
+ code="6303",
+ role=roles.EXPENSE_OTHER,
+ name=_("Foreign Currency Translation"),
+ balance_type="debit",
+ active=True,
+ )
+ entity.create_account(
+ coa_model=coa,
+ code="6304",
+ role=roles.EXPENSE_OTHER,
+ name=_("Interest Expenses"),
+ balance_type="debit",
+ active=True,
+ )
# create_settings(instance.pk)
+
# @background
# def create_groups(instance):
# group_names = ["Inventory", "Accountant", "Sales"]
@@ -1299,32 +1411,40 @@ def create_coa_accounts1(pk):
# group_manager.set_default_permissions()
# instance.user.groups.add(group)
+
# @background
-def create_accounts_for_make(dealer,makes):
+def create_accounts_for_make(dealer, makes):
entity = dealer.entity
coa = entity.get_default_coa()
name = ["Inventory", "Revenue", "Cogs"]
- role = [roles.ASSET_CA_INVENTORY,roles.ASSET_CA_RECEIVABLES, roles.COGS]
- balance_type = ["debit","credit","debit"]
+ role = [roles.ASSET_CA_INVENTORY, roles.ASSET_CA_RECEIVABLES, roles.COGS]
+ balance_type = ["debit", "credit", "debit"]
- for name,role,balance_type in zip(name,role,balance_type):
- create_make_accounts(entity,coa,makes,name,role,balance_type)
+ for name, role, balance_type in zip(name, role, balance_type):
+ create_make_accounts(entity, coa, makes, name, role, balance_type)
-def create_make_accounts(entity,coa,makes,name,role,balance_type):
+
+def create_make_accounts(entity, coa, makes, name, role, balance_type):
for make in makes:
- last_account = entity.get_all_accounts().filter(role=role).order_by('-created').first()
+ last_account = (
+ entity.get_all_accounts().filter(role=role).order_by("-created").first()
+ )
if len(last_account.code) == 4:
code = f"{int(last_account.code)}{1:03d}"
elif len(last_account.code) > 4:
- code = f"{int(last_account.code)+1}"
- acc = entity.get_all_accounts().filter(
- name=f"{name}:{make.name}",
- role=role,
- coa_model=coa,
- balance_type=balance_type,
- active=True
- ).first()
+ code = f"{int(last_account.code) + 1}"
+ acc = (
+ entity.get_all_accounts()
+ .filter(
+ name=f"{name}:{make.name}",
+ role=role,
+ coa_model=coa,
+ balance_type=balance_type,
+ active=True,
+ )
+ .first()
+ )
if not acc:
acc = entity.create_account(
name=f"{name}:{make.name}",
@@ -1332,12 +1452,11 @@ def create_make_accounts(entity,coa,makes,name,role,balance_type):
role=role,
coa_model=coa,
balance_type=balance_type,
- active=True
+ active=True,
)
return acc
-
@background
def send_email(from_, to_, subject, message):
subject = subject
@@ -1354,8 +1473,8 @@ def long_running_task(task_id, *args, **kwargs):
# Simulate work
for i in range(5):
- print(f"Task {task_id} progress: {i+1}/5")
+ print(f"Task {task_id} progress: {i + 1}/5")
result = f"Task {task_id} completed at {datetime.now()}"
print(result)
- return result
\ No newline at end of file
+ return result
diff --git a/inventory/templatetags/custom_filters.py b/inventory/templatetags/custom_filters.py
index 862f212a..0d121a69 100644
--- a/inventory/templatetags/custom_filters.py
+++ b/inventory/templatetags/custom_filters.py
@@ -4,194 +4,209 @@ from django import template
from django.urls import reverse
from calendar import month_abbr
from django.conf import settings
+from django.db.models import Sum
from django.forms import ValidationError
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
register = template.Library()
-@register.filter(name='percentage')
+
+@register.filter(name="percentage")
def percentage(value):
if value is not None:
- return '{0:,.2f}%'.format(value * 100)
+ return "{0:,.2f}%".format(value * 100)
return None
+
@register.filter
def get_item(dictionary, key):
return dictionary.get(key)
-@register.filter(name='add_class')
+@register.filter(name="add_class")
def add_class(field, css_class):
return field.as_widget(attrs={"class": css_class})
-@register.filter(name='attr')
+@register.filter(name="attr")
def attr(field, args):
attrs = {}
- definitions = args.split(',')
+ definitions = args.split(",")
for definition in definitions:
- if ':' in definition:
- key, val = definition.split(':')
+ if ":" in definition:
+ key, val = definition.split(":")
attrs[key.strip()] = val.strip()
else:
attrs[definition.strip()] = True
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):
print(context, base_url)
kwargs = dict()
- entity_slug = context['view'].kwargs['entity_slug']
- kwargs['entity_slug'] = entity_slug
+ entity_slug = context["view"].kwargs["entity_slug"]
+ kwargs["entity_slug"] = entity_slug
- if context['view'].kwargs.get('ledger_pk'):
- kwargs['ledger_pk'] = context['view'].kwargs.get('ledger_pk')
+ if context["view"].kwargs.get("ledger_pk"):
+ kwargs["ledger_pk"] = context["view"].kwargs.get("ledger_pk")
- if context['view'].kwargs.get('account_pk'):
- kwargs['account_pk'] = context['view'].kwargs.get('account_pk')
+ if context["view"].kwargs.get("account_pk"):
+ kwargs["account_pk"] = context["view"].kwargs.get("account_pk")
- if context['view'].kwargs.get('unit_slug'):
- kwargs['unit_slug'] = context['view'].kwargs.get('unit_slug')
+ if context["view"].kwargs.get("unit_slug"):
+ kwargs["unit_slug"] = context["view"].kwargs.get("unit_slug")
- if context['view'].kwargs.get('coa_slug'):
- kwargs['coa_slug'] = context['view'].kwargs.get('coa_slug')
+ if context["view"].kwargs.get("coa_slug"):
+ kwargs["coa_slug"] = context["view"].kwargs.get("coa_slug")
ctx = dict()
- ctx['year'] = context['year']
- ctx['has_year'] = context.get('has_year')
- ctx['has_quarter'] = context.get('has_quarter')
- ctx['has_month'] = context.get('has_month')
- ctx['has_date'] = context.get('has_date')
- ctx['previous_year'] = context['previous_year']
+ ctx["year"] = context["year"]
+ ctx["has_year"] = context.get("has_year")
+ ctx["has_quarter"] = context.get("has_quarter")
+ ctx["has_month"] = context.get("has_month")
+ ctx["has_date"] = context.get("has_date")
+ ctx["previous_year"] = context["previous_year"]
- kwargs['year'] = context['previous_year']
- ctx['previous_year_url'] = reverse(f'{base_url}-year', kwargs=kwargs)
- ctx['next_year'] = context['next_year']
+ kwargs["year"] = context["previous_year"]
+ ctx["previous_year_url"] = reverse(f"{base_url}-year", kwargs=kwargs)
+ ctx["next_year"] = context["next_year"]
- kwargs['year'] = context['next_year']
- ctx['next_year_url'] = reverse(f'{base_url}-year', kwargs=kwargs)
+ kwargs["year"] = context["next_year"]
+ ctx["next_year_url"] = reverse(f"{base_url}-year", kwargs=kwargs)
- kwargs['year'] = context['year']
- ctx['current_year_url'] = reverse(f'{base_url}-year', kwargs=kwargs)
+ kwargs["year"] = context["year"]
+ ctx["current_year_url"] = reverse(f"{base_url}-year", kwargs=kwargs)
dt = get_localdate()
KWARGS_CURRENT_MONTH = {
- 'entity_slug': context['view'].kwargs['entity_slug'],
- 'year': dt.year,
- 'month': dt.month
+ "entity_slug": context["view"].kwargs["entity_slug"],
+ "year": dt.year,
+ "month": dt.month,
}
- if 'unit_slug' in kwargs:
- KWARGS_CURRENT_MONTH['unit_slug'] = kwargs['unit_slug']
- if 'account_pk' in kwargs:
- KWARGS_CURRENT_MONTH['account_pk'] = kwargs['account_pk']
- if 'ledger_pk' in kwargs:
- KWARGS_CURRENT_MONTH['ledger_pk'] = kwargs['ledger_pk']
- if 'coa_slug' in kwargs:
- KWARGS_CURRENT_MONTH['coa_slug'] = kwargs['coa_slug']
+ if "unit_slug" in kwargs:
+ KWARGS_CURRENT_MONTH["unit_slug"] = kwargs["unit_slug"]
+ if "account_pk" in kwargs:
+ KWARGS_CURRENT_MONTH["account_pk"] = kwargs["account_pk"]
+ if "ledger_pk" in kwargs:
+ KWARGS_CURRENT_MONTH["ledger_pk"] = kwargs["ledger_pk"]
+ if "coa_slug" in kwargs:
+ KWARGS_CURRENT_MONTH["coa_slug"] = kwargs["coa_slug"]
- ctx['current_month_url'] = reverse(f'{base_url}-month',
- kwargs=KWARGS_CURRENT_MONTH)
+ ctx["current_month_url"] = reverse(f"{base_url}-month", kwargs=KWARGS_CURRENT_MONTH)
quarter_urls = list()
- ctx['quarter'] = context.get('quarter')
+ ctx["quarter"] = context.get("quarter")
for Q in range(1, 5):
- kwargs['quarter'] = Q
- quarter_urls.append({
- 'url': reverse(f'{base_url}-quarter', kwargs=kwargs),
- 'quarter': Q,
- 'quarter_name': f'Q{Q}'
- })
- del kwargs['quarter']
- ctx['quarter_urls'] = quarter_urls
+ kwargs["quarter"] = Q
+ quarter_urls.append(
+ {
+ "url": reverse(f"{base_url}-quarter", kwargs=kwargs),
+ "quarter": Q,
+ "quarter_name": f"Q{Q}",
+ }
+ )
+ del kwargs["quarter"]
+ ctx["quarter_urls"] = quarter_urls
month_urls = list()
- ctx['month'] = context.get('month')
+ ctx["month"] = context.get("month")
for M in range(1, 13):
- kwargs['month'] = M
- month_urls.append({
- 'url': reverse(f'{base_url}-month', kwargs=kwargs),
- 'month': M,
- 'month_abbr': month_abbr[M]
- })
- ctx['month_urls'] = month_urls
- ctx['from_date'] = context['from_date']
- ctx['to_date'] = context['to_date']
+ kwargs["month"] = M
+ month_urls.append(
+ {
+ "url": reverse(f"{base_url}-month", kwargs=kwargs),
+ "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.update(kwargs)
- ctx['date_navigation_url'] = context.get('date_navigation_url')
+ ctx["date_navigation_url"] = context.get("date_navigation_url")
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):
- user_model = context['user']
- activity = context['request'].GET.get('activity')
- entity_slug = context['view'].kwargs.get('entity_slug')
+ user_model = context["user"]
+ activity = context["request"].GET.get("activity")
+ entity_slug = context["view"].kwargs.get("entity_slug")
if not to_date:
- to_date = context['to_date']
+ to_date = context["to_date"]
io_digest = io_model.digest(
activity=activity,
user_model=user_model,
equity_only=False,
entity_slug=entity_slug,
- unit_slug=context['unit_slug'],
- by_unit=context['by_unit'],
+ unit_slug=context["unit_slug"],
+ by_unit=context["by_unit"],
to_date=to_date,
signs=True,
process_groups=True,
- balance_sheet_statement=True)
+ balance_sheet_statement=True,
+ )
return {
- 'entity_slug': entity_slug,
- 'user_model': user_model,
- 'tx_digest': io_digest.get_io_data(),
+ "entity_slug": entity_slug,
+ "user_model": user_model,
+ "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):
- user_model = context['user']
- activity = context['request'].GET.get('activity')
+ user_model = context["user"]
+ activity = context["request"].GET.get("activity")
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:
- from_date = context['from_date']
+ from_date = context["from_date"]
if not to_date:
- to_date = context['to_date']
+ to_date = context["to_date"]
io_digest = io_model.digest(
activity=activity,
user_model=user_model,
entity_slug=entity_slug,
- unit_slug=context['unit_slug'],
- by_unit=context['by_unit'],
+ unit_slug=context["unit_slug"],
+ by_unit=context["by_unit"],
from_date=from_date,
to_date=to_date,
equity_only=True,
process_groups=True,
income_statement=True,
- signs=True
+ signs=True,
)
return {
- 'entity_slug': entity_slug,
- 'user_model': user_model,
- 'tx_digest': io_digest.get_io_data()
+ "entity_slug": entity_slug,
+ "user_model": user_model,
+ "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):
- user_model = context['user']
- entity_slug = context['view'].kwargs.get('entity_slug')
- from_date = context['from_date']
- to_date = context['to_date']
+ user_model = context["user"]
+ entity_slug = context["view"].kwargs.get("entity_slug")
+ from_date = context["from_date"]
+ to_date = context["to_date"]
io_digest = io_model.digest(
cash_flow_statement=True,
@@ -200,57 +215,75 @@ def cash_flow_statement(context, io_model):
equity_only=False,
signs=True,
entity_slug=entity_slug,
- unit_slug=context['unit_slug'],
- by_unit=context['by_unit'],
+ unit_slug=context["unit_slug"],
+ by_unit=context["by_unit"],
from_date=from_date,
to_date=to_date,
- process_groups=True)
+ process_groups=True,
+ )
return {
- 'entity_slug': entity_slug,
- 'user_model': user_model,
- 'tx_digest': io_digest.get_io_data()
+ "entity_slug": entity_slug,
+ "user_model": user_model,
+ "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):
try:
- entity_slug = context['view'].kwargs.get('entity_slug')
+ entity_slug = context["view"].kwargs.get("entity_slug")
except KeyError:
- entity_slug = context['entity_slug']
+ entity_slug = context["entity_slug"]
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:
- context['date_picker_ids'] = list()
- context['date_picker_ids'].append(date_picker_id)
+ if "date_picker_ids" not in context:
+ context["date_picker_ids"] = list()
+ 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 {
- 'entity_slug': entity_slug,
- 'date_picker_id': date_picker_id,
- 'date_navigation_url': date_navigation_url
+ "entity_slug": entity_slug,
+ "date_picker_id": date_picker_id,
+ "date_navigation_url": date_navigation_url,
}
-@register.simple_tag(name='get_currency')
+
+@register.simple_tag(name="get_currency")
def get_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):
"""Convert a number to words in English."""
units = ["", "one", "two", "three", "four", "five", "six", "seven", "eight", "nine"]
- teens = ["ten", "eleven", "twelve", "thirteen", "fourteen", "fifteen", "sixteen",
- "seventeen", "eighteen", "nineteen"]
- tens = ["", "ten", "twenty", "thirty", "forty", "fifty", "sixty", "seventy",
- "eighty", "ninety"]
+ teens = [
+ "ten",
+ "eleven",
+ "twelve",
+ "thirteen",
+ "fourteen",
+ "fifteen",
+ "sixteen",
+ "seventeen",
+ "eighteen",
+ "nineteen",
+ ]
+ tens = [
+ "",
+ "ten",
+ "twenty",
+ "thirty",
+ "forty",
+ "fifty",
+ "sixty",
+ "seventy",
+ "eighty",
+ "ninety",
+ ]
scales = ["", "thousand", "million", "billion", "trillion"]
if number == 0:
@@ -281,15 +314,47 @@ def number_to_words_english(number):
number = number // 1000
scale_index += 1
- return ' '.join(words)
+ return " ".join(words)
+
def number_to_words_arabic(number):
"""Convert a number to words in Arabic."""
- units = ["", "واحد", "اثنان", "ثلاثة", "أربعة", "خمسة", "ستة", "سبعة", "ثمانية", "تسعة"]
- teens = ["عشرة", "أحد عشر", "اثنا عشر", "ثلاثة عشر", "أربعة عشر", "خمسة عشر",
- "ستة عشر", "سبعة عشر", "ثمانية عشر", "تسعة عشر"]
- tens = ["", "عشرة", "عشرون", "ثلاثون", "أربعون", "خمسون", "ستون", "سبعون",
- "ثمانون", "تسعون"]
+ units = [
+ "",
+ "واحد",
+ "اثنان",
+ "ثلاثة",
+ "أربعة",
+ "خمسة",
+ "ستة",
+ "سبعة",
+ "ثمانية",
+ "تسعة",
+ ]
+ teens = [
+ "عشرة",
+ "أحد عشر",
+ "اثنا عشر",
+ "ثلاثة عشر",
+ "أربعة عشر",
+ "خمسة عشر",
+ "ستة عشر",
+ "سبعة عشر",
+ "ثمانية عشر",
+ "تسعة عشر",
+ ]
+ tens = [
+ "",
+ "عشرة",
+ "عشرون",
+ "ثلاثون",
+ "أربعون",
+ "خمسون",
+ "ستون",
+ "سبعون",
+ "ثمانون",
+ "تسعون",
+ ]
scales = ["", "ألف", "مليون", "مليار", "تريليون"]
if number == 0:
@@ -320,7 +385,8 @@ def number_to_words_arabic(number):
number = number // 1000
scale_index += 1
- return ' '.join(words)
+ return " ".join(words)
+
# @register.filter(name='num2words')
# def num2words(number, language='en'):
@@ -330,25 +396,26 @@ def number_to_words_arabic(number):
# else:
# 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):
try:
- entity_slug = context['view'].kwargs.get('entity_slug')
+ entity_slug = context["view"].kwargs.get("entity_slug")
except KeyError:
- entity_slug = context['entity_slug']
+ entity_slug = context["entity_slug"]
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:
- context['date_picker_ids'] = list()
- context['date_picker_ids'].append(date_picker_id)
+ if "date_picker_ids" not in context:
+ context["date_picker_ids"] = list()
+ 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 {
- 'entity_slug': entity_slug,
- 'date_picker_id': date_picker_id,
- 'date_navigation_url': date_navigation_url
+ "entity_slug": entity_slug,
+ "date_picker_id": date_picker_id,
+ "date_navigation_url": date_navigation_url,
}
@@ -357,7 +424,8 @@ def splitlines(value):
"""Splits text into lines"""
return value.splitlines()
-@register.filter(name='currency_format')
+
+@register.filter(name="currency_format")
def currency_format(value):
if not value:
value = 0.00
@@ -368,55 +436,77 @@ def currency_format(value):
def filter_by_role(accounts, 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):
return {
- 'entity_slug': context['entity_slug'],
- 'po_model': context['po_model'],
- 'po_item_list': queryset
+ "entity_slug": context["entity_slug"],
+ "po_model": context["po_model"],
+ "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):
+ # print(len(itemtxs_formset.forms))
+
+ for form in itemtxs_formset.forms:
+ form.fields["item_model"].queryset = form.fields["item_model"].queryset.filter(
+ item_role="inventory"
+ )
return {
- 'entity_slug': context['view'].kwargs['entity_slug'],
- 'po_model': po_model,
- 'itemtxs_formset': itemtxs_formset,
+ "entity_slug": context["view"].kwargs["entity_slug"],
+ "po_model": po_model,
+ "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):
return {
- 'entity_slug': context['view'].kwargs['entity_slug'],
- 'bill_pk': context['view'].kwargs['bill_pk'],
- 'total_amount__sum': context['total_amount__sum'],
- 'item_formset': item_formset,
+ "entity_slug": context["view"].kwargs["entity_slug"],
+ "bill_pk": context["view"].kwargs["bill_pk"],
+ "total_amount__sum": context["total_amount__sum"],
+ "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):
- transaction_model_qs = object_type.transactionmodel_set.all().with_annotated_details().order_by(
- '-timestamp')
+ transaction_model_qs = (
+ object_type.transactionmodel_set.all()
+ .with_annotated_details()
+ .order_by("-timestamp")
+ )
elif isinstance(object_type, BillModel):
- 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")
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:
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_debits = sum(tx.amount for tx in transaction_model_qs if tx.is_debit())
return {
- 'style': style,
- 'transaction_model_qs': transaction_model_qs,
- 'total_debits': total_debits,
- 'total_credits': total_credits,
- 'object': object_type
+ "style": style,
+ "transaction_model_qs": transaction_model_qs,
+ "total_debits": total_debits,
+ "total_credits": total_credits,
+ "object": object_type,
}
@@ -426,67 +516,67 @@ def get_vehicle_image(car_serie):
Returns the appropriate car image filename based on car series
"""
if not car_serie:
- return 'sedan.png'
+ return "sedan.png"
serie_lower = car_serie.name.lower()
# SUV mapping
- if 'suv' in serie_lower:
- if 'sport' in serie_lower or '3 doors' in serie_lower:
- return 'crossover.png'
+ if "suv" in serie_lower:
+ if "sport" in serie_lower or "3 doors" in serie_lower:
+ return "crossover.png"
else:
- return 'suv.png'
+ return "suv.png"
# Pickup mapping
- elif 'pickup' in serie_lower:
- if 'cabriolet' in serie_lower:
- return 'pickup_cabriolet.png'
- elif 'double' in serie_lower or 'crew' in serie_lower:
- return 'double_pickup.png'
+ elif "pickup" in serie_lower:
+ if "cabriolet" in serie_lower:
+ return "pickup_cabriolet.png"
+ elif "double" in serie_lower or "crew" in serie_lower:
+ return "double_pickup.png"
else:
- return 'single_pickup.png'
+ return "single_pickup.png"
# Van/Minivan mapping
- elif 'minivan' in serie_lower:
- return 'minivan.png'
- elif 'van' in serie_lower:
- if 'cargo' in serie_lower:
- return 'van_cargo.png'
+ elif "minivan" in serie_lower:
+ return "minivan.png"
+ elif "van" in serie_lower:
+ if "cargo" in serie_lower:
+ return "van_cargo.png"
else:
- return 'van.png'
- elif 'compactvan' in serie_lower:
- return 'van.png'
+ return "van.png"
+ elif "compactvan" in serie_lower:
+ return "van.png"
# Hatchback mapping
- elif 'hatchback' in serie_lower:
- return 'hatchback.png'
+ elif "hatchback" in serie_lower:
+ return "hatchback.png"
# Wagon mapping
- elif 'wagon' in serie_lower:
- return 'van.png' # Closest match
+ elif "wagon" in serie_lower:
+ return "van.png" # Closest match
# Coupe/Sports mapping
- elif 'cabriolet' in serie_lower:
- return 'cabriolet.png'
- elif 'coupe' in serie_lower:
- return 'coupe.png'
- elif 'speedster' in serie_lower:
- return 'sport_car.png'
+ elif "cabriolet" in serie_lower:
+ return "cabriolet.png"
+ elif "coupe" in serie_lower:
+ return "coupe.png"
+ elif "speedster" in serie_lower:
+ return "sport_car.png"
# Liftback mapping
- elif 'liftback' in serie_lower:
- return 'hatchback.png' # Closest match
+ elif "liftback" in serie_lower:
+ return "hatchback.png" # Closest match
# Sedan mapping (including 2 doors)
- elif 'sedan' in serie_lower:
- if '2 doors' in serie_lower:
- return 'coupe.png'
+ elif "sedan" in serie_lower:
+ if "2 doors" in serie_lower:
+ return "coupe.png"
else:
- return 'sedan.png'
+ return "sedan.png"
# Default fallback
else:
- return 'sedan.png'
+ return "sedan.png"
@register.filter
@@ -495,23 +585,48 @@ def get_vehicle_type_name(car_serie):
Returns the vehicle type name for styling purposes
"""
if not car_serie:
- return 'sedan'
+ return "sedan"
serie_lower = car_serie.name.lower()
- if 'suv' in serie_lower:
- return 'suv'
- elif 'pickup' in serie_lower:
- return 'pickup'
- elif any(word in serie_lower for word in ['van', 'minivan']):
- return 'van'
- elif 'hatchback' in serie_lower:
- return 'hatchback'
- elif 'wagon' in serie_lower:
- return 'wagon'
- elif any(word in serie_lower for word in ['coupe', 'cabriolet', 'speedster']):
- return 'coupe'
- elif 'liftback' in serie_lower:
- return 'liftback'
+ if "suv" in serie_lower:
+ return "suv"
+ elif "pickup" in serie_lower:
+ return "pickup"
+ elif any(word in serie_lower for word in ["van", "minivan"]):
+ return "van"
+ elif "hatchback" in serie_lower:
+ return "hatchback"
+ elif "wagon" in serie_lower:
+ return "wagon"
+ elif any(word in serie_lower for word in ["coupe", "cabriolet", "speedster"]):
+ return "coupe"
+ elif "liftback" in serie_lower:
+ return "liftback"
else:
- return 'sedan'
\ No newline at end of file
+ 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
diff --git a/inventory/templatetags/num2words_tags.py b/inventory/templatetags/num2words_tags.py
index 788d3080..6cd70f0b 100644
--- a/inventory/templatetags/num2words_tags.py
+++ b/inventory/templatetags/num2words_tags.py
@@ -3,9 +3,10 @@ from num2words import num2words
register = template.Library()
+
@register.filter
-def num_to_words(value, lang='ar'):
+def num_to_words(value, lang="ar"):
try:
return num2words(value, lang=lang)
except:
- return value
\ No newline at end of file
+ return value
diff --git a/inventory/templatetags/tenhal_tag.py b/inventory/templatetags/tenhal_tag.py
index ed862964..111007ee 100644
--- a/inventory/templatetags/tenhal_tag.py
+++ b/inventory/templatetags/tenhal_tag.py
@@ -25,9 +25,12 @@ from django_ledger.io.io_core import get_localdate
register = template.Library()
+
@register.filter()
def to_int(value):
return Decimal(value).quantize(Decimal("0.01"))
+
+
# @register.simple_tag(name='current_version')
# def current_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):
kwargs = dict()
- entity_slug = context['view'].kwargs['entity_slug']
- kwargs['entity_slug'] = entity_slug
+ entity_slug = context["view"].kwargs["entity_slug"]
+ kwargs["entity_slug"] = entity_slug
- if context['view'].kwargs.get('ledger_pk'):
- kwargs['ledger_pk'] = context['view'].kwargs.get('ledger_pk')
+ if context["view"].kwargs.get("ledger_pk"):
+ kwargs["ledger_pk"] = context["view"].kwargs.get("ledger_pk")
- if context['view'].kwargs.get('account_pk'):
- kwargs['account_pk'] = context['view'].kwargs.get('account_pk')
+ if context["view"].kwargs.get("account_pk"):
+ kwargs["account_pk"] = context["view"].kwargs.get("account_pk")
- if context['view'].kwargs.get('unit_slug'):
- kwargs['unit_slug'] = context['view'].kwargs.get('unit_slug')
+ if context["view"].kwargs.get("unit_slug"):
+ kwargs["unit_slug"] = context["view"].kwargs.get("unit_slug")
- if context['view'].kwargs.get('coa_slug'):
- kwargs['coa_slug'] = context['view'].kwargs.get('coa_slug')
+ if context["view"].kwargs.get("coa_slug"):
+ kwargs["coa_slug"] = context["view"].kwargs.get("coa_slug")
ctx = dict()
- ctx['year'] = context['year']
- ctx['has_year'] = context.get('has_year')
- ctx['has_quarter'] = context.get('has_quarter')
- ctx['has_month'] = context.get('has_month')
- ctx['has_date'] = context.get('has_date')
- ctx['previous_year'] = context['previous_year']
+ ctx["year"] = context["year"]
+ ctx["has_year"] = context.get("has_year")
+ ctx["has_quarter"] = context.get("has_quarter")
+ ctx["has_month"] = context.get("has_month")
+ ctx["has_date"] = context.get("has_date")
+ ctx["previous_year"] = context["previous_year"]
- kwargs['year'] = context['previous_year']
- ctx['previous_year_url'] = reverse(f'django_ledger:{base_url}-year', kwargs=kwargs)
- ctx['next_year'] = context['next_year']
+ kwargs["year"] = context["previous_year"]
+ ctx["previous_year_url"] = reverse(f"django_ledger:{base_url}-year", kwargs=kwargs)
+ ctx["next_year"] = context["next_year"]
- kwargs['year'] = context['next_year']
- ctx['next_year_url'] = reverse(f'django_ledger:{base_url}-year', kwargs=kwargs)
+ kwargs["year"] = context["next_year"]
+ ctx["next_year_url"] = reverse(f"django_ledger:{base_url}-year", kwargs=kwargs)
- kwargs['year'] = context['year']
- ctx['current_year_url'] = reverse(f'django_ledger:{base_url}-year', kwargs=kwargs)
+ kwargs["year"] = context["year"]
+ ctx["current_year_url"] = reverse(f"django_ledger:{base_url}-year", kwargs=kwargs)
dt = get_localdate()
KWARGS_CURRENT_MONTH = {
- 'entity_slug': context['view'].kwargs['entity_slug'],
- 'year': dt.year,
- 'month': dt.month
+ "entity_slug": context["view"].kwargs["entity_slug"],
+ "year": dt.year,
+ "month": dt.month,
}
- if 'unit_slug' in kwargs:
- KWARGS_CURRENT_MONTH['unit_slug'] = kwargs['unit_slug']
- if 'account_pk' in kwargs:
- KWARGS_CURRENT_MONTH['account_pk'] = kwargs['account_pk']
- if 'ledger_pk' in kwargs:
- KWARGS_CURRENT_MONTH['ledger_pk'] = kwargs['ledger_pk']
- if 'coa_slug' in kwargs:
- KWARGS_CURRENT_MONTH['coa_slug'] = kwargs['coa_slug']
+ if "unit_slug" in kwargs:
+ KWARGS_CURRENT_MONTH["unit_slug"] = kwargs["unit_slug"]
+ if "account_pk" in kwargs:
+ KWARGS_CURRENT_MONTH["account_pk"] = kwargs["account_pk"]
+ if "ledger_pk" in kwargs:
+ KWARGS_CURRENT_MONTH["ledger_pk"] = kwargs["ledger_pk"]
+ if "coa_slug" in kwargs:
+ KWARGS_CURRENT_MONTH["coa_slug"] = kwargs["coa_slug"]
- ctx['current_month_url'] = reverse(f'django_ledger:{base_url}-month',
- kwargs=KWARGS_CURRENT_MONTH)
+ ctx["current_month_url"] = reverse(
+ f"django_ledger:{base_url}-month", kwargs=KWARGS_CURRENT_MONTH
+ )
quarter_urls = list()
- ctx['quarter'] = context.get('quarter')
+ ctx["quarter"] = context.get("quarter")
for Q in range(1, 5):
- kwargs['quarter'] = Q
- quarter_urls.append({
- 'url': reverse(f'django_ledger:{base_url}-quarter', kwargs=kwargs),
- 'quarter': Q,
- 'quarter_name': f'Q{Q}'
- })
- del kwargs['quarter']
- ctx['quarter_urls'] = quarter_urls
+ kwargs["quarter"] = Q
+ quarter_urls.append(
+ {
+ "url": reverse(f"django_ledger:{base_url}-quarter", kwargs=kwargs),
+ "quarter": Q,
+ "quarter_name": f"Q{Q}",
+ }
+ )
+ del kwargs["quarter"]
+ ctx["quarter_urls"] = quarter_urls
month_urls = list()
- ctx['month'] = context.get('month')
+ ctx["month"] = context.get("month")
for M in range(1, 13):
- kwargs['month'] = M
- month_urls.append({
- 'url': reverse(f'django_ledger:{base_url}-month', kwargs=kwargs),
- 'month': M,
- 'month_abbr': month_abbr[M]
- })
- ctx['month_urls'] = month_urls
- ctx['from_date'] = context['from_date']
- ctx['to_date'] = context['to_date']
+ kwargs["month"] = M
+ month_urls.append(
+ {
+ "url": reverse(f"django_ledger:{base_url}-month", kwargs=kwargs),
+ "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.update(kwargs)
- ctx['date_navigation_url'] = context.get('date_navigation_url')
+ ctx["date_navigation_url"] = context.get("date_navigation_url")
return ctx
diff --git a/inventory/tests.py b/inventory/tests.py
index aa2da193..d1fdf426 100644
--- a/inventory/tests.py
+++ b/inventory/tests.py
@@ -8,7 +8,7 @@ from django_ledger.io.io_core import get_localdate
from django.core.exceptions import ObjectDoesNotExist
from decimal import Decimal
from unittest.mock import MagicMock
-from inventory.models import VatRate
+from inventory.models import VatRate
from inventory.utils import CarFinanceCalculator
@@ -47,6 +47,7 @@ class ModelTest(TestCase):
:ivar car_finances: Car finance object for the car under test.
:type car_finances: CarFinance instance
"""
+
def setUp(self):
email = "RkzgO@example.com"
name = "John Doe"
@@ -178,46 +179,46 @@ class AuthenticationTest(TestCase):
:ivar url: URL for account signup endpoint used in the test cases.
:type url: str
"""
+
def setUp(self):
self.client = Client()
self.url = reverse("account_signup")
+
def test_login(self):
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)
-
def test_valid_data(self):
# Create valid JSON data
data = {
"wizardValidationForm1": {
"email": "test@example.com",
"password": "password123",
- "confirm_password": "password123"
+ "confirm_password": "password123",
},
"wizardValidationForm2": {
"name": "John Doe",
"arabic_name": "جون دو",
- "phone_number": "1234567890"
+ "phone_number": "1234567890",
},
"wizardValidationForm3": {
"crn": "123456",
"vrn": "789012",
- "address": "123 Main St"
- }
+ "address": "123 Main St",
+ },
}
# Send a POST request with the JSON data
response = self.client.post(
- self.url,
- data=json.dumps(data),
- content_type='application/json'
+ self.url, data=json.dumps(data), content_type="application/json"
)
# Check the response
self.assertEqual(response.status_code, 200)
- self.assertEqual(response.json(), {'message': 'User created successfully.'})
-
+ self.assertEqual(response.json(), {"message": "User created successfully."})
def test_passwords_do_not_match(self):
# Create JSON data with mismatched passwords
@@ -225,25 +226,23 @@ class AuthenticationTest(TestCase):
"wizardValidationForm1": {
"email": "test@example.com",
"password": "password123",
- "confirm_password": "differentpassword"
+ "confirm_password": "differentpassword",
},
"wizardValidationForm2": {
"name": "John Doe",
"arabic_name": "جون دو",
- "phone_number": "1234567890"
+ "phone_number": "1234567890",
},
"wizardValidationForm3": {
"crn": "123456",
"vrn": "789012",
- "address": "123 Main St"
- }
+ "address": "123 Main St",
+ },
}
# Send a POST request with the JSON data
response = self.client.post(
- self.url,
- data=json.dumps(data),
- content_type='application/json'
+ self.url, data=json.dumps(data), content_type="application/json"
)
# Check the response
@@ -261,26 +260,26 @@ class AuthenticationTest(TestCase):
"wizardValidationForm2": {
"name": "John Doe",
"arabic_name": "جون دو",
- "phone_number": "1234567890"
+ "phone_number": "1234567890",
},
"wizardValidationForm3": {
"crn": "123456",
"vrn": "789012",
- "address": "123 Main St"
- }
+ "address": "123 Main St",
+ },
}
# Send a POST request with the JSON data
response = self.client.post(
- self.url,
- data=json.dumps(data),
- content_type='application/json'
+ self.url, data=json.dumps(data), content_type="application/json"
)
# Check the response
self.assertEqual(response.status_code, 400)
- self.assertIn("error", response.json()) # Assuming the view returns an error for missing fields
-
+ self.assertIn(
+ "error", response.json()
+ ) # Assuming the view returns an error for missing fields
+
class CarFinanceCalculatorTests(TestCase):
"""
@@ -297,10 +296,11 @@ class CarFinanceCalculatorTests(TestCase):
:ivar vat_rate: Active VAT rate used for testing VAT rate retrieval.
:type vat_rate: VatRate
"""
+
def setUp(self):
# Common setup for all tests
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):
VatRate.objects.all().delete() # Ensure no active VAT
@@ -309,11 +309,13 @@ class CarFinanceCalculatorTests(TestCase):
def test_vat_rate_retrieval(self):
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):
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)
self.assertEqual(calculator.item_transactions, [mock_item])
@@ -321,136 +323,152 @@ class CarFinanceCalculatorTests(TestCase):
mock_item = MagicMock()
mock_item.ce_quantity = 2
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 = {
CarFinanceCalculator.CAR_FINANCE_KEY: {
- 'selling_price': '10000',
- 'cost_price': '8000',
- 'discount_amount': '500',
- 'total_vat': '2000'
+ "selling_price": "10000",
+ "cost_price": "8000",
+ "discount_amount": "500",
+ "total_vat": "2000",
},
CarFinanceCalculator.CAR_INFO_KEY: {
- 'vin': 'VIN123',
- 'make': 'Toyota',
- 'model': 'Camry',
- 'year': 2020,
- 'trim': 'LE',
- 'mileage': 15000
+ "vin": "VIN123",
+ "make": "Toyota",
+ "model": "Camry",
+ "year": 2020,
+ "trim": "LE",
+ "mileage": 15000,
},
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)
car_data = calculator._get_car_data(mock_item)
- self.assertEqual(car_data['item_number'], '123')
- self.assertEqual(car_data['vin'], 'VIN123')
- self.assertEqual(car_data['make'], 'Toyota')
- self.assertEqual(car_data['selling_price'], '10000')
- self.assertEqual(car_data['unit_price'], Decimal('10000'))
- self.assertEqual(car_data['quantity'], 2)
- self.assertEqual(car_data['total'], Decimal('20000'))
- 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["item_number"], "123")
+ self.assertEqual(car_data["vin"], "VIN123")
+ self.assertEqual(car_data["make"], "Toyota")
+ self.assertEqual(car_data["selling_price"], "10000")
+ self.assertEqual(car_data["unit_price"], Decimal("10000"))
+ self.assertEqual(car_data["quantity"], 2)
+ self.assertEqual(car_data["total"], Decimal("20000"))
+ self.assertEqual(car_data["total_vat"], "2000")
+ self.assertEqual(
+ car_data["additional_services"],
+ [{"name": "Service 1", "price": "200", "taxable": True, "price_": "240"}],
+ )
def test_get_additional_services(self):
mock_item1 = MagicMock()
mock_item1.item_model.additional_info = {
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.item_model.additional_info = {
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)
services = calculator._get_additional_services()
self.assertEqual(len(services), 2)
- self.assertEqual(services[0]['name'], 'Service 1')
- self.assertEqual(services[1]['name'], 'Service 2')
- self.assertEqual(services[0]['price_'], '120')
- self.assertEqual(services[1]['price_'], '200')
+ self.assertEqual(services[0]["name"], "Service 1")
+ self.assertEqual(services[1]["name"], "Service 2")
+ self.assertEqual(services[0]["price_"], "120")
+ self.assertEqual(services[1]["price_"], "200")
def test_calculate_totals(self):
mock_item1 = MagicMock()
mock_item1.ce_quantity = 2
mock_item1.item_model.additional_info = {
CarFinanceCalculator.CAR_FINANCE_KEY: {
- 'selling_price': '10000',
- 'discount_amount': '500'
+ "selling_price": "10000",
+ "discount_amount": "500",
},
CarFinanceCalculator.ADDITIONAL_SERVICES_KEY: [
- {'price_': '100'},
- {'price_': '200'}
- ]
+ {"price_": "100"},
+ {"price_": "200"},
+ ],
}
mock_item2 = MagicMock()
mock_item2.quantity = 3
mock_item2.item_model.additional_info = {
CarFinanceCalculator.CAR_FINANCE_KEY: {
- 'selling_price': '20000',
- 'discount_amount': '1000'
+ "selling_price": "20000",
+ "discount_amount": "1000",
},
- CarFinanceCalculator.ADDITIONAL_SERVICES_KEY: [
- {'price_': '300'}
- ]
+ CarFinanceCalculator.ADDITIONAL_SERVICES_KEY: [{"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)
totals = calculator.calculate_totals()
- expected_total_price = (Decimal('10000') * 2 + Decimal('20000') * 3) - (Decimal('500') + Decimal('1000'))
- 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'))
+ expected_total_price = (Decimal("10000") * 2 + Decimal("20000") * 3) - (
+ Decimal("500") + Decimal("1000")
+ )
+ 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_discount'], Decimal('1500'))
- self.assertEqual(totals['total_vat_amount'], expected_vat)
- self.assertEqual(totals['total_additionals'], expected_additionals)
- self.assertEqual(totals['grand_total'], expected_grand_total)
+ self.assertEqual(totals["total_price"], expected_total_price)
+ self.assertEqual(totals["total_discount"], Decimal("1500"))
+ self.assertEqual(totals["total_vat_amount"], expected_vat)
+ self.assertEqual(totals["total_additionals"], expected_additionals)
+ self.assertEqual(totals["grand_total"], expected_grand_total)
def test_get_finance_data(self):
mock_item = MagicMock()
mock_item.ce_quantity = 1
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 = {
CarFinanceCalculator.CAR_FINANCE_KEY: {
- 'selling_price': '15000',
- 'discount_amount': '1000',
- 'total_vat': '2800'
+ "selling_price": "15000",
+ "discount_amount": "1000",
+ "total_vat": "2800",
},
CarFinanceCalculator.CAR_INFO_KEY: {
- 'vin': 'VIN456',
- 'make': 'Honda',
- 'model': 'Civic',
- 'year': 2021
+ "vin": "VIN456",
+ "make": "Honda",
+ "model": "Civic",
+ "year": 2021,
},
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)
finance_data = calculator.get_finance_data()
- self.assertEqual(len(finance_data['cars']), 1)
- self.assertEqual(finance_data['quantity'], 1)
- 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(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'))
\ No newline at end of file
+ self.assertEqual(len(finance_data["cars"]), 1)
+ self.assertEqual(finance_data["quantity"], 1)
+ 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(
+ 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"))
diff --git a/inventory/urls.py b/inventory/urls.py
index 12c83091..e20406d8 100644
--- a/inventory/urls.py
+++ b/inventory/urls.py
@@ -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_tables2.export.export import TableExport
@@ -43,30 +43,39 @@ urlpatterns = [
# ),
# ),
# Tasks
-
- path('tasks/', views.task_list, name='task_list'),
- path('legal/', views.terms_and_privacy, name='terms_and_privacy'),
+ path("tasks/", views.task_list, name="task_list"),
+ path("legal/", views.terms_and_privacy, name="terms_and_privacy"),
# path('tasks//detail/', views.task_detail, name='task_detail'),
# Dashboards
# path("user//settings/", views.UserSettingsView.as_view(), name="user_settings"),
path("pricing/", views.pricing_page, name="pricing_page"),
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(
"dealers/activity/",
views.UserActivityLogListView.as_view(),
name="dealer_activity",
),
- path("dealers//settings/", views.DealerSettingsView, name="dealer_settings"),
+ path(
+ "dealers//settings/",
+ views.DealerSettingsView,
+ name="dealer_settings",
+ ),
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("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"),
# Dealer URLs
- path("dealers//", views.DealerDetailView.as_view(), name="dealer_detail"),
+ path(
+ "dealers//", views.DealerDetailView.as_view(), name="dealer_detail"
+ ),
path(
"dealers//update/",
views.DealerUpdateView.as_view(),
@@ -93,7 +102,9 @@ urlpatterns = [
views.CustomerUpdateView.as_view(),
name="customer_update",
),
- path("customers//delete/", views.delete_customer, name="customer_delete"),
+ path(
+ "customers//delete/", views.delete_customer, name="customer_delete"
+ ),
path(
"customers//opportunities/create/",
views.OpportunityCreateView.as_view(),
@@ -101,19 +112,26 @@ urlpatterns = [
),
path("crm/leads/create/", views.lead_create, name="lead_create"),
path(
- "crm/leads//view/", views.LeadDetailView.as_view(), name="lead_detail"
+ "crm/leads//view/",
+ views.LeadDetailView.as_view(),
+ name="lead_detail",
),
- 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_view/', views.lead_view, name='lead_view'),
-
+ 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_view/", views.lead_view, name="lead_view"),
path("crm/leads/", views.LeadListView.as_view(), name="lead_list"),
path(
- "crm/leads//update/", views.LeadUpdateView.as_view(), name="lead_update"
+ "crm/leads//update/",
+ views.LeadUpdateView.as_view(),
+ name="lead_update",
),
path("crm/leads//delete/", views.LeadDeleteView, name="lead_delete"),
- path("crm/leads//lead-convert/", views.lead_convert, name="lead_convert"),
- path("crm/leads//delete-note/", views.delete_note, name="delete_note_to_lead"),
+ path(
+ "crm/leads//lead-convert/", views.lead_convert, name="lead_convert"
+ ),
+ path(
+ "crm/leads//delete-note/", views.delete_note, name="delete_note_to_lead"
+ ),
path(
"crm//update-note/",
views.update_note,
@@ -211,17 +229,22 @@ urlpatterns = [
),
# path('crm/opportunities//logs/', views.OpportunityLogsView.as_view(), name='opportunity_logs'),
# #######################
- path('stream/', views.sse_stream, name='sse_stream'),
- path('fetch/', views.fetch_notifications, name='fetch_notifications'),
-
+ path("stream/", views.sse_stream, name="sse_stream"),
+ path("fetch/", views.fetch_notifications, name="fetch_notifications"),
# Mark single notification as read
- path('/mark-read/', views.mark_notification_as_read, name='mark_notification_as_read'),
-
+ path(
+ "/mark-read/",
+ views.mark_notification_as_read,
+ name="mark_notification_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
- path('history/', views.notifications_history, name='notifications_history'),
+ path("history/", views.notifications_history, name="notifications_history"),
# #######################
path(
"crm/notifications/",
@@ -238,7 +261,7 @@ urlpatterns = [
views.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
path("vendors/create/", views.VendorCreateView.as_view(), name="vendor_create"),
path("vendors", views.VendorListView.as_view(), name="vendor_list"),
@@ -254,8 +277,8 @@ urlpatterns = [
name="vendor_delete",
),
# Car URLs
- path('cars/upload_cars/', views.upload_cars, name='upload_cars'),
- path('cars//upload_cars/', views.upload_cars, name='upload_cars'),
+ path("cars/upload_cars/", views.upload_cars, name="upload_cars"),
+ path("cars//upload_cars/", views.upload_cars, name="upload_cars"),
path("cars/add/", views.CarCreateView.as_view(), name="car_add"),
path("cars/inventory/", views.CarInventory.as_view(), name="car_inventory_all"),
path(
@@ -288,13 +311,17 @@ urlpatterns = [
path(
"cars//add-color/", views.CarColorCreate.as_view(), name="add_color"
),
- path('car/colors//update/', views.CarColorsUpdateView.as_view(), name='car_colors_update'),
+ path(
+ "car/colors//update/",
+ views.CarColorsUpdateView.as_view(),
+ name="car_colors_update",
+ ),
path(
"cars//location/add/",
views.CarLocationCreateView.as_view(),
name="add_car_location",
),
-path(
+ path(
"cars//location//update",
views.CarLocationUpdateView.as_view(),
name="update_car_location",
@@ -324,13 +351,9 @@ path(
views.CarTransferPreviewView,
name="transfer_preview",
),
- path("cars/inventory/search/",
- views.SearchCodeView.as_view(),
- name="car_search"),
+ path("cars/inventory/search/", views.SearchCodeView.as_view(), name="car_search"),
# path('cars//colors//update/',views.CarColorUpdateView.as_view(),name='color_update'),
- path("cars/reserve//",
- views.reserve_car_view,
- name="reserve_car"),
+ path("cars/reserve//", views.reserve_car_view, name="reserve_car"),
path(
"reservations//",
views.manage_reservation,
@@ -341,15 +364,26 @@ path(
views.CustomCardCreateView.as_view(),
name="add_custom_card",
),
- path('cars//add-registration/',
- views.CarRegistrationCreateView.as_view(),
- name='add_registration'),
-
-#sales list
path(
- 'sales/list/',
+ "cars//add-registration/",
+ views.CarRegistrationCreateView.as_view(),
+ name="add_registration",
+ ),
+ # sales list
+ path(
+ "sales/list/",
views.sales_list_view,
- name='sales_list',
+ name="sales_list",
+ ),
+ path(
+ "sale_orders//",
+ views.SaleOrderDetailView.as_view(),
+ name="order_detail",
+ ),
+ path(
+ "inventory//list/",
+ views.InventoryListView.as_view(),
+ name="inventort_list",
),
# Sales URLs quotation_create
# path(
@@ -401,21 +435,26 @@ path(
# views.payment_create,
# name="payment_create",
# ),
-
# Users URLs
path("user/", views.UserListView.as_view(), name="user_list"),
path("user/create/", views.UserCreateView.as_view(), name="user_create"),
path("user//", views.UserDetailView.as_view(), name="user_detail"),
path("user//groups/", views.UserGroupView, name="user_groups"),
- path("user//update/", views.UserUpdateView.as_view(), name="user_update"),
+ path(
+ "user//update/", views.UserUpdateView.as_view(), name="user_update"
+ ),
path("user//confirm/", views.UserDeleteview, name="user_delete"),
# Group URLs
path("group/create/", views.GroupCreateView.as_view(), name="group_create"),
- path("group//update/", views.GroupUpdateView.as_view(), name="group_update"),
+ path(
+ "group//update/", views.GroupUpdateView.as_view(), name="group_update"
+ ),
path("group//", views.GroupDetailView.as_view(), name="group_detail"),
path("group/", views.GroupListView.as_view(), name="group_list"),
path("group//confirm/", views.GroupDeleteview, name="group_delete"),
- path("group//permission/", views.GroupPermissionView, name="group_permission"),
+ path(
+ "group//permission/", views.GroupPermissionView, name="group_permission"
+ ),
# Organization URLs
path(
"organizations/create/",
@@ -468,76 +507,109 @@ path(
),
# Ledger URLS
# Ledger
- path(
- "ledgers/", views.LedgerModelListView.as_view(), name="ledger_list"
- ),
+ path("ledgers/", views.LedgerModelListView.as_view(), name="ledger_list"),
path(
"ledgers/create/", views.LedgerModelCreateView.as_view(), name="ledger_create"
),
path(
- "ledgers//detail//", views.LedgerModelDetailView.as_view(), name="ledger_detail"
+ "ledgers//detail//",
+ views.LedgerModelDetailView.as_view(),
+ name="ledger_detail",
),
path(
- "ledgers//lock_all_journals//", views.ledger_lock_all_journals, name="lock_all_journals"
+ "ledgers//lock_all_journals//",
+ views.ledger_lock_all_journals,
+ name="lock_all_journals",
),
path(
- "ledgers//unlock_all_journals//", views.ledger_unlock_all_journals, name="unlock_all_journals"
+ "ledgers//unlock_all_journals//",
+ views.ledger_unlock_all_journals,
+ name="unlock_all_journals",
),
path(
- "ledgers//post_all_journals//", views.ledger_post_all_journals, name="post_all_journals"
+ "ledgers//post_all_journals//",
+ views.ledger_post_all_journals,
+ name="post_all_journals",
),
path(
- "ledgers//unpost_all_journals//", views.ledger_unpost_all_journals, name="unpost_all_journals"
+ "ledgers//unpost_all_journals//",
+ views.ledger_unpost_all_journals,
+ name="unpost_all_journals",
),
# path(
# "ledgers/create/", views.LedgerModelCreateView.as_view(), name="ledger_create"
# ),
path(
- "journalentries//list/", views.JournalEntryListView.as_view(), name="journalentry_list"
+ "journalentries//list/",
+ views.JournalEntryListView.as_view(),
+ name="journalentry_list",
),
path(
- "journalentries//create/", views.JournalEntryCreateView.as_view(), name="journalentry_create"
+ "journalentries//create/",
+ views.JournalEntryCreateView.as_view(),
+ name="journalentry_create",
),
path(
- "journalentries//delete/", views.JournalEntryDeleteView, name="journalentry_delete"
+ "journalentries//delete/",
+ views.JournalEntryDeleteView,
+ name="journalentry_delete",
),
path(
"journalentries//transactions/",
views.JournalEntryTransactionsView,
name="journalentry_transactions",
),
- path('journalentries///detail//txs/',
- views.JournalEntryModelTXSDetailView.as_view(),
- name='journalentry_txs'),
+ path(
+ "journalentries///detail//txs/",
+ views.JournalEntryModelTXSDetailView.as_view(),
+ name="journalentry_txs",
+ ),
# ledger actions
-
- path('ledgers//action//post/',
- views.LedgerModelModelActionView.as_view(action_name='post'),
- name='ledger-action-post'),
- path('ledgers//action//post-journal-entries/',
- views.LedgerModelModelActionView.as_view(action_name='post_journal_entries'),
- name='ledger-action-post-journal-entries'),
- path('ledgers//action//unpost/',
- views.LedgerModelModelActionView.as_view(action_name='unpost'),
- name='ledger-action-unpost'),
- path('ledgers//action//lock/',
- views.LedgerModelModelActionView.as_view(action_name='lock'),
- name='ledger-action-lock'),
- path('ledgers//action//lock-journal-entries/',
- views.LedgerModelModelActionView.as_view(action_name='lock_journal_entries'),
- name='ledger-action-lock-journal-entries'),
- path('ledgers//action//unlock/',
- views.LedgerModelModelActionView.as_view(action_name='unlock'),
- name='ledger-action-unlock'),
- path('ledgers//action//hide/',
- views.LedgerModelModelActionView.as_view(action_name='hide'),
- name='ledger-action-hide'),
- path('ledgers//action//unhide/',
- views.LedgerModelModelActionView.as_view(action_name='unhide'),
- name='ledger-action-unhide'),
- path('ledgers//delete//',
- views.LedgerModelDeleteView.as_view(),
- name='ledger-delete'),
+ path(
+ "ledgers//action//post/",
+ views.LedgerModelModelActionView.as_view(action_name="post"),
+ name="ledger-action-post",
+ ),
+ path(
+ "ledgers//action//post-journal-entries/",
+ views.LedgerModelModelActionView.as_view(action_name="post_journal_entries"),
+ name="ledger-action-post-journal-entries",
+ ),
+ path(
+ "ledgers//action//unpost/",
+ views.LedgerModelModelActionView.as_view(action_name="unpost"),
+ name="ledger-action-unpost",
+ ),
+ path(
+ "ledgers//action//lock/",
+ views.LedgerModelModelActionView.as_view(action_name="lock"),
+ name="ledger-action-lock",
+ ),
+ path(
+ "ledgers//action//lock-journal-entries/",
+ views.LedgerModelModelActionView.as_view(action_name="lock_journal_entries"),
+ name="ledger-action-lock-journal-entries",
+ ),
+ path(
+ "ledgers//action//unlock/",
+ views.LedgerModelModelActionView.as_view(action_name="unlock"),
+ name="ledger-action-unlock",
+ ),
+ path(
+ "ledgers//action//hide/",
+ views.LedgerModelModelActionView.as_view(action_name="hide"),
+ name="ledger-action-hide",
+ ),
+ path(
+ "ledgers//action//unhide/",
+ views.LedgerModelModelActionView.as_view(action_name="unhide"),
+ name="ledger-action-unhide",
+ ),
+ path(
+ "ledgers//delete//",
+ views.LedgerModelDeleteView.as_view(),
+ name="ledger-delete",
+ ),
# Bank Account
path(
"bank_accounts/", views.BankAccountListView.as_view(), name="bank_account_list"
@@ -586,7 +658,11 @@ path(
name="estimate_detail",
),
path("sales/estimates/create/", views.create_estimate, name="estimate_create"),
- path("sales/estimates/create//", views.create_estimate, name="estimate_create_from_opportunity"),
+ path(
+ "sales/estimates/create//",
+ views.create_estimate,
+ name="estimate_create_from_opportunity",
+ ),
path(
"sales/estimates//estimate_mark_as/",
views.estimate_mark_as,
@@ -605,10 +681,21 @@ path(
path(
"sales/estimates//send_email", views.send_email_view, name="send_email"
),
- path('sales/estimates//sale_order/', views.create_sale_order, name='create_sale_order'),
- path('sales/estimates//sale_order//details/', views.SaleOrderDetail.as_view(), name='sale_order_details'),
- path('sales/estimates//sale_order/preview/', views.preview_sale_order, name='preview_sale_order'),
-
+ path(
+ "sales/estimates//sale_order/",
+ views.create_sale_order,
+ name="create_sale_order",
+ ),
+ path(
+ "sales/estimates//sale_order//details/",
+ views.SaleOrderDetail.as_view(),
+ name="sale_order_details",
+ ),
+ path(
+ "sales/estimates//sale_order/preview/",
+ views.preview_sale_order,
+ name="preview_sale_order",
+ ),
# Invoice
path("sales/invoices/", views.InvoiceListView.as_view(), name="invoice_list"),
path(
@@ -702,52 +789,82 @@ path(
# Bills
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.BillModelCreateView.as_view(),
- name='bill-create'),
- path('items/bills//create/purchase-order//',
- views.BillModelCreateView.as_view(for_purchase_order=True),
- name='bill-create-po'),
- path('items/bills//create/estimate//',
- views.BillModelCreateView.as_view(for_estimate=True),
- name='bill-create-estimate'),
- path('items/bills//detail//',
- views.BillModelDetailViewView.as_view(),
- name='bill-detail'),
- path('items/bills//update//',
- views.BillModelUpdateViewView.as_view(),
- name='bill-update'),
- path('items/bills//update//items/',
- views.BillModelUpdateViewView.as_view(action_update_items=True),
- name='bill-update-items'),
- ############################################################
- path('items/bills//actions//mark-as-draft/',
- views.BillModelActionMarkAsDraftView.as_view(),
- name='bill-action-mark-as-draft'),
- path('items/bills//actions//mark-as-review/',
- views.BillModelActionMarkAsInReviewView.as_view(),
- name='bill-action-mark-as-review'),
- path('items/bills//actions//mark-as-approved/',
- views.BillModelActionMarkAsApprovedView.as_view(),
- name='bill-action-mark-as-approved'),
- path('items/bills//actions//mark-as-paid/',
- views.BillModelActionMarkAsPaidView.as_view(),
- name='bill-action-mark-as-paid'),
- path('items/bills//actions//mark-as-void/',
- views.BillModelActionVoidView.as_view(),
- name='bill-action-mark-as-void'),
- path('items/bills//actions//mark-as-canceled/',
- views.BillModelActionCanceledView.as_view(),
- name='bill-action-mark-as-canceled'),
- path('items/bills//actions//lock-ledger/',
- views.BillModelActionLockLedgerView.as_view(),
- name='bill-action-lock-ledger'),
- path('items/bills//actions//unlock-ledger/',
- views.BillModelActionUnlockLedgerView.as_view(),
- name='bill-action-unlock-ledger'),
- path('items/bills//actions//force-migration/',
- views.BillModelActionForceMigrateView.as_view(),
- name='bill-action-force-migrate'),
+ path(
+ "items/bills//create/",
+ views.BillModelCreateView.as_view(),
+ name="bill-create",
+ ),
+ path(
+ "items/bills//create/purchase-order//",
+ views.BillModelCreateView.as_view(for_purchase_order=True),
+ name="bill-create-po",
+ ),
+ path(
+ "items/bills//create/estimate//",
+ views.BillModelCreateView.as_view(for_estimate=True),
+ name="bill-create-estimate",
+ ),
+ path(
+ "items/bills//detail//",
+ views.BillModelDetailViewView.as_view(),
+ name="bill-detail",
+ ),
+ path(
+ "items/bills//update//",
+ views.BillModelUpdateViewView.as_view(),
+ name="bill-update",
+ ),
+ path(
+ "items/bills//update//items/",
+ views.BillModelUpdateViewView.as_view(action_update_items=True),
+ name="bill-update-items",
+ ),
+ ############################################################
+ path(
+ "items/bills//actions//mark-as-draft/",
+ views.BillModelActionMarkAsDraftView.as_view(),
+ name="bill-action-mark-as-draft",
+ ),
+ path(
+ "items/bills//actions//mark-as-review/",
+ views.BillModelActionMarkAsInReviewView.as_view(),
+ name="bill-action-mark-as-review",
+ ),
+ path(
+ "items/bills//actions//mark-as-approved/",
+ views.BillModelActionMarkAsApprovedView.as_view(),
+ name="bill-action-mark-as-approved",
+ ),
+ path(
+ "items/bills//actions//mark-as-paid/",
+ views.BillModelActionMarkAsPaidView.as_view(),
+ name="bill-action-mark-as-paid",
+ ),
+ path(
+ "items/bills//actions//mark-as-void/",
+ views.BillModelActionVoidView.as_view(),
+ name="bill-action-mark-as-void",
+ ),
+ path(
+ "items/bills//actions//mark-as-canceled/",
+ views.BillModelActionCanceledView.as_view(),
+ name="bill-action-mark-as-canceled",
+ ),
+ path(
+ "items/bills//actions//lock-ledger/",
+ views.BillModelActionLockLedgerView.as_view(),
+ name="bill-action-lock-ledger",
+ ),
+ path(
+ "items/bills//actions//unlock-ledger/",
+ views.BillModelActionUnlockLedgerView.as_view(),
+ name="bill-action-unlock-ledger",
+ ),
+ path(
+ "items/bills//actions//force-migration/",
+ views.BillModelActionForceMigrateView.as_view(),
+ name="bill-action-force-migrate",
+ ),
# path("items/bills/create/", views.bill_create, name="bill_create"),
path(
"items/bills//bill_detail/",
@@ -775,130 +892,228 @@ path(
views.bill_mark_as_paid,
name="bill_mark_as_paid",
),
-
# orders
path("orders/", views.OrderListView.as_view(), name="order_list_view"),
-
# BALANCE SHEET Reports...
# Entities...
- path('entity//balance-sheet/',
- views.BaseBalanceSheetRedirectView.as_view(),
- name='entity-bs'),
- path('entity//balance-sheet/year//',
- views.FiscalYearBalanceSheetViewBase.as_view(),
- name='entity-bs-year'),
- path('entity//balance-sheet/quarter///',
- views.QuarterlyBalanceSheetView.as_view(),
- name='entity-bs-quarter'),
- path('entity//balance-sheet/month///',
- views.MonthlyBalanceSheetView.as_view(),
- name='entity-bs-month'),
- path('entity//balance-sheet/date////',
- views.DateBalanceSheetView.as_view(),
- name='entity-bs-date'),
+ path(
+ "entity//balance-sheet/",
+ views.BaseBalanceSheetRedirectView.as_view(),
+ name="entity-bs",
+ ),
+ path(
+ "entity//balance-sheet/year//",
+ views.FiscalYearBalanceSheetViewBase.as_view(),
+ name="entity-bs-year",
+ ),
+ path(
+ "entity//balance-sheet/quarter///",
+ views.QuarterlyBalanceSheetView.as_view(),
+ name="entity-bs-quarter",
+ ),
+ path(
+ "entity//balance-sheet/month///",
+ views.MonthlyBalanceSheetView.as_view(),
+ name="entity-bs-month",
+ ),
+ path(
+ "entity//balance-sheet/date////",
+ views.DateBalanceSheetView.as_view(),
+ name="entity-bs-date",
+ ),
# INCOME STATEMENT Reports ----
# Entity .....
- path('entity//income-statement/',
- views.BaseIncomeStatementRedirectViewBase.as_view(),
- name='entity-ic'),
- path('entity//income-statement/year//',
- views.FiscalYearIncomeStatementViewBase.as_view(),
- name='entity-ic-year'),
- path('entity//income-statement/quarter///',
- views.QuarterlyIncomeStatementView.as_view(),
- name='entity-ic-quarter'),
- path('entity//income-statement/month///',
- views.MonthlyIncomeStatementView.as_view(),
- name='entity-ic-month'),
- path('entity//income-statement/date////',
- views.MonthlyIncomeStatementView.as_view(),
- name='entity-ic-date'),
- # CASH FLOW STATEMENTS...
+ path(
+ "entity//income-statement/",
+ views.BaseIncomeStatementRedirectViewBase.as_view(),
+ name="entity-ic",
+ ),
+ path(
+ "entity//income-statement/year//",
+ views.FiscalYearIncomeStatementViewBase.as_view(),
+ name="entity-ic-year",
+ ),
+ path(
+ "entity//income-statement/quarter///",
+ views.QuarterlyIncomeStatementView.as_view(),
+ name="entity-ic-quarter",
+ ),
+ path(
+ "entity//income-statement/month///",
+ views.MonthlyIncomeStatementView.as_view(),
+ name="entity-ic-month",
+ ),
+ path(
+ "entity//income-statement/date////",
+ views.MonthlyIncomeStatementView.as_view(),
+ name="entity-ic-date",
+ ),
+ # CASH FLOW STATEMENTS...
# Entities...
- path('entity//cash-flow-statement/',
- views.BaseCashFlowStatementRedirectViewBase.as_view(),
- name='entity-cf'),
- path('entity//cash-flow-statement/year//',
- views.FiscalYearCashFlowStatementViewBase.as_view(),
- name='entity-cf-year'),
- path('entity//cash-flow-statement/quarter///',
- views.QuarterlyCashFlowStatementView.as_view(),
- name='entity-cf-quarter'),
- path('entity//cash-flow-statement/month///',
- views.MonthlyCashFlowStatementView.as_view(),
- name='entity-cf-month'),
- path('entity//cash-flow-statement/date//