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

View File

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

View File

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

View File

@ -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"),
),
],
),
]

View File

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

View File

@ -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__'
fields = "__all__"

View File

@ -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)
print(response.text)

View File

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

View File

@ -5,20 +5,20 @@ from api import views
router = routers.DefaultRouter()
router.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"),
]

View File

@ -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)
return Response({"success": True, "data": vin_data}, status=status.HTTP_200_OK)

View File

@ -15,13 +15,11 @@ from channels.routing import ProtocolTypeRouter, URLRouter
from channels.auth import AuthMiddlewareStack
from 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)),
}
)

View File

@ -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)),
)

View File

@ -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()

View File

@ -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"]

View File

@ -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()
run()

View File

@ -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()

View File

@ -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
}
"error": "حدث خطأ أثناء تحليل الاستعلام"
if language == "ar"
else f"Error analyzing prompt: {str(e)}",
"language": language,
}

View File

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

View File

@ -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)
return system.get_insights_sync(request)

View File

@ -9,7 +9,9 @@ from django.template.loaders.app_directories import get_app_template_dirs
class Command(BaseCommand):
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}")
)

View File

@ -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",
)
],
},
),
]

View File

@ -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"

View File

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

View File

@ -43,10 +43,7 @@ Provide a clear step-by-step guide with numbered instructions. Include:
Helpful Step-by-Step Instructions:"""
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"

View File

@ -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

View File

@ -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

View File

@ -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,
)

View File

@ -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]

View File

@ -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

View File

@ -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}
url = "/" + "/".join(path[: i + 1]) + "/"
breadcrumbs.append({"name": path[i].capitalize(), "url": url})
return {"breadcrumbs": breadcrumbs}

View File

@ -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']
fields = ["code", "name", "role"]

View File

@ -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,11 @@ class SaleOrderForm(forms.ModelForm):
class Meta:
model = SaleOrder
fields = [
"estimate",
"payment_method",
"opportunity",
"agreed_price",
"down_payment_amount",
"loan_amount",
"expected_delivery_date",
"comments",
"status"
]
"customer","expected_delivery_date","estimate","opportunity","comments","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 +1902,7 @@ class StaffTaskForm(forms.ModelForm):
#############################################################
class ItemInventoryForm(forms.Form):
make = forms.ModelChoiceField(
queryset=CarMake.objects.all(),
@ -1932,65 +1926,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
return csv_file

View File

@ -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
return data

View File

@ -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}"))
self.stdout.write(self.style.ERROR(f"An error occurred: {e}"))

View File

@ -7,24 +7,24 @@ from django.conf import settings
class Command(BaseCommand):
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}%)"
)

View File

@ -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(
"####################################################################################################"
)
)

View File

@ -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"
)
)

View File

@ -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()
connection.close()

View File

@ -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'))
self.stdout.write(
self.style.SUCCESS(f"Successfully deactivated {count} expired plans")
)

View File

@ -4,69 +4,72 @@ from django.db import transaction, models
from django.utils.text import slugify
from django.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}')
raise self.style.ERROR(f"Model not found: {e}")

View File

@ -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

View File

@ -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.")
print("Data population completed successfully.")

View File

@ -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')
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",
)

View File

@ -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.")
self.stdout.write("Finished populating colors.")

View File

@ -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.'))
self.stdout.write(self.style.SUCCESS("Successfully seeded 20 customers."))

View File

@ -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."))

View File

@ -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}"))
self.stdout.write(
self.style.SUCCESS(
f"{action} CarSerie with ID {car_serie.id_car_serie}"
)
)

View File

@ -1,6 +1,8 @@
from django.core.management.base import BaseCommand
from 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)
VatRate.objects.get_or_create(rate=Decimal("0.15"), is_active=True)

View File

@ -4,24 +4,23 @@ from plans.models import Plan, Quota, Pricing, PlanPricing
from decimal import Decimal
from 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'))
self.stdout.write(self.style.SUCCESS("Successfully created plans structure"))

View File

@ -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,
# )

View File

@ -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)
create_coa_accounts(dealer.pk)

View File

@ -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")

View File

@ -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}")
print(f"Error translating '{car_model.name}': {e}")

View File

@ -2,32 +2,37 @@ from django.core.management.base import BaseCommand
from inventory.models import CarMake
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}'))
self.stdout.write(
self.style.SUCCESS(f"Updated CarMake: {car_make.name}")
)

View File

@ -3,35 +3,46 @@ import csv
from django.core.management.base import BaseCommand
from 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}"))
self.stdout.write(
self.style.SUCCESS(
f"{action} CarModel with ID {car_model.id_car_model}"
)
)

View File

@ -3,42 +3,61 @@ import csv
from django.core.management.base import BaseCommand
from 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}"))
self.stdout.write(
self.style.SUCCESS(
f"{action} CarSpecificationValue with ID {car_specification_value.id_car_specification_value}"
)
)

View File

@ -3,45 +3,66 @@ import csv
from django.core.management.base import BaseCommand
from 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}"))
self.stdout.write(
self.style.SUCCESS(
f"{action} CarTrim with ID {car_trim.id_car_trim}"
)
)

View File

@ -4,7 +4,7 @@ from django.utils import timezone
from inventory.utils import get_user_type
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)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,4 +1,4 @@
# Generated by Django 5.2.1 on 2025-06-03 11:36
# Generated by Django 5.2.1 on 2025-06-19 13:51
import django.db.models.deletion
from django.db import migrations, models
@ -7,14 +7,14 @@ from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('inventory', '0016_purchaseorderitem_sale'),
('inventory', '0005_remove_saleorder_car'),
]
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'),
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,
),
]

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

View File

@ -1,5 +1,6 @@
from django.utils.translation import get_language
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:

View File

@ -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,16 @@ 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 +2494,9 @@ 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
######################################################################################################
######################################################################################################
######################################################################################################

View File

@ -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

View File

@ -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,55 @@ 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 +338,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 +371,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 +391,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 +466,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 +517,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 +540,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 +562,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 +585,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 +608,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 +631,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 +695,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 +731,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 +751,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 +786,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 +830,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 +866,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"):

View File

@ -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('<span class="badge badge-phoenix fs-11 {}">{}</span>', badge_class, value)
return format_html(
'<span class="badge badge-phoenix fs-11 {}">{}</span>', badge_class, value
)
def render_stock_type(self, value):
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('<span class="badge badge-phoenix fs-11 {}">{}</span>', badge_class, value)
return format_html(
'<span class="badge badge-phoenix fs-11 {}">{}</span>', badge_class, value
)

File diff suppressed because it is too large Load Diff

View File

@ -4,195 +4,210 @@ from django import template
from django.urls import reverse
from 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
from django.db.models import Case, Value, When, IntegerField
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,
@ -201,57 +216,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:
@ -282,15 +315,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:
@ -321,7 +386,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'):
@ -331,25 +397,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,
}
@ -358,7 +425,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
@ -369,38 +437,47 @@ 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):
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):
# Specific ordering for BillModel (timestamp ascending, then debit before credit)
qs = object_type.get_transaction_queryset(annotated=True)
@ -417,21 +494,25 @@ def transactions_table(object_type: Union[JournalEntryModel, BillModel, InvoiceM
'pk' # Optional: Tie-breaker for consistent order
)
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,
}
@ -441,67 +522,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
@ -510,24 +591,49 @@ 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'
return "sedan"
@register.filter
def status_badge_color(status):
color_map = {
'PENDING_APPROVAL': 'warning',
'APPROVED': 'info',
'IN_FINANCING': 'primary',
'PARTIALLY_PAID': 'success',
'FULLY_PAID': 'success',
'PENDING_DELIVERY': 'warning',
'DELIVERED': 'success',
'CANCELLED': 'danger',
}
return color_map.get(status, 'secondary')
@register.inclusion_tag('inventory/tags/inventory_table.html', takes_context=True)
def inventory_table(context, queryset):
ctx = {
'entity_slug': context['view'].kwargs['entity_slug'],
'inventory_list': queryset
}
ctx.update(queryset.aggregate(inventory_total_value=Sum('total_value')))
return ctx

View File

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

View File

@ -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

View File

@ -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'))
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"))

View File

@ -1,4 +1,4 @@
from django.conf.urls import handler403,handler400,handler404,handler500
from django.conf.urls import handler403, handler400, handler404, handler500
from django.urls import path
from django_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/<int:task_id>/detail/', views.task_detail, name='task_detail'),
# Dashboards
# path("user/<int:pk>/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/<slug:slug>/settings/", views.DealerSettingsView, name="dealer_settings"),
path(
"dealers/<slug:slug>/settings/",
views.DealerSettingsView,
name="dealer_settings",
),
path("dealers/assign-car-makes/", views.assign_car_makes, name="assign_car_makes"),
path("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/<slug:slug>/", views.DealerDetailView.as_view(), name="dealer_detail"),
path(
"dealers/<slug:slug>/", views.DealerDetailView.as_view(), name="dealer_detail"
),
path(
"dealers/<slug:slug>/update/",
views.DealerUpdateView.as_view(),
@ -93,7 +102,9 @@ urlpatterns = [
views.CustomerUpdateView.as_view(),
name="customer_update",
),
path("customers/<slug:slug>/delete/", views.delete_customer, name="customer_delete"),
path(
"customers/<slug:slug>/delete/", views.delete_customer, name="customer_delete"
),
path(
"customers/<slug:slug>/opportunities/create/",
views.OpportunityCreateView.as_view(),
@ -101,19 +112,26 @@ urlpatterns = [
),
path("crm/leads/create/", views.lead_create, name="lead_create"),
path(
"crm/leads/<slug:slug>/view/", views.LeadDetailView.as_view(), name="lead_detail"
"crm/leads/<slug:slug>/view/",
views.LeadDetailView.as_view(),
name="lead_detail",
),
path('update-lead-actions/', views.update_lead_actions, name='update_lead_actions'),
path('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/<slug:slug>/update/", views.LeadUpdateView.as_view(), name="lead_update"
"crm/leads/<slug:slug>/update/",
views.LeadUpdateView.as_view(),
name="lead_update",
),
path("crm/leads/<slug:slug>/delete/", views.LeadDeleteView, name="lead_delete"),
path("crm/leads/<slug:slug>/lead-convert/", views.lead_convert, name="lead_convert"),
path("crm/leads/<int:pk>/delete-note/", views.delete_note, name="delete_note_to_lead"),
path(
"crm/leads/<slug:slug>/lead-convert/", views.lead_convert, name="lead_convert"
),
path(
"crm/leads/<int:pk>/delete-note/", views.delete_note, name="delete_note_to_lead"
),
path(
"crm/<int:pk>/update-note/",
views.update_note,
@ -211,17 +229,22 @@ urlpatterns = [
),
# path('crm/opportunities/<int:pk>/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('<int:notification_id>/mark-read/', views.mark_notification_as_read, name='mark_notification_as_read'),
path(
"<int:notification_id>/mark-read/",
views.mark_notification_as_read,
name="mark_notification_as_read",
),
# Mark all notifications as read
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/<uuid:pk>/upload_cars/', views.upload_cars, name='upload_cars'),
path("cars/upload_cars/", views.upload_cars, name="upload_cars"),
path("cars/<uuid:pk>/upload_cars/", views.upload_cars, name="upload_cars"),
path("cars/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/<slug:slug>/add-color/", views.CarColorCreate.as_view(), name="add_color"
),
path('car/colors/<slug:slug>/update/', views.CarColorsUpdateView.as_view(), name='car_colors_update'),
path(
"car/colors/<slug:slug>/update/",
views.CarColorsUpdateView.as_view(),
name="car_colors_update",
),
path(
"cars/<slug:slug>/location/add/",
views.CarLocationCreateView.as_view(),
name="add_car_location",
),
path(
path(
"cars/<slug:car_pk>/location/<int:pk>/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/<int:car_pk>/colors/<int:pk>/update/',views.CarColorUpdateView.as_view(),name='color_update'),
path("cars/reserve/<slug:slug>/",
views.reserve_car_view,
name="reserve_car"),
path("cars/reserve/<slug:slug>/", views.reserve_car_view, name="reserve_car"),
path(
"reservations/<int:reservation_id>/",
views.manage_reservation,
@ -341,16 +364,20 @@ path(
views.CustomCardCreateView.as_view(),
name="add_custom_card",
),
path('cars/<slug:slug>/add-registration/',
views.CarRegistrationCreateView.as_view(),
name='add_registration'),
#sales list
path(
'sales/list/',
views.sales_list_view,
name='sales_list',
"cars/<slug:slug>/add-registration/",
views.CarRegistrationCreateView.as_view(),
name="add_registration",
),
# sales list
path(
"sales/list/",
views.sales_list_view,
name="sales_list",
),
path('sale_orders/<int:pk>/', views.SaleOrderDetailView.as_view(), name='order_detail'),
path('inventory/<slug:entity_slug>/list/', views.InventoryListView.as_view(), name='inventort_list'),
# Sales URLs quotation_create
# path(
# "sales/quotations/create/",
@ -401,21 +428,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/<slug:slug>/", views.UserDetailView.as_view(), name="user_detail"),
path("user/<slug:slug>/groups/", views.UserGroupView, name="user_groups"),
path("user/<slug:slug>/update/", views.UserUpdateView.as_view(), name="user_update"),
path(
"user/<slug:slug>/update/", views.UserUpdateView.as_view(), name="user_update"
),
path("user/<slug:slug>/confirm/", views.UserDeleteview, name="user_delete"),
# Group URLs
path("group/create/", views.GroupCreateView.as_view(), name="group_create"),
path("group/<int:pk>/update/", views.GroupUpdateView.as_view(), name="group_update"),
path(
"group/<int:pk>/update/", views.GroupUpdateView.as_view(), name="group_update"
),
path("group/<int:pk>/", views.GroupDetailView.as_view(), name="group_detail"),
path("group/", views.GroupListView.as_view(), name="group_list"),
path("group/<int:pk>/confirm/", views.GroupDeleteview, name="group_delete"),
path("group/<int:pk>/permission/", views.GroupPermissionView, name="group_permission"),
path(
"group/<int:pk>/permission/", views.GroupPermissionView, name="group_permission"
),
# Organization URLs
path(
"organizations/create/",
@ -468,76 +500,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/<slug:entity_slug>/detail/<uuid:pk>/", views.LedgerModelDetailView.as_view(), name="ledger_detail"
"ledgers/<slug:entity_slug>/detail/<uuid:pk>/",
views.LedgerModelDetailView.as_view(),
name="ledger_detail",
),
path(
"ledgers/<slug:entity_slug>/lock_all_journals/<uuid:pk>/", views.ledger_lock_all_journals, name="lock_all_journals"
"ledgers/<slug:entity_slug>/lock_all_journals/<uuid:pk>/",
views.ledger_lock_all_journals,
name="lock_all_journals",
),
path(
"ledgers/<slug:entity_slug>/unlock_all_journals/<uuid:pk>/", views.ledger_unlock_all_journals, name="unlock_all_journals"
"ledgers/<slug:entity_slug>/unlock_all_journals/<uuid:pk>/",
views.ledger_unlock_all_journals,
name="unlock_all_journals",
),
path(
"ledgers/<slug:entity_slug>/post_all_journals/<uuid:pk>/", views.ledger_post_all_journals, name="post_all_journals"
"ledgers/<slug:entity_slug>/post_all_journals/<uuid:pk>/",
views.ledger_post_all_journals,
name="post_all_journals",
),
path(
"ledgers/<slug:entity_slug>/unpost_all_journals/<uuid:pk>/", views.ledger_unpost_all_journals, name="unpost_all_journals"
"ledgers/<slug:entity_slug>/unpost_all_journals/<uuid:pk>/",
views.ledger_unpost_all_journals,
name="unpost_all_journals",
),
# path(
# "ledgers/create/", views.LedgerModelCreateView.as_view(), name="ledger_create"
# ),
path(
"journalentries/<uuid:pk>/list/", views.JournalEntryListView.as_view(), name="journalentry_list"
"journalentries/<uuid:pk>/list/",
views.JournalEntryListView.as_view(),
name="journalentry_list",
),
path(
"journalentries/<uuid:pk>/create/", views.JournalEntryCreateView.as_view(), name="journalentry_create"
"journalentries/<uuid:pk>/create/",
views.JournalEntryCreateView.as_view(),
name="journalentry_create",
),
path(
"journalentries/<uuid:pk>/delete/", views.JournalEntryDeleteView, name="journalentry_delete"
"journalentries/<uuid:pk>/delete/",
views.JournalEntryDeleteView,
name="journalentry_delete",
),
path(
"journalentries/<uuid:pk>/transactions/",
views.JournalEntryTransactionsView,
name="journalentry_transactions",
),
path('journalentries/<slug:entity_slug>/<uuid:ledger_pk>/detail/<uuid:je_pk>/txs/',
views.JournalEntryModelTXSDetailView.as_view(),
name='journalentry_txs'),
path(
"journalentries/<slug:entity_slug>/<uuid:ledger_pk>/detail/<uuid:je_pk>/txs/",
views.JournalEntryModelTXSDetailView.as_view(),
name="journalentry_txs",
),
# ledger actions
path('ledgers/<slug:entity_slug>/action/<uuid:ledger_pk>/post/',
views.LedgerModelModelActionView.as_view(action_name='post'),
name='ledger-action-post'),
path('ledgers/<slug:entity_slug>/action/<uuid:ledger_pk>/post-journal-entries/',
views.LedgerModelModelActionView.as_view(action_name='post_journal_entries'),
name='ledger-action-post-journal-entries'),
path('ledgers/<slug:entity_slug>/action/<uuid:ledger_pk>/unpost/',
views.LedgerModelModelActionView.as_view(action_name='unpost'),
name='ledger-action-unpost'),
path('ledgers/<slug:entity_slug>/action/<uuid:ledger_pk>/lock/',
views.LedgerModelModelActionView.as_view(action_name='lock'),
name='ledger-action-lock'),
path('ledgers/<slug:entity_slug>/action/<uuid:ledger_pk>/lock-journal-entries/',
views.LedgerModelModelActionView.as_view(action_name='lock_journal_entries'),
name='ledger-action-lock-journal-entries'),
path('ledgers/<slug:entity_slug>/action/<uuid:ledger_pk>/unlock/',
views.LedgerModelModelActionView.as_view(action_name='unlock'),
name='ledger-action-unlock'),
path('ledgers/<slug:entity_slug>/action/<uuid:ledger_pk>/hide/',
views.LedgerModelModelActionView.as_view(action_name='hide'),
name='ledger-action-hide'),
path('ledgers/<slug:entity_slug>/action/<uuid:ledger_pk>/unhide/',
views.LedgerModelModelActionView.as_view(action_name='unhide'),
name='ledger-action-unhide'),
path('ledgers/<slug:entity_slug>/delete/<uuid:ledger_pk>/',
views.LedgerModelDeleteView.as_view(),
name='ledger-delete'),
path(
"ledgers/<slug:entity_slug>/action/<uuid:ledger_pk>/post/",
views.LedgerModelModelActionView.as_view(action_name="post"),
name="ledger-action-post",
),
path(
"ledgers/<slug:entity_slug>/action/<uuid:ledger_pk>/post-journal-entries/",
views.LedgerModelModelActionView.as_view(action_name="post_journal_entries"),
name="ledger-action-post-journal-entries",
),
path(
"ledgers/<slug:entity_slug>/action/<uuid:ledger_pk>/unpost/",
views.LedgerModelModelActionView.as_view(action_name="unpost"),
name="ledger-action-unpost",
),
path(
"ledgers/<slug:entity_slug>/action/<uuid:ledger_pk>/lock/",
views.LedgerModelModelActionView.as_view(action_name="lock"),
name="ledger-action-lock",
),
path(
"ledgers/<slug:entity_slug>/action/<uuid:ledger_pk>/lock-journal-entries/",
views.LedgerModelModelActionView.as_view(action_name="lock_journal_entries"),
name="ledger-action-lock-journal-entries",
),
path(
"ledgers/<slug:entity_slug>/action/<uuid:ledger_pk>/unlock/",
views.LedgerModelModelActionView.as_view(action_name="unlock"),
name="ledger-action-unlock",
),
path(
"ledgers/<slug:entity_slug>/action/<uuid:ledger_pk>/hide/",
views.LedgerModelModelActionView.as_view(action_name="hide"),
name="ledger-action-hide",
),
path(
"ledgers/<slug:entity_slug>/action/<uuid:ledger_pk>/unhide/",
views.LedgerModelModelActionView.as_view(action_name="unhide"),
name="ledger-action-unhide",
),
path(
"ledgers/<slug:entity_slug>/delete/<uuid:ledger_pk>/",
views.LedgerModelDeleteView.as_view(),
name="ledger-delete",
),
# Bank Account
path(
"bank_accounts/", views.BankAccountListView.as_view(), name="bank_account_list"
@ -586,7 +651,11 @@ path(
name="estimate_detail",
),
path("sales/estimates/create/", views.create_estimate, name="estimate_create"),
path("sales/estimates/create/<slug:slug>/", views.create_estimate, name="estimate_create_from_opportunity"),
path(
"sales/estimates/create/<slug:slug>/",
views.create_estimate,
name="estimate_create_from_opportunity",
),
path(
"sales/estimates/<uuid:pk>/estimate_mark_as/",
views.estimate_mark_as,
@ -605,10 +674,21 @@ path(
path(
"sales/estimates/<uuid:pk>/send_email", views.send_email_view, name="send_email"
),
path('sales/estimates/<uuid:pk>/sale_order/', views.create_sale_order, name='create_sale_order'),
path('sales/estimates/<uuid:pk>/sale_order/<int:order_pk>/details/', views.SaleOrderDetail.as_view(), name='sale_order_details'),
path('sales/estimates/<uuid:pk>/sale_order/preview/', views.preview_sale_order, name='preview_sale_order'),
path(
"sales/estimates/<uuid:pk>/sale_order/",
views.create_sale_order,
name="create_sale_order",
),
path(
"sales/estimates/<uuid:pk>/sale_order/<int:order_pk>/details/",
views.SaleOrderDetail.as_view(),
name="sale_order_details",
),
path(
"sales/estimates/<uuid:pk>/sale_order/preview/",
views.preview_sale_order,
name="preview_sale_order",
),
# Invoice
path("sales/invoices/", views.InvoiceListView.as_view(), name="invoice_list"),
path(
@ -702,52 +782,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/<slug:entity_slug>/create/',
views.BillModelCreateView.as_view(),
name='bill-create'),
path('items/bills/<slug:entity_slug>/create/purchase-order/<uuid:po_pk>/',
views.BillModelCreateView.as_view(for_purchase_order=True),
name='bill-create-po'),
path('items/bills/<slug:entity_slug>/create/estimate/<uuid:ce_pk>/',
views.BillModelCreateView.as_view(for_estimate=True),
name='bill-create-estimate'),
path('items/bills/<slug:entity_slug>/detail/<uuid:bill_pk>/',
views.BillModelDetailViewView.as_view(),
name='bill-detail'),
path('items/bills/<slug:entity_slug>/update/<uuid:bill_pk>/',
views.BillModelUpdateViewView.as_view(),
name='bill-update'),
path('items/bills/<slug:entity_slug>/update/<uuid:bill_pk>/items/',
views.BillModelUpdateViewView.as_view(action_update_items=True),
name='bill-update-items'),
############################################################
path('items/bills/<slug:entity_slug>/actions/<uuid:bill_pk>/mark-as-draft/',
views.BillModelActionMarkAsDraftView.as_view(),
name='bill-action-mark-as-draft'),
path('items/bills/<slug:entity_slug>/actions/<uuid:bill_pk>/mark-as-review/',
views.BillModelActionMarkAsInReviewView.as_view(),
name='bill-action-mark-as-review'),
path('items/bills/<slug:entity_slug>/actions/<uuid:bill_pk>/mark-as-approved/',
views.BillModelActionMarkAsApprovedView.as_view(),
name='bill-action-mark-as-approved'),
path('items/bills/<slug:entity_slug>/actions/<uuid:bill_pk>/mark-as-paid/',
views.BillModelActionMarkAsPaidView.as_view(),
name='bill-action-mark-as-paid'),
path('items/bills/<slug:entity_slug>/actions/<uuid:bill_pk>/mark-as-void/',
views.BillModelActionVoidView.as_view(),
name='bill-action-mark-as-void'),
path('items/bills/<slug:entity_slug>/actions/<uuid:bill_pk>/mark-as-canceled/',
views.BillModelActionCanceledView.as_view(),
name='bill-action-mark-as-canceled'),
path('items/bills/<slug:entity_slug>/actions/<uuid:bill_pk>/lock-ledger/',
views.BillModelActionLockLedgerView.as_view(),
name='bill-action-lock-ledger'),
path('items/bills/<slug:entity_slug>/actions/<uuid:bill_pk>/unlock-ledger/',
views.BillModelActionUnlockLedgerView.as_view(),
name='bill-action-unlock-ledger'),
path('items/bills/<slug:entity_slug>/actions/<uuid:bill_pk>/force-migration/',
views.BillModelActionForceMigrateView.as_view(),
name='bill-action-force-migrate'),
path(
"items/bills/<slug:entity_slug>/create/",
views.BillModelCreateView.as_view(),
name="bill-create",
),
path(
"items/bills/<slug:entity_slug>/create/purchase-order/<uuid:po_pk>/",
views.BillModelCreateView.as_view(for_purchase_order=True),
name="bill-create-po",
),
path(
"items/bills/<slug:entity_slug>/create/estimate/<uuid:ce_pk>/",
views.BillModelCreateView.as_view(for_estimate=True),
name="bill-create-estimate",
),
path(
"items/bills/<slug:entity_slug>/detail/<uuid:bill_pk>/",
views.BillModelDetailViewView.as_view(),
name="bill-detail",
),
path(
"items/bills/<slug:entity_slug>/update/<uuid:bill_pk>/",
views.BillModelUpdateViewView.as_view(),
name="bill-update",
),
path(
"items/bills/<slug:entity_slug>/update/<uuid:bill_pk>/items/",
views.BillModelUpdateViewView.as_view(action_update_items=True),
name="bill-update-items",
),
############################################################
path(
"items/bills/<slug:entity_slug>/actions/<uuid:bill_pk>/mark-as-draft/",
views.BillModelActionMarkAsDraftView.as_view(),
name="bill-action-mark-as-draft",
),
path(
"items/bills/<slug:entity_slug>/actions/<uuid:bill_pk>/mark-as-review/",
views.BillModelActionMarkAsInReviewView.as_view(),
name="bill-action-mark-as-review",
),
path(
"items/bills/<slug:entity_slug>/actions/<uuid:bill_pk>/mark-as-approved/",
views.BillModelActionMarkAsApprovedView.as_view(),
name="bill-action-mark-as-approved",
),
path(
"items/bills/<slug:entity_slug>/actions/<uuid:bill_pk>/mark-as-paid/",
views.BillModelActionMarkAsPaidView.as_view(),
name="bill-action-mark-as-paid",
),
path(
"items/bills/<slug:entity_slug>/actions/<uuid:bill_pk>/mark-as-void/",
views.BillModelActionVoidView.as_view(),
name="bill-action-mark-as-void",
),
path(
"items/bills/<slug:entity_slug>/actions/<uuid:bill_pk>/mark-as-canceled/",
views.BillModelActionCanceledView.as_view(),
name="bill-action-mark-as-canceled",
),
path(
"items/bills/<slug:entity_slug>/actions/<uuid:bill_pk>/lock-ledger/",
views.BillModelActionLockLedgerView.as_view(),
name="bill-action-lock-ledger",
),
path(
"items/bills/<slug:entity_slug>/actions/<uuid:bill_pk>/unlock-ledger/",
views.BillModelActionUnlockLedgerView.as_view(),
name="bill-action-unlock-ledger",
),
path(
"items/bills/<slug:entity_slug>/actions/<uuid:bill_pk>/force-migration/",
views.BillModelActionForceMigrateView.as_view(),
name="bill-action-force-migrate",
),
# path("items/bills/create/", views.bill_create, name="bill_create"),
path(
"items/bills/<uuid:pk>/bill_detail/",
@ -775,130 +885,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/<slug:entity_slug>/balance-sheet/',
views.BaseBalanceSheetRedirectView.as_view(),
name='entity-bs'),
path('entity/<slug:entity_slug>/balance-sheet/year/<int:year>/',
views.FiscalYearBalanceSheetViewBase.as_view(),
name='entity-bs-year'),
path('entity/<slug:entity_slug>/balance-sheet/quarter/<int:year>/<int:quarter>/',
views.QuarterlyBalanceSheetView.as_view(),
name='entity-bs-quarter'),
path('entity/<slug:entity_slug>/balance-sheet/month/<int:year>/<int:month>/',
views.MonthlyBalanceSheetView.as_view(),
name='entity-bs-month'),
path('entity/<slug:entity_slug>/balance-sheet/date/<int:year>/<int:month>/<int:day>/',
views.DateBalanceSheetView.as_view(),
name='entity-bs-date'),
path(
"entity/<slug:entity_slug>/balance-sheet/",
views.BaseBalanceSheetRedirectView.as_view(),
name="entity-bs",
),
path(
"entity/<slug:entity_slug>/balance-sheet/year/<int:year>/",
views.FiscalYearBalanceSheetViewBase.as_view(),
name="entity-bs-year",
),
path(
"entity/<slug:entity_slug>/balance-sheet/quarter/<int:year>/<int:quarter>/",
views.QuarterlyBalanceSheetView.as_view(),
name="entity-bs-quarter",
),
path(
"entity/<slug:entity_slug>/balance-sheet/month/<int:year>/<int:month>/",
views.MonthlyBalanceSheetView.as_view(),
name="entity-bs-month",
),
path(
"entity/<slug:entity_slug>/balance-sheet/date/<int:year>/<int:month>/<int:day>/",
views.DateBalanceSheetView.as_view(),
name="entity-bs-date",
),
# INCOME STATEMENT Reports ----
# Entity .....
path('entity/<slug:entity_slug>/income-statement/',
views.BaseIncomeStatementRedirectViewBase.as_view(),
name='entity-ic'),
path('entity/<slug:entity_slug>/income-statement/year/<int:year>/',
views.FiscalYearIncomeStatementViewBase.as_view(),
name='entity-ic-year'),
path('entity/<slug:entity_slug>/income-statement/quarter/<int:year>/<int:quarter>/',
views.QuarterlyIncomeStatementView.as_view(),
name='entity-ic-quarter'),
path('entity/<slug:entity_slug>/income-statement/month/<int:year>/<int:month>/',
views.MonthlyIncomeStatementView.as_view(),
name='entity-ic-month'),
path('entity/<slug:entity_slug>/income-statement/date/<int:year>/<int:month>/<int:day>/',
views.MonthlyIncomeStatementView.as_view(),
name='entity-ic-date'),
# CASH FLOW STATEMENTS...
path(
"entity/<slug:entity_slug>/income-statement/",
views.BaseIncomeStatementRedirectViewBase.as_view(),
name="entity-ic",
),
path(
"entity/<slug:entity_slug>/income-statement/year/<int:year>/",
views.FiscalYearIncomeStatementViewBase.as_view(),
name="entity-ic-year",
),
path(
"entity/<slug:entity_slug>/income-statement/quarter/<int:year>/<int:quarter>/",
views.QuarterlyIncomeStatementView.as_view(),
name="entity-ic-quarter",
),
path(
"entity/<slug:entity_slug>/income-statement/month/<int:year>/<int:month>/",
views.MonthlyIncomeStatementView.as_view(),
name="entity-ic-month",
),
path(
"entity/<slug:entity_slug>/income-statement/date/<int:year>/<int:month>/<int:day>/",
views.MonthlyIncomeStatementView.as_view(),
name="entity-ic-date",
),
# CASH FLOW STATEMENTS...
# Entities...
path('entity/<slug:entity_slug>/cash-flow-statement/',
views.BaseCashFlowStatementRedirectViewBase.as_view(),
name='entity-cf'),
path('entity/<slug:entity_slug>/cash-flow-statement/year/<int:year>/',
views.FiscalYearCashFlowStatementViewBase.as_view(),
name='entity-cf-year'),
path('entity/<slug:entity_slug>/cash-flow-statement/quarter/<int:year>/<int:quarter>/',
views.QuarterlyCashFlowStatementView.as_view(),
name='entity-cf-quarter'),
path('entity/<slug:entity_slug>/cash-flow-statement/month/<int:year>/<int:month>/',
views.MonthlyCashFlowStatementView.as_view(),
name='entity-cf-month'),
path('entity/<slug:entity_slug>/cash-flow-statement/date/<int:year>/<int:month>/<int:day>/',
views.DateCashFlowStatementView.as_view(),
name='entity-cf-date'),
#Dashboard
path(
"entity/<slug:entity_slug>/cash-flow-statement/",
views.BaseCashFlowStatementRedirectViewBase.as_view(),
name="entity-cf",
),
path(
"entity/<slug:entity_slug>/cash-flow-statement/year/<int:year>/",
views.FiscalYearCashFlowStatementViewBase.as_view(),
name="entity-cf-year",
),
path(
"entity/<slug:entity_slug>/cash-flow-statement/quarter/<int:year>/<int:quarter>/",
views.QuarterlyCashFlowStatementView.as_view(),
name="entity-cf-quarter",
),
path(
"entity/<slug:entity_slug>/cash-flow-statement/month/<int:year>/<int:month>/",
views.MonthlyCashFlowStatementView.as_view(),
name="entity-cf-month",
),
path(
"entity/<slug:entity_slug>/cash-flow-statement/date/<int:year>/<int:month>/<int:day>/",
views.DateCashFlowStatementView.as_view(),
name="entity-cf-date",
),
# Dashboard
# DASHBOARD Views...
path('<slug:entity_slug>/dashboard/',
views.EntityModelDetailHandlerViewBase.as_view(),
name='entity-dashboard'),
path('<slug:entity_slug>/dashboard/year/<int:year>/',
views.FiscalYearEntityModelDashboardView.as_view(),
name='entity-dashboard-year'),
path('<slug:entity_slug>/dashboard/quarter/<int:year>/<int:quarter>/',
views.QuarterlyEntityDashboardView.as_view(),
name='entity-dashboard-quarter'),
path('<slug:entity_slug>/dashboard/month/<int:year>/<int:month>/',
views.MonthlyEntityDashboardView.as_view(),
name='entity-dashboard-month'),
path('<slug:entity_slug>/dashboard/date/<int:year>/<int:month>/<int:day>/',
views.DateEntityDashboardView.as_view(),
name='entity-dashboard-date'),
#dashboard api
path('entity/<slug:entity_slug>/data/net-payables/',
views.PayableNetAPIView.as_view(),
name='entity-json-net-payables'),
path('entity/<slug:entity_slug>/data/net-receivables/',
views.ReceivableNetAPIView.as_view(),
name='entity-json-net-receivables'),
path('entity/<slug:entity_slug>/data/pnl/',
views.PnLAPIView.as_view(),
name='entity-json-pnl'),
# Admin Management...
path('management/', views.management_view, name='management'),
path('management/user_management/', views.user_management, name='user_management'),
path('management/<str:content_type>/<slug:slug>/activate_account/', views.activate_account, name='activate_account'),
path('management/<str:content_type>/<slug:slug>/permenant_delete_account/', views.permenant_delete_account, name='permenant_delete_account'),
path('management/audit_log_dashboard/', views.AuditLogDashboardView, name='audit_log_dashboard'),
path(
"<slug:entity_slug>/dashboard/",
views.EntityModelDetailHandlerViewBase.as_view(),
name="entity-dashboard",
),
path(
"<slug:entity_slug>/dashboard/year/<int:year>/",
views.FiscalYearEntityModelDashboardView.as_view(),
name="entity-dashboard-year",
),
path(
"<slug:entity_slug>/dashboard/quarter/<int:year>/<int:quarter>/",
views.QuarterlyEntityDashboardView.as_view(),
name="entity-dashboard-quarter",
),
path(
"<slug:entity_slug>/dashboard/month/<int:year>/<int:month>/",
views.MonthlyEntityDashboardView.as_view(),
name="entity-dashboard-month",
),
path(
"<slug:entity_slug>/dashboard/date/<int:year>/<int:month>/<int:day>/",
views.DateEntityDashboardView.as_view(),
name="entity-dashboard-date",
),
# dashboard api
path(
"entity/<slug:entity_slug>/data/net-payables/",
views.PayableNetAPIView.as_view(),
name="entity-json-net-payables",
),
path(
"entity/<slug:entity_slug>/data/net-receivables/",
views.ReceivableNetAPIView.as_view(),
name="entity-json-net-receivables",
),
path(
"entity/<slug:entity_slug>/data/pnl/",
views.PnLAPIView.as_view(),
name="entity-json-pnl",
),
# Admin Management...
path("management/", views.management_view, name="management"),
path("management/user_management/", views.user_management, name="user_management"),
path(
"management/<str:content_type>/<slug:slug>/activate_account/",
views.activate_account,
name="activate_account",
),
path(
"management/<str:content_type>/<slug:slug>/permenant_delete_account/",
views.permenant_delete_account,
name="permenant_delete_account",
),
path(
"management/audit_log_dashboard/",
views.AuditLogDashboardView,
name="audit_log_dashboard",
),
#########
# Purchase Order
path('purchase_orders/', views.PurchaseOrderListView.as_view(), name='purchase_order_list'),
path('purchase_orders/new/', views.PurchaseOrderCreateView, name='purchase_order_create'),
path('purchase_orders/<uuid:pk>/detail/', views.PurchaseOrderDetailView.as_view(), name='purchase_order_detail'),
path('purchase_orders/<slug:entity_slug>/<uuid:po_pk>/update/', views.PurchaseOrderUpdateView.as_view(), name='purchase_order_update'),
path('purchase_orders/<slug:entity_slug>/update/<uuid:po_pk>/update-items/',
views.PurchaseOrderUpdateView.as_view(action_update_items=True),
name='purchase_order_update_items'),
path('purchase_orders/inventory_item/create/', views.InventoryItemCreateView, name='inventory_item_create'),
path('purchase_orders/inventory_items_filter/', views.inventory_items_filter, name='inventory_items_filter'),
path('purchase_orders/<slug:entity_slug>/delete/<uuid:po_pk>/',
views.PurchaseOrderModelDeleteView.as_view(),
name='po-delete'),
path('purchase_orders/<slug:entity_slug>/<uuid:po_pk>/upload/',view=views.view_items_inventory,name='view_items_inventory'),
path(
"purchase_orders/",
views.PurchaseOrderListView.as_view(),
name="purchase_order_list",
),
path(
"purchase_orders/new/",
views.PurchaseOrderCreateView,
name="purchase_order_create",
),
path(
"purchase_orders/<uuid:pk>/detail/",
views.PurchaseOrderDetailView.as_view(),
name="purchase_order_detail",
),
path(
"purchase_orders/<slug:entity_slug>/<uuid:po_pk>/update/",
views.PurchaseOrderUpdateView.as_view(),
name="purchase_order_update",
),
path(
"purchase_orders/<slug:entity_slug>/update/<uuid:po_pk>/update-items/",
views.PurchaseOrderUpdateView.as_view(action_update_items=True),
name="purchase_order_update_items",
),
path(
"purchase_orders/inventory_item/create/",
views.InventoryItemCreateView,
name="inventory_item_create",
),
path(
"purchase_orders/inventory_items_filter/",
views.inventory_items_filter,
name="inventory_items_filter",
),
path(
"purchase_orders/<slug:entity_slug>/delete/<uuid:po_pk>/",
views.PurchaseOrderModelDeleteView.as_view(),
name="po-delete",
),
path(
"purchase_orders/<slug:entity_slug>/<uuid:po_pk>/upload/",
view=views.view_items_inventory,
name="view_items_inventory",
),
# Actions....
path('<slug:entity_slug>/action/<uuid:po_pk>/mark-as-draft/',
views.PurchaseOrderMarkAsDraftView.as_view(),
name='po-action-mark-as-draft'),
path('<slug:entity_slug>/action/<uuid:po_pk>/mark-as-review/',
views.PurchaseOrderMarkAsReviewView.as_view(),
name='po-action-mark-as-review'),
path('<slug:entity_slug>/action/<uuid:po_pk>/mark-as-approved/',
views.PurchaseOrderMarkAsApprovedView.as_view(),
name='po-action-mark-as-approved'),
path('<slug:entity_slug>/action/<uuid:po_pk>/mark-as-fulfilled/',
views.PurchaseOrderMarkAsFulfilledView.as_view(),
name='po-action-mark-as-fulfilled'),
path('<slug:entity_slug>/action/<uuid:po_pk>/mark-as-canceled/',
views.PurchaseOrderMarkAsCanceledView.as_view(),
name='po-action-mark-as-canceled'),
path('<slug:entity_slug>/action/<uuid:po_pk>/mark-as-void/',
views.PurchaseOrderMarkAsVoidView.as_view(),
name='po-action-mark-as-void'),
path(
"<slug:entity_slug>/action/<uuid:po_pk>/mark-as-draft/",
views.PurchaseOrderMarkAsDraftView.as_view(),
name="po-action-mark-as-draft",
),
path(
"<slug:entity_slug>/action/<uuid:po_pk>/mark-as-review/",
views.PurchaseOrderMarkAsReviewView.as_view(),
name="po-action-mark-as-review",
),
path(
"<slug:entity_slug>/action/<uuid:po_pk>/mark-as-approved/",
views.PurchaseOrderMarkAsApprovedView.as_view(),
name="po-action-mark-as-approved",
),
path(
"<slug:entity_slug>/action/<uuid:po_pk>/mark-as-fulfilled/",
views.PurchaseOrderMarkAsFulfilledView.as_view(),
name="po-action-mark-as-fulfilled",
),
path(
"<slug:entity_slug>/action/<uuid:po_pk>/mark-as-canceled/",
views.PurchaseOrderMarkAsCanceledView.as_view(),
name="po-action-mark-as-canceled",
),
path(
"<slug:entity_slug>/action/<uuid:po_pk>/mark-as-void/",
views.PurchaseOrderMarkAsVoidView.as_view(),
name="po-action-mark-as-void",
),
]
handler404 = "inventory.views.custom_page_not_found_view"

View File

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

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

View File

@ -43,10 +43,7 @@ Provide a clear step-by-step guide with numbered instructions. Include:
Helpful Step-by-Step Instructions:"""
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"

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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